# SerpentRace Backend API Documentation for Frontend Developers ## Table of Contents 1. [Test User Credentials](#test-user-credentials) 2. [Data Structures & Entities](#data-structures--entities) 3. [Authentication Endpoints](#authentication-endpoints) 4. [User Management](#user-management) 5. [Deck Management](#deck-management) 6. [Organization Management](#organization-management) 7. [Chat System](#chat-system) 8. [Contact Management](#contact-management) 9. [Admin Endpoints](#admin-endpoints) 10. [Import/Export Functionality](#importexport-functionality) 11. [Error Handling](#error-handling) 12. [WebSocket Events](#websocket-events) --- ## Test User Credentials For development and testing, use these pre-configured user accounts: ### Regular User (Verified) - **Username:** `john_doe` - **Password:** `password123` - **Email:** `john.doe@email.com` - **Type:** Regular user (state: 1 - VERIFIED_REGULAR) - **Organization:** None ### Premium User (Organization Member) - **Username:** `jane_premium` - **Password:** `password123` - **Email:** `jane.smith@email.com` - **Type:** Premium user (state: 2 - VERIFIED_PREMIUM) - **Organization:** Tech Solutions Inc ### Teacher (Premium Organization Member) - **Username:** `teacher_bob` - **Password:** `password123` - **Email:** `bob.teacher@eduinst.edu` - **Type:** Premium user (state: 2 - VERIFIED_PREMIUM) - **Organization:** Educational Institute ### Admin User - **Username:** `admin_user` - **Password:** `password123` - **Email:** `admin@serpentrace.com` - **Type:** Admin (state: 5 - ADMIN) - **Organization:** None ### Unverified User - **Username:** `new_user` - **Password:** `password123` - **Email:** `newuser@email.com` - **Type:** Unverified (state: 0 - REGISTERED_NOT_VERIFIED) - **Organization:** None --- ## Data Structures & Entities ### User Entity ```typescript interface User { id: string; // UUID orgid: string | null; // Organization ID (if member) username: string; // Unique username email: string; // Unique email fname: string; // First name lname: string; // Last name type: string; // 'personal' | 'premium' | 'admin' phone: string | null; // Phone number state: UserState; // User status (see enum below) regdate: Date; // Registration date updatedate: Date; // Last update Orglogindate: Date | null; // Last organization login } enum UserState { REGISTERED_NOT_VERIFIED = 0, // Email not verified VERIFIED_REGULAR = 1, // Regular verified user VERIFIED_PREMIUM = 2, // Premium verified user SOFT_DELETE = 3, // Soft deleted DEACTIVATED = 4, // Account deactivated ADMIN = 5 // Admin user } ``` ### Deck Entity ```typescript interface Deck { id: string; // UUID name: string; // Deck name type: DeckType; // Deck type (see enum) userid: string; // Owner's user ID creationdate: Date; // Creation timestamp cards: Card[]; // Array of cards playedNumber: number; // Times played ctype: DeckVisibility; // Visibility type updatedate: Date; // Last update state: DeckState; // Deck status organization: Organization | null; // Organization reference } enum DeckType { LUCK = 0, // Luck-based cards JOKER = 1, // Joker/wild cards QUESTION = 2 // Question-based cards } enum DeckVisibility { PUBLIC = 0, // Public to all PRIVATE = 1, // Private to owner ORGANIZATION = 2 // Shared within organization } enum DeckState { ACTIVE = 0, // Active deck SOFT_DELETE = 1 // Soft deleted } interface Card { id: string; // Card ID type: CardType; // Type of card text: string; // Question/prompt text answer?: string | boolean | null; // Answer (varies by type) options?: string[]; // Multiple choice options } enum CardType { QUIZ = 0, // Multiple choice question SENTENCE_PAIRING = 1, // Sentence completion OWN_ANSWER = 2, // Open-ended question TRUE_FALSE = 3, // True/false question CLOSER = 4 // closer to answer } ``` ### Organization Entity ```typescript interface Organization { id: string; // UUID name: string; // Organization name contactfname: string; // Contact first name contactlname: string; // Contact last name contactphone: string; // Contact phone contactemail: string; // Contact email state: OrganizationState; // Organization status regdate: Date; // Registration date updatedate: Date; // Last update url: string | null; // Organization website userinorg: number; // User count in org maxOrganizationalDecks: number | null; // Max org decks allowed } enum OrganizationState { REGISTERED = 0, // Just registered ACTIVE = 1, // Active organization SOFT_DELETE = 2 // Soft deleted } ``` ### Chat Entity ```typescript interface Chat { id: string; // UUID type: ChatType; // Chat type name: string | null; // Group/game name gameId: string | null; // Associated game ID createdBy: string | null; // Creator's user ID users: string[]; // Participant user IDs messages: Message[]; // Chat messages lastActivity: Date | null; // Last message time createDate: Date; // Chat creation date updateDate: Date; // Last update state: ChatState; // Chat status archiveDate: Date | null; // Archive date } interface Message { id: string; // Message ID date: Date; // Message timestamp userid: string; // Sender's user ID text: string; // Message content } enum ChatType { DIRECT = 'direct', // Direct message GROUP = 'group', // Group chat GAME = 'game' // Game-specific chat } enum ChatState { ACTIVE = 0, // Active chat ARCHIVE = 1, // Archived chat SOFT_DELETE = 2 // Soft deleted } ``` ### Contact Entity ```typescript interface Contact { id: string; // UUID name: string; // Contact name email: string; // Contact email userid: string | null; // Associated user ID type: ContactType; // Contact type txt: string; // Message text state: ContactState; // Contact status createDate: Date; // Creation date updateDate: Date; // Last update adminResponse: string | null; // Admin response responseDate: Date | null; // Response date respondedBy: string | null; // Responding admin ID } enum ContactType { BUG = 0, // Bug report PROBLEM = 1, // Problem report QUESTION = 2, // General question SALES = 3, // Sales inquiry OTHER = 4 // Other type } enum ContactState { ACTIVE = 0, // Active/unresolved RESOLVED = 1, // Resolved SOFT_DELETE = 2 // Soft deleted } ``` --- ## Authentication Endpoints ### User Login **Endpoint:** `POST /api/users/login` **Description:** Authenticate user with username or Email and password **Request Data:** ```typescript { username: string; // 3-50 characters password: string; // 6-100 characters } ``` **Response Data (Success):** ```typescript { token: string; // JWT authentication token its safed in cookie its just a copy user: { id: string; username: string; email: string; fname: string; lname: string; type: string; state: number; orgid: string | null; }; requiresOrgReauth?: boolean; // If org re-auth needed orgLoginUrl?: string; // Organization auth URL organizationName?: string; // Organization name } ``` **Error Responses:** - `401`: Invalid credentials, unverified email, or account restrictions - `400`: Validation error (missing fields, invalid length) - `500`: Internal server error **Account State Restrictions:** Users cannot login if their account state is: - `0` (REGISTERED_NOT_VERIFIED): Email verification required - `3` (SOFT_DELETE): Account has been deleted - `4` (DEACTIVATED): Account has been deactivated **Successful Login States:** - `1` (VERIFIED_REGULAR): Regular user can login - `2` (VERIFIED_PREMIUM): Premium user can login - `5` (ADMIN): Admin user can login **Example Usage:** ```typescript const loginUser = async (username: string, password: string) => { const response = await fetch('/api/users/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (response.ok) { const data = await response.json(); localStorage.setItem('auth_token', data.token); return data; } throw new Error('Login failed'); }; ``` ### User Registration **Endpoint:** `POST /api/users/create` **Description:** Create new user account **Request Data:** ```typescript { username: string; // 3-50 characters, unique email: string; // Valid email format, unique password: string; // 6-100 characters fname: string; // First name lname: string; // Last name type?: string; // 'personal' | 'premium' (default: 'personal') phone?: string; // Optional phone number } ``` **Response Data (Success):** ```typescript { id: string; username: string; email: string; fname: string; lname: string; type: string; state: 0; // REGISTERED_NOT_VERIFIED regdate: Date; } ``` **Error Responses:** - `409`: Username or email already exists - `400`: Validation error - `500`: Internal server error --- ## User Management ### Get User Profile **Endpoint:** `GET /api/users/profile` **Authentication:** Required (Bearer token) **Description:** Get current user's profile information **Request Data:** None (user ID from JWT token) **Response Data:** ```typescript { id: string; orgid: string | null; username: string; email: string; fname: string; lname: string; code: string | null; // Verification token type: string; phone: string | null; state: number; } ``` ### Update User Profile **Endpoint:** `PATCH /api/users/profile` **Authentication:** Required (Bearer token) **Description:** Update current user's profile **Request Data:** ```typescript { fname?: string; lname?: string; email?: string; phone?: string; // Other updatable fields } ``` **Response Data:** Updated user object (same as GET profile) **Error Responses:** - `409`: Email already exists - `400`: Validation error - `404`: User not found --- ## Deck Management ### Get Decks (Paginated) - RECOMMENDED **Endpoint:** `GET /api/decks/page/{from}/{to}` **Authentication:** Required (Bearer token) **Description:** Get user's accessible decks with pagination **URL Parameters:** - `from`: Start index (0-based) - `to`: End index (inclusive) **Response Data:** ```typescript { decks: ShortDeckDto[]; totalCount: number; } interface ShortDeckDto { id: string; name: string; type: number; // DeckType enum playedNumber: number; ctype: number; // DeckVisibility enum } ``` **Deck Access Rules:** - Regular users: Own private decks + all public decks - Premium users: Above + organization decks from their org - Admins: All decks ### Get All Decks - DEPRECATED **Endpoint:** `GET /api/decks` **Authentication:** Required (Bearer token) **Description:** Get all accessible decks (deprecated, use paginated version) **Response:** Array of `ShortDeckDto` ### Create Deck **Endpoint:** `POST /api/decks` **Authentication:** Required (Bearer token) **Description:** Create a new deck **Request Data:** ```typescript { name: string; // Deck name type: DeckType; // 0=LUCK, 1=JOKER, 2=QUESTION cards: Card[]; // Array of cards ctype?: DeckVisibility; // Default: PUBLIC (0) userid: string; // Auto-filled from token } ``` **Response Data:** `ShortDeckDto` **Deck Limits:** - Regular users: 8 decks max - Premium users: 12 decks max - Only premium users can create organizational decks - Organization deck limits set by admin ### Get Deck by ID **Endpoint:** `GET /api/decks/{id}` **Authentication:** Required (Bearer token) **Description:** Get detailed deck information **Response Data:** ```typescript { id: string; name: string; type: number; userid: string; creationdate: Date; cards: Card[]; playedNumber: number; ctype: number; } ``` ### Update Deck **Endpoint:** `PUT /api/decks/{id}` **Authentication:** Required (Bearer token) **Description:** Update existing deck (owner only) **Request Data:** Partial deck data to update **Response Data:** Updated `ShortDeckDto` ### Delete Deck **Endpoint:** `DELETE /api/decks/{id}` **Authentication:** Required (Bearer token) **Description:** Delete deck (owner only) **Response:** `204 No Content` ### Search Decks **Endpoint:** `GET /api/decks/search` **Authentication:** Required (Bearer token) **Description:** Search decks by name or content **Query Parameters:** - `q`: Search query (required) - `limit`: Results limit (1-100, default: 20) - `offset`: Results offset (default: 0) **Response Data:** Array of matching decks --- ## Organization Management ### Get Organizations (Paginated) **Endpoint:** `GET /api/organizations/page/{from}/{to}` **Authentication:** Required (Bearer token) **Description:** Get organizations with pagination **Response Data:** ```typescript { organizations: ShortOrganizationDto[]; totalCount: number; } interface ShortOrganizationDto { id: string; name: string; state: number; userinorg: number; maxOrganizationalDecks?: number | null; } ``` ### Get All Organizations - DEPRECATED **Endpoint:** `GET /api/organizations` **Authentication:** Required (Bearer token) ### Create Organization **Endpoint:** `POST /api/organizations` **Authentication:** Required (Bearer token) **Request Data:** ```typescript { name: string; contactfname: string; contactlname: string; contactphone: string; contactemail: string; description?: string; maxOrganizationalDecks?: number; } ``` ### Get Organization by ID **Endpoint:** `GET /api/organizations/{id}` **Authentication:** Required (Bearer token) **Response Data:** ```typescript { id: string; name: string; contactfname: string; contactlname: string; contactphone: string; contactemail: string; state: number; regdate: Date; updatedate: Date; url: string | null; userinorg: number; maxOrganizationalDecks: number | null; users: string[]; // User IDs } ``` ### Update Organization **Endpoint:** `PUT /api/organizations/{id}` **Authentication:** Required (Bearer token) ### Delete Organization **Endpoint:** `DELETE /api/organizations/{id}` **Authentication:** Required (Bearer token) --- ## Chat System ### Get User Chats **Endpoint:** `GET /api/chats/user-chats` **Authentication:** Required (Bearer token) **Description:** Get all chats for current user **Query Parameters:** - `includeArchived`: boolean (default: false) **Response Data:** ```typescript ShortChatDto[] interface ShortChatDto { id: string; userCount: number; state: number; } ``` ### Get Chat History **Endpoint:** `GET /api/chats/history/{chatId}` **Authentication:** Required (Bearer token) **Description:** Get detailed chat with message history **Response Data:** ```typescript { id: string; users: string[]; // Participant user IDs messages: Message[]; updateDate: Date; state: number; } ``` ### Create Chat **Endpoint:** `POST /api/chats` **Authentication:** Required (Bearer token) **Request Data:** ```typescript { users: string[]; // Participant user IDs type?: string; // 'direct' | 'group' | 'game' name?: string; // Group/game name gameId?: string; // Associated game ID } ``` ### Send Message **Endpoint:** `POST /api/chats/{chatId}/message` **Authentication:** Required (Bearer token) **Request Data:** ```typescript { text: string; // Message content } ``` **Response Data:** Message object with generated ID and timestamp --- ## Contact Management ### Create Contact **Endpoint:** `POST /api/contact` **Authentication:** Optional (can be anonymous) **Description:** Submit contact/support request **Request Data:** ```typescript { name: string; // Contact name email: string; // Contact email type: ContactType; // 0=BUG, 1=PROBLEM, 2=QUESTION, 3=SALES, 4=OTHER txt: string; // Message content } ``` **Response Data:** ```typescript { id: string; name: string; email: string; userid: string | null; // If authenticated type: number; txt: string; state: 0; // ACTIVE createDate: Date; updateDate: Date; } ``` **Contact Types:** - `0` - Bug Report - `1` - Problem Report - `2` - General Question - `3` - Sales Inquiry - `4` - Other --- ## Admin Endpoints All admin endpoints require authentication with admin role (UserState.ADMIN = 5). ### Get Users (Paginated) **Endpoint:** `GET /api/admin/users/page/{from}/{to}` **Authentication:** Required (Admin only) **Query Parameters:** - `includeDeleted`: boolean (default: false) **Response Data:** ```typescript { users: DetailUserDto[]; pagination: { from: number; to: number; returned: number; totalCount: number; includeDeleted: boolean; }; } ``` ### Update User State **Endpoint:** `PATCH /api/admin/users/{userId}/state` **Authentication:** Required (Admin only) **Request Data:** ```typescript { state: UserState; // New user state } ``` ### Get All Contacts **Endpoint:** `GET /api/admin/contacts` **Authentication:** Required (Admin only) **Response Data:** Array of all contact submissions ### Respond to Contact **Endpoint:** `POST /api/admin/contacts/{contactId}/response` **Authentication:** Required (Admin only) **Request Data:** ```typescript { response: string; // Admin response text } ``` --- ## Import/Export Functionality ### Export Deck **Endpoint:** `GET /api/deck-import-export/export/{deckId}` **Authentication:** Required (Bearer token) **Description:** Export user's own deck as encrypted .spr file **Response:** Binary file download ### Import Deck **Endpoint:** `POST /api/deck-import-export/import` **Authentication:** Required (Bearer token) **Description:** Import deck from .spr file **Request:** Multipart form data with file **Response Data:** Created deck object --- ## Error Handling ### Standard Error Response Format ```typescript { error: string; // Error message details?: string; // Additional details (validation errors) code?: string; // Error code } ``` ### HTTP Status Codes - `200` - Success - `201` - Created - `204` - No Content - `400` - Bad Request (validation error) - `401` - Unauthorized (authentication required) - `403` - Forbidden (insufficient permissions) - `404` - Not Found - `409` - Conflict (duplicate data) - `500` - Internal Server Error ### Common Error Scenarios **Authentication Errors:** ```typescript // Missing token { error: "Authentication required" } // Invalid token { error: "Invalid or expired token" } // Insufficient permissions { error: "Admin access required" } ``` **Validation Errors:** ```typescript // Missing required fields { error: "Missing required fields: username, password" } // Invalid field length { error: "Username must be between 3 and 50 characters" } // Invalid email format { error: "Invalid email format" } ``` **Business Logic Errors:** ```typescript // Deck limit exceeded { error: "Deck limit exceeded. Maximum 8 decks allowed for your account type." } // Organization deck restrictions { error: "Only premium users can create organizational decks." } // Ownership restrictions { error: "You can only modify your own decks." } ``` --- ## WebSocket Real-Time Communication ### Connection & Authentication Connect to WebSocket server with JWT authentication: ```typescript import io from 'socket.io-client'; // Connect with JWT token const socket = io('/', { auth: { token: localStorage.getItem('auth_token') || 'your-jwt-token' } }); // Alternative: Pass token via cookie header const socket = io('/', { extraHeaders: { cookie: `auth_token=${localStorage.getItem('auth_token')}` } }); ``` **Connection Events:** ```typescript // Connection successful socket.on('connect', () => { console.log('Connected to WebSocket server'); }); // Authentication failed socket.on('connect_error', (error) => { console.error('WebSocket connection failed:', error.message); // Redirect to login or refresh token }); // Disconnected socket.on('disconnect', (reason) => { console.log('Disconnected:', reason); }); ``` ### Chat Management Events **Initial Chat List:** ```typescript // Receive user's active chats on connection socket.on('chats:list', (chats: Array<{ id: string; type: 'direct' | 'group' | 'game'; name?: string; gameId?: string; users: string[]; lastActivity?: Date; unreadCount: number; isArchived: boolean; }>) => { // Update chat list in UI setChatList(chats); }); ``` **Join/Leave Chat:** ```typescript // Join a chat room socket.emit('chat:join', { chatId: 'chat-uuid' }); // Confirmation of joining socket.on('chat:joined', (data: { chatId: string; messages: Message[]; // Last 10 messages }) => { // Load chat messages into UI }); // Leave a chat room socket.emit('chat:leave', { chatId: 'chat-uuid' }); // Confirmation of leaving socket.on('chat:left', (data: { chatId: string }) => { // Update UI to show chat as inactive }); ``` **Send/Receive Messages:** ```typescript // Send message to chat socket.emit('message:send', { chatId: 'chat-uuid', message: 'Hello everyone!' }); // Receive new message in chat socket.on('message:received', (data: { chatId: string; message: { id: string; date: Date; userid: string; text: string; }; }) => { // Add message to chat UI addMessageToChat(data.chatId, data.message); }); ``` **Rate Limiting:** - Maximum 100 messages per user per minute - Exceeded limit returns error: `{ message: "Rate limit exceeded. Maximum 100 messages per minute allowed." }` ### Chat Creation Events **Create Group Chat:** ```typescript // Create group (premium users only) socket.emit('group:create', { name: 'Study Group', userIds: ['user-1', 'user-2', 'user-3'] }); // Group created successfully socket.on('group:created', (data: { chat: { id: string; type: 'group'; name: string; createdBy: string; users: string[]; messages: Message[]; }; }) => { // Add new group to chat list addChatToList(data.chat); }); ``` **Create Direct Chat:** ```typescript // Create direct message socket.emit('chat:direct', { targetUserId: 'user-uuid' }); // Direct chat created socket.on('chat:direct:created', (data: { chat: { id: string; type: 'direct'; users: string[]; messages: Message[]; }; }) => { // Add direct chat to list }); // Direct chat already exists socket.on('chat:direct:exists', (data: { chatId: string; }) => { // Navigate to existing chat }); ``` **Create Game Chat:** ```typescript // Create game-specific chat socket.emit('game:chat:create', { gameId: 'game-uuid', gameName: 'Quiz Battle', playerIds: ['player-1', 'player-2'] }); // Game chat created socket.on('game:chat:created', (data: { chat: { id: string; type: 'game'; name: string; gameId: string; users: string[]; messages: Message[]; }; }) => { // Show game chat in UI }); // Game chat already exists socket.on('game:chat:exists', (data: { chatId: string; }) => { // Use existing game chat }); ``` ### Chat History & Archive **Get Chat History:** ```typescript // Request full chat history socket.emit('chat:history', { chatId: 'chat-uuid' }); // Receive chat history (active chat) socket.on('chat:history', (data: { chatId: string; messages: Message[]; chatInfo: { type: 'direct' | 'group' | 'game'; name?: string; gameId?: string; users: string[]; }; }) => { // Display full chat history }); // Receive archived chat history socket.on('chat:history:archived', (data: { chatId: string; messages: Message[]; chatType: 'direct' | 'group' | 'game'; isGameChat: boolean; }) => { // Display archived chat with read-only UI }); ``` ### Message Management & Pruning **Message Retention Rules:** - **All chats**: Messages older than 2 weeks are automatically deleted - **Direct & Game chats**: Maximum 10 messages per user kept - **Group chats**: No per-user message limit (time limit only) - **Archive**: Inactive chats (no activity for 30 minutes) are archived - **Cleanup**: Archived messages are cleaned up after 4 weeks ### Error Handling **WebSocket Error Events:** ```typescript // General errors socket.on('error', (error: { message: string }) => { console.error('WebSocket error:', error.message); // Common error messages: // - "Chat not found" // - "Unauthorized to join this chat" // - "Unauthorized to send message to this chat" // - "Message must be a non-empty string" // - "Premium subscription required to create groups" // - "Group name is required" // - "At least one member is required" // - "One or more users not found" // - "Target user not found" // - "Failed to join chat" // - "Failed to send message" // - "Failed to create group" // - "Failed to create direct chat" // - "Failed to create game chat" // - "Failed to get chat history" // Handle error in UI showErrorMessage(error.message); }); ``` ### Complete WebSocket Implementation Example ```typescript // hooks/useWebSocket.ts import { useEffect, useRef, useState } from 'react'; import io, { Socket } from 'socket.io-client'; export const useWebSocket = () => { const socketRef = useRef(null); const [isConnected, setIsConnected] = useState(false); const [chats, setChats] = useState([]); useEffect(() => { const token = localStorage.getItem('auth_token'); if (!token) return; // Connect to WebSocket socketRef.current = io('/', { auth: { token } }); const socket = socketRef.current; // Connection events socket.on('connect', () => { console.log('Connected to WebSocket'); setIsConnected(true); }); socket.on('connect_error', (error) => { console.error('Connection failed:', error.message); setIsConnected(false); }); socket.on('disconnect', () => { console.log('Disconnected from WebSocket'); setIsConnected(false); }); // Chat events socket.on('chats:list', (chatList) => { setChats(chatList); }); socket.on('message:received', (data) => { setChats(prev => prev.map(chat => chat.id === data.chatId ? { ...chat, messages: [...chat.messages, data.message] } : chat )); }); socket.on('chat:joined', (data) => { console.log('Joined chat:', data.chatId); }); socket.on('error', (error) => { console.error('WebSocket error:', error.message); }); return () => { socket.disconnect(); }; }, []); // Helper functions const joinChat = (chatId: string) => { socketRef.current?.emit('chat:join', { chatId }); }; const sendMessage = (chatId: string, message: string) => { socketRef.current?.emit('message:send', { chatId, message }); }; const createDirectChat = (targetUserId: string) => { socketRef.current?.emit('chat:direct', { targetUserId }); }; const createGroup = (name: string, userIds: string[]) => { socketRef.current?.emit('group:create', { name, userIds }); }; return { isConnected, chats, joinChat, sendMessage, createDirectChat, createGroup }; }; ``` ### Rate Limiting & Performance **Message Rate Limits:** - Maximum 100 messages per user per minute - Rate limit counters reset every minute - Counters cleaned up after 5 minutes of inactivity **Connection Management:** - Automatic reconnection on disconnection - User status tracking (online/offline) - Chat room management with Redis caching - Inactive chat archiving after 30 minutes **Performance Considerations:** - Messages pruned automatically (2 weeks retention) - Redis used for active session management - Database archiving for inactive chats - Efficient room-based message broadcasting --- This documentation provides the complete API reference based on the actual backend implementation. All endpoints, data structures, and business rules are derived directly from the TypeScript source code and database schema. --- ## Authentication & Security ### JWT Token Management ```typescript // lib/auth/tokenManager.ts export class TokenManager { private static readonly TOKEN_KEY = 'serpentrace_auth_token'; private static readonly REFRESH_KEY = 'serpentrace_refresh_token'; static setTokens(accessToken: string, refreshToken?: string) { localStorage.setItem(this.TOKEN_KEY, accessToken); if (refreshToken) { localStorage.setItem(this.REFRESH_KEY, refreshToken); } } static getAccessToken(): string | null { return localStorage.getItem(this.TOKEN_KEY); } static getRefreshToken(): string | null { return localStorage.getItem(this.REFRESH_KEY); } static clearTokens() { localStorage.removeItem(this.TOKEN_KEY); localStorage.removeItem(this.REFRESH_KEY); } static isTokenExpired(token: string): boolean { try { const payload = JSON.parse(atob(token.split('.')[1])); return payload.exp * 1000 < Date.now(); } catch { return true; } } } ``` ### Protected Routes ```typescript // components/layout/ProtectedRoute.tsx import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/store/auth'; interface ProtectedRouteProps { children: React.ReactNode; requiredRole?: 'admin' | 'premium' | 'verified'; } export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) { const { user, isAuthenticated, checkAuth } = useAuthStore(); const router = useRouter(); useEffect(() => { if (!isAuthenticated) { checkAuth().then((authenticated) => { if (!authenticated) { router.push('/login'); } }); } }, [isAuthenticated, checkAuth, router]); useEffect(() => { if (isAuthenticated && user && requiredRole) { const hasAccess = checkUserRole(user, requiredRole); if (!hasAccess) { router.push('/unauthorized'); } } }, [isAuthenticated, user, requiredRole, router]); if (!isAuthenticated || !user) { return ; } if (requiredRole && !checkUserRole(user, requiredRole)) { return null; // Will redirect in useEffect } return <>{children}; } function checkUserRole(user: User, requiredRole: string): boolean { switch (requiredRole) { case 'admin': return user.state === UserState.ADMIN; case 'premium': return user.state === UserState.VERIFIED_PREMIUM || user.state === UserState.ADMIN; case 'verified': return user.state >= UserState.VERIFIED_REGULAR; default: return true; } } ``` ### Authentication Context ```typescript // store/auth.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { authAPI } from '@/lib/api/auth'; import { TokenManager } from '@/lib/auth/tokenManager'; interface AuthState { user: User | null; isAuthenticated: boolean; isLoading: boolean; login: (credentials: LoginRequest) => Promise; register: (userData: RegisterRequest) => Promise; logout: () => void; checkAuth: () => Promise; updateUser: (userData: Partial) => void; } export const useAuthStore = create()( persist( (set, get) => ({ user: null, isAuthenticated: false, isLoading: false, login: async (credentials) => { set({ isLoading: true }); try { const response = await authAPI.login(credentials); TokenManager.setTokens(response.data.token); set({ user: response.data.user, isAuthenticated: true, isLoading: false, }); return response.data; } catch (error) { set({ isLoading: false }); throw error; } }, register: async (userData) => { set({ isLoading: true }); try { const response = await authAPI.register(userData); set({ isLoading: false }); return response.data; } catch (error) { set({ isLoading: false }); throw error; } }, logout: () => { TokenManager.clearTokens(); set({ user: null, isAuthenticated: false, }); }, checkAuth: async () => { const token = TokenManager.getAccessToken(); if (!token || TokenManager.isTokenExpired(token)) { set({ isAuthenticated: false, user: null }); return false; } try { const response = await authAPI.getProfile(); set({ user: response.data, isAuthenticated: true, }); return true; } catch { TokenManager.clearTokens(); set({ isAuthenticated: false, user: null }); return false; } }, updateUser: (userData) => { const { user } = get(); if (user) { set({ user: { ...user, ...userData } }); } }, }), { name: 'auth-storage', partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }), } ) ); ``` --- ## Core Features Implementation ### 1. User Registration & Login Forms ```typescript // components/forms/LoginForm.tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useAuthStore } from '@/store/auth'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { toast } from '@/lib/toast'; const loginSchema = z.object({ username: z.string().min(3, 'Username must be at least 3 characters'), password: z.string().min(6, 'Password must be at least 6 characters'), }); type LoginForm = z.infer; export function LoginForm() { const { login } = useAuthStore(); const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(loginSchema), }); const onSubmit = async (data: LoginForm) => { try { const response = await login(data); if (response.requiresOrgReauth) { toast.info(`Please complete authentication with ${response.organizationName}`); window.location.href = response.orgLoginUrl!; return; } toast.success('Login successful!'); window.location.href = '/dashboard'; } catch (error: any) { toast.error(error.response?.data?.message || 'Login failed'); } }; return (
); } ``` ### 2. Deck Creation & Management ```typescript // components/features/decks/DeckEditor.tsx import { useForm, useFieldArray } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { DeckType, DeckVisibility, CardType } from '@/types/deck'; const cardSchema = z.object({ id: z.string(), type: z.nativeEnum(CardType), text: z.string().min(1, 'Question text is required'), answer: z.union([z.string(), z.boolean(), z.null()]).optional(), options: z.array(z.string()).optional(), }); const deckSchema = z.object({ name: z.string().min(1, 'Deck name is required').max(255, 'Name too long'), type: z.nativeEnum(DeckType), ctype: z.nativeEnum(DeckVisibility), cards: z.array(cardSchema).min(1, 'At least one card is required').max(50, 'Maximum 50 cards allowed'), }); type DeckForm = z.infer; interface DeckEditorProps { initialData?: Deck; onSave: (data: CreateDeckRequest) => Promise; onCancel: () => void; } export function DeckEditor({ initialData, onSave, onCancel }: DeckEditorProps) { const { register, control, handleSubmit, watch, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(deckSchema), defaultValues: initialData || { name: '', type: DeckType.QUESTION, ctype: DeckVisibility.PUBLIC, cards: [{ id: crypto.randomUUID(), type: CardType.QUIZ, text: '', answer: '', options: ['', '', '', ''] }], }, }); const { fields, append, remove } = useFieldArray({ control, name: 'cards', }); const deckType = watch('type'); const addCard = () => { append({ id: crypto.randomUUID(), type: CardType.QUIZ, text: '', answer: '', options: ['', '', '', ''], }); }; const onSubmit = async (data: DeckForm) => { try { await onSave(data); toast.success('Deck saved successfully!'); } catch (error: any) { toast.error(error.response?.data?.message || 'Failed to save deck'); } }; return (
{/* Deck Basic Information */}

Deck Information

{/* Cards Section */}

Cards ({fields.length})

{fields.map((field, index) => ( remove(index)} canRemove={fields.length > 1} register={register} errors={errors.cards?.[index]} /> ))}
{/* Actions */}
); } ``` ### 3. Real-time Chat Implementation ```typescript // hooks/useSocket.ts import { useEffect, useRef } from 'react'; import { io, Socket } from 'socket.io-client'; import { useAuthStore } from '@/store/auth'; import { TokenManager } from '@/lib/auth/tokenManager'; export function useSocket() { const socket = useRef(null); const { user, isAuthenticated } = useAuthStore(); useEffect(() => { if (isAuthenticated && user) { socket.current = io(process.env.NEXT_PUBLIC_WS_URL || 'http://localhost:3000', { auth: { token: TokenManager.getAccessToken(), }, }); socket.current.on('connect', () => { console.log('Connected to WebSocket server'); }); socket.current.on('disconnect', () => { console.log('Disconnected from WebSocket server'); }); return () => { socket.current?.disconnect(); }; } }, [isAuthenticated, user]); return socket.current; } ``` ```typescript // components/features/chat/ChatWindow.tsx import { useState, useEffect, useRef } from 'react'; import { useSocket } from '@/hooks/useSocket'; import { useChatStore } from '@/store/chat'; import { useAuthStore } from '@/store/auth'; import { Message, Chat } from '@/types/chat'; interface ChatWindowProps { chatId: string; onClose: () => void; } export function ChatWindow({ chatId, onClose }: ChatWindowProps) { const socket = useSocket(); const { user } = useAuthStore(); const { currentChat, messages, sendMessage, loadChat } = useChatStore(); const [newMessage, setNewMessage] = useState(''); const messagesEndRef = useRef(null); useEffect(() => { loadChat(chatId); }, [chatId, loadChat]); useEffect(() => { if (!socket) return; socket.emit('join-chat', chatId); socket.on('new-message', (message: Message) => { // Handle incoming messages useChatStore.getState().addMessage(chatId, message); }); socket.on('user-typing', (data: { userId: string; isTyping: boolean }) => { // Handle typing indicators }); return () => { socket.off('new-message'); socket.off('user-typing'); socket.emit('leave-chat', chatId); }; }, [socket, chatId]); useEffect(() => { scrollToBottom(); }, [messages]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!newMessage.trim() || !user) return; const message: Omit = { userid: user.id, text: newMessage.trim(), }; try { await sendMessage(chatId, message); setNewMessage(''); } catch (error) { toast.error('Failed to send message'); } }; if (!currentChat) { return
Loading chat...
; } return (
{/* Chat Header */}

{currentChat.name || `Chat with ${currentChat.users.length} users`}

{currentChat.type} chat

{/* Messages */}
{messages.map((message) => (

{message.text}

{new Date(message.date).toLocaleTimeString()}

))}
{/* Message Input */}
setNewMessage(e.target.value)} placeholder="Type a message..." className="flex-1" />
); } ``` --- ## UI/UX Guidelines ### Design System ```typescript // components/ui/Button.tsx import { cn } from '@/lib/utils'; import { ButtonHTMLAttributes, forwardRef } from 'react'; interface ButtonProps extends ButtonHTMLAttributes { variant?: 'default' | 'outline' | 'ghost' | 'destructive'; size?: 'sm' | 'md' | 'lg'; } export const Button = forwardRef( ({ className, variant = 'default', size = 'md', ...props }, ref) => { return ( {isOpen && (

Notifications

{notifications.length === 0 ? (
No notifications
) : ( notifications.map(notification => (
markAsRead(notification.id)} >

{notification.title}

{notification.message}

{!notification.read && (
)}

{notification.timestamp.toLocaleString()}

)) )}
)}
); } ``` --- ## Performance Optimization ### Code Splitting & Lazy Loading ```typescript // app/dashboard/decks/page.tsx import dynamic from 'next/dynamic'; import { Suspense } from 'react'; const DeckManagement = dynamic(() => import('@/components/features/decks/DeckManagement'), { loading: () => , }); export default function DecksPage() { return ( }> ); } ``` ### Virtual Scrolling for Large Lists ```typescript // components/ui/VirtualizedList.tsx import { FixedSizeList as List } from 'react-window'; interface VirtualizedListProps { items: T[]; height: number; itemHeight: number; renderItem: ({ index, style }: { index: number; style: React.CSSProperties }) => JSX.Element; } export function VirtualizedList({ items, height, itemHeight, renderItem }: VirtualizedListProps) { return ( {renderItem} ); } ``` ### Memoization & Optimization ```typescript // hooks/useOptimizedDecks.ts import { useMemo } from 'react'; import { useGetDecksQuery } from '@/store/api/deckApi'; import { Deck, DeckType, DeckVisibility } from '@/types/deck'; interface DeckFilters { search?: string; type?: DeckType; visibility?: DeckVisibility; sortBy?: 'name' | 'date' | 'plays'; sortOrder?: 'asc' | 'desc'; } export function useOptimizedDecks(filters: DeckFilters = {}) { const { data, isLoading, error } = useGetDecksQuery({ from: 0, to: 100, // Adjust based on pagination needs }); const filteredAndSortedDecks = useMemo(() => { if (!data?.decks) return []; let filtered = data.decks; // Apply search filter if (filters.search) { const searchLower = filters.search.toLowerCase(); filtered = filtered.filter(deck => deck.name.toLowerCase().includes(searchLower) || deck.cards.some(card => card.text.toLowerCase().includes(searchLower)) ); } // Apply type filter if (filters.type !== undefined) { filtered = filtered.filter(deck => deck.type === filters.type); } // Apply visibility filter if (filters.visibility !== undefined) { filtered = filtered.filter(deck => deck.ctype === filters.visibility); } // Apply sorting if (filters.sortBy) { filtered.sort((a, b) => { let aValue, bValue; switch (filters.sortBy) { case 'name': aValue = a.name.toLowerCase(); bValue = b.name.toLowerCase(); break; case 'date': aValue = new Date(a.creationdate).getTime(); bValue = new Date(b.creationdate).getTime(); break; case 'plays': aValue = a.playedNumber; bValue = b.playedNumber; break; default: return 0; } if (filters.sortOrder === 'desc') { return aValue < bValue ? 1 : -1; } return aValue > bValue ? 1 : -1; }); } return filtered; }, [data?.decks, filters]); return { decks: filteredAndSortedDecks, totalCount: data?.totalCount || 0, isLoading, error, }; } ``` --- ## Testing Strategy ### Component Testing ```typescript // components/forms/__tests__/LoginForm.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { LoginForm } from '../LoginForm'; import { useAuthStore } from '@/store/auth'; // Mock the auth store jest.mock('@/store/auth'); const mockUseAuthStore = useAuthStore as jest.MockedFunction; describe('LoginForm', () => { const mockLogin = jest.fn(); beforeEach(() => { mockUseAuthStore.mockReturnValue({ login: mockLogin, user: null, isAuthenticated: false, isLoading: false, register: jest.fn(), logout: jest.fn(), checkAuth: jest.fn(), updateUser: jest.fn(), }); }); afterEach(() => { jest.clearAllMocks(); }); it('renders login form fields', () => { render(); expect(screen.getByPlaceholderText('Username')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Password')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument(); }); it('validates required fields', async () => { render(); fireEvent.click(screen.getByRole('button', { name: /sign in/i })); await waitFor(() => { expect(screen.getByText('Username must be at least 3 characters')).toBeInTheDocument(); expect(screen.getByText('Password must be at least 6 characters')).toBeInTheDocument(); }); }); it('submits form with valid credentials', async () => { mockLogin.mockResolvedValue({ token: 'mock-token', user: { id: '1', username: 'testuser' }, requiresOrgReauth: false, }); render(); fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: 'testuser' }, }); fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'password123' }, }); fireEvent.click(screen.getByRole('button', { name: /sign in/i })); await waitFor(() => { expect(mockLogin).toHaveBeenCalledWith({ username: 'testuser', password: 'password123', }); }); }); }); ``` ### API Testing ```typescript // lib/api/__tests__/deckApi.test.ts import { deckApi } from '@/store/api/deckApi'; import { store } from '@/store'; import { setupApiMocking } from '@/test-utils/apiMocking'; const { server } = setupApiMocking(); describe('deckApi', () => { beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); it('fetches decks successfully', async () => { const mockDecks = [ { id: '1', name: 'Test Deck 1', type: 0 }, { id: '2', name: 'Test Deck 2', type: 1 }, ]; const result = await store.dispatch( deckApi.endpoints.getDecks.initiate({ from: 0, to: 10 }) ); expect(result.data).toEqual({ decks: mockDecks, totalCount: 2, }); }); it('creates deck successfully', async () => { const newDeck = { name: 'New Deck', type: 2, cards: [{ id: '1', type: 0, text: 'Test question' }], }; const result = await store.dispatch( deckApi.endpoints.createDeck.initiate(newDeck) ); expect(result.data).toMatchObject(newDeck); }); }); ``` --- ## Deployment Guide ### Environment Configuration ```bash # .env.local NEXT_PUBLIC_API_URL=https://api.serpentrace.com NEXT_PUBLIC_WS_URL=wss://api.serpentrace.com NEXTAUTH_URL=https://serpentrace.com NEXTAUTH_SECRET=your-secret-key-here ``` ### Docker Configuration ```dockerfile # Dockerfile FROM node:18-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --only=production FROM node:18-alpine AS builder WORKDIR /app COPY . . COPY --from=deps /app/node_modules ./node_modules RUN npm run build FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV production RUN addgroup -g 1001 -S nodejs RUN adduser -S nextjs -u 1001 COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"] ``` ### CI/CD Pipeline ```yaml # .github/workflows/deploy.yml name: Deploy to Production on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - run: npm ci - run: npm run test - run: npm run build deploy: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy to production run: | # Add your deployment commands here echo "Deploying to production..." ``` --- ## Security Considerations ### Content Security Policy ```typescript // next.config.js const securityHeaders = [ { key: 'Content-Security-Policy', value: ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss: ws:; `.replace(/\s{2,}/g, ' ').trim() }, ]; module.exports = { async headers() { return [ { source: '/(.*)', headers: securityHeaders, }, ]; }, }; ``` ### Input Sanitization ```typescript // lib/security/sanitize.ts import DOMPurify from 'dompurify'; export function sanitizeHtml(dirty: string): string { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u'], ALLOWED_ATTR: [], }); } export function sanitizeText(text: string): string { return text .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/[<>]/g, '') .trim(); } ``` --- This comprehensive frontend documentation provides a complete guide for implementing a modern, scalable, and secure frontend for the SerpentRace platform. The documentation covers all major aspects from setup to deployment, following best practices and industry standards.