using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Mirea.Api.Dto.Common; using Mirea.Api.Dto.Requests; using Mirea.Api.Dto.Responses; using Mirea.Api.Endpoint.Common.Attributes; using Mirea.Api.Endpoint.Common.Exceptions; using Mirea.Api.Endpoint.Common.MapperDto; using Mirea.Api.Endpoint.Common.Services; using Mirea.Api.Endpoint.Configuration.Model; using Mirea.Api.Security.Common.Model; using Mirea.Api.Security.Services; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using CookieOptions = Mirea.Api.Security.Common.Model.CookieOptions; using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider; namespace Mirea.Api.Endpoint.Controllers.V1; [ApiVersion("1.0")] public class AuthController(IOptionsSnapshot user, IOptionsSnapshot generalConfig, AuthService auth, PasswordHashService passwordService, OAuthService oAuthService) : BaseController { private CookieOptions GetCookieParams() => new() { Domain = HttpContext.GetCurrentDomain(), Path = UrlHelper.GetSubPathWithoutFirstApiName + "api" }; private static string GenerateHtmlResponse( string title, string message, Uri? callback, string traceId, bool isError) { var callbackUrl = callback?.ToString(); var script = callback == null ? string.Empty : $""; var blockInfo = "

" + (callback == null ? "Вернитесь обратно и попробуйте снова позже.

" : $"Если вы не будете автоматически перенаправлены, нажмите ниже.

" + $"Перейти вручную"); return $"{title}

{title}

{blockInfo}

{message}

TraceId={traceId}
{script}"; } /// /// Handles the callback from an OAuth2 provider and finalizes the authorization process. /// /// /// This method processes the response from an OAuth provider after the user authorizes the application. /// Upon successful authorization, it redirects the user back to the specified callback URL. /// /// The authorization code returned by the OAuth provider. /// The state parameter to ensure the request's integrity and prevent CSRF attacks. /// /// An HTML response indicating the success or failure of the authorization process. /// If a callback URL is provided, the user will be redirected to it. /// [HttpGet("OAuth2")] [BadRequestResponse] [Produces("text/html")] [MaintenanceModeIgnore] public async Task OAuth2([FromQuery] string? code, [FromQuery] string? state) { var traceId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state)) return Content(GenerateHtmlResponse( "Ошибка передачи данных!", "Провайдер OAuth не передал нужных данных.", null, traceId, true), "text/html"); var result = await oAuthService.LoginOAuth(GetCookieParams(), HttpContext, HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state); string? callbackUrl = null; if (result.Callback != null) callbackUrl = result.Callback + (result.Callback.Query.Length > 0 ? "&" : "?") + $"result={Uri.EscapeDataString(result.Token)}"; string title, message; if (!result.Success) { if (callbackUrl != null) callbackUrl += $"&traceId={Uri.EscapeDataString(traceId)}"; title = "Ошибка авторизации!"; message = result.ErrorMessage ?? "Произошла ошибка. Попробуйте ещё раз."; } else { title = "Авторизация завершена!"; message = "Вы будете перенаправлены обратно через несколько секунд."; } return Content(GenerateHtmlResponse( title, message, callbackUrl == null ? null : new Uri(callbackUrl), traceId, !result.Success), "text/html"); } /// /// Initiates the OAuth2 authorization process for the selected provider. /// /// /// This method generates a redirect URL for the selected provider and redirects the user to it. /// /// The identifier of the OAuth provider to authorize with. /// The address where the user will need to be redirected after the end of communication with the OAuth provider /// A redirect to the OAuth provider's authorization URL. /// Thrown if the specified provider is not valid. [HttpGet("AuthorizeOAuth2")] [MaintenanceModeIgnore] public ActionResult AuthorizeOAuth2([FromQuery] int provider, [FromQuery] Uri callback) { if (!Enum.IsDefined(typeof(OAuthProvider), provider)) throw new ControllerArgumentException("There is no selected provider"); if (!callback.IsAbsoluteUri) throw new ControllerArgumentException("The callback URL must be absolute."); return Redirect(oAuthService.GetProviderRedirect(GetCookieParams(), HttpContext, HttpContext.GetApiUrl(Url.Action("OAuth2")!), (OAuthProvider)provider, callback).AbsoluteUri); } /// /// Retrieves a list of available OAuth providers with their corresponding authorization URLs. /// /// /// This allows the client to fetch all possible OAuth options and the URLs required to initiate authorization. /// /// A list of available providers and their redirect URLs. [HttpGet("AvailableProviders")] [MaintenanceModeIgnore] public ActionResult> AvailableProviders([FromQuery] Uri callback) => Ok(oAuthService .GetAvailableProviders(HttpContext.GetApiUrl(Url.Action("AuthorizeOAuth2")!)) .Select(x => { if (!callback.IsAbsoluteUri) throw new ControllerArgumentException("The callback URL must be absolute."); x.Redirect = new Uri(x.Redirect + "&callback=" + Uri.EscapeDataString(callback.AbsoluteUri)); return x; }) .ConvertToDto()); /// /// Logs in a user using their username or email and password. /// /// The login request containing username/email and password. /// A TwoFactorAuthentication token if the login is successful; otherwise, a BadRequest response. [HttpPost("Login")] [BadRequestResponse] public async Task> Login([FromBody] LoginRequest request) { var userEntity = user.Value; var tokenResult = await auth.LoginAsync( GetCookieParams(), HttpContext, userEntity.ConvertToSecurity(), request.Password, request.Username); return Ok(tokenResult.ConvertToDto()); } /// /// Performs two-factor authentication for the user. /// /// The request containing the method and code for two-factor authentication. /// A boolean indicating whether the two-factor authentication was successful. [HttpPost("2FA")] [BadRequestResponse] public async Task> TwoFactorAuth([FromBody] TwoFactorAuthRequest request) { var tokenResult = await auth.LoginAsync(GetCookieParams(), HttpContext, request.Method.ConvertFromDto(), request.Code); return Ok(tokenResult); } /// /// Refreshes the authentication token using the existing refresh token. /// /// User's AuthRoles. [HttpGet("ReLogin")] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> ReLogin() { await auth.RefreshTokenAsync(GetCookieParams(), HttpContext); return Ok(AuthRoles.Admin); } /// /// Logs the user out by clearing the refresh token and performing any necessary cleanup. /// /// An Ok response if the logout was successful. [HttpGet("Logout")] public async Task Logout() { await auth.LogoutAsync(GetCookieParams(), HttpContext); 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); [HttpPost("RenewPassword")] [ApiExplorerSettings(IgnoreApi = true)] [Localhost] [BadRequestResponse] public ActionResult RenewPassword([FromBody] string? password = null) { var passwordPolicy = generalConfig.Value.PasswordPolicy; var passwordPolicyService = new PasswordPolicyService(passwordPolicy); if (string.IsNullOrEmpty(password)) password = string.Empty; else passwordPolicyService.ValidatePasswordOrThrow(password); while (!passwordPolicyService.TryValidatePassword(password)) password = GeneratorKey.GenerateAlphaNumeric(passwordPolicy.MinimumLength + 2, includes: "!@#%^"); var (salt, hash) = passwordService.HashPassword(password); var admin = user.Value; admin.Salt = salt; admin.PasswordHash = hash; admin.SaveSetting(); return Ok(password); } }