using JSMR.Application.Circles.Queries.Search; using JSMR.Application.Creators.Queries.Search; using JSMR.Application.Tags.Queries.Search; using JSMR.Application.Users; using JSMR.Application.VoiceWorks.Queries.Search; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Serilog; using System.Diagnostics; using System.Security.Claims; namespace JSMR.Api.Startup; public static class WebApplicationExtensions { public static WebApplication UseAppPipeline(this WebApplication app, IHostEnvironment env) { app.UseCors("ui"); if (env.IsDevelopment()) app.MapOpenApi(); app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.UseSerilogRequestLogging(opts => { opts.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; }); app.Use(async (ctx, next) => { var traceId = Activity.Current?.TraceId.ToString() ?? ctx.TraceIdentifier; using (Serilog.Context.LogContext.PushProperty("TraceId", traceId)) { ctx.Response.Headers["x-trace-id"] = traceId; await next(); } }); return app; } public static void MapAppEndpoints(this WebApplication app) { app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.MapSearchEndpoints(); app.MapAuthenticationEndpoints(); } private static void MapSearchEndpoints(this WebApplication app) { app.MapPost("/api/circles/search", async ( SearchCirclesRequest request, SearchCirclesHandler handler, CancellationToken ct) => { try { var result = await handler.HandleAsync(request, ct); return Results.Ok(result); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { return Results.StatusCode(StatusCodes.Status499ClientClosedRequest); } }); app.MapPost("/api/voiceworks/search", async ( SearchVoiceWorksRequest request, SearchVoiceWorksHandler handler, CancellationToken ct) => { var result = await handler.HandleAsync(request, ct); return Results.Ok(result); }); app.MapPost("/api/tags/search", async ( SearchTagsRequest request, SearchTagsHandler handler, CancellationToken ct) => { var result = await handler.HandleAsync(request, ct); return Results.Ok(result); }); app.MapPost("/api/creators/search", async ( SearchCreatorsRequest request, SearchCreatorsHandler handler, CancellationToken ct) => { var result = await handler.HandleAsync(request, ct); return Results.Ok(result); }); } private static void MapAuthenticationEndpoints(this WebApplication app) { app.MapPost("/auth/login", async (LoginRequest req, IUserRepository users, HttpContext http) => { var user = await users.FindByUsernameAsync(req.Username); if (user is null || !user.IsActive) return Results.Unauthorized(); if (!users.VerifyPassword(user, req.Password)) return Results.Unauthorized(); var claims = new List { new(ClaimTypes.NameIdentifier, user.Id.ToString()), new(ClaimTypes.Name, user.Username), new(ClaimTypes.Role, user.Role ?? "User") }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); await http.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); return Results.Ok(new { ok = true }); }); app.MapPost("/auth/logout", async (HttpContext http) => { await http.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Results.Ok(new { ok = true }); }); app.MapGet("/api/me", (ClaimsPrincipal user) => { return Results.Ok(new { name = user.Identity?.Name, id = user.FindFirstValue(ClaimTypes.NameIdentifier), role = user.FindFirstValue(ClaimTypes.Role) }); }).RequireAuthorization(); } public record LoginRequest(string Username, string Password); }