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"
}
}
}
|