feat(frontend): add CSRF injection, rate-limit i18n, and 429 login handling
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m7s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m19s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m0s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m7s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m19s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m0s
- handleFetch injects X-XSRF-TOKEN + XSRF-TOKEN cookie on all mutating
backend API requests (double-submit cookie pattern); generates a fresh
UUID when no XSRF-TOKEN cookie exists yet
- ErrorCode union gains CSRF_TOKEN_MISSING and TOO_MANY_LOGIN_ATTEMPTS;
getErrorMessage maps both to i18n keys
- de/en/es messages add error_csrf_token_missing and
error_too_many_login_attempts translations
- Login action maps HTTP 429 to fail(429, { ..., rateLimited: true });
page shows a muted clock icon with aria-invalid on rate-limit errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ let {
|
||||
form
|
||||
}: {
|
||||
data: { registered: boolean; reason?: string | null };
|
||||
form?: { error?: string; success?: boolean };
|
||||
form?: { error?: string; rateLimited?: boolean; success?: boolean };
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
@@ -106,7 +106,31 @@ let {
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="text-center font-sans text-xs font-medium text-red-600">{form.error}</div>
|
||||
{#if form?.rateLimited}
|
||||
<div
|
||||
aria-invalid="true"
|
||||
role="alert"
|
||||
class="flex items-center gap-2 font-sans text-xs font-medium text-red-600"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="h-4 w-4 shrink-0 text-ink-3"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{form.error}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center font-sans text-xs font-medium text-red-600">{form.error}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user