Timebot implementation

This commit is contained in:
MarcWieland
2026-06-08 23:34:08 +02:00
parent b8b01871ed
commit 82626bc5b3
17 changed files with 802 additions and 209 deletions
+71 -12
View File
@@ -1,4 +1,5 @@
using System.Security.Claims;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;
@@ -19,6 +20,9 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
options.LogoutPath = "/auth/logout";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
// Cookie-Konfiguration für APIs & WASM
options.Events.OnRedirectToLogin = context =>
{
@@ -60,6 +64,22 @@ builder.Services.AddRazorComponents()
builder.Services.AddMudServices();
builder.Services.AddHttpClient<HolidayService>();
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("auth-limit", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString()
?? httpContext.Request.Headers["X-Forwarded-For"].ToString()
?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 5,
QueueLimit = 0
}));
});
var dbProvider = builder.Configuration["DB_PROVIDER"] ?? "SQLite";
if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase))
@@ -120,6 +140,7 @@ if (app.Configuration.GetValue("EnableHttpsRedirect", !app.Environment.IsDevelop
}
app.UseStaticFiles();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
@@ -165,10 +186,16 @@ app.MapPost("/api/auth/login", async (HttpContext ctx, [FromBody] LoginRequest r
new AuthenticationProperties { IsPersistent = true });
return Results.Ok(new UserInfo { Id = user.Id, Username = user.Username });
});
}).RequireRateLimiting("auth-limit");
app.MapPost("/api/auth/register", async (HttpContext ctx, [FromBody] LoginRequest req, IAuthService authService) =>
app.MapPost("/api/auth/register", async (HttpContext ctx, [FromBody] RegisterRequest req, IAuthService authService) =>
{
if (!string.IsNullOrEmpty(req.Honeypot))
{
// Silently reject bots
return Results.BadRequest("Registrierung fehlgeschlagen.");
}
var (user, error) = await authService.RegisterAsync(req.Username, req.Password);
if (user == null)
return Results.BadRequest(error ?? "Registrierung fehlgeschlagen.");
@@ -183,7 +210,7 @@ app.MapPost("/api/auth/register", async (HttpContext ctx, [FromBody] LoginReques
new AuthenticationProperties { IsPersistent = true });
return Results.Ok(new UserInfo { Id = user.Id, Username = user.Username });
});
}).RequireRateLimiting("auth-limit");
app.MapGet("/auth/logout", async (HttpContext ctx) =>
{
@@ -219,8 +246,11 @@ usersApi.MapPut("/{userId:int}/rename", async (int userId, [FromBody] RenameRequ
// ── Timetracker-API-Endpoints (Protected) ─────────────────────────────────────
var trackerApi = app.MapGroup("/api/tracker").RequireAuthorization();
trackerApi.MapGet("/week", async ([FromQuery] int userId, [FromQuery] string monday, ITimetrackerService trackerService) =>
trackerApi.MapGet("/week", async (ClaimsPrincipal claimsPrincipal, [FromQuery] string monday, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
if (DateOnly.TryParse(monday, out var date))
{
var days = await trackerService.GetWeekAsync(userId, date);
@@ -229,50 +259,78 @@ trackerApi.MapGet("/week", async ([FromQuery] int userId, [FromQuery] string mon
return Results.BadRequest("Ungültiges Datum.");
});
trackerApi.MapPost("/workday", async ([FromBody] WorkDay workDay, ITimetrackerService trackerService) =>
trackerApi.MapPost("/workday", async (ClaimsPrincipal claimsPrincipal, [FromBody] WorkDay workDay, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
workDay.UserId = userId; // Enforce owner
await trackerService.UpsertWorkDayAsync(workDay);
return Results.Ok();
});
trackerApi.MapGet("/settings/{userId:int}", async (int userId, ITimetrackerService trackerService) =>
trackerApi.MapGet("/settings", async (ClaimsPrincipal claimsPrincipal, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
var settings = await trackerService.GetSettingsAsync(userId);
return Results.Ok(settings);
});
trackerApi.MapPost("/settings", async ([FromBody] AppSettings settings, ITimetrackerService trackerService) =>
trackerApi.MapPost("/settings", async (ClaimsPrincipal claimsPrincipal, [FromBody] AppSettings settings, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
settings.UserId = userId; // Enforce owner
await trackerService.SaveSettingsAsync(settings);
return Results.Ok();
});
trackerApi.MapGet("/vacation/{userId:int}/{year:int}", async (int userId, int year, ITimetrackerService trackerService) =>
trackerApi.MapGet("/vacation/{year:int}", async (ClaimsPrincipal claimsPrincipal, int year, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
var days = await trackerService.GetVacationDaysAsync(userId, year);
return Results.Ok(days);
});
trackerApi.MapPost("/vacation", async ([FromBody] VacationDay vacationDay, ITimetrackerService trackerService) =>
trackerApi.MapPost("/vacation", async (ClaimsPrincipal claimsPrincipal, [FromBody] VacationDay vacationDay, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
vacationDay.UserId = userId; // Enforce owner
await trackerService.AddVacationDayAsync(vacationDay);
return Results.Ok();
});
trackerApi.MapDelete("/vacation/{userId:int}/{id:int}", async (int userId, int id, ITimetrackerService trackerService) =>
trackerApi.MapDelete("/vacation/{id:int}", async (ClaimsPrincipal claimsPrincipal, int id, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
await trackerService.RemoveVacationDayAsync(userId, id);
return Results.NoContent();
});
trackerApi.MapPost("/overtime/{userId:int}", async (int userId, [FromBody] AppSettings settings, ITimetrackerService trackerService) =>
trackerApi.MapPost("/overtime", async (ClaimsPrincipal claimsPrincipal, [FromBody] AppSettings settings, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
settings.UserId = userId; // Enforce owner
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) =>
trackerApi.MapGet("/month", async (ClaimsPrincipal claimsPrincipal, [FromQuery] int year, [FromQuery] int month, ITimetrackerService trackerService) =>
{
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
var days = await trackerService.GetMonthAsync(userId, year, month);
return Results.Ok(days);
});
@@ -302,4 +360,5 @@ app.Run();
// ── Models for Request Bodies ──────────────────────────────────────────────────
public record LoginRequest(string Username, string Password);
public record RegisterRequest(string Username, string Password, string? Honeypot = null);
public record RenameRequest(string Username);