using Femto.Api.Sessions; using Femto.Common; using Femto.Modules.Auth.Application; using Femto.Modules.Auth.Contracts; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Femto.Api.Controllers.Auth; [ApiController] [Route("auth")] public class AuthController(ICurrentUserContext currentUserContext, IAuthService authService) : ControllerBase { [HttpPost("login")] public async Task> Login( [FromBody] LoginRequest request, CancellationToken cancellationToken ) { var result = await authService.AuthenticateUserCredentials( request.Username, request.Password, cancellationToken ); if (result is null) return this.BadRequest(); var (user, session) = result; HttpContext.SetSession(session, user); if (request.RememberMe) { var newRememberMeToken = await authService.CreateRememberMeToken(user.Id); HttpContext.SetRememberMeToken(newRememberMeToken); } return new LoginResponse(user.Id, user.Username, user.Roles.Any(r => r == Role.SuperUser)); } [HttpPost("register")] public async Task> Register( [FromBody] RegisterRequest request, CancellationToken cancellationToken ) { var (user, session) = await authService.CreateUserWithCredentials( request.Username, request.Password, request.SignupCode, cancellationToken ); HttpContext.SetSession(session, user); if (request.RememberMe) { var newRememberMeToken = await authService.CreateRememberMeToken(user.Id); HttpContext.SetRememberMeToken(newRememberMeToken); } return new RegisterResponse( user.Id, user.Username, user.Roles.Any(r => r == Role.SuperUser) ); } [HttpPost("change-password")] public async Task ChangePassword( [FromBody] ChangePasswordRequestBody req, CancellationToken cancellationToken ) { if (currentUserContext.CurrentUser is not {} user) return this.BadRequest(); // superuser do what superuser want if (!user.IsSuperUser) { if (user.Id != req.UserId) return this.BadRequest(); var session = await authService.GetSession(this.HttpContext.GetSessionId()!); // require strong authentication to change password // the user can re-enter their password if (session is null || !session.IsStronglyAuthenticated) return this.BadRequest(); } await authService.ChangePassword(req.UserId, req.NewPassword, cancellationToken); // TODO would be better do handle this from inside the auth service. maybe just have it happen in a post-save event handler? await authService.InvalidateUserSessions(req.UserId, cancellationToken); return this.Ok(new {}); } [HttpPost("delete-current-session")] public async Task DeleteSessionV2() { var sessionId = HttpContext.GetSessionId(); if (sessionId is not null) { await authService.DeleteSession(sessionId); HttpContext.DeleteSession(); } var rememberMeToken = HttpContext.GetRememberMeToken(); if (rememberMeToken is not null) { await authService.DeleteRememberMeToken(rememberMeToken); HttpContext.DeleteRememberMeToken(); } return Ok(new { }); } [Obsolete("use POST /auth/delete-current-session")] [HttpDelete("session")] public async Task DeleteSession() { var sessionId = HttpContext.GetSessionId(); if (sessionId is not null) { await authService.DeleteSession(sessionId); HttpContext.DeleteSession(); } var rememberMeToken = HttpContext.GetRememberMeToken(); if (rememberMeToken is not null) { await authService.DeleteRememberMeToken(rememberMeToken); HttpContext.DeleteRememberMeToken(); } return Ok(new { }); } [HttpGet("user/{userId}")] [Authorize] public async Task> GetUserInfo( Guid userId, CancellationToken cancellationToken ) { var currentUser = currentUserContext.CurrentUser; if (currentUser is null || currentUser.Id != userId) return this.BadRequest(); var user = await authService.GetUserWithId(userId, cancellationToken); if (user is null) return this.BadRequest(); return new GetUserInfoResult( user.Id, user.Username, user.Roles.Any(r => r == Role.SuperUser) ); } [Obsolete("use POST /auth/create-signup-code")] [HttpPost("signup-codes")] [Authorize(Roles = "SuperUser")] public async Task CreateSignupCode( [FromBody] CreateSignupCodeRequest request, CancellationToken cancellationToken ) { await authService.AddSignupCode(request.Code, request.Name, cancellationToken); return Ok(new { }); } [Obsolete("use GET /auth/list-signup-codes")] [HttpGet("signup-codes")] [Authorize(Roles = "SuperUser")] public async Task> ListSignupCodes( CancellationToken cancellationToken ) { var codes = await authService.GetSignupCodes(cancellationToken); return new ListSignupCodesResult( codes.Select(c => new SignupCodeDto( c.Code, c.Email, c.Name, c.RedeemedByUserId, c.RedeemedByUsername, c.ExpiresOn )) ); } [HttpPost("create-signup-code")] [Authorize(Roles = "SuperUser")] public async Task CreateSignupCodeV2( [FromBody] CreateSignupCodeRequest request, CancellationToken cancellationToken ) { await authService.AddSignupCode(request.Code, request.Name, cancellationToken); return Ok(new { }); } [HttpGet("list-signup-codes")] [Authorize(Roles = "SuperUser")] public async Task> ListSignupCodesV2( CancellationToken cancellationToken ) { var codes = await authService.GetSignupCodes(cancellationToken); return new ListSignupCodesResult( codes.Select(c => new SignupCodeDto( c.Code, c.Email, c.Name, c.RedeemedByUserId, c.RedeemedByUsername, c.ExpiresOn )) ); } }