2024-09-18 06:00:07 +03:00
using Asp.Versioning ;
using Microsoft.AspNetCore.Authorization ;
2024-06-21 21:52:21 +03:00
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 ;
2024-08-24 04:30:31 +03:00
using Mirea.Api.Endpoint.Common.Attributes ;
2024-08-27 22:52:07 +03:00
using Mirea.Api.Endpoint.Common.Exceptions ;
2024-07-05 01:35:19 +03:00
using Mirea.Api.Endpoint.Common.Services ;
2024-10-07 02:13:35 +03:00
using Mirea.Api.Endpoint.Configuration.Model ;
2024-06-21 21:52:21 +03:00
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 < Admin > 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 ,
2024-07-05 01:59:36 +03:00
Path = UrlHelper . GetSubPathWithoutFirstApiName + "api" ,
2024-08-10 23:11:43 +03:00
Domain = HttpContext . GetCurrentDomain ( ) ,
2024-08-24 02:25:29 +03:00
HttpOnly = true ,
2024-08-10 23:11:43 +03:00
#if ! DEBUG
2024-08-24 02:25:29 +03:00
Secure = true
2024-08-10 23:11:43 +03:00
#endif
2024-06-21 21:52:21 +03:00
} ;
Response . Cookies . Append ( name , value , cookieOptions ) ;
}
2024-08-24 02:26:11 +03:00
private void SetRefreshToken ( string value , DateTimeOffset ? expires = null )
{
2024-06-21 21:52:21 +03:00
SetCookie ( "refresh_token" , value , expires ) ;
2024-08-24 02:26:11 +03:00
SetCookie ( "user_key" , Fingerprint , expires ) ;
}
2024-06-21 21:52:21 +03:00
2024-08-24 02:26:11 +03:00
private void SetFirstToken ( string value , DateTimeOffset ? expires = null )
{
2024-06-21 21:52:21 +03:00
SetCookie ( "authentication_token" , value , expires ) ;
2024-08-24 02:26:11 +03:00
SetCookie ( "user_key" , Fingerprint , expires ) ;
}
2024-06-21 21:52:21 +03:00
[ApiExplorerSettings(IgnoreApi = true)]
public void OnActionExecuting ( ActionExecutingContext context )
{
2024-08-24 02:25:29 +03:00
Ip = HttpContext . Connection . RemoteIpAddress ? . ToString ( ) ! ;
UserAgent = Request . Headers . UserAgent . ToString ( ) ;
Fingerprint = Request . Cookies [ "user_key" ] ? ? string . Empty ;
2024-06-21 21:52:21 +03:00
RefreshToken = Request . Cookies [ "refresh_token" ] ? ? string . Empty ;
if ( ! string . IsNullOrWhiteSpace ( Fingerprint ) ) return ;
Fingerprint = Guid . NewGuid ( ) . ToString ( ) . Replace ( "-" , "" ) ;
}
[ApiExplorerSettings(IgnoreApi = true)]
public void OnActionExecuted ( ActionExecutedContext context ) { }
2024-06-28 22:55:18 +03:00
/// <summary>
/// Handles user authentication by verifying the username/email and password,
/// then generating and returning an authentication token if successful.
/// </summary>
/// <param name="request">The login request containing the username/email and password.</param>
/// <returns>A TokenResponse containing the access token and its expiry if successful, otherwise an Unauthorized response.</returns>
2024-06-21 21:52:21 +03:00
[HttpPost("Login")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task < ActionResult < TokenResponse > > 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
} ) ;
}
2024-06-28 22:55:18 +03:00
/// <summary>
/// Refreshes the authentication token using the existing refresh token.
/// </summary>
/// <returns>A TokenResponse containing the new access token and its expiry if successful, otherwise an Unauthorized response.</returns>
2024-06-21 21:52:21 +03:00
[HttpGet("ReLogin")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task < ActionResult < TokenResponse > > 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 ( ) ;
}
}
2024-06-28 22:55:18 +03:00
/// <summary>
/// Logs the user out by clearing the refresh token and performing any necessary cleanup.
/// </summary>
/// <returns>An Ok response if the logout was successful.</returns>
2024-06-21 21:52:21 +03:00
[HttpGet("Logout")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[Authorize]
public async Task < ActionResult > Logout ( )
{
SetRefreshToken ( "" , DateTimeOffset . MinValue ) ;
SetFirstToken ( "" , DateTimeOffset . MinValue ) ;
await auth . LogoutAsync ( Fingerprint ) ;
return Ok ( ) ;
}
2024-06-28 22:55:18 +03:00
/// <summary>
/// Retrieves the role of the authenticated user.
/// </summary>
/// <returns>The role of the authenticated user.</returns>
2024-06-21 21:52:21 +03:00
[HttpGet("GetRole")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[Authorize]
2024-08-24 04:30:31 +03:00
[CacheMaxAge(0, 0, 1)]
2024-06-21 21:52:21 +03:00
public ActionResult < AuthRoles > GetRole ( ) = > Ok ( AuthRoles . Admin ) ;
2024-08-27 22:52:07 +03:00
[HttpPost("RenewPassword")]
[ApiExplorerSettings(IgnoreApi = true)]
[Localhost]
[BadRequestResponse]
public ActionResult < string > RenewPassword ( [ FromBody ] string? password = null )
{
if ( string . IsNullOrEmpty ( password ) )
password = string . Empty ;
else if ( ! PasswordHashService . HasPasswordInPolicySecurity ( password ) )
throw new ControllerArgumentException ( "The password must be at least 8 characters long and contain at least one uppercase letter and one special character." ) ;
while ( ! PasswordHashService . HasPasswordInPolicySecurity ( password ) )
password = GeneratorKey . GenerateAlphaNumeric ( 16 , includes : "!@#%^" ) ;
var ( salt , hash ) = passwordService . HashPassword ( password ) ;
var admin = user . Value ;
admin . Salt = salt ;
admin . PasswordHash = hash ;
admin . SaveSetting ( ) ;
return Ok ( password ) ;
}
2024-06-21 21:52:21 +03:00
}