using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; using Mirea.Api.Dto.Common; using Mirea.Api.Dto.Requests; using Mirea.Api.Dto.Responses; using Mirea.Api.Endpoint.Common.Services; using Mirea.Api.Endpoint.Common.Settings; using Mirea.Api.Security.Common.Dto.Requests; using Mirea.Api.Security.Services; using System; using System.Security; using System.Threading.Tasks; namespace Mirea.Api.Endpoint.Controllers.V1; [ApiVersion("1.0")] public class AuthController(IOptionsSnapshot user, AuthService auth, PasswordHashService passwordService) : BaseController, IActionFilter { private string Fingerprint { get; set; } = string.Empty; private string Ip { get; set; } = string.Empty; private string UserAgent { get; set; } = string.Empty; private string RefreshToken { get; set; } = string.Empty; private void SetCookie(string name, string value, DateTimeOffset? expires = null) { var cookieOptions = new CookieOptions { Expires = expires, Path = UrlHelper.GetSubPathWithoutFirstApiName + "api", Domain = HttpContext.GetCurrentDomain(), HttpOnly = true, #if !DEBUG Secure = true #endif }; Response.Cookies.Append(name, value, cookieOptions); } private void SetRefreshToken(string value, DateTimeOffset? expires = null) => SetCookie("refresh_token", value, expires); private void SetFirstToken(string value, DateTimeOffset? expires = null) => SetCookie("authentication_token", value, expires); [ApiExplorerSettings(IgnoreApi = true)] public void OnActionExecuting(ActionExecutingContext context) { Ip = HttpContext.Connection.RemoteIpAddress?.ToString()!; UserAgent = Request.Headers.UserAgent.ToString(); Fingerprint = Request.Cookies["user_key"] ?? string.Empty; RefreshToken = Request.Cookies["refresh_token"] ?? string.Empty; if (!string.IsNullOrWhiteSpace(Fingerprint)) return; Fingerprint = Guid.NewGuid().ToString().Replace("-", ""); SetCookie("user_key", Fingerprint); } [ApiExplorerSettings(IgnoreApi = true)] public void OnActionExecuted(ActionExecutedContext context) { } /// /// Handles user authentication by verifying the username/email and password, /// then generating and returning an authentication token if successful. /// /// The login request containing the username/email and password. /// A TokenResponse containing the access token and its expiry if successful, otherwise an Unauthorized response. [HttpPost("Login")] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> Login([FromBody] LoginRequest request) { var userEntity = user.Value; if (!userEntity.Username.Equals(request.Username, StringComparison.OrdinalIgnoreCase) && !userEntity.Email.Equals(request.Username, StringComparison.OrdinalIgnoreCase) || !passwordService.VerifyPassword(request.Password, userEntity.Salt, userEntity.PasswordHash)) return Unauthorized("Invalid username/email or password"); var token = await auth.GenerateAuthTokensAsync(new TokenRequest { Fingerprint = Fingerprint, Ip = Ip, UserAgent = UserAgent }, "1"); SetRefreshToken(token.RefreshToken, token.RefreshExpiresIn); return Ok(new TokenResponse { AccessToken = token.AccessToken, ExpiresIn = token.AccessExpiresIn }); } /// /// Refreshes the authentication token using the existing refresh token. /// /// A TokenResponse containing the new access token and its expiry if successful, otherwise an Unauthorized response. [HttpGet("ReLogin")] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> ReLogin() { if (string.IsNullOrEmpty(RefreshToken)) return Unauthorized(); try { var token = await auth.RefreshTokenAsync( new TokenRequest { Ip = Ip, UserAgent = UserAgent, Fingerprint = Fingerprint }, RefreshToken ); SetRefreshToken(token.RefreshToken, token.RefreshExpiresIn); return Ok(new TokenResponse { AccessToken = token.AccessToken, ExpiresIn = token.AccessExpiresIn }); } catch (SecurityException) { return Unauthorized(); } } /// /// Logs the user out by clearing the refresh token and performing any necessary cleanup. /// /// An Ok response if the logout was successful. [HttpGet("Logout")] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [Authorize] public async Task Logout() { SetRefreshToken("", DateTimeOffset.MinValue); SetFirstToken("", DateTimeOffset.MinValue); await auth.LogoutAsync(Fingerprint); return Ok(); } /// /// Retrieves the role of the authenticated user. /// /// The role of the authenticated user. [HttpGet("GetRole")] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [Authorize] public ActionResult GetRole() => Ok(AuthRoles.Admin); }