using ItWorx.KnowledgeHub.Api.Security; using ItWorx.KnowledgeHub.Application.Identity; using ItWorx.KnowledgeHub.Application.Identity.UseCases; using ItWorx.KnowledgeHub.Application.Ingestion; using ItWorx.KnowledgeHub.Application.Platform; using ItWorx.KnowledgeHub.Contracts.Auth; using ItWorx.KnowledgeHub.Contracts.Errors; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; namespace ItWorx.KnowledgeHub.Api.Endpoints; public static class AuthEndpoints { public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/auth").WithTags("Auth"); group.MapGet("/csrf", GetCsrfTokenAsync); group.MapGet("/session", GetSessionAsync); group.MapPost("/login", LoginAsync) .RequireRateLimiting(RateLimitPolicies.Login); group.MapPost("/logout", LogoutAsync) .AddEndpointFilter(); group.MapPost("/recovery/redeem", RedeemRecoveryAsync) .RequireRateLimiting(RateLimitPolicies.Recovery); return app; } private static Ok GetCsrfTokenAsync(HttpContext httpContext, IAntiforgery antiforgery) { var tokens = antiforgery.GetAndStoreTokens(httpContext); return TypedResults.Ok(new CsrfTokenResponse(tokens.RequestToken!, tokens.HeaderName!)); } private static async Task> GetSessionAsync( GetSessionUseCase useCase, IPlatformSettingsReader platformSettings, CancellationToken cancellationToken) { var session = await useCase.ExecuteAsync(cancellationToken); var ingestion = (await platformSettings.GetEffectiveAsync(cancellationToken)).Values.IngestionProposals; return TypedResults.Ok(ToSessionResponse(session, ingestion)); } private static async Task, ProblemHttpResult>> LoginAsync( [FromBody] LoginRequest request, LoginUseCase loginUseCase, GetSessionUseCase sessionUseCase, IPlatformSettingsReader platformSettings, HttpContext httpContext, CancellationToken cancellationToken) { var correlationId = httpContext.Items[CorrelationIdMiddleware.ItemKey] as string; var outcome = await loginUseCase.ExecuteAsync( new LoginCommand(request.Email, request.Password, correlationId), cancellationToken); switch (outcome.Status) { case PasswordSignInStatus.Success: var session = await sessionUseCase.ExecuteAsync(cancellationToken); var ingestion = (await platformSettings.GetEffectiveAsync(cancellationToken)).Values.IngestionProposals; return TypedResults.Ok(ToSessionResponse(session, ingestion)); case PasswordSignInStatus.LockedOut: return TypedResults.Problem( statusCode: StatusCodes.Status429TooManyRequests, type: ProblemTypes.AccountLockedOut, title: "Account tijdelijk vergrendeld.", detail: "Te veel mislukte aanmeldpogingen. Probeer het later opnieuw."); // Deliberately identical to the default InvalidCredentials case: a disabled // account must not be distinguishable from a wrong password. case PasswordSignInStatus.AccountDisabled: default: return TypedResults.Problem( statusCode: StatusCodes.Status401Unauthorized, type: ProblemTypes.InvalidCredentials, title: "Aanmelden mislukt.", detail: "E-mailadres of wachtwoord is onjuist."); } } private static async Task LogoutAsync( LogoutUseCase useCase, HttpContext httpContext, CancellationToken cancellationToken) { var correlationId = httpContext.Items[CorrelationIdMiddleware.ItemKey] as string; await useCase.ExecuteAsync(correlationId, cancellationToken); return TypedResults.Ok(); } private static async Task> RedeemRecoveryAsync( [FromBody] RecoveryRedeemRequest request, RedeemRecoveryTokenUseCase useCase, HttpContext httpContext, CancellationToken cancellationToken) { var correlationId = httpContext.Items[CorrelationIdMiddleware.ItemKey] as string; var result = await useCase.ExecuteAsync(request.Token, request.NewPassword, correlationId, cancellationToken); return result.Status switch { RedeemRecoveryTokenStatus.Success => TypedResults.Ok(), RedeemRecoveryTokenStatus.InvalidOrExpiredToken => TypedResults.Problem( statusCode: StatusCodes.Status400BadRequest, type: ProblemTypes.InvalidRecoveryToken, title: "Ongeldige of verlopen herstelcode.", detail: "Vraag een nieuwe herstelcode aan bij een beheerder."), _ => TypedResults.Problem( statusCode: StatusCodes.Status422UnprocessableEntity, type: ProblemTypes.Validation, title: "Ongeldig wachtwoord.", detail: string.Join(" ", result.Errors)) }; } private static SessionResponse ToSessionResponse(SessionInfo session, ItWorx.KnowledgeHub.Domain.Platform.IngestionProposalPolicyValues ingestionOptions) => new( session.Authenticated, session.SetupRequired, session.UserId, session.DisplayName, session.Email, session.WorkspaceId, session.Role?.ToString(), session.Authenticated ? new FeatureFlagsResponse(ingestionOptions.Enabled) : null); }