using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.Diagnostics.HealthChecks; using Nexus.Api.DTOs; using Nexus.Api.Integrations; using Nexus.Api.Services; namespace Nexus.Api.Controllers; [ApiController] [Route("api/v1/auth")] public class AuthController( IAuthService authService, IAntiforgery antiforgery, IConfiguration config, IHostEnvironment env) : ControllerBase { [HttpGet("csrf")] public IActionResult GetCsrfToken() { var tokens = antiforgery.GetAndStoreTokens(HttpContext); return Ok(new { token = tokens.RequestToken }); } [HttpPost("login")] [EnableRateLimiting("auth")] public async Task Login([FromBody] LoginRequest request, CancellationToken ct) { if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password)) return Results.ValidationProblem(new Dictionary { ["credentials"] = ["Email and password are required."] }); var session = await authService.LoginAsync(request, ct); if (session is null) return Results.Unauthorized(); SetRefreshCookie(Response, session.RefreshToken); Response.Headers.CacheControl = "no-store"; return Results.Ok(ToAuthResponse(session)); } [HttpPost("refresh")] [EnableRateLimiting("auth")] public async Task Refresh(CancellationToken ct) { if (!Request.Cookies.TryGetValue("nexus_refresh", out var refreshToken)) return Results.Unauthorized(); var session = await authService.RefreshAsync(refreshToken!, ct); if (session is null) { ClearRefreshCookie(Response); return Results.Unauthorized(); } SetRefreshCookie(Response, session.RefreshToken); Response.Headers.CacheControl = "no-store"; return Results.Ok(ToAuthResponse(session)); } [HttpPost("logout")] public async Task Logout(CancellationToken ct) { if (Request.Cookies.TryGetValue("nexus_refresh", out var refreshToken)) await authService.RevokeAsync(refreshToken!, ct); ClearRefreshCookie(Response); return Results.NoContent(); } [HttpGet("me")] public async Task GetMe(CancellationToken ct) { var subject = User.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub)?.Value; if (!Guid.TryParse(subject, out var userId)) return Results.Unauthorized(); var user = await authService.GetUserAsync(userId, ct); return user is null ? Results.Unauthorized() : Results.Ok(new UserInfo { Id = user.Id, Email = user.Email, DisplayName = user.DisplayName, Role = user.Role }); } [HttpPatch("profile")] public async Task UpdateProfile([FromBody] UpdateProfileRequest request, CancellationToken ct) { var subject = User.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub)?.Value; if (!Guid.TryParse(subject, out var userId)) return Results.Unauthorized(); var user = await authService.UpdateProfileAsync(userId, request, ct); return user is null ? Results.NotFound() : Results.Ok(new UserInfo { Id = user.Id, Email = user.Email, DisplayName = user.DisplayName, Role = user.Role }); } [HttpPost("change-password")] public async Task ChangePassword([FromBody] ChangePasswordRequest request, CancellationToken ct) { if (string.IsNullOrWhiteSpace(request.CurrentPassword) || string.IsNullOrWhiteSpace(request.NewPassword)) return Results.ValidationProblem(new Dictionary { ["password"] = ["Current and new passwords are required."] }); if (request.NewPassword.Length < 10) return Results.ValidationProblem(new Dictionary { ["newPassword"] = ["New password must be at least 10 characters."] }); var subject = User.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub)?.Value; if (!Guid.TryParse(subject, out var userId)) return Results.Unauthorized(); var success = await authService.ChangePasswordAsync(userId, request, ct); return success ? Results.Ok(new { message = "Password changed successfully." }) : Results.Problem("Current password is incorrect.", statusCode: 400); } private static AuthResponse ToAuthResponse(AuthSession session) => new() { AccessToken = session.AccessToken, ExpiresAt = session.ExpiresAt, User = session.User }; private void SetRefreshCookie(HttpResponse response, string token) { var days = config.GetValue("Jwt:RefreshTokenExpirationDays") ?? 7; response.Cookies.Append("nexus_refresh", token, new CookieOptions { HttpOnly = true, Secure = !env.IsDevelopment(), SameSite = SameSiteMode.Strict, Path = "/api/v1/auth", MaxAge = TimeSpan.FromDays(days), IsEssential = true }); } private void ClearRefreshCookie(HttpResponse response) { response.Cookies.Delete("nexus_refresh", new CookieOptions { HttpOnly = true, Secure = !env.IsDevelopment(), SameSite = SameSiteMode.Strict, Path = "/api/v1/auth" }); } }