--- id: "ET-AUTH-006" title: "Frontend Specification for Auth Module" type: "Specification" status: "Done" rf_parent: "RF-AUTH-003" epic: "OQI-001" version: "1.0" created_date: "2026-01-25" updated_date: "2026-01-25" --- # ET-AUTH-006: Especificación Técnica - Frontend Auth **Version:** 1.0.0 **Fecha:** 2026-01-25 **Estado:** ✅ Implementado **Épica:** [OQI-001](../_MAP.md) --- ## Resumen Esta especificación detalla la implementación frontend del módulo de autenticación de Trading Platform, incluyendo páginas, componentes reutilizables, servicios de API y flujos de usuario. --- ## Stack Tecnológico | Componente | Tecnología | Versión | |------------|-----------|---------| | Framework | React | 18.2.0 | | Build Tool | Vite | 6.2.0 | | Router | React Router | 6.x | | State Management | Zustand | 4.4.7 | | Data Fetching | TanStack Query | 5.14.0 | | Styling | CSS Modules + Tailwind | 3.x | | Forms | React Hook Form | 7.x | | Validación | Zod | 3.x | | HTTP Client | Axios | 1.x | | Auth Tokens | JWT | - | --- ## Arquitectura Frontend ``` apps/frontend/ ├── src/ │ ├── pages/ │ │ ├── auth/ │ │ │ ├── Login.tsx │ │ │ ├── Register.tsx │ │ │ ├── ForgotPassword.tsx │ │ │ ├── ResetPassword.tsx │ │ │ ├── VerifyEmail.tsx │ │ │ └── AuthCallback.tsx │ ├── components/ │ │ ├── auth/ │ │ │ ├── SocialLoginButtons.tsx │ │ │ ├── PhoneLoginForm.tsx │ │ │ ├── TwoFactorForm.tsx │ │ │ └── PasswordStrengthMeter.tsx │ ├── services/ │ │ ├── api/ │ │ │ └── authService.ts │ ├── stores/ │ │ └── authStore.ts │ ├── hooks/ │ │ ├── useAuth.ts │ │ ├── useAuthForm.ts │ │ └── useSessionManager.ts │ ├── types/ │ │ └── auth.ts │ └── utils/ │ ├── tokenManager.ts │ └── validators.ts ``` --- ## Páginas de Autenticación ### 1. Login Page **Ruta:** `/auth/login` **Responsabilidades:** - Autenticación con email/contraseña - Redirección a OAuth (Google, GitHub, etc.) - Recuperación de contraseña - Manejo de 2FA si está activado - Recordar usuario **Flujo:** ``` 1. Usuario ingresa email y contraseña 2. Validar formato de datos 3. Llamar a /api/v1/auth/login 4. Si 2FA está activado: → Mostrar formulario de verificación 2FA → Usuario ingresa código → Llamar a /api/v1/auth/2fa/verify 5. Guardar tokens en localStorage/sessionStorage 6. Actualizar auth store (Zustand) 7. Redirigir a /dashboard ``` **Props/State:** ```typescript interface LoginPageProps { redirectTo?: string; onSuccess?: () => void; } interface LoginFormData { email: string; password: string; rememberMe: boolean; } ``` **Manejo de Errores:** | Código | Error | Acción | |--------|-------|--------| | INVALID_CREDENTIALS | Credenciales inválidas | Mostrar mensaje de error | | EMAIL_NOT_VERIFIED | Email no verificado | Botón para reenviar verificación | | ACCOUNT_LOCKED | Cuenta bloqueada | Mostrar tiempo de espera | | ACCOUNT_SUSPENDED | Cuenta suspendida | Mostrar mensaje de contacto | | RATE_LIMIT | Demasiados intentos | Desactivar botón por tiempo | --- ### 2. Register Page **Ruta:** `/auth/register` **Responsabilidades:** - Registro con email/contraseña - Validación de contraseña fuerte - Aceptación de términos y condiciones - Registro social (opcional) - Envío de email de verificación **Flujo:** ``` 1. Usuario completa formulario: - Email - Contraseña - Confirmar contraseña - Nombre completo - Aceptar términos 2. Validar localmente (Zod) 3. Mostrar indicador de fortaleza de contraseña 4. Llamar a /api/v1/auth/register 5. Mostrar confirmación de email enviado 6. Redirigir a /auth/verify-email después de 3 segundos ``` **Props/State:** ```typescript interface RegisterFormData { email: string; password: string; confirmPassword: string; firstName: string; lastName: string; acceptTerms: boolean; acceptNewsletter?: boolean; } interface PasswordRequirements { minLength: boolean; hasUppercase: boolean; hasLowercase: boolean; hasNumber: boolean; hasSpecialChar: boolean; } ``` **Validaciones:** - Email válido y único - Contraseña: mínimo 12 caracteres, 1 mayúscula, 1 minúscula, 1 número, 1 carácter especial - Contraseñas coinciden - Edad mínima 18 años (si aplica) - Términos aceptados --- ### 3. Forgot Password Page **Ruta:** `/auth/forgot-password` **Responsabilidades:** - Solicitar recuperación de contraseña - Validar email - Enviar link de recuperación - Mostrar confirmación **Flujo:** ``` 1. Usuario ingresa email 2. Validar formato 3. Llamar a /api/v1/auth/forgot-password 4. Mostrar mensaje de confirmación "Si la cuenta existe, recibirás instrucciones por email" 5. Mostrar campo para ingresar código alternativo 6. Opción de volver a login ``` **Seguridad:** - No revelar si email existe - Rate limiting en lado cliente - Email enviado con validación de seguridad --- ### 4. Reset Password Page **Ruta:** `/auth/reset-password?token=xxx` **Responsabilidades:** - Validar token de recuperación - Permitir cambio de contraseña - Verificar nueva contraseña **Flujo:** ``` 1. Extraer token del URL 2. Validar que token es válido en cliente 3. Usuario ingresa nueva contraseña 4. Mostrar indicador de fortaleza 5. Validar confirmación 6. Llamar a /api/v1/auth/reset-password 7. Mostrar confirmación 8. Redirigir a /auth/login después de 3 segundos ``` **Manejo de Errores:** | Error | Acción | |-------|--------| | Token inválido | Mostrar formulario para solicitar nuevo | | Token expirado | Botón para reenviar | --- ### 5. Verify Email Page **Ruta:** `/auth/verify-email` **Responsabilidades:** - Mostrar instrucciones de verificación - Permitir ingreso manual de código - Resenviar email de verificación - Validar código de verificación **Flujo:** ``` 1. Mostrar mensaje: "Revisa tu email para verificar tu cuenta" 2. Campo para ingresar código de 6 dígitos 3. Link para reenviar email (con rate limiting) 4. Contador de tiempo restante para expiración 5. Llamar a /api/v1/auth/verify-email con token 6. Mostrar confirmación 7. Botón "Continuar a login" ``` **Validaciones:** - Código de 6 dígitos - No expirado - Formato correcto --- ### 6. Auth Callback Page **Ruta:** `/auth/callback/:provider?code=xxx&state=xxx` **Responsabilidades:** - Procesar callback de OAuth - Intercambiar código por tokens - Manejar errores de OAuth - Redirigir según resultado **Flujo:** ``` 1. Página muestra loading spinner 2. Extraer code y state del URL 3. Validar state para prevenir CSRF 4. Llamar a /api/v1/auth/oauth/{provider} 5. Si éxito: → Guardar tokens → Redirigir a redirectTo o /dashboard 6. Si error: → Mostrar mensaje de error → Botón para volver a intentar → Botón para ir a login ``` **Providers Soportados:** - Google - GitHub - Apple (iOS) - Microsoft --- ## Componentes Reutilizables ### SocialLoginButtons **Ubicación:** `src/components/auth/SocialLoginButtons.tsx` **Props:** ```typescript interface SocialLoginButtonsProps { onSuccess?: (user: AuthUser) => void; onError?: (error: Error) => void; className?: string; showLabels?: boolean; providers?: ('google' | 'github' | 'apple' | 'microsoft')[]; } ``` **Funcionalidades:** ``` 1. Botones para cada proveedor OAuth 2. Iconos proporcionados por providers 3. Hover y estados activos 4. Loading state durante autenticación 5. Manejo de errores con mensajes claros 6. Responsivo en mobile ``` **Rendering:** ```jsx ``` **Estilos:** - Google: Azul (#4285F4) - GitHub: Negro (#333) - Apple: Negro (#000) - Microsoft: Azul (#00A4EF) - Ancho: 100% en mobile, auto en desktop - Altura: 48px - Spacing entre botones: 12px --- ### PhoneLoginForm **Ubicación:** `src/components/auth/PhoneLoginForm.tsx` **Props:** ```typescript interface PhoneLoginFormProps { onSuccess?: (user: AuthUser, tokens: TokenResponse) => void; onError?: (error: Error) => void; defaultCountry?: string; disabled?: boolean; } interface PhoneLoginFormState { step: 'phone' | 'otp'; phone: string; code: string; channel: 'sms' | 'whatsapp'; expiresAt: Date; loading: boolean; error?: string; } ``` **Flujo de Pasos:** **Paso 1: Ingreso de Teléfono** ``` 1. Selector de país con bandera 2. Campo de teléfono internacional 3. Selector de canal (SMS/WhatsApp) 4. Validar formato con libphonenumber-js 5. Botón "Enviar código" ``` **Paso 2: Verificación OTP** ``` 1. Mostrar último 4 dígitos del teléfono 2. Campo de 6 dígitos (auto-focus después de cada) 3. Countdown timer (5 minutos) 4. Link "Reenviar código" (después de 30s) 5. Link "Cambiar número" 6. Botón "Verificar código" ``` **Validaciones:** ```typescript const phoneRegex = /^\+[1-9]\d{1,14}$/; const otpRegex = /^\d{6}$/; ``` --- ### TwoFactorForm **Ubicación:** `src/components/auth/TwoFactorForm.tsx` **Props:** ```typescript interface TwoFactorFormProps { tempToken: string; methods: ('totp' | 'backup_code' | 'sms')[]; onSuccess: (tokens: TokenResponse) => void; onError: (error: Error) => void; } interface TwoFactorFormState { method: 'totp' | 'backup_code' | 'sms'; code: string; remainingAttempts: number; loading: boolean; } ``` **Métodos Soportados:** 1. **TOTP (Authenticator)** - Campo de 6 dígitos - Auto-focus y validación en tiempo real - Contador de tiempo (30s por código) 2. **Backup Codes** - Campo de 8 caracteres (sin guiones) - Validación de formato - Mensaje de "código usado" 3. **SMS** - Reenviador de OTP a teléfono - Campo de 6 dígitos - Countdown timer --- ### PasswordStrengthMeter **Ubicación:** `src/components/auth/PasswordStrengthMeter.tsx` **Props:** ```typescript interface PasswordStrengthMeterProps { password: string; showRequirements?: boolean; minLength?: number; className?: string; } interface PasswordRequirements { minLength: boolean; // >= 12 caracteres hasUppercase: boolean; // >= 1 mayúscula hasLowercase: boolean; // >= 1 minúscula hasNumber: boolean; // >= 1 número hasSpecialChar: boolean; // >= 1 carácter especial } ``` **Rendering:** ``` Barra de fortaleza: ┌──────────────────────┐ │ █████░░░░░░░░░░░░░░│ Muy débil └──────────────────────┘ Requisitos: ✓ Mínimo 12 caracteres ✗ Una mayúscula ✓ Una minúscula ✓ Un número ✗ Carácter especial ``` **Colores:** - Rojo: Muy débil - Naranja: Débil - Amarillo: Medio - Verde claro: Fuerte - Verde oscuro: Muy fuerte --- ## Servicios de API ### authService.ts **Ubicación:** `src/services/api/authService.ts` **Métodos:** ```typescript class AuthService { // Login login(email: string, password: string): Promise // Registro register(data: RegisterRequest): Promise // Logout logout(): Promise // Renovar tokens refreshTokens(refreshToken: string): Promise // Obtener usuario actual getCurrentUser(): Promise // OAuth getOAuthUrl(provider: string, redirectTo?: string): Promise<{ authUrl: string; state: string }> oauth(provider: string, code: string, state: string): Promise unlinkOAuth(provider: string): Promise // Teléfono sendPhoneOTP(phone: string, channel: 'sms' | 'whatsapp'): Promise<{ expiresAt: Date }> verifyPhoneOTP(phone: string, code: string): Promise // 2FA setupTwoFactor(): Promise<{ secret: string; qrCode: string; otpauthUrl: string }> enableTwoFactor(code: string): Promise<{ backupCodes: string[] }> verifyTwoFactor(tempToken: string, code: string): Promise disableTwoFactor(code: string): Promise // Contraseña forgotPassword(email: string): Promise resetPassword(token: string, password: string): Promise // Email verifyEmail(token: string): Promise resendVerificationEmail(email: string): Promise // Sesiones getSessions(): Promise revokeSession(sessionId: string): Promise revokeAllOtherSessions(): Promise } export const authService = new AuthService(); ``` **Configuración Base:** ```typescript const apiClient = axios.create({ baseURL: process.env.VITE_API_URL || 'http://localhost:3080/api/v1', timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); // Interceptor para agregar token apiClient.interceptors.request.use((config) => { const token = tokenManager.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Interceptor para renovar token expirado apiClient.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { const refreshToken = tokenManager.getRefreshToken(); if (refreshToken) { // Intentar renovar const response = await refreshTokens(refreshToken); // Reintentar solicitud original } else { // Redirigir a login } } return Promise.reject(error); } ); ``` --- ## Estado Global (Zustand) ### authStore.ts **Ubicación:** `src/stores/authStore.ts` ```typescript interface AuthState { // Estado user: AuthUser | null; isAuthenticated: boolean; isLoading: boolean; error: Error | null; tokens: TokenResponse | null; // Métodos setUser: (user: AuthUser | null) => void; setTokens: (tokens: TokenResponse | null) => void; login: (email: string, password: string) => Promise; register: (data: RegisterRequest) => Promise; logout: () => Promise; refreshUser: () => Promise; clearError: () => void; // 2FA setRequires2FA: (requires: boolean) => void; setTempToken: (token: string) => void; } export const useAuthStore = create((set, get) => ({ user: null, isAuthenticated: false, isLoading: false, error: null, tokens: null, setUser: (user) => set({ user, isAuthenticated: !!user }), setTokens: (tokens) => { set({ tokens }); if (tokens) { tokenManager.setTokens(tokens); } else { tokenManager.clear(); } }, login: async (email, password) => { // Implementación }, logout: async () => { set({ user: null, tokens: null, isAuthenticated: false }); tokenManager.clear(); }, // ... otros métodos })); ``` --- ## Hooks Personalizados ### useAuth() ```typescript function useAuth() { const store = useAuthStore(); return { user: store.user, isAuthenticated: store.isAuthenticated, isLoading: store.isLoading, login: store.login, logout: store.logout, register: store.register, }; } // Uso: function MyComponent() { const { user, isAuthenticated, logout } = useAuth(); if (!isAuthenticated) return null; return
Bienvenido {user?.firstName}
; } ``` ### useAuthForm() ```typescript function useAuthForm(formType: 'login' | 'register' | 'resetPassword') { const form = useForm({ resolver: zodResolver(getSchema(formType)), mode: 'onBlur', }); const onSubmit = async (data) => { // Validar y enviar }; return { form, isSubmitting: form.formState.isSubmitting, errors: form.formState.errors, onSubmit: form.handleSubmit(onSubmit), }; } ``` ### useSessionManager() ```typescript function useSessionManager() { const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(false); const loadSessions = async () => { setLoading(true); const data = await authService.getSessions(); setSessions(data); setLoading(false); }; const revokeSession = async (id: string) => { await authService.revokeSession(id); await loadSessions(); }; useEffect(() => { loadSessions(); }, []); return { sessions, loading, revokeSession }; } ``` --- ## Flujos de Usuario ### Flujo: Registrarse con Email ``` 1. Usuario accede a /auth/register 2. Completa formulario: - Email - Contraseña (con validación en tiempo real) - Nombre - Acepta términos 3. Validación local (Zod) 4. POST /api/v1/auth/register 5. Respuesta: Usuario con status "pending_verification" 6. Redirigir a /auth/verify-email?email=xxx 7. Usuario ingresa código del email 8. POST /api/v1/auth/verify-email 9. Email verificado 10. Redirigir a /auth/login con mensaje "Cuenta verificada" ``` ### Flujo: Iniciar Sesión con Email/Contraseña ``` 1. Usuario accede a /auth/login 2. Ingresa email y contraseña 3. POST /api/v1/auth/login 4. Respuesta de servidor: a) Sin 2FA: tokens (accessToken, refreshToken) b) Con 2FA: tempToken y métodos disponibles 5. Si sin 2FA: - Guardar tokens en store - Redirigir a /dashboard 6. Si con 2FA: - Mostrar formulario de verificación - Usuario ingresa código - POST /api/v1/auth/2fa/verify - Guardar tokens - Redirigir a /dashboard ``` ### Flujo: Iniciar Sesión con OAuth (Google) ``` 1. Usuario haz clic en "Iniciar sesión con Google" 2. GET /api/v1/auth/oauth/google/url?redirectTo=/dashboard 3. Respuesta: authUrl + state 4. Redirigir a Google 5. Usuario autentica y otorga permisos 6. Google redirige a /auth/callback/google?code=xxx&state=xxx 7. POST /api/v1/auth/oauth/google { code, state } 8. Respuesta: tokens 9. Guardar en store 10. Redirigir a /dashboard ``` ### Flujo: Cambiar Contraseña Olvidada ``` 1. Usuario accede a /auth/forgot-password 2. Ingresa email 3. POST /api/v1/auth/forgot-password 4. Mostrar mensaje: "Si la cuenta existe..." 5. Usuario recibe email con link 6. Link dirigido a /auth/reset-password?token=xxx 7. Usuario ingresa nueva contraseña 8. POST /api/v1/auth/reset-password { token, password } 9. Mostrar confirmación 10. Redirigir a /auth/login ``` ### Flujo: Activar 2FA (TOTP) ``` 1. Usuario va a Settings > Seguridad 2. Haz clic en "Activar autenticación de dos factores" 3. POST /api/v1/auth/2fa/setup 4. Respuesta: QR code + secret 5. Usuario escanea con Google Authenticator/Authy 6. Usuario ingresa código de 6 dígitos 7. POST /api/v1/auth/2fa/enable { code } 8. Respuesta: Backup codes 9. Usuario descargar/copia backup codes 10. Mostrar confirmación: "2FA activado" ``` ### Flujo: Iniciar Sesión con Teléfono ``` 1. Usuario accede a /auth/phone-login 2. Selecciona país y ingresa teléfono 3. Elige canal: SMS o WhatsApp 4. POST /api/v1/auth/phone/send { phone, channel } 5. Mostrar "Código enviado a +52 555 1234..." 6. Usuario ingresa código de 6 dígitos 7. POST /api/v1/auth/phone/verify { phone, code } 8. Si usuario nuevo: Crear account automáticamente 9. Respuesta: tokens 10. Redirigir a /dashboard ``` --- ## Seguridad ### Protección CSRF ```typescript // Usar state token en OAuth const state = crypto.randomUUID(); sessionStorage.setItem('oauth_state', state); // Validar en callback const urlState = new URLSearchParams(location.search).get('state'); if (urlState !== sessionStorage.getItem('oauth_state')) { throw new Error('Invalid state token'); } ``` ### Manejo de Tokens ```typescript class TokenManager { setTokens(tokens: TokenResponse) { sessionStorage.setItem('accessToken', tokens.accessToken); localStorage.setItem('refreshToken', tokens.refreshToken); } getAccessToken(): string | null { return sessionStorage.getItem('accessToken'); } getRefreshToken(): string | null { return localStorage.getItem('refreshToken'); } clear() { sessionStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); } isTokenExpired(token: string): boolean { const decoded = jwtDecode(token); return decoded.exp * 1000 < Date.now(); } } ``` ### Protección XSS - Usar React (que escapa automáticamente) - Sanitizar HTML si es necesario: `DOMPurify` - Content Security Policy headers ### Validación de Inputs ```typescript const schemas = { login: z.object({ email: z.string().email('Email inválido'), password: z.string().min(1, 'Requerido'), }), register: z.object({ email: z.string().email('Email inválido'), password: z.string() .min(12, 'Mínimo 12 caracteres') .regex(/[A-Z]/, 'Debe contener mayúscula') .regex(/[a-z]/, 'Debe contener minúscula') .regex(/[0-9]/, 'Debe contener número') .regex(/[^a-zA-Z0-9]/, 'Debe contener carácter especial'), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: 'Las contraseñas no coinciden', path: ['confirmPassword'], }), }; ``` --- ## Rutas Protegidas ```typescript function ProtectedRoute({ children }: { children: ReactNode }) { const { isAuthenticated, isLoading } = useAuth(); if (isLoading) return ; if (!isAuthenticated) return ; return children; } // Uso: } /> } /> ``` --- ## Testing ### Unit Tests ```typescript describe('authService.login', () => { it('debe retornar tokens al login exitoso', async () => { const result = await authService.login('user@example.com', 'password'); expect(result.data.tokens).toBeDefined(); expect(result.data.user).toBeDefined(); }); it('debe lanzar error con credenciales inválidas', async () => { await expect( authService.login('user@example.com', 'wrong') ).rejects.toThrow('INVALID_CREDENTIALS'); }); }); ``` ### Integration Tests ```typescript describe('Login Flow', () => { it('debe completar flujo de login y redirigir', async () => { render(); await userEvent.type(screen.getByLabelText('Email'), 'user@example.com'); await userEvent.type(screen.getByLabelText('Password'), 'SecurePass123!'); await userEvent.click(screen.getByText('Iniciar sesión')); await waitFor(() => { expect(window.location.pathname).toBe('/dashboard'); }); }); }); ``` --- ## Tipos TypeScript **Ubicación:** `src/types/auth.ts` ```typescript export interface AuthUser { id: string; email: string; firstName: string; lastName: string; phone?: string; role: 'investor' | 'admin' | 'support'; status: 'active' | 'pending_verification' | 'suspended'; emailVerified: boolean; phoneVerified: boolean; twoFactorEnabled: boolean; createdAt: Date; updatedAt: Date; profile?: { displayName: string; avatarUrl?: string; preferredLanguage: string; timezone: string; }; oauthProviders: string[]; } export interface TokenResponse { accessToken: string; refreshToken: string; expiresIn: number; tokenType: 'Bearer'; } export interface LoginResponse { success: boolean; data: { user: AuthUser; tokens?: TokenResponse; requires2FA?: boolean; tempToken?: string; methods?: string[]; }; } export interface Session { id: string; device: { type: 'desktop' | 'mobile' | 'tablet'; browser: string; browserVersion?: string; os: string; osVersion: string; }; location: { city: string; country: string; countryCode: string; }; ipAddress: string; lastActivity: Date; createdAt: Date; isCurrent: boolean; } ``` --- ## Checklist de Implementación - [ ] Páginas de autenticación creadas - [ ] Componentes reutilizables implementados - [ ] Servicios de API configurados - [ ] Estado Zustand inicializado - [ ] Hooks personalizados desarrollados - [ ] Rutas protegidas implementadas - [ ] Validaciones con Zod aplicadas - [ ] Manejo de errores completado - [ ] Tests unitarios escritos - [ ] Tests de integración completados - [ ] Security review realizado - [ ] Documentación de componentes lista - [ ] CHANGELOG actualizado --- ## Performance ### Code Splitting ```typescript const LoginPage = lazy(() => import('./pages/auth/LoginPage')); const RegisterPage = lazy(() => import('./pages/auth/RegisterPage')); }> } /> ``` ### Memoización ```typescript const SocialLoginButtons = memo(function SocialLoginButtons(props) { // Componente }); ``` ### Request Caching ```typescript const { data: user } = useQuery({ queryKey: ['auth', 'me'], queryFn: () => authService.getCurrentUser(), staleTime: 5 * 60 * 1000, // 5 minutos }); ``` --- ## Referencias - [ET-AUTH-001: OAuth Specification](./ET-AUTH-001-oauth.md) - [ET-AUTH-002: JWT Specification](./ET-AUTH-002-jwt.md) - [ET-AUTH-004: API Endpoints](./ET-AUTH-004-api.md) - [ET-AUTH-005: Security](./ET-AUTH-005-security.md) - [React Documentation](https://react.dev) - [React Router Documentation](https://reactrouter.com) - [Zustand Documentation](https://github.com/pmndrs/zustand) - [React Hook Form Documentation](https://react-hook-form.com) - [Zod Validation Library](https://zod.dev)