From 1dd3825ea070641cb034139d33b99d2b70ea6a98 Mon Sep 17 00:00:00 2001 From: Rory& Date: Tue, 30 Jun 2026 18:35:29 +0200 Subject: [PATCH] Various C# continuations --- .../SpacebarAuthenticationService.cs | 51 ++++-- .../Spacebar.Models.Api/RegisterRequest.cs | 2 +- extra/admin-api/Spacebar.Offload/Program.cs | 32 +++- .../Spacebar.Offload/Spacebar.Offload.csproj | 2 + .../Auth/GuildMembersController.cs | 0 .../Controllers/Auth/RegisterController.cs | 148 ++++++++++++++++++ extra/admin-api/Spacebar.UApi/Program.cs | 2 +- .../Spacebar.UApi/Spacebar.UApi.csproj | 2 + .../appsettings.Development.json | 17 +- 9 files changed, 237 insertions(+), 19 deletions(-) delete mode 100644 extra/admin-api/Spacebar.UApi/Controllers/Auth/GuildMembersController.cs create mode 100644 extra/admin-api/Spacebar.UApi/Controllers/Auth/RegisterController.cs diff --git a/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs b/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs index 421c23055..9c00df53c 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs @@ -1,6 +1,8 @@ +using System.Diagnostics.CodeAnalysis; using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using ArcaneLibs.Collections; +using ArcaneLibs.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; @@ -15,9 +17,9 @@ public class SpacebarAuthenticationService(ILogger GenerateAccessTokenAsync(string userId) { - // // await db.Sessions.AddAsync(new() { - // - // // }) - // } + public async Task GenerateAccessTokenAsync(long userId, bool isAdminSession = false) { + if (!_isInitialised) await InitializeAsync(); + // TODO: check for duplicate session IDs + var sess = db.Sessions.Add(new() { + UserId = userId, + SessionId = Random.Shared.GetString("ABCDEFGHIJKLMNOPQRSTUVEXYZ", 10), + IsAdminSession = isAdminSession, + Status = "unknown", + ClientStatus = "{}", + ClientInfo = "{}" + }); + await db.SaveChangesAsync(); + + var res = Handler.CreateJwtSecurityToken(new() { + Claims = new Dictionary() { + { "id", userId.ToString() }, + { "iat", new DateTimeOffset(sess.Entity.CreatedAt).ToUnixTimeSeconds() }, + { "kid", SHA256.Create().ComputeHash(_publicKey.ECDsa.ExportSubjectPublicKeyInfoPem().AsBytes().ToArray()) }, + { "ver", 3 }, + { "did", sess.Entity.SessionId }, + }, + SigningCredentials = new SigningCredentials(_privateKey, "ES512") + }); + + return Handler.WriteToken(res); + } } \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Api/RegisterRequest.cs b/extra/admin-api/Models/Spacebar.Models.Api/RegisterRequest.cs index 232f5f83e..8998db996 100644 --- a/extra/admin-api/Models/Spacebar.Models.Api/RegisterRequest.cs +++ b/extra/admin-api/Models/Spacebar.Models.Api/RegisterRequest.cs @@ -22,7 +22,7 @@ public class RegisterRequest { public string? Invite { get; set; } [JsonPropertyName("date_of_birth")] - public DateTimeOffset? DateOfBirth { get; set; } + public DateOnly? DateOfBirth { get; set; } [JsonPropertyName("gift_code_sku_id")] public string? GiftCodeSkuId { get; set; } diff --git a/extra/admin-api/Spacebar.Offload/Program.cs b/extra/admin-api/Spacebar.Offload/Program.cs index fd849954a..59cc3d0c2 100644 --- a/extra/admin-api/Spacebar.Offload/Program.cs +++ b/extra/admin-api/Spacebar.Offload/Program.cs @@ -1,7 +1,10 @@ +using System.Diagnostics.Metrics; using System.Text.Json; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; using Microsoft.EntityFrameworkCore; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using Spacebar.Interop.Authentication; using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Models.Db.Contexts; @@ -12,11 +15,28 @@ if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("APPSETTINGS_P builder.Configuration.AddJsonFile(Environment.GetEnvironmentVariable("APPSETTINGS_PATH")!); // Add services to the container. +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource=> { + resource.AddService(serviceName: builder.Environment.ApplicationName, serviceNamespace: builder.Environment.ApplicationName); + }) + .WithMetrics(builder => + { + builder.AddPrometheusExporter(); + + builder.AddMeter("Microsoft.AspNetCore.Hosting", "Microsoft.AspNetCore.Server.Kestrel", "System.Runtime", "Microsoft.Extensions.Diagnostics.ResourceMonitoring", "Microsoft.AspNetCore.Routing", "Microsoft.AspNetCore.Diagnostics", "Microsoft.AspNetCore.RateLimiting", "Microsoft.AspNetCore.HeaderParsing", "Microsoft.AspNetCore.Server.Kestrel"); + builder.AddView("http.server.request.duration", + new ExplicitBucketHistogramConfiguration + { + Boundaries = [0.0, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 10] + }); + }); -builder.Services.AddControllers(options => { +builder.Services.AddControllers(options => +{ options.MaxValidationDepth = null; // options.MaxIAsyncEnumerableBufferLimit = 1; -}).AddJsonOptions(options => { +}).AddJsonOptions(options => +{ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.Never; options.JsonSerializerOptions.WriteIndented = true; options.JsonSerializerOptions.MaxDepth = 100; @@ -25,7 +45,8 @@ builder.Services.AddControllers(options => { // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); -builder.Services.AddDbContextPool(options => { +builder.Services.AddDbContextPool(options => +{ options .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) .EnableDetailedErrors(); @@ -37,8 +58,11 @@ builder.Services.AddScoped(); var app = builder.Build(); +app.MapPrometheusScrapingEndpoint(); + // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) { +if (app.Environment.IsDevelopment()) +{ app.MapOpenApi(); } diff --git a/extra/admin-api/Spacebar.Offload/Spacebar.Offload.csproj b/extra/admin-api/Spacebar.Offload/Spacebar.Offload.csproj index 36a60fc2b..6dddf197c 100644 --- a/extra/admin-api/Spacebar.Offload/Spacebar.Offload.csproj +++ b/extra/admin-api/Spacebar.Offload/Spacebar.Offload.csproj @@ -12,6 +12,8 @@ + + diff --git a/extra/admin-api/Spacebar.UApi/Controllers/Auth/GuildMembersController.cs b/extra/admin-api/Spacebar.UApi/Controllers/Auth/GuildMembersController.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/extra/admin-api/Spacebar.UApi/Controllers/Auth/RegisterController.cs b/extra/admin-api/Spacebar.UApi/Controllers/Auth/RegisterController.cs new file mode 100644 index 000000000..ce697e7cd --- /dev/null +++ b/extra/admin-api/Spacebar.UApi/Controllers/Auth/RegisterController.cs @@ -0,0 +1,148 @@ +using System.Diagnostics; +using System.Text.Json.Nodes; +using ArcaneLibs.Collections; +using ArcaneLibs.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.Interop.Authentication.AspNetCore; +using Spacebar.Models.Db.Contexts; +using Spacebar.DataMappings.Generic; +using Spacebar.Interop.Authentication; +using Spacebar.Models.Api; +using Spacebar.Models.Generic; + +namespace Spacebar.UApi.Controllers; + +[Route("/api/v{_}/auth/register")] +[Route("/api/auth/register")] +[ApiController] +public class RegisterController(ILogger logger, SpacebarDbContext db, SpacebarAuthenticationService authService) : ControllerBase { + private static ExpiringSemaphoreCache _sem = new(); + + [HttpPost] + public async Task GetMemberAsync(RegisterRequest req) { + Task task; + + lock (_sem) + task = _sem.GetOrAdd($"{req.Email}{req.DateOfBirth}{req.Username}", async () => { + var sw = Stopwatch.StartNew(); + + // TODO: reg tokens - do we even want to continue supporting referrer-based tokens? + // TODO: config: register.allowNewRegistration + + if (!req.Consent) + throw new SpacebarApiException("Consent is required to continue") { + Code = 0, Request = Request.Path, Errors = new() { + { + "consent", + new JsonObject() { + { "code", "CONSENT_REQUIRED" }, + { "message", "You must consent to register on this instance." } + } + } + } + }; + + // TODO: config reg.disabled + // TODO: captchas + // TODO: do we even want to support multiaccounts on a single email? + // TODO: ip checks + // TODO: gift_code_sku_id? + // TODO: check password strength + // TODO: ability to config DoB checks + if (!req.DateOfBirth.HasValue || (DateTime.UtcNow - req.DateOfBirth.Value.ToDateTime(new TimeOnly(0, 0, 0))).TotalDays < (13 * 365)) + throw new SpacebarApiException("Invalid date of birth") { + Code = 0, Request = Request.Path, Errors = new() { + { + "date_of_birth", + new JsonObject() { + { "code", "DATE_OF_BIRTH_INVALID" }, + { "message", "You must enter a valid date of birth to continue" } + } + } + } + }; + + // TODO: password min length config, optional password + if (string.IsNullOrWhiteSpace(req.Password) || req.Password!.Length < 4) + throw new SpacebarApiException("Invalid password") { + Code = 0, Request = Request.Path, Errors = new() { + { + "password", + new JsonObject() { + { "code", "PASSWORD_REQUIREMENTS_MIN_LENGTH" }, + { "message", "Your password must be at least 4 characters." } + } + } + } + }; + + // TODO: require invite + // TODO: global register ratelimit + // TODO: configurable username length limits + if (string.IsNullOrWhiteSpace(req.Username) || req.Username.Length < 4 || req.Username.Length > 255) + throw new SpacebarApiException("Invalid username length") { + Code = 0, Request = Request.Path, Errors = new() { + { + "username", + new JsonObject() { + { "code", "BASE_TYPE_BAD_LENGTH" }, + { "message", "Username must be between 4 and 255 characters." } + } + } + } + }; + + var existingDiscrims = await db.Users.Where(x => x.Username == req.Username).ToListAsync(); + // TODO: throw error if out of slots + string newDiscrim; + do { + newDiscrim = Random.Shared.Next(0, 1000).ToString("0000"); + } while (existingDiscrims.Any(x => x.Discriminator == newDiscrim)); + + var user = db.Users.Add(new() { + Username = req.Username, + Discriminator = newDiscrim, + Email = req.Email, + Id = GenerateSnowflake(), + Data = new JsonObject() { + { "hash", BCrypt.Net.BCrypt.HashPassword(req.Password, 12) }, + { "valid_tokens_since", DateTime.Now } + }.ToJson(), + // settings? + Premium = true, + PremiumSince = DateTime.Now, + PremiumType = 2, + Verified = true, + CreatedAt = DateTime.Now, + Bot = false, + Fingerprints = new(), + Bio = "" + }); + + await db.SaveChangesAsync(); + // TODO: verification email + // TODO: autojoin + // TODO: handle invite key + + return new RegisterResponse() { + Token = await authService.GenerateAccessTokenAsync(user.Entity.Id) + }; + }, + TimeSpan.FromMinutes(1)); + return await task; + } + + // TODO: move + private static long _snowflakeIdx = 0; + + public static long GenerateSnowflake() { + _snowflakeIdx %= 4095; + return + ((DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds()) << 22) + | ((long)(Environment.CurrentManagedThreadId % 31) << 17) //worker ID + | ((long)(Environment.ProcessId % 31) << 12) // process ID + | ((long)_snowflakeIdx++) + ; + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.UApi/Program.cs b/extra/admin-api/Spacebar.UApi/Program.cs index dd17d291b..7a2f12ca2 100644 --- a/extra/admin-api/Spacebar.UApi/Program.cs +++ b/extra/admin-api/Spacebar.UApi/Program.cs @@ -34,7 +34,7 @@ builder.Services.AddDbContextPool(options => { options .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) .EnableDetailedErrors(); -}); +}, 64); if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CONFIG_PATH"))) builder.Services.AddSingleton(sp => { diff --git a/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj b/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj index 9ba61941c..dae669b78 100644 --- a/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj +++ b/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj @@ -20,6 +20,7 @@ + @@ -30,5 +31,6 @@ + diff --git a/extra/admin-api/Spacebar.UApi/appsettings.Development.json b/extra/admin-api/Spacebar.UApi/appsettings.Development.json index 524b10da7..b08c097f2 100644 --- a/extra/admin-api/Spacebar.UApi/appsettings.Development.json +++ b/extra/admin-api/Spacebar.UApi/appsettings.Development.json @@ -1,12 +1,23 @@ { "Logging": { +// "LogLevel": { +// "Default": "Information", +// "Microsoft.AspNetCore": "Warning" +// } "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Trace", + "Microsoft.AspNetCore.Server.Kestrel.Connections": "Information", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets": "Information", + "Microsoft.AspNetCore.Mvc": "Warning", + "Microsoft.AspNetCore.HostFiltering": "Warning", + "Microsoft.AspNetCore.Cors": "Warning", + // "Microsoft.EntityFrameworkCore": "Warning" + "Microsoft.EntityFrameworkCore.Database.Command": "Information" } }, "ConnectionStrings": { - "Spacebar": "Host=127.0.0.1; Username=postgres; Database=spacebar; Port=5433; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;" + "Spacebar": "Host=127.0.0.1; Username=postgres; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;" }, "Spacebar": { "Authentication": { @@ -14,7 +25,7 @@ "PrivateKeyPath": "../../../jwt.key" }, "UApi":{ - "FallbackApiEndpoint": "http://localhost:3113" + "FallbackApiEndpoint": "http://localhost:3001" } } }