mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-03-27 00:21:42 +01:00
Add user accounts base
This commit is contained in:
parent
8629954ffe
commit
3bffcdeb13
@ -1,10 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Controllers.Admin;
|
||||
|
||||
[Route("admin")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
[Authorize(Policy = Policies.RequireAdmin)]
|
||||
public class AdminController : Controller
|
||||
{
|
||||
|
||||
|
77
VoidCat/Controllers/AuthController.cs
Normal file
77
VoidCat/Controllers/AuthController.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Controllers;
|
||||
|
||||
[Route("auth")]
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly IUserManager _manager;
|
||||
private readonly VoidSettings _settings;
|
||||
|
||||
public AuthController(IUserManager userStore, VoidSettings settings)
|
||||
{
|
||||
_manager = userStore;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("login")]
|
||||
public async Task<LoginResponse> Login([FromBody] LoginRequest req)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await _manager.Login(req.Username, req.Password);
|
||||
var token = CreateToken(user);
|
||||
var tokenWriter = new JwtSecurityTokenHandler();
|
||||
return new(tokenWriter.WriteToken(token), null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(null, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("register")]
|
||||
public async Task<LoginResponse> Register([FromBody] LoginRequest req)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newUser = await _manager.Register(req.Username, req.Password);
|
||||
var token = CreateToken(newUser);
|
||||
var tokenWriter = new JwtSecurityTokenHandler();
|
||||
return new(tokenWriter.WriteToken(token), null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(null, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private JwtSecurityToken CreateToken(VoidUser user)
|
||||
{
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.JwtSettings.Key));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new Claim[]
|
||||
{
|
||||
new(ClaimTypes.Sid, user.Id.ToString()),
|
||||
new(ClaimTypes.Expiration, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
||||
new(ClaimTypes.AuthorizationDecision, string.Join(",", user.Roles))
|
||||
};
|
||||
|
||||
return new JwtSecurityToken(_settings.JwtSettings.Issuer, claims: claims, expires: DateTime.UtcNow.AddHours(6),
|
||||
signingCredentials: credentials);
|
||||
}
|
||||
|
||||
|
||||
public record LoginRequest(string Username, string Password);
|
||||
|
||||
public record LoginResponse(string? Jwt, string? Error = null);
|
||||
}
|
3
VoidCat/Model/Bandwidth.cs
Normal file
3
VoidCat/Model/Bandwidth.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public sealed record Bandwidth(ulong Ingress, ulong Egress);
|
7
VoidCat/Model/EgressRequest.cs
Normal file
7
VoidCat/Model/EgressRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public sealed record EgressRequest(Guid Id, IEnumerable<RangeRequest> Ranges)
|
||||
{
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public static class Extensions
|
||||
@ -18,6 +21,135 @@ public static class Extensions
|
||||
{
|
||||
var h = headers
|
||||
.FirstOrDefault(a => a.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
|
||||
}
|
||||
}
|
||||
|
||||
public static string ToHex(this byte[] data)
|
||||
{
|
||||
return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
|
||||
}
|
||||
|
||||
private static int HexToInt(char c)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '0':
|
||||
return 0;
|
||||
case '1':
|
||||
return 1;
|
||||
case '2':
|
||||
return 2;
|
||||
case '3':
|
||||
return 3;
|
||||
case '4':
|
||||
return 4;
|
||||
case '5':
|
||||
return 5;
|
||||
case '6':
|
||||
return 6;
|
||||
case '7':
|
||||
return 7;
|
||||
case '8':
|
||||
return 8;
|
||||
case '9':
|
||||
return 9;
|
||||
case 'a':
|
||||
case 'A':
|
||||
return 10;
|
||||
case 'b':
|
||||
case 'B':
|
||||
return 11;
|
||||
case 'c':
|
||||
case 'C':
|
||||
return 12;
|
||||
case 'd':
|
||||
case 'D':
|
||||
return 13;
|
||||
case 'e':
|
||||
case 'E':
|
||||
return 14;
|
||||
case 'f':
|
||||
case 'F':
|
||||
return 15;
|
||||
default:
|
||||
throw new FormatException("Unrecognized hex char " + c);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly byte[,] ByteLookup = new byte[,]
|
||||
{
|
||||
// low nibble
|
||||
{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
|
||||
},
|
||||
// high nibble
|
||||
{
|
||||
0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
|
||||
0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0
|
||||
}
|
||||
};
|
||||
|
||||
private static byte[] FromHex(this string input)
|
||||
{
|
||||
var result = new byte[(input.Length + 1) >> 1];
|
||||
var lastCell = result.Length - 1;
|
||||
var lastChar = input.Length - 1;
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
result[lastCell - (i >> 1)] |= ByteLookup[i & 1, HexToInt(input[lastChar - i])];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string HashPassword(this string password)
|
||||
{
|
||||
return password.HashPassword("pbkdf2");
|
||||
}
|
||||
|
||||
public static string HashPassword(this string password, string algo, string? saltHex = null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(password);
|
||||
switch (algo)
|
||||
{
|
||||
case "sha256":
|
||||
{
|
||||
var hash = SHA256.Create().ComputeHash(bytes);
|
||||
return $"sha256:${hash.ToHex()}";
|
||||
}
|
||||
case "sha512":
|
||||
{
|
||||
var hash = SHA512.Create().ComputeHash(bytes);
|
||||
return $"sha512:${hash.ToHex()}";
|
||||
}
|
||||
case "pbkdf2":
|
||||
{
|
||||
const int saltSize = 32;
|
||||
const int iterations = 310_000;
|
||||
|
||||
var salt = new byte[saltSize];
|
||||
if (saltHex == default)
|
||||
{
|
||||
RandomNumberGenerator.Fill(salt);
|
||||
}
|
||||
else
|
||||
{
|
||||
salt = saltHex.FromHex();
|
||||
}
|
||||
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(bytes, salt, iterations);
|
||||
return $"pbkdf2:{salt.ToHex()}:${pbkdf2.GetBytes(salt.Length).ToHex()}";
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unknown algo", nameof(algo));
|
||||
}
|
||||
|
||||
public static bool CheckPassword(this VoidUser vu, string password)
|
||||
{
|
||||
var hashParts = vu.PasswordHash.Split(":");
|
||||
return vu.PasswordHash == password.HashPassword(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
||||
}
|
||||
}
|
||||
|
9
VoidCat/Model/IngressPayload.cs
Normal file
9
VoidCat/Model/IngressPayload.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public sealed record IngressPayload(Stream InStream, VoidFileMeta Meta, string Hash)
|
||||
{
|
||||
public Guid? Id { get; init; }
|
||||
public Guid? EditSecret { get; init; }
|
||||
|
||||
public bool IsAppend => Id.HasValue && EditSecret.HasValue;
|
||||
}
|
12
VoidCat/Model/Roles.cs
Normal file
12
VoidCat/Model/Roles.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public static class Roles
|
||||
{
|
||||
public const string User = "User";
|
||||
public const string Admin = "Admin";
|
||||
}
|
||||
|
||||
public static class Policies
|
||||
{
|
||||
public const string RequireAdmin = "RequireAdmin";
|
||||
}
|
6
VoidCat/Model/VoidUser.cs
Normal file
6
VoidCat/Model/VoidUser.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public sealed record VoidUser(Guid Id, string Email, string PasswordHash)
|
||||
{
|
||||
public IEnumerable<string> Roles { get; init; } = Enumerable.Empty<string>();
|
||||
}
|
@ -11,6 +11,8 @@ using VoidCat.Services.InMemory;
|
||||
using VoidCat.Services.Migrations;
|
||||
using VoidCat.Services.Paywall;
|
||||
using VoidCat.Services.Redis;
|
||||
using VoidCat.Services.Stats;
|
||||
using VoidCat.Services.Users;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var services = builder.Services;
|
||||
@ -38,6 +40,7 @@ services.AddControllers().AddNewtonsoftJson((opt) =>
|
||||
opt.SerializerSettings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
|
||||
opt.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
|
||||
});
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
@ -52,6 +55,14 @@ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization((opt) =>
|
||||
{
|
||||
opt.AddPolicy(Policies.RequireAdmin, (auth) =>
|
||||
{
|
||||
auth.RequireRole(Roles.Admin);
|
||||
});
|
||||
});
|
||||
|
||||
// void.cat services
|
||||
//
|
||||
services.AddVoidMigrations();
|
||||
@ -67,20 +78,24 @@ services.AddTransient<IStatsCollector, PrometheusStatsCollector>();
|
||||
// paywall
|
||||
services.AddVoidPaywall();
|
||||
|
||||
// users
|
||||
services.AddTransient<IUserStore, UserStore>();
|
||||
services.AddTransient<IUserManager, UserManager>();
|
||||
|
||||
if (useRedis)
|
||||
{
|
||||
services.AddTransient<ICache, RedisCache>();
|
||||
services.AddTransient<RedisStatsController>();
|
||||
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddTransient<IPaywallStore, RedisPaywallStore>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddMemoryCache();
|
||||
services.AddTransient<ICache, InMemoryCache>();
|
||||
services.AddTransient<InMemoryStatsController>();
|
||||
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddTransient<IPaywallStore, InMemoryPaywallStore>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
@ -103,4 +118,4 @@ app.UseEndpoints(ep =>
|
||||
ep.MapFallbackToFile("index.html");
|
||||
});
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
|
@ -0,0 +1,5 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IAggregateStatsCollector : IStatsCollector
|
||||
{
|
||||
}
|
10
VoidCat/Services/Abstractions/ICache.cs
Normal file
10
VoidCat/Services/Abstractions/ICache.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface ICache
|
||||
{
|
||||
ValueTask<T?> Get<T>(string key);
|
||||
ValueTask Set<T>(string key, T value, TimeSpan? expire = null);
|
||||
|
||||
ValueTask<string[]> GetList(string key);
|
||||
ValueTask AddToList(string key, string value);
|
||||
}
|
@ -11,34 +11,4 @@ public interface IFileStore
|
||||
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
|
||||
|
||||
IAsyncEnumerable<PublicVoidFile> ListFiles();
|
||||
}
|
||||
|
||||
public sealed record IngressPayload(Stream InStream, VoidFileMeta Meta, string Hash)
|
||||
{
|
||||
public Guid? Id { get; init; }
|
||||
public Guid? EditSecret { get; init; }
|
||||
|
||||
public bool IsAppend => Id.HasValue && EditSecret.HasValue;
|
||||
}
|
||||
|
||||
public sealed record EgressRequest(Guid Id, IEnumerable<RangeRequest> Ranges)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed record RangeRequest(long? TotalSize, long? Start, long? End)
|
||||
{
|
||||
private const long DefaultBufferSize = 1024L * 512L;
|
||||
|
||||
public long? Size
|
||||
=> Start.HasValue ? (End ?? Math.Min(TotalSize!.Value, Start.Value + DefaultBufferSize)) - Start.Value : End;
|
||||
|
||||
public bool IsForFullFile
|
||||
=> Start is 0 && !End.HasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Return Content-Range header content for this range
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string ToContentRange()
|
||||
=> $"bytes {Start}-{End ?? (Start + Size - 1L)}/{TotalSize?.ToString() ?? "*"}";
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Paywall;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
@ -6,20 +5,4 @@ namespace VoidCat.Services.Abstractions;
|
||||
public interface IPaywallFactory
|
||||
{
|
||||
ValueTask<IPaywallProvider> CreateProvider(PaywallServices svc);
|
||||
}
|
||||
|
||||
public interface IPaywallProvider
|
||||
{
|
||||
ValueTask<PaywallOrder?> CreateOrder(PublicVoidFile file);
|
||||
|
||||
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
|
||||
}
|
||||
|
||||
public interface IPaywallStore
|
||||
{
|
||||
ValueTask<PaywallOrder?> GetOrder(Guid id);
|
||||
ValueTask SaveOrder(PaywallOrder order);
|
||||
|
||||
ValueTask<PaywallConfig?> GetConfig(Guid id);
|
||||
ValueTask SetConfig(Guid id, PaywallConfig config);
|
||||
}
|
||||
}
|
11
VoidCat/Services/Abstractions/IPaywallProvider.cs
Normal file
11
VoidCat/Services/Abstractions/IPaywallProvider.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Paywall;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IPaywallProvider
|
||||
{
|
||||
ValueTask<PaywallOrder?> CreateOrder(PublicVoidFile file);
|
||||
|
||||
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
|
||||
}
|
12
VoidCat/Services/Abstractions/IPaywallStore.cs
Normal file
12
VoidCat/Services/Abstractions/IPaywallStore.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using VoidCat.Model.Paywall;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IPaywallStore
|
||||
{
|
||||
ValueTask<PaywallOrder?> GetOrder(Guid id);
|
||||
ValueTask SaveOrder(PaywallOrder order);
|
||||
|
||||
ValueTask<PaywallConfig?> GetConfig(Guid id);
|
||||
ValueTask SetConfig(Guid id, PaywallConfig config);
|
||||
}
|
@ -1,19 +1,7 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IAggregateStatsCollector : IStatsCollector
|
||||
{
|
||||
}
|
||||
|
||||
public interface IStatsCollector
|
||||
{
|
||||
ValueTask TrackIngress(Guid id, ulong amount);
|
||||
ValueTask TrackEgress(Guid id, ulong amount);
|
||||
}
|
||||
|
||||
public interface IStatsReporter
|
||||
{
|
||||
ValueTask<Bandwidth> GetBandwidth();
|
||||
ValueTask<Bandwidth> GetBandwidth(Guid id);
|
||||
}
|
||||
|
||||
public sealed record Bandwidth(ulong Ingress, ulong Egress);
|
||||
}
|
9
VoidCat/Services/Abstractions/IStatsReporter.cs
Normal file
9
VoidCat/Services/Abstractions/IStatsReporter.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IStatsReporter
|
||||
{
|
||||
ValueTask<Bandwidth> GetBandwidth();
|
||||
ValueTask<Bandwidth> GetBandwidth(Guid id);
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IUserManager
|
||||
{
|
||||
ValueTask<VoidUser> Get(string email, string password);
|
||||
ValueTask<VoidUser> Get(Guid id);
|
||||
ValueTask Set(VoidUser user);
|
||||
ValueTask<VoidUser> Login(string username, string password);
|
||||
ValueTask<VoidUser> Register(string username, string password);
|
||||
}
|
||||
|
||||
public sealed record VoidUser(Guid Id, string Email, string PasswordHash);
|
11
VoidCat/Services/Abstractions/IUserStore.cs
Normal file
11
VoidCat/Services/Abstractions/IUserStore.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IUserStore
|
||||
{
|
||||
ValueTask<Guid?> LookupUser(string email);
|
||||
ValueTask<VoidUser?> Get(Guid id);
|
||||
ValueTask Set(VoidUser user);
|
||||
IAsyncEnumerable<VoidUser> ListUsers(CancellationToken cts);
|
||||
}
|
19
VoidCat/Services/Abstractions/RangeRequest.cs
Normal file
19
VoidCat/Services/Abstractions/RangeRequest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public sealed record RangeRequest(long? TotalSize, long? Start, long? End)
|
||||
{
|
||||
private const long DefaultBufferSize = 1024L * 512L;
|
||||
|
||||
public long? Size
|
||||
=> Start.HasValue ? (End ?? Math.Min(TotalSize!.Value, Start.Value + DefaultBufferSize)) - Start.Value : End;
|
||||
|
||||
public bool IsForFullFile
|
||||
=> Start is 0 && !End.HasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Return Content-Range header content for this range
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string ToContentRange()
|
||||
=> $"bytes {Start}-{End ?? (Start + Size - 1L)}/{TotalSize?.ToString() ?? "*"}";
|
||||
}
|
46
VoidCat/Services/InMemory/InMemoryCache.cs
Normal file
46
VoidCat/Services/InMemory/InMemoryCache.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.InMemory;
|
||||
|
||||
public class InMemoryCache : ICache
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public InMemoryCache(IMemoryCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public ValueTask<T?> Get<T>(string key)
|
||||
{
|
||||
return ValueTask.FromResult(_cache.Get<T?>(key));
|
||||
}
|
||||
|
||||
public ValueTask Set<T>(string key, T value, TimeSpan? expire = null)
|
||||
{
|
||||
if (expire.HasValue)
|
||||
{
|
||||
_cache.Set(key, value, expire.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_cache.Set(key, value);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<string[]> GetList(string key)
|
||||
{
|
||||
return ValueTask.FromResult(_cache.Get<string[]>(key));
|
||||
}
|
||||
|
||||
public ValueTask AddToList(string key, string value)
|
||||
{
|
||||
var list = new HashSet<string>(GetList(key).Result);
|
||||
list.Add(value);
|
||||
_cache.Set(key, list.ToArray());
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.InMemory;
|
||||
|
||||
public class InMemoryPaywallStore : IPaywallStore
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public InMemoryPaywallStore(IMemoryCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||
{
|
||||
return ValueTask.FromResult(_cache.Get(id) as PaywallConfig);
|
||||
}
|
||||
|
||||
public ValueTask SetConfig(Guid id, PaywallConfig config)
|
||||
{
|
||||
_cache.Set(id, config);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||
{
|
||||
return ValueTask.FromResult(_cache.Get(id) as PaywallOrder);
|
||||
}
|
||||
|
||||
public ValueTask SaveOrder(PaywallOrder order)
|
||||
{
|
||||
_cache.Set(order.Id, order,
|
||||
order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5));
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.InMemory;
|
||||
|
@ -27,6 +27,7 @@ public static class Paywall
|
||||
public static IServiceCollection AddVoidPaywall(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IPaywallFactory, PaywallFactory>();
|
||||
services.AddTransient<IPaywallStore, PaywallStore>();
|
||||
|
||||
// strike
|
||||
services.AddTransient<StrikeApi>();
|
||||
|
44
VoidCat/Services/Paywall/PaywallStore.cs
Normal file
44
VoidCat/Services/Paywall/PaywallStore.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Paywall;
|
||||
|
||||
public class PaywallStore : IPaywallStore
|
||||
{
|
||||
private readonly ICache _cache;
|
||||
|
||||
public PaywallStore(ICache database)
|
||||
{
|
||||
_cache = database;
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||
{
|
||||
var cfg = await _cache.Get<PaywallConfig>(ConfigKey(id));
|
||||
return cfg?.Service switch
|
||||
{
|
||||
PaywallServices.None => await _cache.Get<NoPaywallConfig>(ConfigKey(id)),
|
||||
PaywallServices.Strike => await _cache.Get<StrikePaywallConfig>(ConfigKey(id)),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask SetConfig(Guid id, PaywallConfig config)
|
||||
{
|
||||
await _cache.Set(ConfigKey(id), config);
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||
{
|
||||
return await _cache.Get<PaywallOrder>(OrderKey(id));
|
||||
}
|
||||
|
||||
public async ValueTask SaveOrder(PaywallOrder order)
|
||||
{
|
||||
await _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}";
|
||||
}
|
37
VoidCat/Services/Redis/RedisCache.cs
Normal file
37
VoidCat/Services/Redis/RedisCache.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Redis;
|
||||
|
||||
public class RedisCache : ICache
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
|
||||
public RedisCache(IDatabase db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async ValueTask<T?> Get<T>(string key)
|
||||
{
|
||||
var json = await _db.StringGetAsync(key);
|
||||
return json.HasValue ? JsonConvert.DeserializeObject<T>(json) : default;
|
||||
}
|
||||
|
||||
public async ValueTask Set<T>(string key, T value, TimeSpan? expire = null)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(value);
|
||||
await _db.StringSetAsync(key, json, expire);
|
||||
}
|
||||
|
||||
public async ValueTask<string[]> GetList(string key)
|
||||
{
|
||||
return (await _db.SetMembersAsync(key)).ToStringArray();
|
||||
}
|
||||
|
||||
public async ValueTask AddToList(string key, string value)
|
||||
{
|
||||
await _db.SetAddAsync(key, value);
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Redis;
|
||||
|
||||
public class RedisPaywallStore : IPaywallStore
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
|
||||
public RedisPaywallStore(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||
{
|
||||
var json = await _database.StringGetAsync(ConfigKey(id));
|
||||
var cfg = json.HasValue ? JsonConvert.DeserializeObject<PaywallBlank>(json) : default;
|
||||
return cfg?.Service switch
|
||||
{
|
||||
PaywallServices.None => JsonConvert.DeserializeObject<NoPaywallConfig>(json),
|
||||
PaywallServices.Strike => JsonConvert.DeserializeObject<StrikePaywallConfig>(json),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask SetConfig(Guid id, PaywallConfig config)
|
||||
{
|
||||
await _database.StringSetAsync(ConfigKey(id), JsonConvert.SerializeObject(config));
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||
{
|
||||
var json = await _database.StringGetAsync(OrderKey(id));
|
||||
return json.HasValue ? JsonConvert.DeserializeObject<PaywallOrder>(json) : default;
|
||||
}
|
||||
|
||||
public async ValueTask SaveOrder(PaywallOrder order)
|
||||
{
|
||||
await _database.StringSetAsync(OrderKey(order.Id), JsonConvert.SerializeObject(order),
|
||||
order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
private RedisKey ConfigKey(Guid id) => $"paywall:config:{id}";
|
||||
private RedisKey OrderKey(Guid id) => $"paywall:order:{id}";
|
||||
|
||||
internal class PaywallBlank
|
||||
{
|
||||
public PaywallServices Service { get; init; }
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using StackExchange.Redis;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Redis;
|
||||
|
@ -1,6 +1,6 @@
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services;
|
||||
namespace VoidCat.Services.Stats;
|
||||
|
||||
public class AggregateStatsCollector : IAggregateStatsCollector
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using Prometheus;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services;
|
||||
namespace VoidCat.Services.Stats;
|
||||
|
||||
public class PrometheusStatsCollector : IStatsCollector
|
||||
{
|
35
VoidCat/Services/Users/UserManager.cs
Normal file
35
VoidCat/Services/Users/UserManager.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Users;
|
||||
|
||||
public class UserManager : IUserManager
|
||||
{
|
||||
private readonly IUserStore _store;
|
||||
|
||||
public UserManager(IUserStore store)
|
||||
{
|
||||
_store = store;
|
||||
}
|
||||
|
||||
public async ValueTask<VoidUser> Login(string email, string password)
|
||||
{
|
||||
var userId = await _store.LookupUser(email);
|
||||
if (!userId.HasValue) throw new InvalidOperationException("User does not exist");
|
||||
|
||||
var user = await _store.Get(userId.Value);
|
||||
if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist");
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async ValueTask<VoidUser> Register(string email, string password)
|
||||
{
|
||||
var existingUser = await _store.LookupUser(email);
|
||||
if (existingUser != default) throw new InvalidOperationException("User already exists");
|
||||
|
||||
var newUser = new VoidUser(Guid.NewGuid(), email, password.HashPassword());
|
||||
await _store.Set(newUser);
|
||||
return newUser;
|
||||
}
|
||||
}
|
55
VoidCat/Services/Users/UserStore.cs
Normal file
55
VoidCat/Services/Users/UserStore.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Users;
|
||||
|
||||
public class UserStore : IUserStore
|
||||
{
|
||||
private const string UserList = "users";
|
||||
private readonly ICache _cache;
|
||||
|
||||
public UserStore(ICache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public async ValueTask<Guid?> LookupUser(string email)
|
||||
{
|
||||
return await _cache.Get<Guid>(MapKey(email));
|
||||
}
|
||||
|
||||
public async ValueTask<VoidUser?> Get(Guid id)
|
||||
{
|
||||
return await _cache.Get<VoidUser>(MapKey(id));
|
||||
}
|
||||
|
||||
public async ValueTask Set(VoidUser user)
|
||||
{
|
||||
await _cache.Set(MapKey(user.Id), user);
|
||||
await _cache.AddToList(UserList, user.Id.ToString());
|
||||
await _cache.Set(MapKey(user.Email), user.Id.ToString());
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<VoidUser> ListUsers([EnumeratorCancellation] CancellationToken cts = default)
|
||||
{
|
||||
var users = (await _cache.GetList(UserList))?.Select(Guid.Parse);
|
||||
if (users != default)
|
||||
{
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
var loadUsers = await Task.WhenAll(users.Select(async a => await Get(a)));
|
||||
foreach (var user in loadUsers)
|
||||
{
|
||||
if (user != default)
|
||||
{
|
||||
yield return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string MapKey(Guid id) => $"user:{id}";
|
||||
private static string MapKey(string email) => $"user:email:{email}";
|
||||
}
|
@ -10,7 +10,7 @@ export function Login(props) {
|
||||
async function login(e) {
|
||||
e.target.disabled = true;
|
||||
|
||||
let req = await fetch("/login", {
|
||||
let req = await fetch("/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username, password
|
||||
|
Loading…
x
Reference in New Issue
Block a user