mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-03-19 21:41:43 +01:00
Delete unverified accounts
Misc cleanup
This commit is contained in:
parent
99207c4514
commit
990d636fba
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<PaywallOrder?> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace VoidCat.Model;
|
||||
@ -13,7 +14,12 @@ public class Base58GuidConverter : JsonConverter<Guid>
|
||||
{
|
||||
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;
|
||||
|
@ -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<string>? 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);
|
||||
}
|
||||
}
|
@ -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<IUserStore, UserStore>();
|
||||
services.AddTransient<IUserManager, UserManager>();
|
||||
services.AddTransient<IEmailVerification, EmailVerification>();
|
||||
|
||||
// background services
|
||||
services.AddHostedService<DeleteUnverifiedAccounts>();
|
||||
|
||||
if (useRedis)
|
||||
{
|
||||
services.AddTransient<ICache, RedisCache>();
|
||||
|
@ -4,9 +4,10 @@ public interface ICache
|
||||
{
|
||||
ValueTask<T?> Get<T>(string key);
|
||||
ValueTask Set<T>(string key, T value, TimeSpan? expire = null);
|
||||
ValueTask Delete(string key);
|
||||
|
||||
ValueTask<string[]> GetList(string key);
|
||||
ValueTask AddToList(string key, string value);
|
||||
ValueTask RemoveFromList(string key, string value);
|
||||
|
||||
ValueTask Delete(string key);
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ namespace VoidCat.Services.Abstractions;
|
||||
public interface IFileInfoManager
|
||||
{
|
||||
ValueTask<PublicVoidFile?> Get(Guid id);
|
||||
ValueTask Delete(Guid id);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ public interface IPaywallStore
|
||||
ValueTask<PaywallOrder?> GetOrder(Guid id);
|
||||
ValueTask SaveOrder(PaywallOrder order);
|
||||
|
||||
ValueTask<PaywallConfig?> GetConfig(Guid id);
|
||||
ValueTask SetConfig(Guid id, PaywallConfig config);
|
||||
ValueTask<PaywallConfig?> Get(Guid id);
|
||||
ValueTask Set(Guid id, PaywallConfig config);
|
||||
ValueTask Delete(Guid id);
|
||||
}
|
@ -6,4 +6,5 @@ public interface IStatsReporter
|
||||
{
|
||||
ValueTask<Bandwidth> GetBandwidth();
|
||||
ValueTask<Bandwidth> GetBandwidth(Guid id);
|
||||
ValueTask Delete(Guid id);
|
||||
}
|
@ -8,5 +8,6 @@ public interface IUserStore
|
||||
ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
|
||||
ValueTask Set(InternalVoidUser user);
|
||||
ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request);
|
||||
ValueTask Update(PublicVoidUser newUser);
|
||||
ValueTask UpdateProfile(PublicVoidUser newUser);
|
||||
ValueTask Delete(PrivateVoidUser user);
|
||||
}
|
47
VoidCat/Services/Background/DeleteUnverifiedAccounts.cs
Normal file
47
VoidCat/Services/Background/DeleteUnverifiedAccounts.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ public class FileInfoManager : IFileInfoManager
|
||||
public async ValueTask<PublicVoidFile?> Get(Guid id)
|
||||
{
|
||||
var meta = _metadataStore.Get<VoidFileMeta>(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);
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,14 @@ public class InMemoryCache : ICache
|
||||
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)
|
||||
{
|
||||
_cache.Remove(key);
|
||||
|
@ -34,6 +34,12 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter
|
||||
public ValueTask<Bandwidth> 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?;
|
||||
|
@ -12,7 +12,7 @@ public class PaywallStore : IPaywallStore
|
||||
_cache = database;
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||
public async ValueTask<PaywallConfig?> Get(Guid id)
|
||||
{
|
||||
var cfg = await _cache.Get<NoPaywallConfig>(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<PaywallOrder?> GetOrder(Guid id)
|
||||
@ -33,12 +38,12 @@ public class PaywallStore : IPaywallStore
|
||||
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));
|
||||
}
|
||||
|
||||
private string ConfigKey(Guid id) => $"paywall:config:{id}";
|
||||
private string OrderKey(Guid id) => $"paywall:order:{id}";
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -6,11 +6,13 @@ namespace VoidCat.Services.Users;
|
||||
public class UserStore : IUserStore
|
||||
{
|
||||
private const string UserList = "users";
|
||||
private readonly ILogger<UserStore> _logger;
|
||||
private readonly ICache _cache;
|
||||
|
||||
public UserStore(ICache cache)
|
||||
public UserStore(ICache cache, ILogger<UserStore> logger)
|
||||
{
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
@ -32,7 +43,9 @@ public class UserStore : IUserStore
|
||||
|
||||
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
|
||||
{
|
||||
(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<InternalVoidUser>(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")}";
|
||||
}
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user