using Foxchat.Identity.Middleware; using Foxchat.Identity.Database; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Identity; using Foxchat.Identity.Database.Models; using Foxchat.Core; using System.Diagnostics; using Foxchat.Identity.Utils; using NodaTime; using Microsoft.EntityFrameworkCore; namespace Foxchat.Identity.Controllers.Oauth; [ApiController] [ClientAuthenticate] [Route("/_fox/ident/oauth/password")] public class PasswordAuthController(ILogger logger, IdentityContext db, IClock clock) : ControllerBase { private readonly PasswordHasher _passwordHasher = new(); [HttpPost("register")] public async Task Register([FromBody] RegisterRequest req) { var app = HttpContext.GetApplicationOrThrow(); var appToken = HttpContext.GetToken() ?? throw new UnreachableException(); // GetApplicationOrThrow already gets the token and throws if it's null var appScopes = appToken.Scopes.ExpandScopes(); if (req.Scopes.Except(appScopes).Any()) throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", req.Scopes.Except(appScopes)); var acct = new Account { Username = req.Username, Email = req.Email, Role = Account.AccountRole.User }; db.Add(acct); var hashedPassword = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password)); acct.Password = hashedPassword; // TODO: make token expiry configurable var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); await db.SaveChangesAsync(); return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr)); } [HttpPost("login")] public async Task Login([FromBody] LoginRequest req) { var app = HttpContext.GetApplicationOrThrow(); var appToken = HttpContext.GetToken() ?? throw new UnreachableException(); var appScopes = appToken.Scopes.ExpandScopes(); if (req.Scopes.Except(appScopes).Any()) throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", req.Scopes.Except(appScopes)); var acct = await db.Accounts.FirstOrDefaultAsync(a => a.Email == req.Email) ?? throw new ApiError.NotFound("No user with that email found, or password is incorrect"); var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(acct, acct.Password, req.Password)); if (pwResult == PasswordVerificationResult.Failed) throw new ApiError.NotFound("No user with that email found, or password is incorrect"); if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) acct.Password = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password)); var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); await db.SaveChangesAsync(); return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr)); } public record RegisterRequest(string Username, string Password, string Email, string[] Scopes); public record LoginRequest(string Email, string Password, string[] Scopes); public record AuthResponse(Ulid Id, string Username, string Email, string Token); }