**Authentication Refactor:** Remove authStore and its associated tests, transitioning to the new authentication model. Add dynamic loading for PasswordResetConfirmForm to optimize performance. Include a theme initialization script in layout.tsx to prevent FOUC.

This commit is contained in:
2025-11-02 14:00:05 +01:00
parent 92b7de352c
commit b181182c3b
22 changed files with 390 additions and 127 deletions

View File

@@ -8,7 +8,7 @@
import { useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useAuthStore } from '@/stores/authStore';
import { useAuthStore } from '@/lib/stores/authStore';
import { useMe } from '@/lib/api/hooks/useAuth';
import config from '@/config/app.config';

View File

@@ -7,7 +7,7 @@
'use client';
import { useEffect } from 'react';
import { useAuthStore } from '@/stores/authStore';
import { useAuthStore } from '@/lib/stores/authStore';
/**
* AuthInitializer - Initializes auth state from encrypted storage on mount

View File

@@ -1,8 +1,5 @@
// Authentication components
// Initialization
export { AuthInitializer } from './AuthInitializer';
// Route protection
export { AuthGuard } from './AuthGuard';

View File

@@ -0,0 +1,101 @@
/**
* FormField Component
* Reusable form field with integrated label, input, and error display
* Designed for react-hook-form with proper accessibility attributes
*/
'use client';
import { ComponentProps, ReactNode } from 'react';
import { FieldError } from 'react-hook-form';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
export interface FormFieldProps extends Omit<ComponentProps<typeof Input>, 'children'> {
/** Field label text */
label: string;
/** Field name/id - optional if provided via register() */
name?: string;
/** Is field required? Shows asterisk if true */
required?: boolean;
/** Form error object from react-hook-form */
error?: FieldError;
/** Label description or helper text */
description?: string;
/** Additional content after input (e.g., password requirements) */
children?: ReactNode;
}
/**
* FormField - Standardized form field with label and error handling
*
* Features:
* - Automatic error ID generation for accessibility
* - Required indicator
* - Error message display
* - Helper text/description support
* - Full ARIA attribute support
*
* @example
* ```tsx
* <FormField
* label="Email"
* name="email"
* type="email"
* required
* error={form.formState.errors.email}
* disabled={isSubmitting}
* {...form.register('email')}
* />
* ```
*/
export function FormField({
label,
name: explicitName,
required = false,
error,
description,
children,
...inputProps
}: FormFieldProps) {
// Extract name from inputProps (from register()) or use explicit name
// register() adds a name property that may not be in the type
const registerName = ('name' in inputProps) ? (inputProps as { name: string }).name : undefined;
const name = explicitName || registerName;
if (!name) {
throw new Error('FormField: name must be provided either explicitly or via register()');
}
const errorId = error ? `${name}-error` : undefined;
const descriptionId = description ? `${name}-description` : undefined;
const ariaDescribedBy = [errorId, descriptionId].filter(Boolean).join(' ') || undefined;
return (
<div className="space-y-2">
{label && (
<Label htmlFor={name}>
{label}
{required && <span className="text-destructive"> *</span>}
</Label>
)}
{description && (
<p id={descriptionId} className="text-sm text-muted-foreground">
{description}
</p>
)}
<Input
id={name}
aria-invalid={!!error}
aria-describedby={ariaDescribedBy}
{...inputProps}
/>
{error && (
<p id={errorId} className="text-sm text-destructive" role="alert">
{error.message}
</p>
)}
{children}
</div>
);
}

View File

@@ -0,0 +1,5 @@
// Shared form components and utilities
export { FormField } from './FormField';
export type { FormFieldProps } from './FormField';
export { useFormError } from './useFormError';
export type { UseFormErrorReturn } from './useFormError';

View File

@@ -0,0 +1,91 @@
/**
* useFormError Hook
* Handles server error state and API error parsing for forms
* Standardizes error handling across all form components
*/
import { useState, useCallback } from 'react';
import { UseFormReturn, FieldValues, Path } from 'react-hook-form';
import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/errors';
export interface UseFormErrorReturn {
/** Current server error message */
serverError: string | null;
/** Set server error manually */
setServerError: (error: string | null) => void;
/** Handle API error and update form with field-specific errors */
handleFormError: (error: unknown) => void;
/** Clear all errors */
clearErrors: () => void;
}
/**
* useFormError - Standardized form error handling
*
* Features:
* - Server error state management
* - API error parsing with type guards
* - Automatic field error mapping to react-hook-form
* - General error message extraction
*
* @param form - react-hook-form instance
* @returns Error handling utilities
*
* @example
* ```tsx
* const form = useForm<LoginFormData>({...});
* const { serverError, handleFormError, clearErrors } = useFormError(form);
*
* const onSubmit = async (data: LoginFormData) => {
* try {
* clearErrors();
* await loginMutation.mutateAsync(data);
* } catch (error) {
* handleFormError(error);
* }
* };
* ```
*/
export function useFormError<TFieldValues extends FieldValues>(
form: UseFormReturn<TFieldValues>
): UseFormErrorReturn {
const [serverError, setServerError] = useState<string | null>(null);
const handleFormError = useCallback(
(error: unknown) => {
// Handle API errors with type guard
if (isAPIErrorArray(error)) {
// Set general error message
const generalError = getGeneralError(error);
if (generalError) {
setServerError(generalError);
}
// Set field-specific errors
const fieldErrors = getFieldErrors(error);
Object.entries(fieldErrors).forEach(([field, message]) => {
// Check if field exists in form values to avoid setting invalid fields
if (field in form.getValues()) {
form.setError(field as Path<TFieldValues>, { message });
}
});
} else {
// Unexpected error format
setServerError('An unexpected error occurred. Please try again.');
}
},
[form]
);
const clearErrors = useCallback(() => {
setServerError(null);
form.clearErrors();
}, [form]);
return {
serverError,
setServerError,
handleFormError,
clearErrors,
};
}

View File

@@ -8,7 +8,7 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useAuthStore } from '@/stores/authStore';
import { useAuthStore } from '@/lib/stores/authStore';
import { useLogout } from '@/lib/api/hooks/useAuth';
import {
DropdownMenu,