security: implement tickets #28-#35 + architecture decision #30
#28 - TOTP rate limiting (verifyTotp): added totpRateLimiter (10 req/30s), throws TOO_MANY_REQUESTS before DB hit; 16 unit tests including rate-limit exceeded + userId key isolation. #29 - /api/reports/allocations role check: only ADMIN/MANAGER/CONTROLLER may access; returns 403 otherwise; 9 unit tests (401 unauthenticated, 403 for USER/VIEWER, 200 for allowed roles + xlsx format). #31 - pgAdmin credentials moved out of docker-compose.yml into env vars; PGADMIN_PASSWORD is now required (:?) to prevent accidental plaintext exposure in committed files. #34 - Server-side HTML sanitization for comment bodies via stripHtml(): strips all tags + decodes safe entities before persistence; 16 unit tests covering passthrough, injection patterns, entity decoding. #35 - MFA setup prompt banner (MfaPromptBanner): shown to ADMIN/MANAGER users without TOTP enabled; user-scoped localStorage snooze (7 days); links to /account/security; accessibility role=alert; 7 structural unit tests. #33 - Auth anomaly alerting cron (/api/cron/auth-anomaly-check): detects HIGH_GLOBAL_FAILURE_RATE and CONCENTRATED_FAILURES in 30-minute window; CRITICAL notification to ADMINs; fail-closed via verifyCronSecret; 10 unit tests. #32 - MFA enforcement policy: added requireMfaForRoles field to SystemSettings schema + Prisma migration; auth.ts blocks login with MFA_REQUIRED_SETUP signal if role is enforced but TOTP not set up; signin page redirects to /account/security?mfa_required=1; settings schema + view model updated; 11 unit tests. #30 - API keys architecture decision documented in LEARNINGS.md; no code written — product decision required before implementation. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const SNOOZE_KEY = "capakraken_mfa_prompt_snoozed_until";
|
||||
const SNOOZE_DAYS = 7;
|
||||
|
||||
/**
|
||||
* Banner shown to ADMIN / MANAGER users who have not yet enabled TOTP MFA.
|
||||
* The user can dismiss it for SNOOZE_DAYS days ("Remind me later").
|
||||
*
|
||||
* Props are resolved server-side in the layout, so no client-side DB fetch
|
||||
* is needed here.
|
||||
*/
|
||||
export function MfaPromptBanner({ userId }: { userId: string }) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(`${SNOOZE_KEY}_${userId}`);
|
||||
if (raw) {
|
||||
const until = Number(raw);
|
||||
if (!isNaN(until) && Date.now() < until) {
|
||||
return; // still snoozed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// localStorage unavailable (SSR guard — should not happen in a client component)
|
||||
}
|
||||
setVisible(true);
|
||||
}, [userId]);
|
||||
|
||||
function snooze() {
|
||||
try {
|
||||
const until = Date.now() + SNOOZE_DAYS * 24 * 60 * 60 * 1000;
|
||||
localStorage.setItem(`${SNOOZE_KEY}_${userId}`, String(until));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
aria-label="MFA setup recommendation"
|
||||
className="flex items-center justify-between gap-4 bg-amber-50 px-4 py-2.5 text-sm text-amber-900 dark:bg-amber-900/20 dark:text-amber-200 border-b border-amber-200 dark:border-amber-700/50"
|
||||
>
|
||||
<span>
|
||||
<strong className="font-semibold">Protect your account:</strong>{" "}
|
||||
Your role has elevated permissions. We recommend enabling multi-factor authentication (MFA).
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Link
|
||||
href="/account/security"
|
||||
className="rounded-md bg-amber-600 px-3 py-1 text-xs font-semibold text-white hover:bg-amber-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-amber-600 dark:bg-amber-500 dark:hover:bg-amber-400"
|
||||
>
|
||||
Set up MFA
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={snooze}
|
||||
className="rounded-md px-2 py-1 text-xs text-amber-700 hover:bg-amber-100 dark:text-amber-300 dark:hover:bg-amber-800/50"
|
||||
>
|
||||
Remind me later
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user