2025-09-06 21:44:52 -03:00
//Elecciones.Api/Program.cs
2025-08-14 13:12:16 -03:00
using Elecciones.Database ;
using Microsoft.EntityFrameworkCore ;
2025-08-15 17:31:51 -03:00
using Serilog ;
2025-08-29 09:54:22 -03:00
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 ;
2025-09-03 17:54:49 -03:00
using Microsoft.AspNetCore.HttpOverrides ;
2025-08-14 13:12:16 -03:00
2025-08-15 17:31:51 -03:00
// Esta es la estructura estándar y recomendada.
2025-08-14 12:37:57 -03:00
var builder = WebApplication . CreateBuilder ( args ) ;
2025-09-06 21:44:52 -03:00
// 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"
} ) ;
2025-08-14 13:12:16 -03:00
2025-08-15 17:31:51 -03:00
// 2. Añadir servicios al contenedor.
2025-08-14 13:12:16 -03:00
var connectionString = builder . Configuration . GetConnectionString ( "DefaultConnection" ) ;
builder . Services . AddDbContext < EleccionesDbContext > ( options = >
options . UseSqlServer ( connectionString ) ) ;
2025-08-29 09:54:22 -03:00
builder . Services . AddScoped < IPasswordHasher , PasswordHasher > ( ) ;
2025-08-14 13:12:16 -03:00
2025-08-29 09:54:22 -03:00
builder . Services . AddControllers ( ) . AddJsonOptions ( options = >
{
// Esto le dice al serializador que maneje las referencias circulares
options . JsonSerializerOptions . ReferenceHandler = ReferenceHandler . IgnoreCycles ;
} ) ;
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins" ;
2025-08-14 13:12:16 -03:00
builder . Services . AddCors ( options = >
{
2025-08-29 09:54:22 -03:00
options . AddPolicy ( name : MyAllowSpecificOrigins ,
policy = >
{
policy . WithOrigins (
2025-09-03 18:56:01 -03:00
"http://localhost:5173" ,
2025-09-03 17:06:39 -03:00
"http://localhost:5174" ,
2025-09-03 18:56:01 -03:00
"http://192.168.5.128:8700" ,
2025-09-03 17:06:39 -03:00
"https://www.eldia.com" ,
2025-09-05 13:40:33 -03:00
"https://extras.eldia.com" ,
"https://eldia.mustang.cloud"
2025-08-30 11:31:45 -03:00
)
2025-08-29 09:54:22 -03:00
. AllowAnyHeader ( )
. AllowAnyMethod ( ) ;
} ) ;
} ) ;
builder . Services . AddAuthentication ( JwtBearerDefaults . AuthenticationScheme )
. AddJwtBearer ( options = >
2025-08-14 13:12:16 -03:00
{
2025-08-29 09:54:22 -03:00
options . TokenValidationParameters = new TokenValidationParameters
2025-08-14 15:51:19 -03:00
{
2025-08-29 09:54:22 -03:00
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" ] ! ) )
} ;
2025-08-14 13:12:16 -03:00
} ) ;
builder . Services . AddEndpointsApiExplorer ( ) ;
builder . Services . AddSwaggerGen ( ) ;
2025-08-14 12:37:57 -03:00
2025-09-03 17:54:49 -03:00
builder . Services . Configure < ForwardedHeadersOptions > ( options = >
{
options . ForwardedHeaders =
ForwardedHeaders . XForwardedFor | ForwardedHeaders . XForwardedProto ;
options . KnownNetworks . Clear ( ) ;
options . KnownProxies . Clear ( ) ;
} ) ;
2025-08-15 17:31:51 -03:00
// 3. Construir la aplicación.
2025-08-14 12:37:57 -03:00
var app = builder . Build ( ) ;
2025-09-06 21:44:52 -03:00
// --- 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}" ) ;
}
}
2025-09-03 17:54:49 -03:00
app . UseForwardedHeaders ( ) ;
2025-08-29 09:54:22 -03:00
// Seeder para el usuario admin
using ( var scope = app . Services . CreateScope ( ) )
{
var services = scope . ServiceProvider ;
try
{
var context = services . GetRequiredService < EleccionesDbContext > ( ) ;
var hasher = services . GetRequiredService < IPasswordHasher > ( ) ;
if ( ! context . AdminUsers . Any ( ) )
{
var ( hash , salt ) = hasher . HashPassword ( "PTP847elec" ) ;
context . AdminUsers . Add ( new Elecciones . Database . Entities . AdminUser
{
Username = "admin" ,
PasswordHash = hash ,
PasswordSalt = salt
} ) ;
context . SaveChanges ( ) ;
Console . WriteLine ( "--> Admin user seeded." ) ;
}
}
catch ( Exception ex )
{
var logger = services . GetRequiredService < ILogger < Program > > ( ) ;
logger . LogError ( ex , "An error occurred while seeding the database." ) ;
}
}
// Seeder para las bancas vacías
using ( var scope = app . Services . CreateScope ( ) )
{
var services = scope . ServiceProvider ;
var context = services . GetRequiredService < EleccionesDbContext > ( ) ;
if ( ! context . Bancadas . Any ( ) )
{
var bancas = new List < Bancada > ( ) ;
// 92 bancas de diputados
2025-08-30 11:31:45 -03:00
for ( int i = 1 ; i < = 92 ; i + + ) // Bucle de 1 a 92
2025-08-29 09:54:22 -03:00
{
2025-08-30 11:31:45 -03:00
bancas . Add ( new Bancada
{
Camara = Elecciones . Core . Enums . TipoCamara . Diputados ,
NumeroBanca = i // Asignamos el número de banca
} ) ;
2025-08-29 09:54:22 -03:00
}
// 46 bancas de senadores
2025-08-30 11:31:45 -03:00
for ( int i = 1 ; i < = 46 ; i + + ) // Bucle de 1 a 46
2025-08-29 09:54:22 -03:00
{
2025-08-30 11:31:45 -03:00
bancas . Add ( new Bancada
{
Camara = Elecciones . Core . Enums . TipoCamara . Senadores ,
NumeroBanca = i // Asignamos el número de banca
} ) ;
2025-08-29 09:54:22 -03:00
}
context . Bancadas . AddRange ( bancas ) ;
context . SaveChanges ( ) ;
2025-08-30 11:31:45 -03:00
Console . WriteLine ( "--> Seeded 138 bancas físicas." ) ;
2025-08-29 09:54:22 -03:00
}
}
// Seeder para las configuraciones por defecto
using ( var scope = app . Services . CreateScope ( ) )
{
var services = scope . ServiceProvider ;
var context = services . GetRequiredService < EleccionesDbContext > ( ) ;
2025-09-06 21:44:52 -03:00
// Lista de configuraciones por defecto a asegurar
var defaultConfiguraciones = new Dictionary < string , string >
2025-08-29 09:54:22 -03:00
{
2025-09-06 21:44:52 -03:00
{ "MostrarOcupantes" , "true" } ,
{ "TickerResultadosCantidad" , "3" } ,
{ "ConcejalesResultadosCantidad" , "5" } ,
{ "Worker_Resultados_Activado" , "false" } ,
{ "Worker_Bajas_Activado" , "false" } ,
{ "Worker_Prioridad" , "Resultados" } ,
{ "Logging_Level" , "Information" }
} ;
foreach ( var config in defaultConfiguraciones )
2025-09-01 14:04:40 -03:00
{
2025-09-06 21:44:52 -03:00
if ( ! context . Configuraciones . Any ( c = > c . Clave = = config . Key ) )
{
context . Configuraciones . Add ( new Configuracion { Clave = config . Key , Valor = config . Value } ) ;
}
2025-09-01 14:04:40 -03:00
}
2025-09-06 21:44:52 -03:00
context . SaveChanges ( ) ;
Console . WriteLine ( "--> Seeded default configurations." ) ;
2025-08-29 09:54:22 -03:00
}
2025-09-22 17:56:04 -03:00
// --- SEEDER FINAL Y AUTOSUFICIENTE (CON DATOS DE RECUENTO) ---
2025-09-17 11:31:17 -03:00
using ( var scope = app . Services . CreateScope ( ) )
{
var services = scope . ServiceProvider ;
var context = services . GetRequiredService < EleccionesDbContext > ( ) ;
var logger = services . GetRequiredService < ILogger < Program > > ( ) ;
const int eleccionNacionalId = 2 ;
if ( ! context . ResultadosVotos . Any ( r = > r . EleccionId = = eleccionNacionalId ) )
{
2025-09-22 17:56:04 -03:00
logger . LogInformation ( "--> No se encontraron datos para la elección nacional ID {EleccionId}. Generando datos de simulación COMPLETOS..." , eleccionNacionalId ) ;
2025-09-17 11:31:17 -03:00
var eleccionNacional = await context . Elecciones . FindAsync ( eleccionNacionalId ) ? ? new Eleccion { Id = eleccionNacionalId , Nombre = "Elecciones Nacionales 2025" , Nivel = "Nacional" , DistritoId = "00" , Fecha = new DateOnly ( 2025 , 10 , 26 ) } ;
if ( ! context . Elecciones . Local . Any ( e = > e . Id = = eleccionNacionalId ) ) context . Elecciones . Add ( eleccionNacional ) ;
var categoriaDiputadosNac = await context . CategoriasElectorales . FindAsync ( 2 ) ? ? new CategoriaElectoral { Id = 2 , Nombre = "DIPUTADOS NACIONALES" , Orden = 3 } ;
if ( ! context . CategoriasElectorales . Local . Any ( c = > c . Id = = 2 ) ) context . CategoriasElectorales . Add ( categoriaDiputadosNac ) ;
await context . SaveChangesAsync ( ) ;
var provinciasMaestras = new Dictionary < string , string >
{
2025-09-19 17:19:10 -03:00
{ "01" , "CIUDAD AUTONOMA DE BUENOS AIRES" } , { "02" , "BUENOS AIRES" } , { "03" , "CATAMARCA" } , { "04" , "CORDOBA" } ,
2025-09-17 11:31:17 -03:00
{ "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 ( ) ;
logger . LogInformation ( "--> Verificados/creados los 24 ámbitos geográficos de Nivel 10." ) ;
var provinciasEnDb = await context . AmbitosGeograficos . AsNoTracking ( ) . Where ( a = > a . NivelId = = 10 ) . ToListAsync ( ) ;
foreach ( var provincia in provinciasEnDb )
{
2025-09-22 17:56:04 -03:00
if ( ! await context . AmbitosGeograficos . AnyAsync ( a = > a . NivelId = = 30 & & a . DistritoId = = provincia . DistritoId ) )
2025-09-17 11:31:17 -03:00
{
2025-09-22 17:56:04 -03:00
logger . LogWarning ( "--> No se encontraron municipios para {Provincia}. Creando 5 de ejemplo." , provincia . Nombre ) ;
2025-09-17 11:31:17 -03:00
for ( int i = 1 ; i < = 5 ; i + + )
{
2025-09-22 17:56:04 -03:00
context . AmbitosGeograficos . Add ( new AmbitoGeografico { Nombre = $"{provincia.Nombre} - Depto. {i}" , NivelId = 30 , DistritoId = provincia . DistritoId } ) ;
2025-09-17 11:31:17 -03:00
}
}
}
await context . SaveChangesAsync ( ) ;
var todosLosPartidos = await context . AgrupacionesPoliticas . Take ( 5 ) . ToListAsync ( ) ;
if ( ! todosLosPartidos . Any ( ) ) {
2025-09-22 17:56:04 -03:00
logger . LogWarning ( "--> No hay partidos, no se pueden generar votos." ) ;
2025-09-17 11:31:17 -03:00
return ;
}
var nuevosResultados = new List < ResultadoVoto > ( ) ;
2025-09-22 17:56:04 -03:00
var nuevosEstados = new List < EstadoRecuentoGeneral > ( ) ;
2025-09-17 11:31:17 -03:00
var rand = new Random ( ) ;
2025-09-22 17:56:04 -03:00
long totalVotosNacional = 0 ;
int totalMesasNacional = 0 ;
int totalMesasEscrutadasNacional = 0 ;
2025-09-17 11:31:17 -03:00
foreach ( var provincia in provinciasEnDb )
{
2025-09-22 17:56:04 -03:00
var municipiosDeProvincia = await context . AmbitosGeograficos . AsNoTracking ( ) . Where ( a = > a . NivelId = = 30 & & a . DistritoId = = provincia . DistritoId ) . ToListAsync ( ) ;
2025-09-17 11:31:17 -03:00
if ( ! municipiosDeProvincia . Any ( ) ) continue ;
2025-09-22 17:56:04 -03:00
long totalVotosProvincia = 0 ;
2025-09-17 11:31:17 -03:00
int partidoIndex = rand . Next ( todosLosPartidos . Count ) ;
foreach ( var municipio in municipiosDeProvincia )
{
2025-09-22 17:56:04 -03:00
var partidoGanador = todosLosPartidos [ partidoIndex + + % todosLosPartidos . Count ] ;
var votosGanador = rand . Next ( 25000 , 70000 ) ;
nuevosResultados . Add ( new ResultadoVoto { EleccionId = eleccionNacionalId , AmbitoGeograficoId = municipio . Id , CategoriaId = categoriaDiputadosNac . Id , AgrupacionPoliticaId = partidoGanador . Id , CantidadVotos = votosGanador } ) ;
totalVotosProvincia + = votosGanador ;
var otrosPartidos = todosLosPartidos . Where ( p = > p . Id ! = partidoGanador . Id ) . OrderBy ( p = > rand . Next ( ) ) . Take ( rand . Next ( 3 , todosLosPartidos . Count ) ) ;
2025-09-17 11:31:17 -03:00
foreach ( var competidor in otrosPartidos ) {
2025-09-22 17:56:04 -03:00
var votosCompetidor = rand . Next ( 1000 , 24000 ) ;
nuevosResultados . Add ( new ResultadoVoto { EleccionId = eleccionNacionalId , AmbitoGeograficoId = municipio . Id , CategoriaId = categoriaDiputadosNac . Id , AgrupacionPoliticaId = competidor . Id , CantidadVotos = votosCompetidor } ) ;
totalVotosProvincia + = votosCompetidor ;
2025-09-17 11:31:17 -03:00
}
}
2025-09-22 17:56:04 -03:00
// --- LÓGICA DE DATOS DE RECUENTO POR PROVINCIA ---
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 = categoriaDiputadosNac . Id ,
FechaTotalizacion = DateTime . UtcNow ,
MesasEsperadas = mesasEsperadasProvincia ,
MesasTotalizadas = mesasTotalizadasProvincia ,
MesasTotalizadasPorcentaje = ( decimal ) mesasTotalizadasProvincia * 100 / mesasEsperadasProvincia ,
CantidadElectores = cantidadElectoresProvincia ,
CantidadVotantes = ( int ) ( cantidadElectoresProvincia * participacionProvincia ) ,
ParticipacionPorcentaje = participacionProvincia * 100
} ) ;
totalVotosNacional + = totalVotosProvincia ;
totalMesasNacional + = mesasEsperadasProvincia ;
totalMesasEscrutadasNacional + = mesasTotalizadasProvincia ;
2025-09-17 11:31:17 -03:00
}
2025-09-22 17:56:04 -03:00
// --- LÓGICA DE DATOS DE RECUENTO A NIVEL NACIONAL ---
var ambitoNacional = await context . AmbitosGeograficos . AsNoTracking ( ) . FirstOrDefaultAsync ( a = > a . NivelId = = 0 ) ;
if ( ambitoNacional = = null ) {
ambitoNacional = new AmbitoGeografico { Nombre = "Nacional" , NivelId = 0 , DistritoId = "00" } ;
context . AmbitosGeograficos . Add ( ambitoNacional ) ;
await context . SaveChangesAsync ( ) ;
}
var participacionNacional = ( decimal ) ( rand . Next ( 70 , 88 ) / 100.0 ) ;
nuevosEstados . Add ( new EstadoRecuentoGeneral {
EleccionId = eleccionNacionalId , AmbitoGeograficoId = ambitoNacional . Id , CategoriaId = categoriaDiputadosNac . Id ,
FechaTotalizacion = DateTime . UtcNow ,
MesasEsperadas = totalMesasNacional ,
MesasTotalizadas = totalMesasEscrutadasNacional ,
MesasTotalizadasPorcentaje = ( decimal ) totalMesasEscrutadasNacional * 100 / totalMesasNacional ,
CantidadElectores = totalMesasNacional * 350 ,
CantidadVotantes = ( int ) ( ( totalMesasNacional * 350 ) * participacionNacional ) ,
ParticipacionPorcentaje = participacionNacional * 100
} ) ;
2025-09-17 11:31:17 -03:00
if ( nuevosResultados . Any ( ) ) {
await context . ResultadosVotos . AddRangeAsync ( nuevosResultados ) ;
2025-09-22 17:56:04 -03:00
await context . EstadosRecuentosGenerales . AddRangeAsync ( nuevosEstados ) ;
2025-09-17 11:31:17 -03:00
await context . SaveChangesAsync ( ) ;
2025-09-22 17:56:04 -03:00
logger . LogInformation ( "--> Se generaron {Votos} registros de votos y {Estados} de estados de recuento." , nuevosResultados . Count , nuevosEstados . Count ) ;
2025-09-17 11:31:17 -03:00
} else {
2025-09-22 17:56:04 -03:00
logger . LogWarning ( "--> No se generaron datos de simulación." ) ;
2025-09-17 11:31:17 -03:00
}
}
}
2025-08-29 09:54:22 -03:00
// Configurar el pipeline de peticiones HTTP.
2025-08-15 17:31:51 -03:00
// Añadimos el logging de peticiones de Serilog aquí.
app . UseSerilogRequestLogging ( ) ;
2025-08-14 13:12:16 -03:00
2025-08-14 12:37:57 -03:00
if ( app . Environment . IsDevelopment ( ) )
{
2025-08-14 13:12:16 -03:00
app . UseSwagger ( ) ;
app . UseSwaggerUI ( ) ;
2025-08-14 12:37:57 -03:00
}
2025-08-29 09:54:22 -03:00
// 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.
2025-08-14 13:12:16 -03:00
app . UseAuthorization ( ) ;
2025-08-29 09:54:22 -03:00
// 6. Mapea los controladores a los endpoints que el enrutador descubrió.
2025-08-14 13:12:16 -03:00
app . MapControllers ( ) ;
2025-08-15 17:31:51 -03:00
// 5. Ejecutar la aplicación.
2025-08-14 13:12:16 -03:00
app . Run ( ) ;