diff --git a/Infrastructure/AuthAttemptMonitor.cs b/Infrastructure/AuthAttemptMonitor.cs index 4b48c37..cebaee3 100644 --- a/Infrastructure/AuthAttemptMonitor.cs +++ b/Infrastructure/AuthAttemptMonitor.cs @@ -6,26 +6,10 @@ public sealed class AuthAttemptMonitor(ILogger logger) { private static readonly TimeSpan FailureWindow = TimeSpan.FromMinutes(10); private const int AlertThreshold = 5; - private static readonly Action LogAuthFailure = - LoggerMessage.Define( - LogLevel.Warning, - new EventId(2001, nameof(LogAuthFailure)), - "Auth failure scope={Scope} actor={Actor} ip={Ip} reason={Reason} failuresInWindow={Count}"); - private static readonly Action LogSecurityAlert = - LoggerMessage.Define( - LogLevel.Error, - new EventId(2002, nameof(LogSecurityAlert)), - "Security alert: repeated auth failures scope={Scope} actor={Actor} ip={Ip} failuresInWindow={Count} windowMinutes={WindowMinutes}"); - private static readonly Action LogRateLimited = - LoggerMessage.Define( - LogLevel.Warning, - new EventId(2003, nameof(LogRateLimited)), - "Rate limit rejection path={Path} ip={Ip} userId={UserId}"); - private static readonly Action LogSessionExpired = - LoggerMessage.Define( - LogLevel.Warning, - new EventId(2004, nameof(LogSessionExpired)), - "Session expired by absolute lifetime path={Path} ip={Ip} startedAt={StartedAt:o}"); + private static readonly Action LogAuthFailure = LoggerMessage.Define(LogLevel.Warning, new EventId(2001, nameof(LogAuthFailure)), "Auth failure scope={Scope} actor={Actor} ip={Ip} reason={Reason} failuresInWindow={Count}"); + private static readonly Action LogSecurityAlert = LoggerMessage.Define(LogLevel.Error, new EventId(2002, nameof(LogSecurityAlert)), "Security alert: repeated auth failures scope={Scope} actor={Actor} ip={Ip} failuresInWindow={Count} windowMinutes={WindowMinutes}"); + private static readonly Action LogRateLimited = LoggerMessage.Define(LogLevel.Warning, new EventId(2003, nameof(LogRateLimited)), "Rate limit rejection path={Path} ip={Ip} userId={UserId}"); + private static readonly Action LogSessionExpired = LoggerMessage.Define(LogLevel.Warning, new EventId(2004, nameof(LogSessionExpired)), "Session expired by absolute lifetime path={Path} ip={Ip} startedAt={StartedAt:o}"); private readonly ConcurrentDictionary _failures = new(StringComparer.Ordinal); @@ -34,12 +18,13 @@ public sealed class AuthAttemptMonitor(ILogger logger) var now = DateTimeOffset.UtcNow; var key = BuildKey(context, scope, actor); - var state = _failures.AddOrUpdate( - key, - _ => new AttemptState(1, now, now), - (_, previous) => previous.LastFailureAt + FailureWindow < now - ? new AttemptState(1, now, now) - : previous with { Count = previous.Count + 1, LastFailureAt = now }); + var state = _failures.AddOrUpdate(key, _ => new AttemptState(1, now, now), (_, previous) => previous.LastFailureAt + FailureWindow < now + ? new AttemptState(1, now, now) + : previous with + { + Count = previous.Count + 1, + LastFailureAt = now + }); LogAuthFailure(logger, scope, actor, GetRemoteIp(context), reason, state.Count, null); @@ -56,22 +41,12 @@ public sealed class AuthAttemptMonitor(ILogger logger) public void RecordRateLimited(HttpContext context) { - LogRateLimited( - logger, - context.Request.Path.Value ?? string.Empty, - GetRemoteIp(context), - context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "anonymous", - null); + LogRateLimited(logger, context.Request.Path.Value ?? string.Empty, GetRemoteIp(context), context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "anonymous", null); } public void RecordSessionExpired(HttpContext context, DateTimeOffset startedAt) { - LogSessionExpired( - logger, - context.Request.Path.Value ?? string.Empty, - GetRemoteIp(context), - startedAt, - null); + LogSessionExpired(logger, context.Request.Path.Value ?? string.Empty, GetRemoteIp(context), startedAt, null); } private static string BuildKey(HttpContext context, string scope, string actor) diff --git a/wwwroot/data/i18n/faq/de.md b/wwwroot/data/i18n/faq/de.md index d7f6c36..184766b 100644 --- a/wwwroot/data/i18n/faq/de.md +++ b/wwwroot/data/i18n/faq/de.md @@ -196,7 +196,7 @@ Warte kurz und versuche es dann erneut. ## Daten & Datenschutz -- Vorschläge, Stimmen und Phasenstatus werden in einer gemeinsamen **SQLite-Datenbank** gespeichert. -- Passwörter werden als gesalzene PBKDF2-SHA256-Hashes gespeichert (nicht im Klartext). +- Vorschläge, Stimmen und Phasenstatus werden in einer gemeinsamen Datenbank gespeichert. +- Passwörter werden als gesalzene Hashes gespeichert (nicht im Klartext). - Beim Abmelden wird dein Authentifizierungs-Cookie gelöscht und die Eingaben in Login/Registrierung werden zurückgesetzt. - Wenn ein Admin dein Spielerkonto löscht, werden auch deine Vorschläge und Stimmen entfernt. diff --git a/wwwroot/data/i18n/faq/en.md b/wwwroot/data/i18n/faq/en.md index 658e4d6..853c868 100644 --- a/wwwroot/data/i18n/faq/en.md +++ b/wwwroot/data/i18n/faq/en.md @@ -200,7 +200,7 @@ Wait briefly, then retry. ## Data & Privacy -- Suggestions, votes, and phase states are stored in a shared **SQLite database**. -- Passwords are stored as salted PBKDF2-SHA256 hashes (not plaintext). +- Suggestions, votes, and phase states are stored in a shared database. +- Passwords are stored as salted hashes (not plaintext). - Logging out clears your authentication cookie and resets login/register form inputs. - If an admin deletes your player account, your suggestions and votes are removed as well.