--- name: auth-flow description: IntelliFill authentication flow patterns using Supabase Auth, JWT tokens, and backend auth mode version: 1.0.0 author: IntelliFill Team lastUpdated: 2025-12-12 --- # IntelliFill Authentication Flow Skill This skill provides comprehensive guidance for working with authentication in the IntelliFill project, covering Supabase integration, JWT token handling, protected routes, and backend auth mode. --- ## Table of Contents 1. [Overview](#overview) 2. [Architecture](#architecture) 3. [Backend Auth Routes](#backend-auth-routes) 4. [Frontend Auth Store](#frontend-auth-store) 5. [Protected Routes](#protected-routes) 6. [Token Management](#token-management) 7. [Password Reset Flow](#password-reset-flow) 8. [Backend Auth Mode](#backend-auth-mode) 9. [Best Practices](#best-practices) 10. [Common Patterns](#common-patterns) 11. [Troubleshooting](#troubleshooting) --- ## Overview IntelliFill uses a **dual-auth architecture** that combines: - **Supabase Auth** - Handles user authentication, password hashing, and session management - **Prisma Database** - Stores user profiles, roles, and business logic - **Backend API** - Centralized auth routing at `/api/auth/v2/*` - **Frontend Store** - Zustand-based state management with persistence ### Key Features - Server-side JWT verification using Supabase - Automatic token refresh with retry logic - Protected route components with loading states - Backend auth mode (no direct Supabase dependency in frontend) - Rate limiting on auth endpoints - Account lockout after failed attempts - Password reset with email verification --- ## Architecture ### Authentication Flow Diagram ``` ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ Frontend │────────▶│ Backend │────────▶│ Supabase │ │ (React) │ POST │ (Express) │ Auth │ Auth API │ │ │ /login │ │ Verify │ │ └─────────────┘ └─────────────┘ └──────────────┘ │ │ │ │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ Zustand │ │ Prisma │ │ Supabase │ │ Store │ │ Database │ │ User Table │ │ (Persisted) │ │ User Profile│ │ (Auth) │ └─────────────┘ └─────────────┘ └──────────────┘ ``` ### Key Components | Component | Location | Purpose | |-----------|----------|---------| | **Auth Routes** | `quikadmin/src/api/supabase-auth.routes.ts` | Backend API endpoints | | **Auth Middleware** | `quikadmin/src/middleware/supabaseAuth.ts` | JWT verification | | **Auth Store** | `quikadmin-web/src/stores/backendAuthStore.ts` | Frontend state | | **Auth Service** | `quikadmin-web/src/services/authService.ts` | API calls | | **Protected Route** | `quikadmin-web/src/components/ProtectedRoute.tsx` | Route guard | | **API Client** | `quikadmin-web/src/services/api.ts` | Axios with interceptors | --- ## Backend Auth Routes ### Available Endpoints All auth routes are under `/api/auth/v2/*`: ```typescript POST /api/auth/v2/register # Create new user account POST /api/auth/v2/login # Authenticate user POST /api/auth/v2/logout # Invalidate session POST /api/auth/v2/refresh # Refresh access token GET /api/auth/v2/me # Get current user profile POST /api/auth/v2/forgot-password # Request password reset POST /api/auth/v2/verify-reset-token # Verify reset token POST /api/auth/v2/reset-password # Reset password with token POST /api/auth/v2/change-password # Change password (authenticated) ``` ### Register Endpoint **Request:** ```typescript POST /api/auth/v2/register Content-Type: application/json { "email": "user@example.com", "password": "SecurePass123", "fullName": "John Doe", "role": "user" // Optional: "user" | "admin" } ``` **Response:** ```typescript { "success": true, "message": "User registered successfully", "data": { "user": { "id": "uuid", "email": "user@example.com", "firstName": "John", "lastName": "Doe", "role": "user", "emailVerified": true // Auto-verified in dev mode }, "tokens": { "accessToken": "eyJhbGc...", "refreshToken": "eyJhbGc...", "expiresIn": 3600, "tokenType": "Bearer" } } } ``` **Password Requirements:** - Minimum 8 characters - At least one uppercase letter - At least one lowercase letter - At least one number **Rate Limiting:** - Max 3 registrations per hour per IP - Returns 429 if exceeded ### Login Endpoint **Request:** ```typescript POST /api/auth/v2/login Content-Type: application/json { "email": "user@example.com", "password": "SecurePass123" } ``` **Response:** ```typescript { "success": true, "message": "Login successful", "data": { "user": { "id": "uuid", "email": "user@example.com", "firstName": "John", "lastName": "Doe", "role": "user", "emailVerified": true, "lastLogin": "2025-12-12T10:00:00Z", "createdAt": "2025-12-01T10:00:00Z" }, "tokens": { "accessToken": "eyJhbGc...", "refreshToken": "eyJhbGc...", "expiresIn": 3600, "tokenType": "Bearer" } } } ``` **Error Codes:** - `401` - Invalid credentials - `403` - Account deactivated - `429` - Rate limit exceeded (5 attempts per 15 minutes) ### Refresh Token Endpoint **Request:** ```typescript POST /api/auth/v2/refresh Content-Type: application/json { "refreshToken": "eyJhbGc..." } ``` **Response:** ```typescript { "success": true, "message": "Token refreshed successfully", "data": { "tokens": { "accessToken": "eyJhbGc...", // New access token "refreshToken": "eyJhbGc...", // New refresh token "expiresIn": 3600, "tokenType": "Bearer" } } } ``` --- ## Frontend Auth Store ### Store Structure The auth store is located at `quikadmin-web/src/stores/backendAuthStore.ts`. **State Interface:** ```typescript interface AuthState { user: AuthUser | null; tokens: AuthTokens | null; company: { id: string } | null; isAuthenticated: boolean; isInitialized: boolean; isLoading: boolean; error: AppError | null; loginAttempts: number; isLocked: boolean; lockExpiry: number | null; lastActivity: number; rememberMe: boolean; } ``` ### Usage in Components **Basic Usage:** ```typescript import { useBackendAuthStore } from '@/stores/backendAuthStore'; function MyComponent() { const { user, isAuthenticated, login, logout } = useBackendAuthStore(); if (!isAuthenticated) { return ; } return (

Welcome, {user?.firstName}!

); } ``` **Selective State Subscription:** ```typescript import { useBackendAuthStore } from '@/stores/backendAuthStore'; function Header() { // Only re-renders when user changes const user = useBackendAuthStore(state => state.user); const logout = useBackendAuthStore(state => state.logout); return (
{user?.email}
); } ``` ### Auth Actions **Login:** ```typescript const login = useBackendAuthStore(state => state.login); try { await login({ email: 'user@example.com', password: 'SecurePass123', rememberMe: true }); // User is now authenticated } catch (error) { console.error('Login failed:', error.message); } ``` **Register:** ```typescript const register = useBackendAuthStore(state => state.register); try { await register({ email: 'user@example.com', password: 'SecurePass123', fullName: 'John Doe' }); // User is registered and authenticated } catch (error) { console.error('Registration failed:', error.message); } ``` **Logout:** ```typescript const logout = useBackendAuthStore(state => state.logout); await logout(); // User is logged out, tokens cleared, redirected to login ``` **Check Session:** ```typescript const checkSession = useBackendAuthStore(state => state.checkSession); if (checkSession()) { // Session is valid } else { // Session expired, redirect to login } ``` ### Error Handling The store provides structured error handling: ```typescript const { error, clearError } = useBackendAuthStore(); useEffect(() => { if (error) { toast.error(error.message); clearError(); } }, [error]); ``` **Error Structure:** ```typescript interface AppError { id: string; code: string; // e.g., 'INVALID_CREDENTIALS', 'ACCOUNT_DEACTIVATED' message: string; details?: unknown; timestamp: number; severity: 'low' | 'medium' | 'high' | 'critical'; component: string; resolved: boolean; } ``` ### Account Lockout The store tracks failed login attempts: ```typescript const { loginAttempts, isLocked, lockExpiry } = useBackendAuthStore(); if (isLocked) { const timeLeft = Math.ceil((lockExpiry! - Date.now()) / 1000 / 60); console.log(`Account locked for ${timeLeft} minutes`); } // After 5 failed attempts, account is locked for 15 minutes ``` --- ## Protected Routes ### ProtectedRoute Component Located at `quikadmin-web/src/components/ProtectedRoute.tsx`. **Usage:** ```typescript import { ProtectedRoute } from '@/components/ProtectedRoute'; function App() { return ( } /> } /> {/* Protected routes */} }> } /> } /> } /> ); } ``` ### How It Works 1. **Initialization Check:** - On mount, calls `initialize()` if not already initialized - Shows loading spinner during initialization 2. **Session Validation:** - Calls `checkSession()` to validate tokens - Checks token expiration synchronously 3. **Redirect Logic:** - If session invalid → redirect to `/login` - Preserves current location in state for return redirect 4. **Loading State:** ```typescript if (!isInitialized || isLoading) { return (

Loading...

); } ``` ### Return URL After Login The ProtectedRoute preserves the original location: ```typescript // In ProtectedRoute // In Login component import { useLocation, useNavigate } from 'react-router-dom'; function Login() { const location = useLocation(); const navigate = useNavigate(); const login = useBackendAuthStore(state => state.login); async function handleLogin(credentials) { await login(credentials); const from = location.state?.from?.pathname || '/'; navigate(from, { replace: true }); } } ``` --- ## Token Management ### Automatic Token Refresh The API client (`quikadmin-web/src/services/api.ts`) automatically refreshes tokens: ```typescript // Axios response interceptor api.interceptors.response.use( response => response, async error => { if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; // Shared refresh promise prevents multiple simultaneous refreshes if (!refreshPromise) { refreshPromise = refreshToken(); } const newToken = await refreshPromise; if (newToken) { // Retry original request with new token originalRequest.headers.Authorization = `Bearer ${newToken}`; return api(originalRequest); } // Refresh failed, logout user await logout(); window.location.href = '/login'; } return Promise.reject(error); } ); ``` ### Token Storage Tokens are persisted in localStorage: ```typescript // In backendAuthStore.ts persist( immer((set, get) => ({ /* store logic */ })), { name: 'intellifill-backend-auth', storage: createJSONStorage(() => localStorage), partialize: (state) => ({ user: state.user, tokens: state.tokens, company: state.company, isAuthenticated: state.isAuthenticated, rememberMe: state.rememberMe, lastActivity: state.lastActivity, }), version: 1, } ) ``` ### Token Expiration Handling **Frontend:** - Access token expires in 3600 seconds (1 hour) - Refresh token used to get new access token - If refresh fails, user is logged out **Backend:** - Uses Supabase `getUser()` for server-side validation - Never uses `getSession()` (client-side only) --- ## Password Reset Flow ### Request Password Reset **Frontend:** ```typescript import { useBackendAuthStore } from '@/stores/backendAuthStore'; function ForgotPassword() { const requestPasswordReset = useBackendAuthStore( state => state.requestPasswordReset ); async function handleSubmit(email: string) { try { await requestPasswordReset(email); toast.success('Password reset email sent (if account exists)'); } catch (error) { toast.error('Failed to send reset email'); } } } ``` **Backend Endpoint:** ```typescript POST /api/auth/v2/forgot-password Content-Type: application/json { "email": "user@example.com", "redirectUrl": "https://app.example.com/reset-password" // Optional } ``` **Response (Always Success):** ```typescript { "success": true, "message": "If an account exists for this email, you will receive a password reset link shortly." } ``` **Security Note:** Always returns success to prevent email enumeration. ### Verify Reset Token **Frontend:** ```typescript const verifyResetToken = useBackendAuthStore( state => state.verifyResetToken ); useEffect(() => { const token = new URLSearchParams(location.search).get('token'); if (token) { verifyResetToken(token) .then(() => setTokenValid(true)) .catch(() => setTokenValid(false)); } }, []); ``` ### Reset Password **Frontend:** ```typescript const resetPassword = useBackendAuthStore(state => state.resetPassword); async function handleReset(token: string, newPassword: string) { try { await resetPassword(token, newPassword); toast.success('Password reset successfully. Please login.'); navigate('/login'); } catch (error) { toast.error('Failed to reset password'); } } ``` **Backend Endpoint:** ```typescript POST /api/auth/v2/reset-password Content-Type: application/json { "token": "reset-token-from-email", "newPassword": "NewSecurePass123" } ``` **Flow:** 1. User requests reset → email sent 2. User clicks link in email → redirected with token 3. Frontend verifies token validity 4. User enters new password 5. Backend updates password in Supabase 6. All sessions invalidated 7. User redirected to login --- ## Backend Auth Mode ### Configuration Set in `quikadmin-web/.env`: ```env # Enable backend auth mode (recommended for local dev) VITE_USE_BACKEND_AUTH=true VITE_API_URL=http://localhost:3002/api # Supabase vars NOT required when using backend auth mode # VITE_SUPABASE_URL=... # VITE_SUPABASE_ANON_KEY=... ``` ### Benefits 1. **No Supabase SDK in Frontend** - Smaller bundle size 2. **Centralized Auth** - All auth goes through backend API 3. **Simpler Configuration** - Only need backend API URL 4. **No CORS Issues** - Backend handles Supabase communication 5. **Better Security** - Supabase credentials not exposed to frontend ### How It Works **Without Backend Auth Mode:** ``` Frontend ──▶ Supabase Auth API (direct) Frontend ──▶ Backend API (for data) ``` **With Backend Auth Mode:** ``` Frontend ──▶ Backend API ──▶ Supabase Auth API Frontend ──▶ Backend API ──▶ Database ``` ### Implementation **Unified Auth Export:** ```typescript // quikadmin-web/src/stores/auth.ts export { useBackendAuthStore as useAuthStore } from './backendAuthStore'; ``` **All Components Use:** ```typescript import { useAuthStore } from '@/stores/auth'; // Works with backend auth mode automatically ``` --- ## Best Practices ### 1. Always Use Middleware for Protected Routes **Backend:** ```typescript import { authenticateSupabase } from '@/middleware/supabaseAuth'; router.get('/protected', authenticateSupabase, async (req, res) => { // req.user is available and verified const userId = req.user.id; }); ``` ### 2. Validate User Status **Backend Middleware:** ```typescript // Check if account is active if (!user.isActive) { return res.status(403).json({ error: 'Account is deactivated', code: 'ACCOUNT_DEACTIVATED' }); } ``` ### 3. Handle Token Refresh Gracefully **Frontend:** ```typescript // Use shared refresh promise to prevent stampede let refreshPromise: Promise | null = null; if (!refreshPromise) { refreshPromise = refreshToken(); } const newToken = await refreshPromise; ``` ### 4. Implement Rate Limiting **Backend:** ```typescript const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts message: 'Too many authentication attempts' }); router.post('/login', authLimiter, loginHandler); ``` ### 5. Use Server-Side Token Verification **Backend:** ```typescript // ALWAYS use getUser() for server-side auth const supabaseUser = await verifySupabaseToken(token); // NEVER use getSession() (client-side only) ``` ### 6. Clear Sessions on Password Change **Backend:** ```typescript // After password change, invalidate all sessions await supabaseAdmin.auth.admin.signOut(userId, 'global'); ``` ### 7. Implement Account Lockout **Frontend Store:** ```typescript if (state.loginAttempts >= 5) { state.isLocked = true; state.lockExpiry = Date.now() + (15 * 60 * 1000); // 15 minutes } ``` ### 8. Persist Minimal State **Store Configuration:** ```typescript partialize: (state) => ({ user: state.user, tokens: state.tokens, // Don't persist: error, isLoading, loginAttempts }) ``` --- ## Common Patterns ### Login Form with Error Handling ```typescript import { useBackendAuthStore } from '@/stores/backendAuthStore'; function LoginForm() { const login = useBackendAuthStore(state => state.login); const error = useBackendAuthStore(state => state.error); const isLoading = useBackendAuthStore(state => state.isLoading); const clearError = useBackendAuthStore(state => state.clearError); async function handleSubmit(e: FormEvent) { e.preventDefault(); clearError(); try { await login({ email, password, rememberMe }); // Redirect handled by ProtectedRoute } catch (err) { // Error is already in store } } return (
{error && ( {error.message} )} setEmail(e.target.value)} disabled={isLoading} /> setPassword(e.target.value)} disabled={isLoading} />
); } ``` ### Role-Based Access Control ```typescript import { useBackendAuthStore } from '@/stores/backendAuthStore'; function AdminPanel() { const user = useBackendAuthStore(state => state.user); if (user?.role !== 'admin') { return ; } return
Admin Panel
; } ``` ### Auth Status Indicator ```typescript import { useBackendAuthStore } from '@/stores/backendAuthStore'; function AuthStatus() { const { user, isAuthenticated, isLoading } = useBackendAuthStore(); if (isLoading) { return ; } if (!isAuthenticated) { return Login; } return (
{user?.firstName?.[0]}{user?.lastName?.[0]} {user?.email}
); } ``` ### Session Timeout Warning ```typescript import { useBackendAuthStore } from '@/stores/backendAuthStore'; function SessionTimeout() { const lastActivity = useBackendAuthStore(state => state.lastActivity); const logout = useBackendAuthStore(state => state.logout); useEffect(() => { const TIMEOUT = 30 * 60 * 1000; // 30 minutes const interval = setInterval(() => { if (Date.now() - lastActivity > TIMEOUT) { logout(); toast.warning('Session expired due to inactivity'); } }, 60 * 1000); // Check every minute return () => clearInterval(interval); }, [lastActivity, logout]); return null; } ``` --- ## Troubleshooting ### Issue: "Invalid or expired token" **Cause:** Token expired and refresh failed **Solution:** ```typescript // Check token expiration const tokens = useBackendAuthStore.getState().tokens; if (tokens) { const expiresAt = Date.now() + (tokens.expiresIn * 1000); console.log('Token expires in:', expiresAt - Date.now(), 'ms'); } // Force logout and re-login const logout = useBackendAuthStore.getState().logout; await logout(); ``` ### Issue: "Account is deactivated" **Cause:** User account `isActive` is false in database **Solution:** ```sql -- Reactivate user in database UPDATE "User" SET "isActive" = true WHERE email = 'user@example.com'; ``` ### Issue: Infinite redirect loop **Cause:** ProtectedRoute redirects to login, login redirects to protected route **Solution:** ```typescript // In Login component, check if already authenticated const isAuthenticated = useBackendAuthStore(state => state.isAuthenticated); useEffect(() => { if (isAuthenticated) { navigate('/'); } }, [isAuthenticated]); ``` ### Issue: Token refresh stampede **Cause:** Multiple API calls trigger refresh simultaneously **Solution:** Already implemented in `api.ts`: ```typescript // Shared refresh promise let refreshPromise: Promise | null = null; if (!refreshPromise) { refreshPromise = refreshToken(); } ``` ### Issue: "User not found in database" **Cause:** User exists in Supabase but not in Prisma **Solution:** ```typescript // Check Supabase user const { data } = await supabaseAdmin.auth.admin.listUsers(); console.log('Supabase users:', data.users); // Check Prisma user const user = await prisma.user.findUnique({ where: { id: 'supabase-user-id' } }); // Create missing Prisma user if (!user) { await prisma.user.create({ data: { id: supabaseUser.id, email: supabaseUser.email, // ... other fields } }); } ``` ### Issue: CORS errors **Cause:** Frontend making direct Supabase calls **Solution:** Enable backend auth mode: ```env VITE_USE_BACKEND_AUTH=true ``` --- ## Related Documentation - **Backend Auth Routes:** `N:\IntelliFill\quikadmin\src\api\supabase-auth.routes.ts` - **Backend Middleware:** `N:\IntelliFill\quikadmin\src\middleware\supabaseAuth.ts` - **Frontend Store:** `N:\IntelliFill\quikadmin-web\src\stores\backendAuthStore.ts` - **Frontend Service:** `N:\IntelliFill\quikadmin-web\src\services\authService.ts` - **Protected Route:** `N:\IntelliFill\quikadmin-web\src\components\ProtectedRoute.tsx` - **API Client:** `N:\IntelliFill\quikadmin-web\src\services\api.ts` - **CLAUDE.local.md:** `N:\IntelliFill\CLAUDE.local.md` --- **Last Updated:** 2025-12-12 **Maintained By:** IntelliFill Team