Add e2e auth test suite, retention crons, and email-verification UX
CI / build-and-test (push) Has been cancelled
CI / build-and-test (push) Has been cancelled
- E2E suite (23 tests, `npm run test:e2e -w @lawdesk/api`): boots the real Fastify app against a disposable Dockerized Postgres (never a real DB) and covers signup/login/lockout/rate limits, CSRF (incl. forged-token rejection), logout, password reset, email verification, the superadmin verified-email promotion gate, and cross-firm tenancy isolation - packages/db: DATABASE_SSL=disable opt-out for local/test databases that don't speak TLS; refused in production - retention-sweep.ts cron enforcing Privacy Policy windows (sessions, tokens, login attempts, tool usage, contact messages, audit log) + sweep-orphaned-storage.ts Spaces reconciliation + scripts/README - Expose emailVerified on the session user; in-app verify-email banner with resend, and verified=1|0 toasts on the login page - Silence Fastify logger under NODE_ENV=test; fix footer resource link; document login-attempt/tool-usage retention in the Privacy Policy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
97e1d4c60b
commit
d9b807662a
@@ -2,6 +2,7 @@ import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { useMe } from '@/hooks/useAuth';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { Topbar } from './Topbar';
|
||||
import { VerifyEmailBanner } from './VerifyEmailBanner';
|
||||
|
||||
export function AppLayout() {
|
||||
const me = useMe();
|
||||
@@ -19,6 +20,7 @@ export function AppLayout() {
|
||||
<Sidebar />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Topbar />
|
||||
<VerifyEmailBanner />
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
import { MailWarning } from 'lucide-react';
|
||||
import { useMe } from '@/hooks/useAuth';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
export function VerifyEmailBanner() {
|
||||
const me = useMe();
|
||||
const [state, setState] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
|
||||
|
||||
// Only when the API explicitly says unverified — older cached sessions omit the flag.
|
||||
if (!me.data || me.data.emailVerified !== false) return null;
|
||||
|
||||
async function resend() {
|
||||
setState('sending');
|
||||
try {
|
||||
await api.post('/api/auth/resend-verification');
|
||||
setState('sent');
|
||||
} catch {
|
||||
setState('error');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-amber-200 bg-amber-50 px-4 py-2.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-amber-900">
|
||||
<MailWarning className="h-4 w-4 flex-none text-amber-600" />
|
||||
<span>
|
||||
Please verify your email — we sent a link to <strong>{me.data.email}</strong>.
|
||||
</span>
|
||||
{state === 'sent' ? (
|
||||
<span className="font-medium text-emerald-700">Verification email sent ✓</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resend}
|
||||
disabled={state === 'sending'}
|
||||
className="font-semibold text-amber-800 underline underline-offset-2 hover:text-amber-950 disabled:opacity-60"
|
||||
>
|
||||
{state === 'sending' ? 'Sending…' : 'Resend email'}
|
||||
</button>
|
||||
)}
|
||||
{state === 'error' && (
|
||||
<span className="text-rose-700">Couldn't send — try again in a few minutes.</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const COLUMNS = [
|
||||
{
|
||||
title: 'Resources',
|
||||
links: [
|
||||
{ href: '/resources', label: 'Resource Hub' },
|
||||
{ href: '/#resources', label: 'Resource Hub' },
|
||||
{ href: '/blog', label: 'Blog' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface AuthUser {
|
||||
role: string;
|
||||
isSuperadmin?: boolean;
|
||||
isSuspended?: boolean;
|
||||
emailVerified?: boolean;
|
||||
}
|
||||
|
||||
interface MeResponse {
|
||||
|
||||
@@ -25,6 +25,9 @@ export default function LoginPage() {
|
||||
const me = useMe();
|
||||
const login = useLogin();
|
||||
|
||||
// Landing target of the email-verification link: /login?verified=1|0
|
||||
const verified = new URLSearchParams(location.search).get('verified');
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -60,6 +63,18 @@ export default function LoginPage() {
|
||||
</>
|
||||
}
|
||||
>
|
||||
{verified === '1' && (
|
||||
<p className="mb-4 rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-700">
|
||||
Email verified — thanks! Sign in to continue.
|
||||
</p>
|
||||
)}
|
||||
{verified === '0' && (
|
||||
<p className="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
||||
That verification link is invalid or has expired. Sign in and use “Resend
|
||||
email” to get a fresh one.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Field
|
||||
label="Email"
|
||||
|
||||
@@ -151,6 +151,7 @@ export default function PrivacyPage() {
|
||||
'Audit log — kept for 24 months.',
|
||||
'Payment records — kept as required by tax and accounting laws (typically 7 years).',
|
||||
'Server logs — kept for 30 days.',
|
||||
'Login-attempt and free-tool usage records (email, IP address) — kept for 90 days, then deleted.',
|
||||
'Contact-form messages — kept for 24 months, then deleted.',
|
||||
]}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user