Delete unverified accounts

Misc cleanup
This commit is contained in:
Kieran 2022-03-04 20:05:01 +00:00
parent 99207c4514
commit 990d636fba
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
20 changed files with 180 additions and 44 deletions

View File

@ -10,12 +10,14 @@ namespace VoidCat.Controllers.Admin;
public class AdminController : Controller public class AdminController : Controller
{ {
private readonly IFileStore _fileStore; private readonly IFileStore _fileStore;
private readonly IFileInfoManager _fileInfo;
private readonly IUserStore _userStore; private readonly IUserStore _userStore;
public AdminController(IFileStore fileStore, IUserStore userStore) public AdminController(IFileStore fileStore, IUserStore userStore, IFileInfoManager fileInfo)
{ {
_fileStore = fileStore; _fileStore = fileStore;
_userStore = userStore; _userStore = userStore;
_fileInfo = fileInfo;
} }
[HttpPost] [HttpPost]
@ -27,9 +29,11 @@ public class AdminController : Controller
[HttpDelete] [HttpDelete]
[Route("file/{id}")] [Route("file/{id}")]
public ValueTask DeleteFile([FromRoute] string id) public async Task DeleteFile([FromRoute] string id)
{ {
return _fileStore.DeleteFile(id.FromBase58Guid()); var gid = id.FromBase58Guid();
await _fileStore.DeleteFile(gid);
await _fileInfo.Delete(gid);
} }
[HttpPost] [HttpPost]
@ -39,4 +43,4 @@ public class AdminController : Controller
var result = await _userStore.ListUsers(request); var result = await _userStore.ListUsers(request);
return await result.GetResults(); return await result.GetResults();
} }
} }

View File

@ -110,7 +110,7 @@ namespace VoidCat.Controllers
{ {
var gid = id.FromBase58Guid(); var gid = id.FromBase58Guid();
var file = await _fileInfo.Get(gid); var file = await _fileInfo.Get(gid);
var config = await _paywall.GetConfig(gid); var config = await _paywall.Get(gid);
var provider = await _paywallFactory.CreateProvider(config!.Service); var provider = await _paywallFactory.CreateProvider(config!.Service);
return await provider.CreateOrder(file!); return await provider.CreateOrder(file!);
@ -121,7 +121,7 @@ namespace VoidCat.Controllers
public async ValueTask<PaywallOrder?> GetOrderStatus([FromRoute] string id, [FromRoute] Guid order) public async ValueTask<PaywallOrder?> GetOrderStatus([FromRoute] string id, [FromRoute] Guid order)
{ {
var gid = id.FromBase58Guid(); var gid = id.FromBase58Guid();
var config = await _paywall.GetConfig(gid); var config = await _paywall.Get(gid);
var provider = await _paywallFactory.CreateProvider(config!.Service); var provider = await _paywallFactory.CreateProvider(config!.Service);
return await provider.GetOrderStatus(order); return await provider.GetOrderStatus(order);
@ -139,12 +139,12 @@ namespace VoidCat.Controllers
if (req.Strike != default) if (req.Strike != default)
{ {
await _paywall.SetConfig(gid, req.Strike!); await _paywall.Set(gid, req.Strike!);
return Ok(); return Ok();
} }
// if none set, set NoPaywallConfig // if none set, set NoPaywallConfig
await _paywall.SetConfig(gid, new NoPaywallConfig()); await _paywall.Set(gid, new NoPaywallConfig());
return Ok(); return Ok();
} }
} }

View File

@ -44,7 +44,7 @@ public class UserController : Controller
if (!loggedUser.Flags.HasFlag(VoidUserFlags.EmailVerified)) return Forbid(); if (!loggedUser.Flags.HasFlag(VoidUserFlags.EmailVerified)) return Forbid();
await _store.Update(user); await _store.UpdateProfile(user);
return Ok(); return Ok();
} }

View File

@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace VoidCat.Model; namespace VoidCat.Model;
@ -13,7 +14,12 @@ public class Base58GuidConverter : JsonConverter<Guid>
{ {
if (reader.TokenType == JsonToken.String && existingValue == Guid.Empty) if (reader.TokenType == JsonToken.String && existingValue == Guid.Empty)
{ {
return (reader.Value as string)?.FromBase58Guid() ?? existingValue; var str = reader.Value as string;
if ((str?.Contains('-') ?? false) && Guid.TryParse(str, out var g))
{
return g;
}
return str?.FromBase58Guid() ?? existingValue;
} }
return existingValue; return existingValue;

View File

@ -27,7 +27,7 @@ public static class Extensions
var claimSub = context?.User?.Claims?.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value; var claimSub = context?.User?.Claims?.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;
return Guid.TryParse(claimSub, out var g) ? g : null; return Guid.TryParse(claimSub, out var g) ? g : null;
} }
public static IEnumerable<string>? GetUserRoles(this HttpContext context) public static IEnumerable<string>? GetUserRoles(this HttpContext context)
{ {
return context?.User?.Claims?.Where(a => a.Type == ClaimTypes.Role) return context?.User?.Claims?.Where(a => a.Type == ClaimTypes.Role)
@ -38,7 +38,7 @@ public static class Extensions
{ {
return GetUserRoles(context)?.Contains(role) ?? false; return GetUserRoles(context)?.Contains(role) ?? false;
} }
public static Guid FromBase58Guid(this string base58) public static Guid FromBase58Guid(this string base58)
{ {
var enc = new NBitcoin.DataEncoders.Base58Encoder(); var enc = new NBitcoin.DataEncoders.Base58Encoder();
@ -140,14 +140,29 @@ public static class Extensions
public static string HashPassword(this string password) public static string HashPassword(this string password)
{ {
return password.HashPassword("pbkdf2"); return password.Hash("pbkdf2");
} }
public static string HashPassword(this string password, string algo, string? saltHex = null) public static string Hash(this string password, string algo, string? saltHex = null)
{ {
var bytes = Encoding.UTF8.GetBytes(password); var bytes = Encoding.UTF8.GetBytes(password);
return Hash(bytes, algo, saltHex);
}
public static string Hash(this byte[] bytes, string algo, string? saltHex = null)
{
switch (algo) switch (algo)
{ {
case "md5":
{
var hash = MD5.Create().ComputeHash(bytes);
return $"md5:{hash.ToHex()}";
}
case "sha1":
{
var hash = SHA1.Create().ComputeHash(bytes);
return $"sha1:{hash.ToHex()}";
}
case "sha256": case "sha256":
{ {
var hash = SHA256.Create().ComputeHash(bytes); var hash = SHA256.Create().ComputeHash(bytes);
@ -184,6 +199,6 @@ public static class Extensions
public static bool CheckPassword(this InternalVoidUser vu, string password) public static bool CheckPassword(this InternalVoidUser vu, string password)
{ {
var hashParts = vu.PasswordHash.Split(":"); var hashParts = vu.PasswordHash.Split(":");
return vu.PasswordHash == password.HashPassword(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null); return vu.PasswordHash == password.Hash(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
} }
} }

View File

@ -7,6 +7,7 @@ using StackExchange.Redis;
using VoidCat.Model; using VoidCat.Model;
using VoidCat.Services; using VoidCat.Services;
using VoidCat.Services.Abstractions; using VoidCat.Services.Abstractions;
using VoidCat.Services.Background;
using VoidCat.Services.Files; using VoidCat.Services.Files;
using VoidCat.Services.InMemory; using VoidCat.Services.InMemory;
using VoidCat.Services.Migrations; using VoidCat.Services.Migrations;
@ -95,6 +96,9 @@ services.AddTransient<IUserStore, UserStore>();
services.AddTransient<IUserManager, UserManager>(); services.AddTransient<IUserManager, UserManager>();
services.AddTransient<IEmailVerification, EmailVerification>(); services.AddTransient<IEmailVerification, EmailVerification>();
// background services
services.AddHostedService<DeleteUnverifiedAccounts>();
if (useRedis) if (useRedis)
{ {
services.AddTransient<ICache, RedisCache>(); services.AddTransient<ICache, RedisCache>();

View File

@ -4,9 +4,10 @@ public interface ICache
{ {
ValueTask<T?> Get<T>(string key); ValueTask<T?> Get<T>(string key);
ValueTask Set<T>(string key, T value, TimeSpan? expire = null); ValueTask Set<T>(string key, T value, TimeSpan? expire = null);
ValueTask Delete(string key);
ValueTask<string[]> GetList(string key); ValueTask<string[]> GetList(string key);
ValueTask AddToList(string key, string value); ValueTask AddToList(string key, string value);
ValueTask RemoveFromList(string key, string value);
ValueTask Delete(string key);
} }

View File

@ -5,4 +5,5 @@ namespace VoidCat.Services.Abstractions;
public interface IFileInfoManager public interface IFileInfoManager
{ {
ValueTask<PublicVoidFile?> Get(Guid id); ValueTask<PublicVoidFile?> Get(Guid id);
ValueTask Delete(Guid id);
} }

View File

@ -7,6 +7,7 @@ public interface IPaywallStore
ValueTask<PaywallOrder?> GetOrder(Guid id); ValueTask<PaywallOrder?> GetOrder(Guid id);
ValueTask SaveOrder(PaywallOrder order); ValueTask SaveOrder(PaywallOrder order);
ValueTask<PaywallConfig?> GetConfig(Guid id); ValueTask<PaywallConfig?> Get(Guid id);
ValueTask SetConfig(Guid id, PaywallConfig config); ValueTask Set(Guid id, PaywallConfig config);
ValueTask Delete(Guid id);
} }

View File

@ -6,4 +6,5 @@ public interface IStatsReporter
{ {
ValueTask<Bandwidth> GetBandwidth(); ValueTask<Bandwidth> GetBandwidth();
ValueTask<Bandwidth> GetBandwidth(Guid id); ValueTask<Bandwidth> GetBandwidth(Guid id);
ValueTask Delete(Guid id);
} }

View File

@ -8,5 +8,6 @@ public interface IUserStore
ValueTask<T?> Get<T>(Guid id) where T : VoidUser; ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
ValueTask Set(InternalVoidUser user); ValueTask Set(InternalVoidUser user);
ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request); ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request);
ValueTask Update(PublicVoidUser newUser); ValueTask UpdateProfile(PublicVoidUser newUser);
ValueTask Delete(PrivateVoidUser user);
} }

View File

@ -0,0 +1,47 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Background;
public class DeleteUnverifiedAccounts : BackgroundService
{
private readonly ILogger<DeleteUnverifiedAccounts> _logger;
private readonly IUserStore _userStore;
private readonly IUserUploadsStore _userUploads;
private readonly IFileInfoManager _fileInfo;
private readonly IFileStore _fileStore;
public DeleteUnverifiedAccounts(ILogger<DeleteUnverifiedAccounts> logger, IUserStore userStore,
IUserUploadsStore uploadsStore, IFileInfoManager fileInfo, IFileStore fileStore)
{
_userStore = userStore;
_logger = logger;
_userUploads = uploadsStore;
_fileInfo = fileInfo;
_fileStore = fileStore;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var accounts = await _userStore.ListUsers(new(0, Int32.MaxValue));
await foreach (var account in accounts.Results.WithCancellation(stoppingToken))
{
if (!account.Flags.HasFlag(VoidUserFlags.EmailVerified) &&
account.Created.AddDays(7) < DateTimeOffset.UtcNow)
{
_logger.LogInformation("Deleting un-verified account: {Id}", account.Id.ToBase58());
await _userStore.Delete(account);
var files = await _userUploads.ListFiles(account.Id, new(0, Int32.MinValue));
await foreach (var file in files.Results)
{
await _fileStore.DeleteFile(file.Id);
await _fileInfo.Delete(file.Id);
}
}
}
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}

View File

@ -22,7 +22,7 @@ public class FileInfoManager : IFileInfoManager
public async ValueTask<PublicVoidFile?> Get(Guid id) public async ValueTask<PublicVoidFile?> Get(Guid id)
{ {
var meta = _metadataStore.Get<VoidFileMeta>(id); var meta = _metadataStore.Get<VoidFileMeta>(id);
var paywall = _paywallStore.GetConfig(id); var paywall = _paywallStore.Get(id);
var bandwidth = _statsReporter.GetBandwidth(id); var bandwidth = _statsReporter.GetBandwidth(id);
await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask()); await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask());
@ -40,4 +40,11 @@ public class FileInfoManager : IFileInfoManager
Uploader = user?.Flags.HasFlag(VoidUserFlags.PublicProfile) == true ? user : null Uploader = user?.Flags.HasFlag(VoidUserFlags.PublicProfile) == true ? user : null
}; };
} }
public async ValueTask Delete(Guid id)
{
await _metadataStore.Delete(id);
await _paywallStore.Delete(id);
await _statsReporter.Delete(id);
}
} }

View File

@ -44,6 +44,14 @@ public class InMemoryCache : ICache
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }
public ValueTask RemoveFromList(string key, string value)
{
var list = new HashSet<string>(GetList(key).Result);
list.Remove(value);
_cache.Set(key, list.ToArray());
return ValueTask.CompletedTask;
}
public ValueTask Delete(string key) public ValueTask Delete(string key)
{ {
_cache.Remove(key); _cache.Remove(key);

View File

@ -34,6 +34,12 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter
public ValueTask<Bandwidth> GetBandwidth(Guid id) public ValueTask<Bandwidth> GetBandwidth(Guid id)
=> ValueTask.FromResult(GetBandwidthInternal(id)); => ValueTask.FromResult(GetBandwidthInternal(id));
public ValueTask Delete(Guid id)
{
_cache.Remove(EgressKey(id));
return ValueTask.CompletedTask;
}
private Bandwidth GetBandwidthInternal(Guid id) private Bandwidth GetBandwidthInternal(Guid id)
{ {
var i = _cache.Get(IngressKey(id)) as ulong?; var i = _cache.Get(IngressKey(id)) as ulong?;

View File

@ -12,7 +12,7 @@ public class PaywallStore : IPaywallStore
_cache = database; _cache = database;
} }
public async ValueTask<PaywallConfig?> GetConfig(Guid id) public async ValueTask<PaywallConfig?> Get(Guid id)
{ {
var cfg = await _cache.Get<NoPaywallConfig>(ConfigKey(id)); var cfg = await _cache.Get<NoPaywallConfig>(ConfigKey(id));
return cfg?.Service switch return cfg?.Service switch
@ -23,9 +23,14 @@ public class PaywallStore : IPaywallStore
}; };
} }
public async ValueTask SetConfig(Guid id, PaywallConfig config) public ValueTask Set(Guid id, PaywallConfig config)
{ {
await _cache.Set(ConfigKey(id), config); return _cache.Set(ConfigKey(id), config);
}
public ValueTask Delete(Guid id)
{
return _cache.Delete(ConfigKey(id));
} }
public async ValueTask<PaywallOrder?> GetOrder(Guid id) public async ValueTask<PaywallOrder?> GetOrder(Guid id)
@ -33,12 +38,12 @@ public class PaywallStore : IPaywallStore
return await _cache.Get<PaywallOrder>(OrderKey(id)); return await _cache.Get<PaywallOrder>(OrderKey(id));
} }
public async ValueTask SaveOrder(PaywallOrder order) public ValueTask SaveOrder(PaywallOrder order)
{ {
await _cache.Set(OrderKey(order.Id), order, return _cache.Set(OrderKey(order.Id), order,
order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5)); order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5));
} }
private string ConfigKey(Guid id) => $"paywall:config:{id}"; private string ConfigKey(Guid id) => $"paywall:config:{id}";
private string OrderKey(Guid id) => $"paywall:order:{id}"; private string OrderKey(Guid id) => $"paywall:order:{id}";
} }

View File

@ -35,6 +35,11 @@ public class RedisCache : ICache
await _db.SetAddAsync(key, value); await _db.SetAddAsync(key, value);
} }
public async ValueTask RemoveFromList(string key, string value)
{
await _db.SetRemoveAsync(key, value);
}
public async ValueTask Delete(string key) public async ValueTask Delete(string key)
{ {
await _db.KeyDeleteAsync(key); await _db.KeyDeleteAsync(key);

View File

@ -33,6 +33,12 @@ public class RedisStatsController : IStatsReporter, IStatsCollector
return new((ulong)ingress.Result, (ulong)egress.Result); return new((ulong)ingress.Result, (ulong)egress.Result);
} }
public async ValueTask Delete(Guid id)
{
await _redis.KeyDeleteAsync(formatEgressKey(id));
await _redis.KeyDeleteAsync(formatIngressKey(id));
}
public async ValueTask TrackIngress(Guid id, ulong amount) public async ValueTask TrackIngress(Guid id, ulong amount)
{ {
await Task.WhenAll( await Task.WhenAll(

View File

@ -6,11 +6,13 @@ namespace VoidCat.Services.Users;
public class UserStore : IUserStore public class UserStore : IUserStore
{ {
private const string UserList = "users"; private const string UserList = "users";
private readonly ILogger<UserStore> _logger;
private readonly ICache _cache; private readonly ICache _cache;
public UserStore(ICache cache) public UserStore(ICache cache, ILogger<UserStore> logger)
{ {
_cache = cache; _cache = cache;
_logger = logger;
} }
public async ValueTask<Guid?> LookupUser(string email) public async ValueTask<Guid?> LookupUser(string email)
@ -20,7 +22,16 @@ public class UserStore : IUserStore
public async ValueTask<T?> Get<T>(Guid id) where T : VoidUser public async ValueTask<T?> Get<T>(Guid id) where T : VoidUser
{ {
return await _cache.Get<T>(MapKey(id)); try
{
return await _cache.Get<T>(MapKey(id));
}
catch (FormatException)
{
_logger.LogWarning("Corrupt user data at: {Key}", MapKey(id));
}
return default;
} }
public async ValueTask Set(InternalVoidUser user) public async ValueTask Set(InternalVoidUser user)
@ -32,7 +43,9 @@ public class UserStore : IUserStore
public async ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request) public async ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request)
{ {
var users = (await _cache.GetList(UserList))?.Select(Guid.Parse); var users = (await _cache.GetList(UserList))
.Select<string, Guid?>(a => Guid.TryParse(a, out var g) ? g : null)
.Where(a => a.HasValue).Select(a => a.Value);
users = (request.SortBy, request.SortOrder) switch users = (request.SortBy, request.SortOrder) switch
{ {
(PagedSortBy.Id, PageSortOrder.Asc) => users?.OrderBy(a => a), (PagedSortBy.Id, PageSortOrder.Asc) => users?.OrderBy(a => a),
@ -60,8 +73,8 @@ public class UserStore : IUserStore
Results = EnumerateUsers(users?.Skip(request.PageSize * request.Page).Take(request.PageSize)) Results = EnumerateUsers(users?.Skip(request.PageSize * request.Page).Take(request.PageSize))
}; };
} }
public async ValueTask Update(PublicVoidUser newUser) public async ValueTask UpdateProfile(PublicVoidUser newUser)
{ {
var oldUser = await Get<InternalVoidUser>(newUser.Id); var oldUser = await Get<InternalVoidUser>(newUser.Id);
if (oldUser == null) return; if (oldUser == null) return;
@ -73,7 +86,14 @@ public class UserStore : IUserStore
await Set(oldUser); await Set(oldUser);
} }
public async ValueTask Delete(PrivateVoidUser user)
{
await _cache.Delete(MapKey(user.Id));
await _cache.RemoveFromList(UserList, user.Id.ToString());
await _cache.Delete(MapKey(user.Email));
}
private static string MapKey(Guid id) => $"user:{id}"; private static string MapKey(Guid id) => $"user:{id}";
private static string MapKey(string email) => $"user:email:{email}"; private static string MapKey(string email) => $"user:email:{email.Hash("md5")}";
} }

View File

@ -31,12 +31,10 @@ export function Profile() {
async function loadProfile() { async function loadProfile() {
let p = await Api.getUser(params.id); let p = await Api.getUser(params.id);
if (p.ok) { if (p.ok && p.status === 200) {
if (p.status === 200) { setProfile(await p.json());
setProfile(await p.json()); } else {
} else { setNoProfile(true);
setNoProfile(true);
}
} }
} }
@ -95,8 +93,8 @@ export function Profile() {
} }
async function saveUser(e) { async function saveUser(e) {
if(!btnDisable(e.target)) return; if (!btnDisable(e.target)) return;
let r = await Api.updateUser({ let r = await Api.updateUser({
id: profile.id, id: profile.id,
avatar: profile.avatar, avatar: profile.avatar,
@ -112,8 +110,8 @@ export function Profile() {
} }
async function submitCode(e) { async function submitCode(e) {
if(!btnDisable(e.target)) return; if (!btnDisable(e.target)) return;
let r = await Api.submitVerifyCode(profile.id, emailCode); let r = await Api.submitVerifyCode(profile.id, emailCode);
if (r.ok) { if (r.ok) {
await loadProfile(); await loadProfile();