summary refs log tree commit diff
path: root/extra/admin-api/Spacebar.UApi/Controllers/Auth
diff options
context:
space:
mode:
Diffstat (limited to 'extra/admin-api/Spacebar.UApi/Controllers/Auth')
-rw-r--r--extra/admin-api/Spacebar.UApi/Controllers/Auth/GuildMembersController.cs0
-rw-r--r--extra/admin-api/Spacebar.UApi/Controllers/Auth/RegisterController.cs148
2 files changed, 148 insertions, 0 deletions
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