LANCommander/LANCommander.Server/Endpoints/AuthApiEndpoints.cs
2026-05-11 22:29:41 -05:00

227 lines
7.3 KiB
C#

using System.Security.Claims;
using AutoMapper;
using LANCommander.SDK.Models;
using LANCommander.Server.Models;
using LANCommander.Server.Services;
using LANCommander.Server.Services.Exceptions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using ZiggyCreatures.Caching.Fusion;
using AuthenticationService = LANCommander.Server.Services.AuthenticationService;
namespace LANCommander.Server.Endpoints;
public static class AuthApiEndpoints
{
public static void MapAuthApiEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/Auth");
group.MapPost("/Login", LoginAsync);
group.MapGet("/Login", LoginByProviderAsync);
group.MapPost("/Logout", LogoutAsync);
group.MapPost("/Validate", ValidateAsync).RequireAuthorization();
group.MapPost("/Refresh", RefreshAsync);
group.MapPost("/Register", RegisterAsync);
group.MapGet("/AuthenticationProviders", GetAuthenticationProvidersAsync);
group.MapGet("/Token/{code:guid}", RedeemTokenAsync);
}
internal static async Task<IResult> LoginAsync(
[FromBody] LoginModel model,
[FromServices] AuthenticationService authenticationService,
[FromServices] ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger("AuthApi");
try
{
var token = await authenticationService.LoginAsync(model.Username, model.Password);
logger.LogDebug("Successfully logged in user {UserName}", model.Username);
return TypedResults.Ok(token);
}
catch (UserAuthenticationException ex)
{
List<ErrorResponse.ErrorInfo> errorDetails =
ex.IdentityResult?.Errors.Select(FromIdentityResult).ToList() ?? [];
if (errorDetails.Count == 0)
{
errorDetails.Add(new ErrorResponse.ErrorInfo { Message = ex.Message });
}
var errorResponse = new ErrorResponse
{
Error = "AuthenticationFailed",
Message = "User authentication failed.",
Details = errorDetails,
};
return TypedResults.BadRequest(errorResponse);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while trying to log in {UserName}", model.Username);
return TypedResults.BadRequest(ex.Message);
}
}
internal static async Task<IResult> LoginByProviderAsync(
HttpContext httpContext,
[FromServices] AuthenticationService authenticationService,
[FromServices] IFusionCache cache,
string provider = "",
string requestId = "")
{
var user = httpContext.User;
if (!string.IsNullOrWhiteSpace(provider) && !(user?.Identity?.IsAuthenticated ?? false))
{
var redirectUri = $"/api/Auth/Login?provider={Uri.EscapeDataString(provider)}";
if (!string.IsNullOrWhiteSpace(requestId))
redirectUri += $"&requestId={Uri.EscapeDataString(requestId)}";
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
{ "Action", AuthenticationProviderActionType.Login }
})
{
RedirectUri = redirectUri
};
return TypedResults.Challenge(properties, [provider]);
}
if (!string.IsNullOrWhiteSpace(provider) && (user?.Identity?.IsAuthenticated ?? false))
{
var code = !string.IsNullOrWhiteSpace(requestId) ? requestId : Guid.NewGuid().ToString();
var token = await authenticationService.LoginAsync(user!.Identity!.Name);
await cache.SetAsync($"AuthToken/{code}", token, TimeSpan.FromMinutes(5));
return TypedResults.Redirect($"/RedeemToken/{code}");
}
return TypedResults.BadRequest();
}
internal static async Task<IResult> LogoutAsync(
ClaimsPrincipal userPrincipal,
[FromServices] UserService userService,
[FromServices] ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger("AuthApi");
if (userPrincipal?.Identity?.IsAuthenticated ?? false)
await userService.SignOut();
logger.LogInformation("Logged out user {UserName}", userPrincipal?.Identity?.Name);
return TypedResults.Ok();
}
internal static IResult ValidateAsync(ClaimsPrincipal userPrincipal)
{
if (userPrincipal?.Identity?.IsAuthenticated ?? false)
return TypedResults.Ok();
return TypedResults.Unauthorized();
}
internal static async Task<IResult> RefreshAsync(
AuthToken token,
[FromServices] AuthenticationService authenticationService)
{
try
{
var refreshed = await authenticationService.RefreshTokenAsync(token);
return TypedResults.Ok(refreshed);
}
catch (Exception ex)
{
return TypedResults.BadRequest(ex);
}
}
internal static async Task<IResult> RegisterAsync(
[FromBody] RegisterModel model,
[FromServices] AuthenticationService authenticationService,
[FromServices] ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger("AuthApi");
try
{
var token = await authenticationService.RegisterAsync(model.UserName, model.Password);
return TypedResults.Ok(token);
}
catch (UserRegistrationException ex)
{
List<ErrorResponse.ErrorInfo> errorDetails =
ex.IdentityResult?.Errors.Select(FromIdentityResult).ToList() ?? [];
if (errorDetails.Count == 0)
{
errorDetails.Add(new ErrorResponse.ErrorInfo { Message = ex.Message });
}
var errorResponse = new ErrorResponse
{
Error = "UserRegistrationFailed",
Message = "User registration failed.",
Details = errorDetails,
};
return TypedResults.BadRequest(errorResponse);
}
catch (Exception ex)
{
logger.LogError(ex, ex.Message);
return TypedResults.BadRequest(ex.Message);
}
}
internal static IResult GetAuthenticationProvidersAsync(
[FromServices] IMapper mapper,
[FromServices] IOptions<Settings.Settings> settings)
{
var providers = mapper.Map<IEnumerable<AuthenticationProvider>>(
settings.Value.Server.Authentication.AuthenticationProviders);
return TypedResults.Ok(providers);
}
internal static async Task<IResult> RedeemTokenAsync(
Guid code,
[FromServices] IFusionCache cache)
{
var token = await cache.GetOrDefaultAsync<AuthToken>($"AuthToken/{code}", null);
if (token == null)
return TypedResults.NotFound();
await cache.RemoveAsync($"AuthToken/{code}");
return TypedResults.Ok(token);
}
private static ErrorResponse.ErrorInfo FromIdentityResult(IdentityError error)
{
return new ErrorResponse.ErrorInfo
{
Key = error.Code,
Message = error.Description,
};
}
}