Timebot implementation
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["timetracker.styles.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["timetracker.Client.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="alternate icon" type="image/png" href="favicon.png" />
|
||||
|
||||
@@ -55,7 +55,7 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<(User? User, string? Error)> RegisterAsync(string username, string password) {
|
||||
public async Task<(User? User, string? Error)> RegisterAsync(string username, string password, string? honeypot = null) {
|
||||
if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
|
||||
return (null, "Benutzername muss mindestens 3 Zeichen lang sein.");
|
||||
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user