using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using SlipItIn.Server.Data; using SlipItIn.Server.Hubs; using SlipItIn.Server.Services; using System.Security.Claims; using System.Text; var builder = WebApplication.CreateBuilder(args); // Aspire Service Defaults (OpenTelemetry, Health Checks, Service Discovery) builder.AddServiceDefaults(); builder.AddNpgsqlDataSource(connectionName: "postgresdb"); builder.Services.AddDbContextFactory((sp, options) => options.UseNpgsql(sp.GetRequiredService())); // Services registrieren builder.Services.AddTransient(); // JWT Authentication var jwtKey = builder.Configuration["Jwt:Key"]; if (string.IsNullOrWhiteSpace(jwtKey) || jwtKey.Length < 32) { if (builder.Environment.IsDevelopment()) { jwtKey = "DevOnlyKey-NotForProduction-ChangeMe-0123456789abcdef"; } else { throw new InvalidOperationException( "Jwt:Key ist nicht konfiguriert oder kürzer als 32 Zeichen. " + "In Production muss ein eigener Schlüssel via Konfiguration/Umgebungsvariable gesetzt werden."); } } var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "SlipItInServer"; var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "SlipItInClient"; builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtIssuer, ValidAudience = jwtAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) }; // SignalR-Support für JWT in Query String options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) { context.Token = accessToken; } return Task.CompletedTask; }, OnTokenValidated = async context => { var userIdClaim = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!int.TryParse(userIdClaim, out var userId)) { context.Fail("Invalid user claim."); return; } var dbFactory = context.HttpContext.RequestServices.GetRequiredService>(); await using var db = await dbFactory.CreateDbContextAsync(); var userExists = await db.Users.AnyAsync(u => u.Id == userId && u.IsActive); if (!userExists) { context.Fail("User no longer exists or is inactive."); } } }; }); builder.Services.AddAuthorization(); // SignalR builder.Services.AddSignalR(); // CORS für Development builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); // Controller & OpenAPI builder.Services.AddControllers(); builder.Services.AddOpenApi(); var app = builder.Build(); // Aspire Default Endpoints (Health Checks) app.MapDefaultEndpoints(); // Migrations anwenden und Testdaten seeden (nur wenn die Datenbank leer ist) using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); await DbSeeder.SeedAsync(db); } // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseCors("AllowAll"); // Beim Emulator/geräteübergreifenden Zugriff würde die HTTPS-Umleitung // den Client auf eine nicht vertrauenswürdige Dev-Zertifikats-URL schicken. if (!app.Environment.IsDevelopment()) { app.UseHttpsRedirection(); } app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapHub("/hubs/game"); app.Run();