From 990d636fba790d9ea381cbf10b73f02388a124b5 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 4 Mar 2022 20:05:01 +0000 Subject: [PATCH] Delete unverified accounts Misc cleanup --- VoidCat/Controllers/Admin/AdminController.cs | 12 +++-- VoidCat/Controllers/UploadController.cs | 8 ++-- VoidCat/Controllers/UserController.cs | 2 +- VoidCat/Model/Base58GuidConverter.cs | 8 +++- VoidCat/Model/Extensions.cs | 25 ++++++++-- VoidCat/Program.cs | 4 ++ VoidCat/Services/Abstractions/ICache.cs | 3 +- .../Services/Abstractions/IFileInfoManager.cs | 1 + .../Services/Abstractions/IPaywallStore.cs | 5 +- .../Services/Abstractions/IStatsReporter.cs | 1 + VoidCat/Services/Abstractions/IUserStore.cs | 3 +- .../Background/DeleteUnverifiedAccounts.cs | 47 +++++++++++++++++++ VoidCat/Services/Files/FileInfoManager.cs | 9 +++- VoidCat/Services/InMemory/InMemoryCache.cs | 8 ++++ .../InMemory/InMemoryStatsController.cs | 6 +++ VoidCat/Services/Paywall/PaywallStore.cs | 17 ++++--- VoidCat/Services/Redis/RedisCache.cs | 5 ++ .../Services/Redis/RedisStatsController.cs | 6 +++ VoidCat/Services/Users/UserStore.cs | 36 ++++++++++---- VoidCat/spa/src/Profile.js | 18 ++++--- 20 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 VoidCat/Services/Background/DeleteUnverifiedAccounts.cs diff --git a/VoidCat/Controllers/Admin/AdminController.cs b/VoidCat/Controllers/Admin/AdminController.cs index 45258d7..81298c3 100644 --- a/VoidCat/Controllers/Admin/AdminController.cs +++ b/VoidCat/Controllers/Admin/AdminController.cs @@ -10,12 +10,14 @@ namespace VoidCat.Controllers.Admin; public class AdminController : Controller { private readonly IFileStore _fileStore; + private readonly IFileInfoManager _fileInfo; private readonly IUserStore _userStore; - public AdminController(IFileStore fileStore, IUserStore userStore) + public AdminController(IFileStore fileStore, IUserStore userStore, IFileInfoManager fileInfo) { _fileStore = fileStore; _userStore = userStore; + _fileInfo = fileInfo; } [HttpPost] @@ -27,9 +29,11 @@ public class AdminController : Controller [HttpDelete] [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] @@ -39,4 +43,4 @@ public class AdminController : Controller var result = await _userStore.ListUsers(request); return await result.GetResults(); } -} +} \ No newline at end of file diff --git a/VoidCat/Controllers/UploadController.cs b/VoidCat/Controllers/UploadController.cs index 31d2c4b..de0c698 100644 --- a/VoidCat/Controllers/UploadController.cs +++ b/VoidCat/Controllers/UploadController.cs @@ -110,7 +110,7 @@ namespace VoidCat.Controllers { var gid = id.FromBase58Guid(); 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); return await provider.CreateOrder(file!); @@ -121,7 +121,7 @@ namespace VoidCat.Controllers public async ValueTask GetOrderStatus([FromRoute] string id, [FromRoute] Guid order) { var gid = id.FromBase58Guid(); - var config = await _paywall.GetConfig(gid); + var config = await _paywall.Get(gid); var provider = await _paywallFactory.CreateProvider(config!.Service); return await provider.GetOrderStatus(order); @@ -139,12 +139,12 @@ namespace VoidCat.Controllers if (req.Strike != default) { - await _paywall.SetConfig(gid, req.Strike!); + await _paywall.Set(gid, req.Strike!); return Ok(); } // if none set, set NoPaywallConfig - await _paywall.SetConfig(gid, new NoPaywallConfig()); + await _paywall.Set(gid, new NoPaywallConfig()); return Ok(); } } diff --git a/VoidCat/Controllers/UserController.cs b/VoidCat/Controllers/UserController.cs index b0d52e1..1153e61 100644 --- a/VoidCat/Controllers/UserController.cs +++ b/VoidCat/Controllers/UserController.cs @@ -44,7 +44,7 @@ public class UserController : Controller if (!loggedUser.Flags.HasFlag(VoidUserFlags.EmailVerified)) return Forbid(); - await _store.Update(user); + await _store.UpdateProfile(user); return Ok(); } diff --git a/VoidCat/Model/Base58GuidConverter.cs b/VoidCat/Model/Base58GuidConverter.cs index ea5510f..481bb91 100644 --- a/VoidCat/Model/Base58GuidConverter.cs +++ b/VoidCat/Model/Base58GuidConverter.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using Newtonsoft.Json; namespace VoidCat.Model; @@ -13,7 +14,12 @@ public class Base58GuidConverter : JsonConverter { 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; diff --git a/VoidCat/Model/Extensions.cs b/VoidCat/Model/Extensions.cs index 61812a6..8e2406f 100644 --- a/VoidCat/Model/Extensions.cs +++ b/VoidCat/Model/Extensions.cs @@ -27,7 +27,7 @@ public static class Extensions var claimSub = context?.User?.Claims?.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value; return Guid.TryParse(claimSub, out var g) ? g : null; } - + public static IEnumerable? GetUserRoles(this HttpContext context) { return context?.User?.Claims?.Where(a => a.Type == ClaimTypes.Role) @@ -38,7 +38,7 @@ public static class Extensions { return GetUserRoles(context)?.Contains(role) ?? false; } - + public static Guid FromBase58Guid(this string base58) { var enc = new NBitcoin.DataEncoders.Base58Encoder(); @@ -140,14 +140,29 @@ public static class Extensions 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); + return Hash(bytes, algo, saltHex); + } + + public static string Hash(this byte[] bytes, string algo, string? saltHex = null) + { 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": { var hash = SHA256.Create().ComputeHash(bytes); @@ -184,6 +199,6 @@ public static class Extensions public static bool CheckPassword(this InternalVoidUser vu, string password) { 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); } } \ No newline at end of file diff --git a/VoidCat/Program.cs b/VoidCat/Program.cs index 1b2b0e7..d8217a6 100644 --- a/VoidCat/Program.cs +++ b/VoidCat/Program.cs @@ -7,6 +7,7 @@ using StackExchange.Redis; using VoidCat.Model; using VoidCat.Services; using VoidCat.Services.Abstractions; +using VoidCat.Services.Background; using VoidCat.Services.Files; using VoidCat.Services.InMemory; using VoidCat.Services.Migrations; @@ -95,6 +96,9 @@ services.AddTransient(); services.AddTransient(); services.AddTransient(); +// background services +services.AddHostedService(); + if (useRedis) { services.AddTransient(); diff --git a/VoidCat/Services/Abstractions/ICache.cs b/VoidCat/Services/Abstractions/ICache.cs index dff01f4..3ebbd40 100644 --- a/VoidCat/Services/Abstractions/ICache.cs +++ b/VoidCat/Services/Abstractions/ICache.cs @@ -4,9 +4,10 @@ public interface ICache { ValueTask Get(string key); ValueTask Set(string key, T value, TimeSpan? expire = null); + ValueTask Delete(string key); ValueTask GetList(string key); ValueTask AddToList(string key, string value); + ValueTask RemoveFromList(string key, string value); - ValueTask Delete(string key); } diff --git a/VoidCat/Services/Abstractions/IFileInfoManager.cs b/VoidCat/Services/Abstractions/IFileInfoManager.cs index 8138e75..868aa49 100644 --- a/VoidCat/Services/Abstractions/IFileInfoManager.cs +++ b/VoidCat/Services/Abstractions/IFileInfoManager.cs @@ -5,4 +5,5 @@ namespace VoidCat.Services.Abstractions; public interface IFileInfoManager { ValueTask Get(Guid id); + ValueTask Delete(Guid id); } diff --git a/VoidCat/Services/Abstractions/IPaywallStore.cs b/VoidCat/Services/Abstractions/IPaywallStore.cs index ed9eddc..d7bc1ac 100644 --- a/VoidCat/Services/Abstractions/IPaywallStore.cs +++ b/VoidCat/Services/Abstractions/IPaywallStore.cs @@ -7,6 +7,7 @@ public interface IPaywallStore ValueTask GetOrder(Guid id); ValueTask SaveOrder(PaywallOrder order); - ValueTask GetConfig(Guid id); - ValueTask SetConfig(Guid id, PaywallConfig config); + ValueTask Get(Guid id); + ValueTask Set(Guid id, PaywallConfig config); + ValueTask Delete(Guid id); } \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IStatsReporter.cs b/VoidCat/Services/Abstractions/IStatsReporter.cs index 470f8c7..e2612aa 100644 --- a/VoidCat/Services/Abstractions/IStatsReporter.cs +++ b/VoidCat/Services/Abstractions/IStatsReporter.cs @@ -6,4 +6,5 @@ public interface IStatsReporter { ValueTask GetBandwidth(); ValueTask GetBandwidth(Guid id); + ValueTask Delete(Guid id); } \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IUserStore.cs b/VoidCat/Services/Abstractions/IUserStore.cs index 8c9dd28..1b8f18c 100644 --- a/VoidCat/Services/Abstractions/IUserStore.cs +++ b/VoidCat/Services/Abstractions/IUserStore.cs @@ -8,5 +8,6 @@ public interface IUserStore ValueTask Get(Guid id) where T : VoidUser; ValueTask Set(InternalVoidUser user); ValueTask> ListUsers(PagedRequest request); - ValueTask Update(PublicVoidUser newUser); + ValueTask UpdateProfile(PublicVoidUser newUser); + ValueTask Delete(PrivateVoidUser user); } \ No newline at end of file diff --git a/VoidCat/Services/Background/DeleteUnverifiedAccounts.cs b/VoidCat/Services/Background/DeleteUnverifiedAccounts.cs new file mode 100644 index 0000000..4f6876c --- /dev/null +++ b/VoidCat/Services/Background/DeleteUnverifiedAccounts.cs @@ -0,0 +1,47 @@ +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.Background; + +public class DeleteUnverifiedAccounts : BackgroundService +{ + private readonly ILogger _logger; + private readonly IUserStore _userStore; + private readonly IUserUploadsStore _userUploads; + private readonly IFileInfoManager _fileInfo; + private readonly IFileStore _fileStore; + + public DeleteUnverifiedAccounts(ILogger 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); + } +} \ No newline at end of file diff --git a/VoidCat/Services/Files/FileInfoManager.cs b/VoidCat/Services/Files/FileInfoManager.cs index b9b63cc..50411b7 100644 --- a/VoidCat/Services/Files/FileInfoManager.cs +++ b/VoidCat/Services/Files/FileInfoManager.cs @@ -22,7 +22,7 @@ public class FileInfoManager : IFileInfoManager public async ValueTask Get(Guid id) { var meta = _metadataStore.Get(id); - var paywall = _paywallStore.GetConfig(id); + var paywall = _paywallStore.Get(id); var bandwidth = _statsReporter.GetBandwidth(id); 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 }; } + + public async ValueTask Delete(Guid id) + { + await _metadataStore.Delete(id); + await _paywallStore.Delete(id); + await _statsReporter.Delete(id); + } } diff --git a/VoidCat/Services/InMemory/InMemoryCache.cs b/VoidCat/Services/InMemory/InMemoryCache.cs index 527e7f5..6871f28 100644 --- a/VoidCat/Services/InMemory/InMemoryCache.cs +++ b/VoidCat/Services/InMemory/InMemoryCache.cs @@ -44,6 +44,14 @@ public class InMemoryCache : ICache return ValueTask.CompletedTask; } + public ValueTask RemoveFromList(string key, string value) + { + var list = new HashSet(GetList(key).Result); + list.Remove(value); + _cache.Set(key, list.ToArray()); + return ValueTask.CompletedTask; + } + public ValueTask Delete(string key) { _cache.Remove(key); diff --git a/VoidCat/Services/InMemory/InMemoryStatsController.cs b/VoidCat/Services/InMemory/InMemoryStatsController.cs index 0a61b90..e99d729 100644 --- a/VoidCat/Services/InMemory/InMemoryStatsController.cs +++ b/VoidCat/Services/InMemory/InMemoryStatsController.cs @@ -34,6 +34,12 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter public ValueTask GetBandwidth(Guid id) => ValueTask.FromResult(GetBandwidthInternal(id)); + public ValueTask Delete(Guid id) + { + _cache.Remove(EgressKey(id)); + return ValueTask.CompletedTask; + } + private Bandwidth GetBandwidthInternal(Guid id) { var i = _cache.Get(IngressKey(id)) as ulong?; diff --git a/VoidCat/Services/Paywall/PaywallStore.cs b/VoidCat/Services/Paywall/PaywallStore.cs index 4f16956..2b5a958 100644 --- a/VoidCat/Services/Paywall/PaywallStore.cs +++ b/VoidCat/Services/Paywall/PaywallStore.cs @@ -12,7 +12,7 @@ public class PaywallStore : IPaywallStore _cache = database; } - public async ValueTask GetConfig(Guid id) + public async ValueTask Get(Guid id) { var cfg = await _cache.Get(ConfigKey(id)); 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 GetOrder(Guid id) @@ -33,12 +38,12 @@ public class PaywallStore : IPaywallStore return await _cache.Get(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)); } private string ConfigKey(Guid id) => $"paywall:config:{id}"; private string OrderKey(Guid id) => $"paywall:order:{id}"; -} +} \ No newline at end of file diff --git a/VoidCat/Services/Redis/RedisCache.cs b/VoidCat/Services/Redis/RedisCache.cs index f1f87f2..f476868 100644 --- a/VoidCat/Services/Redis/RedisCache.cs +++ b/VoidCat/Services/Redis/RedisCache.cs @@ -35,6 +35,11 @@ public class RedisCache : ICache await _db.SetAddAsync(key, value); } + public async ValueTask RemoveFromList(string key, string value) + { + await _db.SetRemoveAsync(key, value); + } + public async ValueTask Delete(string key) { await _db.KeyDeleteAsync(key); diff --git a/VoidCat/Services/Redis/RedisStatsController.cs b/VoidCat/Services/Redis/RedisStatsController.cs index cff830b..2a41aa8 100644 --- a/VoidCat/Services/Redis/RedisStatsController.cs +++ b/VoidCat/Services/Redis/RedisStatsController.cs @@ -33,6 +33,12 @@ public class RedisStatsController : IStatsReporter, IStatsCollector 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) { await Task.WhenAll( diff --git a/VoidCat/Services/Users/UserStore.cs b/VoidCat/Services/Users/UserStore.cs index e5f6909..a9b5b3f 100644 --- a/VoidCat/Services/Users/UserStore.cs +++ b/VoidCat/Services/Users/UserStore.cs @@ -6,11 +6,13 @@ namespace VoidCat.Services.Users; public class UserStore : IUserStore { private const string UserList = "users"; + private readonly ILogger _logger; private readonly ICache _cache; - public UserStore(ICache cache) + public UserStore(ICache cache, ILogger logger) { _cache = cache; + _logger = logger; } public async ValueTask LookupUser(string email) @@ -20,7 +22,16 @@ public class UserStore : IUserStore public async ValueTask Get(Guid id) where T : VoidUser { - return await _cache.Get(MapKey(id)); + try + { + return await _cache.Get(MapKey(id)); + } + catch (FormatException) + { + _logger.LogWarning("Corrupt user data at: {Key}", MapKey(id)); + } + + return default; } public async ValueTask Set(InternalVoidUser user) @@ -32,7 +43,9 @@ public class UserStore : IUserStore public async ValueTask> ListUsers(PagedRequest request) { - var users = (await _cache.GetList(UserList))?.Select(Guid.Parse); + var users = (await _cache.GetList(UserList)) + .Select(a => Guid.TryParse(a, out var g) ? g : null) + .Where(a => a.HasValue).Select(a => a.Value); users = (request.SortBy, request.SortOrder) switch { (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)) }; } - - public async ValueTask Update(PublicVoidUser newUser) + + public async ValueTask UpdateProfile(PublicVoidUser newUser) { var oldUser = await Get(newUser.Id); if (oldUser == null) return; @@ -73,7 +86,14 @@ public class UserStore : IUserStore 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(string email) => $"user:email:{email}"; -} + private static string MapKey(string email) => $"user:email:{email.Hash("md5")}"; +} \ No newline at end of file diff --git a/VoidCat/spa/src/Profile.js b/VoidCat/spa/src/Profile.js index 351c55e..3be47a3 100644 --- a/VoidCat/spa/src/Profile.js +++ b/VoidCat/spa/src/Profile.js @@ -31,12 +31,10 @@ export function Profile() { async function loadProfile() { let p = await Api.getUser(params.id); - if (p.ok) { - if (p.status === 200) { - setProfile(await p.json()); - } else { - setNoProfile(true); - } + if (p.ok && p.status === 200) { + setProfile(await p.json()); + } else { + setNoProfile(true); } } @@ -95,8 +93,8 @@ export function Profile() { } async function saveUser(e) { - if(!btnDisable(e.target)) return; - + if (!btnDisable(e.target)) return; + let r = await Api.updateUser({ id: profile.id, avatar: profile.avatar, @@ -112,8 +110,8 @@ export function Profile() { } async function submitCode(e) { - if(!btnDisable(e.target)) return; - + if (!btnDisable(e.target)) return; + let r = await Api.submitVerifyCode(profile.id, emailCode); if (r.ok) { await loadProfile();