using System.Net; using Microsoft.Extensions.Primitives; using Pos.Transactions.Bff.Application.Adapters; using Pos.Transactions.Bff.Application.Handlers; using Pos.Transactions.Bff.Contracts.Requests; using Pos.Transactions.Bff.Rest.Security; const string CorrelationHeaderName = "x-correlation-id"; const string SessionAccessCookieName = "thalos_session"; const string SessionRefreshCookieName = "thalos_refresh"; var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient((serviceProvider, httpClient) => { var configuration = serviceProvider.GetRequiredService(); var operationsBaseAddress = configuration["OperationsService:BaseAddress"] ?? "http://operations-service:8080"; httpClient.BaseAddress = new Uri($"{operationsBaseAddress.TrimEnd('/')}/"); }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHttpClient("ThalosAuth"); var app = builder.Build(); app.Use(async (context, next) => { var correlationId = ResolveCorrelationId(context); context.Items[CorrelationHeaderName] = correlationId; context.Request.Headers[CorrelationHeaderName] = correlationId; context.Response.Headers[CorrelationHeaderName] = correlationId; await next(); }); app.MapGet("/api/pos/transactions/summary", async ( string contextId, HttpContext context, IHttpClientFactory httpClientFactory, IConfiguration configuration, IGetPosTransactionSummaryHandler handler, CancellationToken ct) => { var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct); if (authError is not null) { return authError; } var request = new GetPosTransactionSummaryRequest(contextId); return Results.Ok(await handler.HandleAsync(request, ct)); }); app.MapGet("/api/pos/transactions/recent-payments", async ( string contextId, HttpContext context, IHttpClientFactory httpClientFactory, IConfiguration configuration, IGetRecentPosPaymentsHandler handler, CancellationToken ct) => { var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct); if (authError is not null) { return authError; } var request = new GetPosTransactionSummaryRequest(contextId); return Results.Ok(await handler.HandleAsync(request, ct)); }); app.MapGet("/api/pos/transactions/{transactionId}", async ( string transactionId, string contextId, HttpContext context, IHttpClientFactory httpClientFactory, IConfiguration configuration, IGetPosTransactionDetailHandler handler, CancellationToken ct) => { var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct); if (authError is not null) { return authError; } var request = new GetPosTransactionDetailRequest(contextId, transactionId); return Results.Ok(await handler.HandleAsync(request, ct)); }); app.MapPost("/api/pos/transactions/payments", async ( CapturePosPaymentRequest request, HttpContext context, IHttpClientFactory httpClientFactory, IConfiguration configuration, ICapturePosPaymentHandler handler, CancellationToken ct) => { var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct); if (authError is not null) { return authError; } return Results.Ok(await handler.HandleAsync(request, ct)); }); app.MapGet("/health", () => Results.Ok(new { status = "ok", service = "pos-transactions-bff" })); app.MapGet("/healthz", () => Results.Ok(new { status = "ok", service = "pos-transactions-bff" })); app.Run(); string ResolveCorrelationId(HttpContext context) { if (context.Items.TryGetValue(CorrelationHeaderName, out var itemValue) && itemValue is string itemCorrelationId && !string.IsNullOrWhiteSpace(itemCorrelationId)) { return itemCorrelationId; } if (context.Request.Headers.TryGetValue(CorrelationHeaderName, out var headerValue) && !StringValues.IsNullOrEmpty(headerValue)) { return headerValue.ToString(); } return context.TraceIdentifier; } async Task EnforceSessionAsync( HttpContext context, IHttpClientFactory httpClientFactory, IConfiguration configuration, CancellationToken ct) { var correlationId = ResolveCorrelationId(context); if (!context.Request.Cookies.ContainsKey(SessionAccessCookieName) && !context.Request.Cookies.ContainsKey(SessionRefreshCookieName)) { return ErrorResponse(StatusCodes.Status401Unauthorized, "session_missing", "No active session.", correlationId); } var thalosBaseAddress = configuration["ThalosAuth:BaseAddress"] ?? "http://thalos-bff:8080"; using var request = new HttpRequestMessage( HttpMethod.Get, $"{thalosBaseAddress.TrimEnd('/')}/api/identity/session/me"); request.Headers.TryAddWithoutValidation(CorrelationHeaderName, correlationId); var cookieHeader = BuildForwardCookieHeader(context); if (!string.IsNullOrWhiteSpace(cookieHeader)) { request.Headers.TryAddWithoutValidation("Cookie", cookieHeader); } HttpResponseMessage response; try { response = await httpClientFactory.CreateClient("ThalosAuth").SendAsync(request, ct); } catch (HttpRequestException) { return ErrorResponse( StatusCodes.Status503ServiceUnavailable, "identity_unavailable", "Identity service is temporarily unavailable.", correlationId); } catch (TaskCanceledException) { return ErrorResponse( StatusCodes.Status503ServiceUnavailable, "identity_timeout", "Identity service did not respond in time.", correlationId); } using (response) { if (response.StatusCode == HttpStatusCode.Forbidden) { return ErrorResponse(StatusCodes.Status403Forbidden, "forbidden", "Permission denied.", correlationId); } if (response.StatusCode == HttpStatusCode.Unauthorized) { return ErrorResponse(StatusCodes.Status401Unauthorized, "unauthorized", "Unauthorized request.", correlationId); } if (!response.IsSuccessStatusCode) { return ErrorResponse(StatusCodes.Status401Unauthorized, "session_invalid", "Session validation failed.", correlationId); } var payload = await response.Content.ReadAsStringAsync(ct); if (!SessionMePayloadParser.IsAuthenticated(payload)) { return ErrorResponse(StatusCodes.Status401Unauthorized, "session_invalid", "Session validation failed.", correlationId); } } return null; } static string BuildForwardCookieHeader(HttpContext context) { var cookies = new List(); if (context.Request.Cookies.TryGetValue(SessionAccessCookieName, out var accessCookie) && !string.IsNullOrWhiteSpace(accessCookie)) { cookies.Add($"{SessionAccessCookieName}={accessCookie}"); } if (context.Request.Cookies.TryGetValue(SessionRefreshCookieName, out var refreshCookie) && !string.IsNullOrWhiteSpace(refreshCookie)) { cookies.Add($"{SessionRefreshCookieName}={refreshCookie}"); } return string.Join("; ", cookies); } static IResult ErrorResponse(int statusCode, string code, string message, string correlationId) { return Results.Json(new AuthErrorResponse(code, message, correlationId), statusCode: statusCode); } sealed record AuthErrorResponse(string Code, string Message, string CorrelationId);