summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-06-30 18:35:29 +0200
committerRory& <root@rory.gay>2026-06-30 18:35:29 +0200
commit1dd3825ea070641cb034139d33b99d2b70ea6a98 (patch)
treecb10dd36b72ed2114546238ab877404fe89740a6
parentVarious tests stuff (diff)
downloadserver-ts-1dd3825ea070641cb034139d33b99d2b70ea6a98.tar.xz
Various C# continuations
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs51
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Api/RegisterRequest.cs2
-rw-r--r--extra/admin-api/Spacebar.Offload/Program.cs32
-rw-r--r--extra/admin-api/Spacebar.Offload/Spacebar.Offload.csproj2
-rw-r--r--extra/admin-api/Spacebar.UApi/Controllers/Auth/GuildMembersController.cs0
-rw-r--r--extra/admin-api/Spacebar.UApi/Controllers/Auth/RegisterController.cs148
-rw-r--r--extra/admin-api/Spacebar.UApi/Program.cs2
-rw-r--r--extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj2
-rw-r--r--extra/admin-api/Spacebar.UApi/appsettings.Development.json17
9 files changed, 237 insertions, 19 deletions
diff --git a/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs b/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs

index 421c2305..9c00df53 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<SpacebarAuthenticationService private static bool _isInitialised; private static readonly JwtSecurityTokenHandler Handler = new(); + private static ECDsaSecurityKey _publicKey = null!, _privateKey = null!; private static readonly TokenValidationParameters TokenValidationParameters = new() { - // IssuerSigningKey = new ECDsaSecurityKey(key), ValidAlgorithms = ["ES512"], LogValidationExceptions = true, // These are required to be false for the token to be valid as they aren't provided by the token @@ -29,10 +31,18 @@ public class SpacebarAuthenticationService(ILogger<SpacebarAuthenticationService public async Task InitializeAsync() { if (_isInitialised) return; - var secretFile = await File.ReadAllTextAsync(config.PublicKeyPath); - var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); - key.ImportFromPem(secretFile); - TokenValidationParameters.IssuerSigningKey = new ECDsaSecurityKey(key); + + var publicKeyFile = await File.ReadAllTextAsync(config.PublicKeyPath); + var rawPublicKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + rawPublicKey.ImportFromPem(publicKeyFile); + _publicKey = new ECDsaSecurityKey(rawPublicKey); + + var privateKeyFile = await File.ReadAllTextAsync(config.PrivateKeyPath); + var rawPrivateKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + rawPrivateKey.ImportFromPem(privateKeyFile); + _privateKey = new ECDsaSecurityKey(rawPrivateKey); + + TokenValidationParameters.IssuerSigningKey = _publicKey; _isInitialised = true; } @@ -72,9 +82,30 @@ public class SpacebarAuthenticationService(ILogger<SpacebarAuthenticationService config.AuthCacheExpiry); } - // public async Task<string> GenerateAccessTokenAsync(string userId) { - // // await db.Sessions.AddAsync(new() { - // - // // }) - // } + public async Task<string> 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<string, object>() { + { "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 232f5f83..8998db99 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 fd849954..59cc3d0c 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<SpacebarDbContext>(options => { +builder.Services.AddDbContextPool<SpacebarDbContext>(options => +{ options .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) .EnableDetailedErrors(); @@ -37,8 +58,11 @@ builder.Services.AddScoped<SpacebarAspNetAuthenticationService>(); 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 36a60fc2..6dddf197 100644 --- a/extra/admin-api/Spacebar.Offload/Spacebar.Offload.csproj +++ b/extra/admin-api/Spacebar.Offload/Spacebar.Offload.csproj
@@ -12,6 +12,8 @@ <ItemGroup> <ProjectReference Include="..\DataMappings\Spacebar.DataMappings.Generic\Spacebar.DataMappings.Generic.csproj" Condition="'$(ContinuousIntegrationBuild)'!='true'"/> + <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.16.0-beta.1" /> + <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.16.0" /> <PackageReference Include="Spacebar.DataMappings.Generic" Version="*-preview*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> <ProjectReference Include="..\Interop\Spacebar.Interop.Authentication.AspNetCore\Spacebar.Interop.Authentication.AspNetCore.csproj" Condition="'$(ContinuousIntegrationBuild)'!='true'"/> <PackageReference Include="Spacebar.Interop.Authentication.AspNetCore" Version="*-preview*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> 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 e69de29b..00000000 --- a/extra/admin-api/Spacebar.UApi/Controllers/Auth/GuildMembersController.cs +++ /dev/null
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 00000000..ce697e7c --- /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<RegisterController> logger, SpacebarDbContext db, SpacebarAuthenticationService authService) : ControllerBase { + private static ExpiringSemaphoreCache<RegisterResponse> _sem = new(); + + [HttpPost] + public async Task<RegisterResponse> GetMemberAsync(RegisterRequest req) { + Task<RegisterResponse> 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 dd17d291..7a2f12ca 100644 --- a/extra/admin-api/Spacebar.UApi/Program.cs +++ b/extra/admin-api/Spacebar.UApi/Program.cs
@@ -34,7 +34,7 @@ builder.Services.AddDbContextPool<SpacebarDbContext>(options => { options .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) .EnableDetailedErrors(); -}); +}, 64); if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CONFIG_PATH"))) builder.Services.AddSingleton<Config>(sp => { diff --git a/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj b/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj
index 9ba61941..dae669b7 100644 --- a/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj +++ b/extra/admin-api/Spacebar.UApi/Spacebar.UApi.csproj
@@ -20,6 +20,7 @@ <ProjectReference Include="..\Models\Spacebar.Models.Config\Spacebar.Models.Config.csproj"/> <ProjectReference Include="..\Models\Spacebar.Models.Db\Spacebar.Models.Db.csproj"/> <ProjectReference Include="..\Models\Spacebar.Models.Generic\Spacebar.Models.Generic.csproj"/> + <ProjectReference Include="..\Models\Spacebar.Models.Api\Spacebar.Models.Api.csproj" /> </ItemGroup> <!-- For nix... --> @@ -30,5 +31,6 @@ <PackageReference Include="Spacebar.Models.Config" Version="*-preview*"/> <PackageReference Include="Spacebar.Models.Db" Version="*-preview*"/> <PackageReference Include="Spacebar.Models.Generic" Version="*-preview*"/> + <PackageReference Include="Spacebar.Models.Api" Version="*-preview*"/> </ItemGroup> </Project> diff --git a/extra/admin-api/Spacebar.UApi/appsettings.Development.json b/extra/admin-api/Spacebar.UApi/appsettings.Development.json
index 524b10da..b08c097f 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" } } }