Files
Antifraude.Net/Antifraude.Net/GestionaDenunciasAN/Program.cs
2026-04-06 17:12:06 +02:00

259 lines
9.2 KiB
C#

using System.Globalization;
using System.Security.Claims;
using System.Text.RegularExpressions;
using GestionaDenunciasAN.Components;
using GestionaDenunciasAN.Configuration;
using GestionaDenunciasAN.Models;
using GestionaDenunciasAN.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.GetCultureInfo("es-ES");
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo("es-ES");
builder.Services.Configure<GestionaOptions>(builder.Configuration.GetSection("Gestiona"));
builder.Services.Configure<GlobalLeaksOptions>(builder.Configuration.GetSection(GlobalLeaksOptions.SectionName));
builder.Services.Configure<ComplaintStorageOptions>(builder.Configuration.GetSection(ComplaintStorageOptions.SectionName));
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "denuncias.auth";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.LoginPath = "/";
options.LogoutPath = "/api/auth/logout";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = false;
});
builder.Services.AddAuthorization();
builder.Services.AddDataProtection();
builder.Services.AddServerSideBlazor().AddCircuitOptions(option => { option.DetailedErrors = true; });
builder.Services.AddHttpContextAccessor();
builder.Services.AddBlazorBootstrap();
builder.Services.AddAntiforgery();
builder.Services.AddScoped<UserState>();
builder.Services.AddSingleton<AppSessionLifetime>();
builder.Services.AddSingleton<LoginRateLimiter>();
builder.Services.AddSingleton<GlobalLeaksSessionStore>();
builder.Services.AddScoped<GlobalLeaksClient>();
builder.Services.AddScoped<IDenunciaStore, MySqlDenunciaStore>();
builder.Services.AddScoped<IInboxTrackingService, InboxTrackingService>();
builder.Services.AddScoped<DenunciaInboxService>();
builder.Services.AddScoped<GestionaDocumentWorkflowService>();
builder.Services.AddHttpClient<IGestionaService, GestionaService>((sp, client) =>
{
var opts = sp.GetRequiredService<IOptions<GestionaOptions>>().Value;
client.BaseAddress = new Uri(opts.ApiBase);
client.DefaultRequestHeaders.Add("X-Gestiona-Access-Token", opts.AccessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.Use(async (context, next) =>
{
context.Response.Headers.XFrameOptions = "DENY";
context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers["Referrer-Policy"] = "no-referrer";
context.Response.Headers.ContentSecurityPolicy =
"default-src 'self'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net";
await next();
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
var path = context.Request.Path;
var isPublic =
path == "/" ||
path.StartsWithSegments("/api") ||
path.StartsWithSegments("/_blazor") ||
path.StartsWithSegments("/_framework") ||
path.StartsWithSegments("/bootstrap") ||
path.StartsWithSegments("/Content") ||
path.StartsWithSegments("/Scripts") ||
path.StartsWithSegments("/css") ||
path.StartsWithSegments("/js") ||
path.StartsWithSegments("/favicon") ||
Path.HasExtension(path.Value);
if (context.User.Identity?.IsAuthenticated == true)
{
var username = context.User.Identity?.Name?.Trim();
var appSessionLifetime = context.RequestServices.GetRequiredService<AppSessionLifetime>();
var cookieStartupStamp = context.User.FindFirst("app_startup_stamp")?.Value;
var hasStoredCredentials = false;
var cookieBelongsToCurrentStartup =
!string.IsNullOrWhiteSpace(cookieStartupStamp) &&
string.Equals(cookieStartupStamp, appSessionLifetime.StartupStamp, StringComparison.Ordinal);
if (!string.IsNullOrWhiteSpace(username))
{
var sessionStore = context.RequestServices.GetRequiredService<GlobalLeaksSessionStore>();
var storedSession = await sessionStore.GetAsync(username, context.RequestAborted);
hasStoredCredentials =
storedSession is not null &&
!string.IsNullOrWhiteSpace(storedSession.Username) &&
!string.IsNullOrWhiteSpace(storedSession.Password);
}
if (!hasStoredCredentials || !cookieBelongsToCurrentStartup)
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (!isPublic)
{
var returnUrl = $"{context.Request.Path}{context.Request.QueryString}";
context.Response.Redirect($"/?returnUrl={Uri.EscapeDataString(returnUrl)}");
return;
}
}
}
if (isPublic || context.User.Identity?.IsAuthenticated == true)
{
await next();
return;
}
var loginReturnUrl = $"{context.Request.Path}{context.Request.QueryString}";
context.Response.Redirect($"/?returnUrl={Uri.EscapeDataString(loginReturnUrl)}");
});
app.UseAntiforgery();
var api = app.MapGroup("/api");
api.MapPost("/auth/login", async (
LoginRequest request,
HttpContext httpContext,
GlobalLeaksClient globalLeaksClient,
GlobalLeaksSessionStore sessionStore,
LoginRateLimiter rateLimiter,
CancellationToken cancellationToken) =>
{
var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var appSessionLifetime = httpContext.RequestServices.GetRequiredService<AppSessionLifetime>();
if (!rateLimiter.AllowAttempt(ip))
{
return Results.Json(
new ApiError("Demasiados intentos. Espera un minuto."),
statusCode: StatusCodes.Status429TooManyRequests);
}
if (string.IsNullOrWhiteSpace(request.Username) ||
string.IsNullOrWhiteSpace(request.Password) ||
string.IsNullOrWhiteSpace(request.Authcode))
{
return Results.Json(
new ApiError("Debes indicar usuario, contraseña y código 2FA."),
statusCode: StatusCodes.Status400BadRequest);
}
if (!Regex.IsMatch(request.Authcode.Trim(), @"^\d{6}$"))
{
return Results.Json(
new ApiError("El código 2FA debe tener exactamente 6 dígitos."),
statusCode: StatusCodes.Status400BadRequest);
}
try
{
var session = await globalLeaksClient.LoginAsync(
request.Username.Trim(),
request.Password,
request.Authcode.Trim(),
cancellationToken);
var resolvedUsername = string.IsNullOrWhiteSpace(session.Username)
? request.Username.Trim()
: session.Username.Trim();
session = new GlSession(session.Id, resolvedUsername, session.Role);
await sessionStore.SaveAsync(
session.Username,
request.Password,
session.Id,
session.Role,
cancellationToken);
var claims = new List<Claim>
{
new(ClaimTypes.Name, session.Username),
new("app_startup_stamp", appSessionLifetime.StartupStamp),
};
if (!string.IsNullOrWhiteSpace(session.Role))
{
claims.Add(new Claim("gl_role", session.Role));
}
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
var authProperties = new AuthenticationProperties
{
IsPersistent = false,
AllowRefresh = true,
};
await httpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
authProperties);
return Results.Ok(new LoginResponse(session.Username));
}
catch (GlobalLeaksValidationException ex)
{
return Results.Json(new ApiError(ex.Message), statusCode: ex.StatusCode);
}
catch
{
return Results.Json(
new ApiError("No se ha podido conectar con GlobalLeaks."),
statusCode: StatusCodes.Status502BadGateway);
}
}).DisableAntiforgery();
api.MapPost("/auth/logout", async (
HttpContext httpContext,
GlobalLeaksSessionStore sessionStore,
CancellationToken cancellationToken) =>
{
var username = httpContext.User.Identity?.Name;
if (!string.IsNullOrWhiteSpace(username))
{
await sessionStore.DeleteAsync(username, cancellationToken);
}
await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.Ok(new { ok = true });
}).DisableAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();