feat(unit-2-auth): Implement authentication pages and guards

Completes Unit 2 Code Generation (Steps 1-21 of 24):
- SetupPage: First Owner account creation with language preference
- InviteCompletePage: User invitation completion with token validation
- InitGuard: System initialization status checking
- RoleGuard: Role-based access control for protected routes
- API hooks: useSetup, useValidateInvitation, useCompleteInvitation
- Shared password validation schema via Zod
- Error display components: FormErrorBanner, FieldError
- MSW mock handlers for setup and invitation flows
- i18n translations (en/nl) for auth screens
- Router updates: public routes (/setup, /invite/complete) + role guards
- Unit/integration tests: 25/29 passing (86% pass rate)
- Build:  PASSED, Lint:  PASSED, Tests: 86% PASSED

Stories: US-01, US-02, US-03, US-04, US-05, US-06, US-07, US-13, US-14
Remaining: Step 22 (README), Step 23 (final verification), Step 24 (commit)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 09:58:28 +02:00
co-authored by Claude Haiku 4.5
parent f9689d2091
commit 53879a891a
18 changed files with 700 additions and 572 deletions
+18
View File
@@ -0,0 +1,18 @@
interface FieldErrorProps {
message?: string;
testId?: string;
}
export function FieldError({ message, testId }: FieldErrorProps) {
if (!message) return null;
return (
<span
className="block mt-1 text-xs text-red-600 font-medium"
role="alert"
data-testid={testId}
>
{message}
</span>
);
}
@@ -0,0 +1,33 @@
import { X } from 'lucide-react';
interface FormError {
message: string;
code?: string;
}
interface FormErrorBannerProps {
error: FormError | null;
onDismiss: () => void;
}
export function FormErrorBanner({ error, onDismiss }: FormErrorBannerProps) {
if (!error) return null;
return (
<div
data-testid="form-error-banner"
className="mb-4 flex items-start justify-between gap-3 rounded-lg bg-red-50 p-4 text-red-800 border border-red-200 animate-in fade-in"
>
<div className="flex-1">
<p className="text-sm font-medium">{error.message}</p>
</div>
<button
onClick={onDismiss}
className="flex-shrink-0 text-red-600 hover:text-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 rounded"
aria-label="Dismiss error"
>
<X size={18} />
</button>
</div>
);
}