using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using ROLAC.API.DTOs.Auth; using ROLAC.API.Services; namespace ROLAC.API.Controllers; [ApiController] [Route("api/auth")] public class AuthController : ControllerBase { private const string CookieName = "rolac_rt"; private const int CookieMaxAge = 30 * 24 * 60 * 60; // 30 days in seconds private readonly IAuthService _authService; private readonly IWebHostEnvironment _env; public AuthController(IAuthService authService, IWebHostEnvironment env) { _authService = authService; _env = env; } // ------------------------------------------------------------------------- // POST /api/auth/login // ------------------------------------------------------------------------- /// Authenticates a user and returns an access token. [HttpPost("login")] [AllowAnonymous] [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Login([FromBody] LoginRequest request) { try { var ip = HttpContext.Connection.RemoteIpAddress?.ToString(); var device = Request.Headers.UserAgent.FirstOrDefault(); var (response, raw) = await _authService.LoginAsync(request, ip, device); SetRefreshCookie(raw); return Ok(response); } catch (UnauthorizedAccessException ex) { return Unauthorized(new { message = ex.Message }); } } // ------------------------------------------------------------------------- // POST /api/auth/refresh // ------------------------------------------------------------------------- /// /// Rotates the refresh token (read from the HttpOnly cookie) and returns a /// new access token. /// [HttpPost("refresh")] [AllowAnonymous] [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Refresh() { var raw = Request.Cookies[CookieName]; if (string.IsNullOrEmpty(raw)) return Unauthorized(new { message = "Refresh token not found." }); try { var ip = HttpContext.Connection.RemoteIpAddress?.ToString(); var (response, newRaw) = await _authService.RefreshAsync(raw, ip); SetRefreshCookie(newRaw); return Ok(response); } catch (UnauthorizedAccessException ex) { ClearRefreshCookie(); return Unauthorized(new { message = ex.Message }); } } // ------------------------------------------------------------------------- // GET /api/auth/me (dev-only diagnostic — remove before production) // ------------------------------------------------------------------------- /// /// Returns the claims ASP.NET Core parsed from the Bearer token. /// Use this to debug 401 vs 403: if you get 200 here, the JWT validates /// fine; if you then get 403 on /api/users the role claim isn't matching. /// [HttpGet("me")] [Authorize] // no role restriction — just needs a valid JWT public IActionResult GetMe() { var claims = User.Claims .Select(c => new { c.Type, c.Value }) .ToList(); return Ok(new { isAuthenticated = User.Identity?.IsAuthenticated, authenticationType = User.Identity?.AuthenticationType, name = User.Identity?.Name, claims, }); } // ------------------------------------------------------------------------- // POST /api/auth/logout // ------------------------------------------------------------------------- /// Revokes the current refresh token and clears the cookie. [HttpPost("logout")] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Logout() { var raw = Request.Cookies[CookieName]; if (!string.IsNullOrEmpty(raw)) await _authService.LogoutAsync(raw); ClearRefreshCookie(); return NoContent(); } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- /// /// Secure is set to true everywhere except in the local /// Development environment so that the cookie works over plain HTTP during /// dev and smoke-test runs, but is always HTTPS-only in staging/production. /// private void SetRefreshCookie(string rawToken) => Response.Cookies.Append(CookieName, rawToken, new CookieOptions { HttpOnly = true, Secure = !_env.IsDevelopment(), SameSite = SameSiteMode.Strict, MaxAge = TimeSpan.FromSeconds(CookieMaxAge), Path = "/api/auth", }); private void ClearRefreshCookie() => Response.Cookies.Delete(CookieName, new CookieOptions { HttpOnly = true, Secure = !_env.IsDevelopment(), SameSite = SameSiteMode.Strict, Path = "/api/auth", }); }