Files
Elecciones-2025/Elecciones-Web/src/Elecciones.Api/Program.cs
dmolinari 316f49f25b feat(Worker): Adaptación integral para la API de Elecciones Nacionales
Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito.

Cambios principales:

- **Refactorización de `SondearResumenProvincialAsync`:**
  - Se elimina la dependencia del endpoint obsoleto `/getResumen`.
  - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados.

- **Expansión de `SondearResultadosMunicipalesAsync`:**
  - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad.
  - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios.

- **Modificación del Modelo de Datos:**
  - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`.
  - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio.

- **Ajustes de Nulabilidad en API Service:**
  - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625.

- **Deshabilitación de Seeders de Ejemplo:**
  - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00

442 lines
23 KiB
C#

//Elecciones.Api/Program.cs
using Elecciones.Database;
using Microsoft.EntityFrameworkCore;
using Serilog;
using Elecciones.Core.Services;
using Elecciones.Infrastructure.Services;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Elecciones.Database.Entities;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.HttpOverrides;
using Elecciones.Core.Enums;
using Microsoft.OpenApi.Models;
// Esta es la estructura estándar y recomendada.
var builder = WebApplication.CreateBuilder(args);
// 1. Registra el servicio del interruptor como un Singleton.
// Esto asegura que toda la aplicación comparta la MISMA instancia del interruptor.
builder.Services.AddSingleton<LoggingSwitchService>();
builder.Host.UseSerilog((context, services, configuration) =>
{
// 2. Obtenemos la instancia del interruptor que acabamos de registrar.
var loggingSwitch = services.GetRequiredService<LoggingSwitchService>();
configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
// 3. Establecemos el nivel mínimo de logging controlado por el interruptor.
.MinimumLevel.ControlledBy(loggingSwitch.LevelSwitch)
.WriteTo.Console()
.WriteTo.File("logs/api-.log", rollingInterval: RollingInterval.Day); // o "logs/worker-.log"
});
// 2. Añadir servicios al contenedor.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<EleccionesDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddScoped<IPasswordHasher, PasswordHasher>();
builder.Services.AddControllers().AddJsonOptions(options =>
{
// Esto le dice al serializador que maneje las referencias circulares
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
policy =>
{
policy.WithOrigins(
"http://localhost:5173",
"http://localhost:5174",
"http://192.168.5.128:8700",
"https://www.eldia.com",
"https://extras.eldia.com",
"https://eldia.mustang.cloud"
)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddEndpointsApiExplorer();
//builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(options =>
{
// 1. Definir el esquema de seguridad que usaremos (Bearer Token)
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "Autorización JWT usando el esquema Bearer. Ingresa 'Bearer' [espacio] y luego tu token. Ejemplo: 'Bearer 12345abcdef'",
Name = "Authorization", // El nombre del header HTTP
In = ParameterLocation.Header, // Dónde se ubicará el token (en el header)
Type = SecuritySchemeType.ApiKey, // El tipo de esquema
Scheme = "Bearer" // El nombre del esquema
});
// 2. Aplicar este requisito de seguridad a todos los endpoints que lo necesiten
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer" // Debe coincidir con el nombre que le dimos en AddSecurityDefinition
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
// 3. Construir la aplicación.
var app = builder.Build();
// --- LÓGICA PARA LEER EL NIVEL DE LOGGING AL INICIO ---
// Creamos un scope temporal para leer la configuración de la BD
using (var scope = app.Services.CreateScope()) // O 'host.Services.CreateScope()'
{
var services = scope.ServiceProvider;
try
{
// El resto de la lógica no cambia
var dbContext = services.GetRequiredService<EleccionesDbContext>();
var loggingSwitchService = services.GetRequiredService<LoggingSwitchService>();
var logLevelConfig = await dbContext.Configuraciones
.AsNoTracking()
.FirstOrDefaultAsync(c => c.Clave == "Logging_Level");
if (logLevelConfig != null)
{
loggingSwitchService.SetLoggingLevel(logLevelConfig.Valor);
Console.WriteLine($"--> Nivel de logging inicial establecido desde la BD a: {logLevelConfig.Valor}");
}
}
catch (Exception ex)
{
// Si hay un error (ej. la BD no está disponible al arrancar), se usará el nivel por defecto 'Information'.
Console.WriteLine($"--> No se pudo establecer el nivel de logging desde la BD: {ex.Message}");
}
}
app.UseForwardedHeaders();
// --- INICIO DEL BLOQUE DE SEEDERS UNIFICADO Y CORREGIDO ---
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<EleccionesDbContext>();
var logger = services.GetRequiredService<ILogger<Program>>();
var hasher = services.GetRequiredService<IPasswordHasher>();
// --- PASO 1: Añadir esta bandera de control ---
bool generarDatosDeEjemplo = false; // <-- Poner en 'false' para deshabilitar
// --- SEEDER 1: DATOS ESTRUCTURALES BÁSICOS (se ejecutan una sola vez si la BD está vacía) ---
// Estos son los datos maestros que NUNCA cambian.
// Usuario Admin
if (!await context.AdminUsers.AnyAsync())
{
var (hash, salt) = hasher.HashPassword("PTP847elec");
context.AdminUsers.Add(new AdminUser { Username = "admin", PasswordHash = hash, PasswordSalt = salt });
await context.SaveChangesAsync();
logger.LogInformation("--> Admin user seeded.");
}
// Elecciones
if (!await context.Elecciones.AnyAsync())
{
context.Elecciones.AddRange(
new Eleccion { Id = 1, Nombre = "Elecciones Provinciales 2025", Nivel = "Provincial", DistritoId = "02", Fecha = new DateOnly(2025, 10, 26) },
new Eleccion { Id = 2, Nombre = "Elecciones Nacionales 2025", Nivel = "Nacional", DistritoId = "00", Fecha = new DateOnly(2025, 10, 26) }
);
await context.SaveChangesAsync();
logger.LogInformation("--> Seeded Eleccion entities.");
}
// Bancas Físicas
if (!await context.Bancadas.AnyAsync())
{
var bancas = new List<Bancada>();
for (int i = 1; i <= 92; i++) { bancas.Add(new Bancada { EleccionId = 1, Camara = TipoCamara.Diputados, NumeroBanca = i }); }
for (int i = 1; i <= 46; i++) { bancas.Add(new Bancada { EleccionId = 1, Camara = TipoCamara.Senadores, NumeroBanca = i }); }
for (int i = 1; i <= 257; i++) { bancas.Add(new Bancada { EleccionId = 2, Camara = TipoCamara.Diputados, NumeroBanca = i }); }
for (int i = 1; i <= 72; i++) { bancas.Add(new Bancada { EleccionId = 2, Camara = TipoCamara.Senadores, NumeroBanca = i }); }
await context.Bancadas.AddRangeAsync(bancas);
await context.SaveChangesAsync();
logger.LogInformation("--> Seeded {Count} bancas físicas para ambas elecciones.", bancas.Count);
}
// Configuraciones por Defecto
var defaultConfiguraciones = new Dictionary<string, string> {
{ "MostrarOcupantes", "true" }, { "TickerResultadosCantidad", "3" }, { "ConcejalesResultadosCantidad", "5" },
{ "Worker_Resultados_Activado", "false" }, { "Worker_Bajas_Activado", "false" }, { "Worker_Prioridad", "Resultados" },
{ "Logging_Level", "Information" }, { "PresidenciaDiputadosNacional", "" }, { "PresidenciaDiputadosNacional_TipoBanca", "ganada" },
{ "PresidenciaSenadoNacional_TipoBanca", "ganada" }
};
foreach (var config in defaultConfiguraciones)
{
if (!await context.Configuraciones.AnyAsync(c => c.Clave == config.Key))
context.Configuraciones.Add(new Configuracion { Clave = config.Key, Valor = config.Value });
}
await context.SaveChangesAsync();
logger.LogInformation("--> Default configurations verified/seeded.");
// --- PASO 2: Envolver todo el bloque del Seeder 2 en esta condición ---
if (generarDatosDeEjemplo)
{
// --- SEEDER 2: DATOS DE EJEMPLO PARA ELECCIÓN NACIONAL (se ejecuta solo si faltan sus votos) ---
const int eleccionNacionalId = 2;
if (!await context.ResultadosVotos.AnyAsync(r => r.EleccionId == eleccionNacionalId))
{
logger.LogInformation("--> No se encontraron datos de votos para la elección nacional ID {EleccionId}. Generando datos de simulación...", eleccionNacionalId);
// PASO A: VERIFICAR/CREAR DEPENDENCIAS (Ámbitos, Categorías)
if (!await context.CategoriasElectorales.AnyAsync(c => c.Id == 1))
context.CategoriasElectorales.Add(new CategoriaElectoral { Id = 1, Nombre = "SENADORES NACIONALES", Orden = 2 });
if (!await context.CategoriasElectorales.AnyAsync(c => c.Id == 2))
context.CategoriasElectorales.Add(new CategoriaElectoral { Id = 2, Nombre = "DIPUTADOS NACIONALES", Orden = 3 });
var provinciasMaestras = new Dictionary<string, string> {
{ "01", "CIUDAD AUTONOMA DE BUENOS AIRES" }, { "02", "BUENOS AIRES" }, { "03", "CATAMARCA" }, { "04", "CORDOBA" }, { "05", "CORRIENTES" },
{ "06", "CHACO" }, { "07", "CHUBUT" }, { "08", "ENTRE RIOS" }, { "09", "FORMOSA" }, { "10", "JUJUY" }, { "11", "LA PAMPA" },
{ "12", "LA RIOJA" }, { "13", "MENDOZA" }, { "14", "MISIONES" }, { "15", "NEUQUEN" }, { "16", "RIO NEGRO" }, { "17", "SALTA" },
{ "18", "SAN JUAN" }, { "19", "SAN LUIS" }, { "20", "SANTA CRUZ" }, { "21", "SANTA FE" }, { "22", "SANTIAGO DEL ESTERO" },
{ "23", "TIERRA DEL FUEGO" }, { "24", "TUCUMAN" }
};
foreach (var p in provinciasMaestras)
{
if (!await context.AmbitosGeograficos.AnyAsync(a => a.NivelId == 10 && a.DistritoId == p.Key))
context.AmbitosGeograficos.Add(new AmbitoGeografico { Nombre = p.Value, NivelId = 10, DistritoId = p.Key });
}
await context.SaveChangesAsync();
var provinciasEnDb = await context.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10).ToListAsync();
foreach (var provincia in provinciasEnDb)
{
if (!await context.AmbitosGeograficos.AnyAsync(a => a.NivelId == 30 && a.DistritoId == provincia.DistritoId))
{
for (int i = 1; i <= 5; i++)
context.AmbitosGeograficos.Add(new AmbitoGeografico { Nombre = $"{provincia.Nombre} - Depto. {i}", NivelId = 30, DistritoId = provincia.DistritoId });
}
}
await context.SaveChangesAsync();
logger.LogInformation("--> Datos maestros para Elección Nacional (Ámbitos, Categorías) verificados/creados.");
// PASO B: GENERAR DATOS TRANSACCIONALES (Votos, Recuentos, etc.)
var todosLosPartidos = await context.AgrupacionesPoliticas.Take(5).ToListAsync();
if (!todosLosPartidos.Any())
{
logger.LogWarning("--> No hay partidos en la BD, no se pueden generar votos de ejemplo.");
return; // Salir si no hay partidos para evitar errores
}
// (La lógica interna de generación de votos y recuentos que ya tenías y funcionaba)
// ... (el código de generación de `nuevosResultados` y `nuevosEstados` va aquí, sin cambios)
var nuevosResultados = new List<ResultadoVoto>();
var nuevosEstados = new List<EstadoRecuentoGeneral>();
var rand = new Random();
var provinciasQueRenuevanSenadores = new HashSet<string> { "01", "06", "08", "15", "16", "17", "22", "23" };
var categoriaDiputadosNac = await context.CategoriasElectorales.FindAsync(2);
var categoriaSenadoresNac = await context.CategoriasElectorales.FindAsync(1);
long totalVotosNacionalDip = 0, totalVotosNacionalSen = 0;
int totalMesasNacionalDip = 0, totalMesasNacionalSen = 0;
int totalMesasEscrutadasNacionalDip = 0, totalMesasEscrutadasNacionalSen = 0;
foreach (var provincia in provinciasEnDb)
{
var municipiosDeProvincia = await context.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 30 && a.DistritoId == provincia.DistritoId).ToListAsync();
if (!municipiosDeProvincia.Any()) continue;
var categoriasParaProcesar = new List<CategoriaElectoral> { categoriaDiputadosNac! };
if (provinciasQueRenuevanSenadores.Contains(provincia.DistritoId!))
categoriasParaProcesar.Add(categoriaSenadoresNac!);
foreach (var categoria in categoriasParaProcesar)
{
long totalVotosProvinciaCategoria = 0;
int partidoIndex = rand.Next(todosLosPartidos.Count);
foreach (var municipio in municipiosDeProvincia)
{
var partidoGanador = todosLosPartidos[partidoIndex++ % todosLosPartidos.Count];
var votosGanador = rand.Next(25000, 70000);
nuevosResultados.Add(new ResultadoVoto { EleccionId = eleccionNacionalId, AmbitoGeograficoId = municipio.Id, CategoriaId = categoria.Id, AgrupacionPoliticaId = partidoGanador.Id, CantidadVotos = votosGanador });
totalVotosProvinciaCategoria += votosGanador;
var otrosPartidos = todosLosPartidos.Where(p => p.Id != partidoGanador.Id).OrderBy(p => rand.Next()).Take(rand.Next(3, todosLosPartidos.Count));
foreach (var competidor in otrosPartidos)
{
var votosCompetidor = rand.Next(1000, 24000);
nuevosResultados.Add(new ResultadoVoto { EleccionId = eleccionNacionalId, AmbitoGeograficoId = municipio.Id, CategoriaId = categoria.Id, AgrupacionPoliticaId = competidor.Id, CantidadVotos = votosCompetidor });
totalVotosProvinciaCategoria += votosCompetidor;
}
}
var mesasEsperadasProvincia = municipiosDeProvincia.Count * rand.Next(15, 30);
var mesasTotalizadasProvincia = (int)(mesasEsperadasProvincia * (rand.Next(75, 99) / 100.0));
var cantidadElectoresProvincia = mesasEsperadasProvincia * 350;
var participacionProvincia = (decimal)(rand.Next(65, 85) / 100.0);
nuevosEstados.Add(new EstadoRecuentoGeneral
{
EleccionId = eleccionNacionalId,
AmbitoGeograficoId = provincia.Id,
CategoriaId = categoria.Id,
FechaTotalizacion = DateTime.UtcNow,
MesasEsperadas = mesasEsperadasProvincia,
MesasTotalizadas = mesasTotalizadasProvincia,
MesasTotalizadasPorcentaje = mesasEsperadasProvincia > 0 ? (decimal)mesasTotalizadasProvincia * 100 / mesasEsperadasProvincia : 0,
CantidadElectores = cantidadElectoresProvincia,
CantidadVotantes = (int)(cantidadElectoresProvincia * participacionProvincia),
ParticipacionPorcentaje = participacionProvincia * 100
});
if (categoriaDiputadosNac != null && categoria.Id == categoriaDiputadosNac.Id)
{
totalVotosNacionalDip += totalVotosProvinciaCategoria; totalMesasNacionalDip += mesasEsperadasProvincia; totalMesasEscrutadasNacionalDip += mesasTotalizadasProvincia;
}
else
{
totalVotosNacionalSen += totalVotosProvinciaCategoria; totalMesasNacionalSen += mesasEsperadasProvincia; totalMesasEscrutadasNacionalSen += mesasTotalizadasProvincia;
}
}
}
var ambitoNacional = await context.AmbitosGeograficos.AsNoTracking().FirstOrDefaultAsync(a => a.NivelId == 0);
if (ambitoNacional != null && categoriaDiputadosNac != null && categoriaSenadoresNac != null)
{
var participacionNacionalDip = (decimal)(rand.Next(70, 88) / 100.0);
nuevosEstados.Add(new EstadoRecuentoGeneral
{
EleccionId = eleccionNacionalId,
AmbitoGeograficoId = ambitoNacional.Id,
CategoriaId = categoriaDiputadosNac.Id,
FechaTotalizacion = DateTime.UtcNow,
MesasEsperadas = totalMesasNacionalDip,
MesasTotalizadas = totalMesasEscrutadasNacionalDip,
MesasTotalizadasPorcentaje = totalMesasNacionalDip > 0 ? (decimal)totalMesasEscrutadasNacionalDip * 100 / totalMesasNacionalDip : 0,
CantidadElectores = totalMesasNacionalDip * 350,
CantidadVotantes = (int)((totalMesasNacionalDip * 350) * participacionNacionalDip),
ParticipacionPorcentaje = participacionNacionalDip * 100
});
var participacionNacionalSen = (decimal)(rand.Next(70, 88) / 100.0);
nuevosEstados.Add(new EstadoRecuentoGeneral
{
EleccionId = eleccionNacionalId,
AmbitoGeograficoId = ambitoNacional.Id,
CategoriaId = categoriaSenadoresNac.Id,
FechaTotalizacion = DateTime.UtcNow,
MesasEsperadas = totalMesasNacionalSen,
MesasTotalizadas = totalMesasEscrutadasNacionalSen,
MesasTotalizadasPorcentaje = totalMesasNacionalSen > 0 ? (decimal)totalMesasEscrutadasNacionalSen * 100 / totalMesasNacionalSen : 0,
CantidadElectores = totalMesasNacionalSen * 350,
CantidadVotantes = (int)((totalMesasNacionalSen * 350) * participacionNacionalSen),
ParticipacionPorcentaje = participacionNacionalSen * 100
});
}
else
{
logger.LogWarning("--> No se encontró el ámbito nacional (NivelId == 0) o las categorías electorales nacionales. No se agregaron estados nacionales.");
}
if (nuevosResultados.Any())
{
await context.ResultadosVotos.AddRangeAsync(nuevosResultados);
await context.EstadosRecuentosGenerales.AddRangeAsync(nuevosEstados);
await context.SaveChangesAsync();
logger.LogInformation("--> Se generaron {Votos} registros de votos y {Estados} de estados de recuento.", nuevosResultados.Count, nuevosEstados.Count);
}
// PASO C: GENERAR BANCAS PREVIAS Y PROYECCIONES
if (!await context.BancasPrevias.AnyAsync(b => b.EleccionId == eleccionNacionalId))
{
var bancasPrevias = new List<BancaPrevia> {
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = todosLosPartidos[0].Id, Cantidad = 40 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = todosLosPartidos[1].Id, Cantidad = 35 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = todosLosPartidos[2].Id, Cantidad = 30 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = todosLosPartidos[3].Id, Cantidad = 15 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = todosLosPartidos[4].Id, Cantidad = 10 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = todosLosPartidos[0].Id, Cantidad = 18 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = todosLosPartidos[1].Id, Cantidad = 15 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = todosLosPartidos[2].Id, Cantidad = 8 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = todosLosPartidos[3].Id, Cantidad = 4 },
new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = todosLosPartidos[4].Id, Cantidad = 3 },
};
await context.BancasPrevias.AddRangeAsync(bancasPrevias);
await context.SaveChangesAsync();
logger.LogInformation("--> Seeded Bancas Previas para la Elección Nacional.");
}
}
}
}
// --- FIN DEL BLOQUE DE SEEDERS UNIFICADO ---
// Configurar el pipeline de peticiones HTTP.
// Añadimos el logging de peticiones de Serilog aquí.
app.UseSerilogRequestLogging();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 1. Redirección a HTTPS (si se usa)
//app.UseHttpsRedirection();
// 2. Middleware de Enrutamiento. ¡CLAVE!
// Determina a qué endpoint irá la petición.
app.UseRouting();
// 3. Middleware de CORS.
// Ahora se ejecuta sabiendo a qué endpoint se dirige la petición.
app.UseCors(MyAllowSpecificOrigins);
// 4. Middleware de Autenticación.
// Identifica quién es el usuario a partir del token.
app.UseAuthentication();
// 5. Middleware de Autorización.
// Verifica si el usuario identificado tiene permiso para acceder al endpoint.
app.UseAuthorization();
// 6. Mapea los controladores a los endpoints que el enrutador descubrió.
app.MapControllers();
// 5. Ejecutar la aplicación.
app.Run();