using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MudBlazor.Services; using timetracker.Server.Components; using timetracker.Data; using timetracker.Shared; var builder = WebApplication.CreateBuilder(args); // Add Authentication builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.LogoutPath = "/auth/logout"; options.ExpireTimeSpan = TimeSpan.FromDays(30); options.SlidingExpiration = true; // Cookie-Konfiguration für APIs & WASM options.Events.OnRedirectToLogin = context => { if (context.Request.Path.StartsWithSegments("/api")) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; } else { context.Response.Redirect(context.RedirectUri); } return Task.CompletedTask; }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireClaim(ClaimTypes.Name, "marc")); }); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddHttpContextAccessor(); // Add SignalR builder.Services.AddSignalR(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); // Register DB-backed services as the interfaces builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveWebAssemblyComponents(); builder.Services.AddMudServices(); builder.Services.AddHttpClient(); var dbProvider = builder.Configuration["DB_PROVIDER"] ?? "SQLite"; if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase)) { var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? builder.Configuration["ConnectionStrings:DefaultConnection"]; builder.Services.AddDbContextFactory(options => options.UseNpgsql(connectionString)); } else { var dbPath = Environment.GetEnvironmentVariable("TIMETRACKER_DB_PATH") ?? Path.Combine(builder.Environment.ContentRootPath, "timetracker.db"); builder.Services.AddDbContextFactory(options => options.UseSqlite($"Data Source={dbPath}")); } var app = builder.Build(); // Migrate or Ensure Database Created using (var scope = app.Services.CreateScope()) { var factory = scope.ServiceProvider.GetRequiredService>(); await using var db = await factory.CreateDbContextAsync(); if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase)) { await db.Database.EnsureCreatedAsync(); } else { await db.Database.MigrateAsync(); } } var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }; forwardedHeadersOptions.KnownProxies.Clear(); forwardedHeadersOptions.KnownIPNetworks.Clear(); app.UseForwardedHeaders(forwardedHeadersOptions); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); } else { app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseHsts(); } app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); if (app.Configuration.GetValue("EnableHttpsRedirect", !app.Environment.IsDevelopment())) { app.UseHttpsRedirection(); } app.UseStaticFiles(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); app.MapStaticAssets(); // Map Hub app.MapHub("/hubs/notifications"); // Map Blazor WASM app.MapRazorComponents() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(timetracker.Client._Imports).Assembly); // ── Auth-API-Endpoints ──────────────────────────────────────────────────────── app.MapGet("/api/auth/me", (HttpContext ctx) => { if (ctx.User.Identity?.IsAuthenticated == true) { var idClaim = ctx.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var name = ctx.User.Identity.Name ?? ""; if (int.TryParse(idClaim, out var id)) { return Results.Ok(new UserInfo { Id = id, Username = name }); } } return Results.Unauthorized(); }); app.MapPost("/api/auth/login", async (HttpContext ctx, [FromBody] LoginRequest req, IAuthService authService) => { var user = await authService.LoginAsync(req.Username, req.Password); if (user == null) return Results.BadRequest("Benutzername oder Passwort falsch."); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username) }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties { IsPersistent = true }); return Results.Ok(new UserInfo { Id = user.Id, Username = user.Username }); }); app.MapPost("/api/auth/register", async (HttpContext ctx, [FromBody] LoginRequest req, IAuthService authService) => { var (user, error) = await authService.RegisterAsync(req.Username, req.Password); if (user == null) return Results.BadRequest(error ?? "Registrierung fehlgeschlagen."); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username) }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties { IsPersistent = true }); return Results.Ok(new UserInfo { Id = user.Id, Username = user.Username }); }); app.MapGet("/auth/logout", async (HttpContext ctx) => { await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Results.Redirect("/login"); }); // ── Admin-API-Endpoints (Protected) ─────────────────────────────────────────── var usersApi = app.MapGroup("/api/users").RequireAuthorization("AdminOnly"); usersApi.MapGet("/", async (IAuthService authService) => { var users = await authService.GetAllUsersAsync(); return Results.Ok(users); }); usersApi.MapDelete("/{userId:int}", async (int userId, IAuthService authService) => { await authService.DeleteUserAsync(userId); return Results.NoContent(); }); usersApi.MapPut("/{userId:int}/rename", async (int userId, [FromBody] RenameRequest req, IAuthService authService) => { var error = await authService.RenameUserAsync(userId, req.Username); if (error != null) { return Results.BadRequest(error); } return Results.Ok(); }); // ── Timetracker-API-Endpoints (Protected) ───────────────────────────────────── var trackerApi = app.MapGroup("/api/tracker").RequireAuthorization(); trackerApi.MapGet("/week", async ([FromQuery] int userId, [FromQuery] string monday, ITimetrackerService trackerService) => { if (DateOnly.TryParse(monday, out var date)) { var days = await trackerService.GetWeekAsync(userId, date); return Results.Ok(days); } return Results.BadRequest("Ungültiges Datum."); }); trackerApi.MapPost("/workday", async ([FromBody] WorkDay workDay, ITimetrackerService trackerService) => { await trackerService.UpsertWorkDayAsync(workDay); return Results.Ok(); }); trackerApi.MapGet("/settings/{userId:int}", async (int userId, ITimetrackerService trackerService) => { var settings = await trackerService.GetSettingsAsync(userId); return Results.Ok(settings); }); trackerApi.MapPost("/settings", async ([FromBody] AppSettings settings, ITimetrackerService trackerService) => { await trackerService.SaveSettingsAsync(settings); return Results.Ok(); }); trackerApi.MapGet("/vacation/{userId:int}/{year:int}", async (int userId, int year, ITimetrackerService trackerService) => { var days = await trackerService.GetVacationDaysAsync(userId, year); return Results.Ok(days); }); trackerApi.MapPost("/vacation", async ([FromBody] VacationDay vacationDay, ITimetrackerService trackerService) => { await trackerService.AddVacationDayAsync(vacationDay); return Results.Ok(); }); trackerApi.MapDelete("/vacation/{userId:int}/{id:int}", async (int userId, int id, ITimetrackerService trackerService) => { await trackerService.RemoveVacationDayAsync(userId, id); return Results.NoContent(); }); trackerApi.MapPost("/overtime/{userId:int}", async (int userId, [FromBody] AppSettings settings, ITimetrackerService trackerService) => { var ts = await trackerService.GetTotalOvertimeAsync(userId, settings); return Results.Ok(ts.TotalHours); }); trackerApi.MapGet("/month", async ([FromQuery] int userId, [FromQuery] int year, [FromQuery] int month, ITimetrackerService trackerService) => { var days = await trackerService.GetMonthAsync(userId, year, month); return Results.Ok(days); }); // ── Holiday-API-Endpoints (Protected) ───────────────────────────────────────── var holidaysApi = app.MapGroup("/api/holidays").RequireAuthorization(); holidaysApi.MapGet("/", async ([FromQuery] int year, [FromQuery] string? stateCode, IHolidayService holidayService) => { var list = await holidayService.GetHolidaysAsync(year, stateCode); return Results.Ok(list); }); holidaysApi.MapPost("/fetch/{year:int}", async (int year, IHolidayService holidayService) => { var (success, message) = await holidayService.FetchAndStoreAsync(year); return Results.Ok(new { Success = success, Message = message }); }); holidaysApi.MapDelete("/{id:int}", async (int id, IHolidayService holidayService) => { await holidayService.DeleteAsync(id); return Results.NoContent(); }); app.Run(); // ── Models for Request Bodies ────────────────────────────────────────────────── public record LoginRequest(string Username, string Password); public record RenameRequest(string Username);