Adds auth pages

This commit is contained in:
2026-06-21 00:15:28 +02:00
parent 7dfc3a9692
commit ab93a5c7d1
28 changed files with 2785 additions and 45 deletions
@@ -0,0 +1,36 @@
import type { ReactNode } from 'react';
import { Link } from '@tanstack/react-router';
import { useTranslation } from 'react-i18next';
import type { UserRole } from '@/api/types';
import { useAuth } from '@/contexts/auth-context';
interface RoleGuardProps {
allowedRoles: UserRole[];
children: ReactNode;
}
export function RoleGuard({ allowedRoles, children }: RoleGuardProps) {
const { t } = useTranslation();
const { user } = useAuth();
if (user !== null && allowedRoles.includes(user.role)) {
return <>{children}</>;
}
const requiredLabel = allowedRoles.join(' or ');
return (
<div
data-testid="access-denied-message"
className="flex flex-col items-center justify-center gap-4 py-16 text-center"
>
<h1 className="text-2xl font-semibold">{t('errors.accessDenied')}</h1>
<p className="max-w-sm text-sm text-muted-foreground">
{t('errors.accessDeniedDetail', { roles: requiredLabel })}
</p>
<Link to="/dashboard" className="text-sm text-primary underline-offset-4 hover:underline">
{t('nav.dashboard')}
</Link>
</div>
);
}
@@ -0,0 +1,28 @@
import { X } from 'lucide-react';
interface FormBannerErrorProps {
error: { message: string } | null;
onDismiss: () => void;
}
export function FormBannerError({ error, onDismiss }: FormBannerErrorProps) {
if (error === null) return null;
return (
<div
role="alert"
data-testid="form-error-banner"
className="flex items-start justify-between gap-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"
>
<span>{error.message}</span>
<button
type="button"
onClick={onDismiss}
aria-label="Dismiss error"
className="mt-0.5 shrink-0 hover:opacity-70"
>
<X className="h-4 w-4" />
</button>
</div>
);
}
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { Eye, EyeOff } from 'lucide-react';
import type { UseFormRegisterReturn } from 'react-hook-form';
import { Input } from '@/components/ui/input';
interface PasswordFieldProps extends UseFormRegisterReturn {
id: string;
placeholder?: string;
autoComplete?: string;
}
export function PasswordField({ id, placeholder, autoComplete = 'new-password', ...register }: PasswordFieldProps) {
const [show, setShow] = useState(false);
return (
<div className="relative">
<Input
{...register}
id={id}
type={show ? 'text' : 'password'}
placeholder={placeholder}
autoComplete={autoComplete}
data-testid={`${id}-input`}
className="pr-10"
/>
<button
type="button"
onClick={() => setShow((prev) => !prev)}
data-testid={`${id}-toggle`}
aria-label={show ? 'Hide password' : 'Show password'}
className="absolute inset-y-0 right-0 flex items-center px-3 text-muted-foreground hover:text-foreground"
tabIndex={-1}
>
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
);
}