diff --git a/Documentations/FRONTEND_IMPLEMENTATION_GUIDE.md b/Documentations/FRONTEND_IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 36bf89a2..00000000 --- a/Documentations/FRONTEND_IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,2698 +0,0 @@ -# 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. diff --git a/Documentations/FRONTEND_IMPLEMENTATION_GUIDE_COMPLETE.md b/Documentations/FRONTEND_IMPLEMENTATION_GUIDE_COMPLETE.md new file mode 100644 index 00000000..22c5cde4 --- /dev/null +++ b/Documentations/FRONTEND_IMPLEMENTATION_GUIDE_COMPLETE.md @@ -0,0 +1,1439 @@ +# SerpentRace Backend API Documentation for Frontend Developers +## Complete API Reference with All Endpoints + +## Table of Contents +1. [Test User Credentials](#test-user-credentials) +2. [Data Structures & Entities](#data-structures--entities) +3. [Base URL & Service Info](#base-url--service-info) +4. [Authentication Endpoints](#authentication-endpoints) +5. [User Management](#user-management) +6. [Deck Management](#deck-management) +7. [Organization Management](#organization-management) +8. [Chat System](#chat-system) +9. [Contact Management](#contact-management) +10. [Import/Export Functionality](#importexport-functionality) +11. [Admin Endpoints](#admin-endpoints) +12. [Error Handling](#error-handling) +13. [WebSocket Real-Time Communication](#websocket-real-time-communication) + +--- + +## 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 DTOs +```typescript +interface ShortUserDto { + id: string; // UUID + username: string; // Username + state: number; // UserState enum + authLevel: 0 | 1; // 0 = regular, 1 = admin +} + +interface DetailUserDto { + id: string; // UUID + orgid: string | null; // Organization ID (if member) + username: string; // Unique username + email: string; // Email address + fname: string; // First name + lname: string; // Last name + code: string | null; // Verification code + type: string; // 'personal' | 'premium' | 'admin' + phone: string | null; // Phone number + state: number; // UserState enum value +} + +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 DTOs +```typescript +interface ShortDeckDto { + id: string; // UUID + name: string; // Deck name + type: number; // DeckType enum value + playedNumber: number; // Times played + ctype: number; // DeckVisibility enum value +} + +interface DetailDeckDto { + id: string; // UUID + name: string; // Deck name + type: number; // DeckType enum value + userid: string; // Owner's user ID + creationdate: Date; // Creation timestamp + cards: Card[]; // Array of cards + playedNumber: number; // Times played + ctype: number; // DeckVisibility enum value +} + +interface Card { + text: string; // Question/prompt text + type?: number; // CardType enum (optional) + answer?: string | null; // Answer (varies by type) +} + +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 +} + +enum CardType { + QUIZ = 0, // Multiple choice question + SENTENCE_PAIRING = 1, // Sentence completion + OWN_ANSWER = 2, // Custom answer + TRUE_FALSE = 3, // True/False question + CLOSER = 4 // Closer to answer +} +``` + +### Organization DTOs +```typescript +interface ShortOrganizationDto { + id: string; // UUID + name: string; // Organization name + contactfname: string; // Contact first name + contactlname: string; // Contact last name + contactemail: string; // Contact email + state: number; // OrganizationState enum + regdate: Date; // Registration date + maxOrganizationalDecks: number | null; // Max org decks allowed +} + +enum OrganizationState { + REGISTERED = 0, // Just registered + ACTIVE = 1, // Active organization + SOFT_DELETE = 2 // Soft deleted +} +``` + +### Chat DTOs +```typescript +interface ShortChatDto { + id: string; // UUID + userCount: number; // Number of participants + state: number; // ChatState enum value +} + +interface DetailChatDto { + id: string; // UUID + users: string[]; // Participant user IDs + messages: Message[]; // Message history + updateDate: Date; // Last update + state: number; // ChatState enum value +} + +interface Message { + id: string; // Message UUID + date: Date; // Message timestamp + userid: string; // Sender 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 DTOs +```typescript +interface ContactDto { + id: string; // UUID + name: string; // Contact name + email: string; // Contact email + userid: string | null; // User ID if logged in + type: number; // ContactType enum + txt: string; // Message content + state: number; // ContactState enum + 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 +} +``` + +--- + +## Base URL & Service Info + +**Base URL:** `http://localhost:3000` (development) + +### Service Information +**Endpoint:** `GET /` + +**Authentication:** None required + +**Response Data:** +```typescript +{ + service: "SerpentRace Backend API"; + status: "running"; + version: "1.0.0"; + endpoints: { + swagger: "/api-docs"; + users: "/api/users"; + organizations: "/api/organizations"; + decks: "/api/decks"; + chats: "/api/chats"; + contacts: "/api/contacts"; + admin: "/api/admin"; + deckImportExport: "/api/deck-import-export"; + health: "/health"; + }; + websocket: { + enabled: true; + events: string[]; // WebSocket event names + }; +} +``` + +### Health Check +**Endpoint:** `GET /health` + +**Authentication:** None required + +**Response Data:** +```typescript +{ + status: "healthy" | "unhealthy"; + timestamp: string; // ISO timestamp + service: "SerpentRace Backend API"; + version: "1.0.0"; + environment: string; // "development" | "production" + database: { + connected: boolean; // Database connection status + type: string; // Database type + }; + websocket: { + enabled: boolean; // WebSocket status + }; + uptime: number; // Process uptime in seconds +} +``` + +--- + +## Authentication Endpoints + +### User Login +**Endpoint:** `POST /api/users/login` + +**Authentication:** None required + +**Validation Rules:** +- `username`: 3-50 characters +- `password`: 6-100 characters + +**Request Data:** +```typescript +{ + username: string; // Username or email + password: string; // Password +} +``` + +**Response Data (Success):** +```typescript +{ + token: string; // JWT token (also set as cookie) + user: ShortUserDto; // User information + organizationName?: string; // Organization name (if member) +} +``` + +**Error Responses:** +- `400`: Validation error +- `401`: Invalid credentials, unverified email, or account restrictions +- `500`: Internal server error + +### User Registration +**Endpoint:** `POST /api/users/create` + +**Authentication:** None required + +**Validation Rules:** +- `username`: 3-50 characters, unique +- `email`: valid email format, unique +- `password`: 6-100 characters + +**Request Data:** +```typescript +{ + username: string; // Unique username + email: string; // Valid email + password: string; // Password + fname?: string; // First name (optional) + lname?: string; // Last name (optional) + phone?: string; // Phone number (optional) + type?: string; // User type (optional) +} +``` + +**Response Data (Success):** +```typescript +{ + id: string; // User UUID + username: string; // Username + email: string; // Email + regdate: Date; // Registration date +} +``` + +**Error Responses:** +- `400`: Validation error +- `409`: Username or email already exists +- `500`: Internal server error + +--- + +## User Management + +### Get User Profile +**Endpoint:** `GET /api/users/profile` + +**Authentication:** Required + +**Response Data:** `DetailUserDto` + +### Update User Profile +**Endpoint:** `PATCH /api/users/profile` + +**Authentication:** Required + +**Request Data:** Partial `DetailUserDto` fields to update + +**Response Data:** Updated `DetailUserDto` + +**Error Responses:** +- `400`: Validation error +- `404`: User not found +- `409`: Email already exists +- `500`: Internal server error + +--- + +## Deck Management + +### Get Decks (Paginated) - RECOMMENDED +**Endpoint:** `GET /api/decks/page/{from}/{to}` + +**Authentication:** Required + +**URL Parameters:** +- `from`: Start index (0-based, ≥ 0) +- `to`: End index (inclusive, ≥ from) + +**Response Data:** +```typescript +{ + decks: ShortDeckDto[]; // Array of deck summaries + totalCount: number; // Total available decks +} +``` + +### Create Deck +**Endpoint:** `POST /api/decks` + +**Authentication:** Required + +**Request Data:** +```typescript +{ + name: string; // Deck name (required) + type?: number; // DeckType enum (optional) + cards?: Card[]; // Initial cards (optional) + ctype?: number; // DeckVisibility enum (optional) +} +``` + +**Response Data:** `ShortDeckDto` + +### Search Decks +**Endpoint:** `GET /api/decks/search` + +**Authentication:** Required + +**Query Parameters:** +- `query`: Search query (required, non-empty) +- `limit`: Results limit (1-100, default: 20) +- `offset`: Results offset (≥ 0, default: 0) + +**Response Data:** Array of matching `ShortDeckDto` + +### Get Deck by ID +**Endpoint:** `GET /api/decks/{id}` + +**Authentication:** Required + +**URL Parameters:** +- `id`: Deck UUID + +**Response Data:** `DetailDeckDto` + +### Update Deck +**Endpoint:** `PUT /api/decks/{id}` + +**Authentication:** Required (owner only) + +**URL Parameters:** +- `id`: Deck UUID + +**Request Data:** Partial `DetailDeckDto` fields to update + +**Response Data:** Updated `ShortDeckDto` + +### Delete Deck (Soft Delete) +**Endpoint:** `DELETE /api/decks/{id}` + +**Authentication:** Required (owner only) + +**URL Parameters:** +- `id`: Deck UUID + +**Response Data:** +```typescript +{ + success: boolean; // Deletion success status +} +``` + +--- + +## Organization Management + +### Get Organizations (Paginated) - RECOMMENDED +**Endpoint:** `GET /api/organizations/page/{from}/{to}` + +**Authentication:** Required + +**URL Parameters:** +- `from`: Start index (0-based, ≥ 0) +- `to`: End index (inclusive, ≥ from) + +**Response Data:** +```typescript +{ + organizations: ShortOrganizationDto[]; + totalCount: number; +} +``` + +### Search Organizations +**Endpoint:** `GET /api/organizations/search` + +**Authentication:** Required + +**Query Parameters:** +- `query`: Search query (required, non-empty) +- `limit`: Results limit (1-100, default: 20) +- `offset`: Results offset (≥ 0, default: 0) + +**Response Data:** Array of matching organizations + +### Get Organization Login URL +**Endpoint:** `GET /api/organizations/{orgId}/login-url` + +**Authentication:** Required + +**URL Parameters:** +- `orgId`: Organization UUID + +**Response Data:** +```typescript +{ + loginUrl: string; // Organization login URL + organizationName: string; // Organization name +} +``` + +### Process Organization Auth Callback +**Endpoint:** `POST /api/organizations/auth-callback` + +**Authentication:** Required + +**Request Data:** +```typescript +{ + organizationId: string; // Organization UUID + status: "ok" | "not_ok"; // Authentication status + authToken?: string; // Authentication token (optional) +} +``` + +**Response Data:** +```typescript +{ + success: boolean; // Processing success + message: string; // Result message + updatedFields?: string[]; // Fields that were updated +} +``` + +--- + +## Chat System + +### Get User Chats +**Endpoint:** `GET /api/chats/user-chats` + +**Authentication:** Required + +**Query Parameters:** +- `includeArchived`: boolean (default: false) + +**Response Data:** Array of `ShortChatDto` + +### Get Chat History +**Endpoint:** `GET /api/chats/history/{chatId}` + +**Authentication:** Required + +**URL Parameters:** +- `chatId`: Chat UUID (validated) + +**Response Data:** +```typescript +{ + id: string; // Chat UUID + users: string[]; // Participants + messages: Message[]; // Message history + isArchived: boolean; // Archive status + state: number; // Chat state +} +``` + +### Create Chat +**Endpoint:** `POST /api/chats/create` + +**Authentication:** Required + +**Validation Rules:** +- `type`: must be 'direct' or 'group' +- `userIds`: non-empty array +- `name`: required for groups + +**Request Data:** +```typescript +{ + type: 'direct' | 'group'; // Chat type + userIds: string[]; // Participant user IDs + name?: string; // Group name (required for groups) +} +``` + +**Response Data:** +```typescript +{ + id: string; // Chat UUID + type: ChatType; // Chat type + name: string | null; // Chat name + users: string[]; // Participants + messages: Message[]; // Empty initially +} +``` + +### Send Message (REST - for testing) +**Endpoint:** `POST /api/chats/message` + +**Authentication:** Required + +**Validation Rules:** +- `chatId`: valid UUID +- `message`: 1-2000 characters + +**Request Data:** +```typescript +{ + chatId: string; // Chat UUID + message: string; // Message content +} +``` + +**Response Data:** `Message` object + +### Archive Chat +**Endpoint:** `POST /api/chats/archive/{chatId}` + +**Authentication:** Required + +**URL Parameters:** +- `chatId`: Chat UUID (validated) + +**Response Data:** +```typescript +{ + success: boolean; + message: string; +} +``` + +### Restore Chat from Archive +**Endpoint:** `POST /api/chats/restore/{chatId}` + +**Authentication:** Required + +**URL Parameters:** +- `chatId`: Chat UUID (validated) + +**Response Data:** +```typescript +{ + success: boolean; + message: string; +} +``` + +### Get Archived Game Chats +**Endpoint:** `GET /api/chats/archived/game/{gameId}` + +**Authentication:** Required + +**URL Parameters:** +- `gameId`: Game UUID (validated) + +**Response Data:** Array of archived chat objects + +--- + +## Contact Management + +### Create Contact +**Endpoint:** `POST /api/contact` + +**Authentication:** Optional + +**Validation Rules:** +- `name`, `email`, `type`, `txt`: required fields +- `type`: must be 0-4 (ContactType enum) + +**Request Data:** +```typescript +{ + name: string; // Contact name (required) + email: string; // Contact email (required) + type: ContactType; // Contact type (0-4) + txt: string; // Message content (required) +} +``` + +**Response Data:** `ContactDto` + +**Error Responses:** +- `400`: Missing fields or invalid contact type +- `500`: Internal server error + +--- + +## Import/Export Functionality + +### Export Deck +**Endpoint:** `GET /api/deck-import-export/export/{deckId}` + +**Authentication:** Required (deck owner only) + +**URL Parameters:** +- `deckId`: Deck UUID + +**Response:** Binary .spr file download + +**Headers:** +- `Content-Type`: `application/octet-stream` +- `Content-Disposition`: `attachment; filename="deckname.spr"` + +### Import Deck +**Endpoint:** `POST /api/deck-import-export/import` + +**Authentication:** Required + +**Request:** Multipart form data +- `file`: .spr or JSON file (max 10MB) + +**Response Data:** +```typescript +{ + success: boolean; + message: string; + deckId: string; // Created deck ID +} +``` + +--- + +## Admin Endpoints + +All admin endpoints require authentication with admin role (UserState.ADMIN = 5). + +### User Management (Admin) + +#### Get Users (Paginated) - RECOMMENDED +**Endpoint:** `GET /api/admin/users/page/{from}/{to}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**URL Parameters:** +- `from`: Start index (0-based) +- `to`: End index (inclusive, max page size: 100) + +**Response Data:** +```typescript +{ + users: DetailUserDto[]; // Array of detailed user objects + pagination: { + from: number; // Start index + to: number; // End index + returned: number; // Actual returned count + totalCount: number; // Total user count + includeDeleted: boolean; // Include deleted flag + }; +} +``` + +#### Get User by ID (Admin) +**Endpoint:** `GET /api/admin/users/{userId}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** `DetailUserDto` or null + +#### Search Users (Admin) +**Endpoint:** `GET /api/admin/users/search/{searchTerm}` + +**URL Parameters:** +- `searchTerm`: Search term (2-100 characters) + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Array of matching users + +#### Update User (Admin) +**Endpoint:** `PATCH /api/admin/users/{userId}` + +**Request Data:** Partial user fields to update + +**Response Data:** Updated `DetailUserDto` + +#### Deactivate User (Admin) +**Endpoint:** `POST /api/admin/users/{userId}/deactivate` + +**Response Data:** +```typescript +{ + message: string; + user: DetailUserDto; +} +``` + +#### Delete User (Admin) +**Endpoint:** `DELETE /api/admin/users/{userId}` + +**Response Data:** +```typescript +{ + message: string; +} +``` + +### Deck Management (Admin) + +#### Get Decks (Paginated, Admin) +**Endpoint:** `GET /api/admin/decks/page/{from}/{to}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Same as regular deck pagination but unrestricted + +#### Get Deck by ID (Admin) +**Endpoint:** `GET /api/admin/decks/{id}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** `DetailDeckDto` + +#### Search Decks (Admin) +**Endpoint:** `GET /api/admin/decks/search/{searchTerm}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Array of matching decks + +#### Hard Delete Deck (Admin) +**Endpoint:** `DELETE /api/admin/decks/{id}/hard` + +**Response Data:** +```typescript +{ + success: boolean; +} +``` + +### Organization Management (Admin) + +#### Create Organization (Admin) +**Endpoint:** `POST /api/admin/organizations` + +**Request Data:** Organization creation data + +**Response Data:** Created organization object + +#### Update Organization (Admin) +**Endpoint:** `PATCH /api/admin/organizations/{id}` + +**Request Data:** Partial organization fields to update + +**Response Data:** Updated organization object + +#### Get Organizations (Paginated, Admin) +**Endpoint:** `GET /api/admin/organizations/page/{from}/{to}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Organization pagination with unrestricted access + +#### Get Organization by ID (Admin) +**Endpoint:** `GET /api/admin/organizations/{id}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Organization object + +#### Search Organizations (Admin) +**Endpoint:** `GET /api/admin/organizations/search/{searchTerm}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Array of matching organizations + +#### Soft Delete Organization (Admin) +**Endpoint:** `DELETE /api/admin/organizations/{id}` + +**Response Data:** +```typescript +{ + success: boolean; +} +``` + +#### Hard Delete Organization (Admin) +**Endpoint:** `DELETE /api/admin/organizations/{id}/hard` + +**Response Data:** +```typescript +{ + success: boolean; +} +``` + +### Chat Management (Admin) + +#### Get Chats (Paginated, Admin) +**Endpoint:** `GET /api/admin/chats/page/{from}/{to}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** +```typescript +{ + chats: DetailChatDto[]; + pagination: { + from: number; + to: number; + returned: number; + totalCount: number; + includeDeleted: boolean; + }; +} +``` + +#### Get Chat by ID (Admin) +**Endpoint:** `GET /api/admin/chats/{id}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** `DetailChatDto` + +### Contact Management (Admin) + +#### Get Contacts (Paginated, Admin) +**Endpoint:** `GET /api/admin/contacts/page/{from}/{to}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Contact pagination with full access + +#### Get Contact by ID (Admin) +**Endpoint:** `GET /api/admin/contacts/{id}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** `ContactDto` + +#### Search Contacts (Admin) +**Endpoint:** `GET /api/admin/contacts/search/{searchTerm}` + +**Query Parameters:** +- `includeDeleted`: boolean (default: false) + +**Response Data:** Array of matching contacts + +#### Respond to Contact (Admin) +**Endpoint:** `PUT /api/admin/contacts/{id}/respond` + +**Request Data:** +```typescript +{ + adminResponse: string; // Admin response (required) + sendEmail?: boolean; // Send email to contact (optional) + language?: string; // Response language (optional) +} +``` + +**Response Data:** +```typescript +{ + success: boolean; + message: string; + contact: ContactDto; + emailSent: boolean; + emailError: string | null; +} +``` + +#### Resend Contact Email (Admin) +**Endpoint:** `POST /api/admin/contacts/{id}/resend-email` + +**Request Data:** +```typescript +{ + language?: string; // Email language (optional) +} +``` + +**Response Data:** +```typescript +{ + success: boolean; + message: string; +} +``` + +#### Soft Delete Contact (Admin) +**Endpoint:** `DELETE /api/admin/contacts/{id}` + +**Response Data:** +```typescript +{ + success: boolean; +} +``` + +#### Hard Delete Contact (Admin) +**Endpoint:** `DELETE /api/admin/contacts/{id}/hard` + +**Response Data:** +```typescript +{ + success: boolean; +} +``` + +### Import/Export (Admin) + +#### Import Deck from JSON (Admin) +**Endpoint:** `POST /api/admin/decks/import` + +**Request:** Multipart form data +- `file`: JSON file (max 10MB) + +**Response Data:** +```typescript +{ + success: boolean; + message: string; + deckId: string; +} +``` + +#### Export Deck as JSON (Admin) +**Endpoint:** `GET /api/admin/decks/{deckId}/export` + +**Response:** JSON file download + +**Headers:** +- `Content-Type`: `application/json` +- `Content-Disposition`: `attachment; filename="deckname.json"` + +--- + +## Error Handling + +### Standard Error Response Format +```typescript +{ + error: string; // Error message + details?: string; // Additional details (development only) + timestamp?: string; // Error timestamp +} +``` + +### 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 +- `503` - Service Unavailable + +### Common Error Scenarios + +**Authentication Errors:** +```typescript +// Missing or invalid token +{ error: "Authentication required" } + +// Account state restrictions +{ error: "Please verify your email address" } +{ error: "Account has been deactivated" } + +// Admin access required +{ 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 parameters +{ error: "Invalid page parameters. \"from\" and \"to\" must be valid numbers with to >= from >= 0" } + +// Invalid file type +{ error: "Only JSON and .spr files are allowed" } +``` + +**Business Logic Errors:** +```typescript +// Ownership restrictions +{ error: "Access denied - you can only export your own decks" } + +// Feature restrictions +{ error: "Premium subscription required to create groups" } + +// Duplicate data +{ error: "Deck with this name already exists" } +{ error: "Username or email already exists" } +``` + +--- + +## WebSocket Real-Time Communication + +### Connection & Authentication + +Connect to WebSocket server with JWT authentication: + +```typescript +import io from 'socket.io-client'; + +// Option 1: JWT token in auth +const socket = io('http://localhost:3000', { + auth: { + token: 'your-jwt-token' + } +}); + +// Option 2: Cookie authentication +const socket = io('http://localhost:3000', { + withCredentials: true +}); +``` + +### Connection Events +```typescript +// Connection successful +socket.on('connect', () => { + console.log('Connected to WebSocket server'); +}); + +// Authentication failed +socket.on('connect_error', (error) => { + console.error('Connection failed:', error.message); +}); + +// Disconnected +socket.on('disconnect', (reason) => { + console.log('Disconnected:', reason); +}); + +// General errors +socket.on('error', (error: { message: string }) => { + console.error('WebSocket error:', error.message); +}); +``` + +### Chat Management Events + +**Initial Chat List:** +```typescript +// Automatically sent on connection +socket.on('chats:list', (chats: Array<{ + id: string; + type: ChatType; + name: string | null; + users: string[]; + lastActivity: Date | null; + isArchived: boolean; +}>) => { + // Update chat list in UI +}); +``` + +**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[]; + users: string[]; +}) => { + // Load chat messages +}); + +// Leave a chat room +socket.emit('chat:leave', { chatId: 'chat-uuid' }); + +// Confirmation of leaving +socket.on('chat:left', (data: { chatId: string }) => { + // Update UI +}); +``` + +### Real-time Messaging + +**Send/Receive Messages:** +```typescript +// Send message +socket.emit('message:send', { + chatId: 'chat-uuid', + message: 'Hello everyone!' +}); + +// Receive message +socket.on('message:received', (data: { + chatId: string; + message: Message; + senderInfo?: { + username: string; + fname: string; + lname: string; + }; +}) => { + // Add message to chat UI +}); + +// Message sent confirmation +socket.on('message:sent', (data: { + chatId: string; + messageId: string; + timestamp: Date; +}) => { + // Update UI +}); +``` + +**Rate Limiting:** 100 messages per user per minute + +### Chat Creation Events + +**Create Group Chat (Premium Only):** +```typescript +// Create group +socket.emit('group:create', { + name: 'Study Group', + userIds: ['user-uuid-1', 'user-uuid-2'] +}); + +// Group created +socket.on('group:created', (data: { + chat: { + id: string; + type: 'group'; + name: string; + users: string[]; + createdBy: string; + }; +}) => { + // Add to chat list +}); + +// Creation failed +socket.on('group:creation:failed', (data: { + error: string; +}) => { + // Show error +}); +``` + +**Create Direct Chat:** +```typescript +// Create or get direct chat +socket.emit('chat:direct', { + targetUserId: 'user-uuid' +}); + +// Chat created +socket.on('chat:direct:created', (data: { + chat: { + id: string; + type: 'direct'; + users: string[]; + }; +}) => { + // Add to list +}); + +// Chat already exists +socket.on('chat:direct:exists', (data: { + chatId: string; +}) => { + // Navigate to existing chat +}); +``` + +**Create Game Chat:** +```typescript +// Create game chat +socket.emit('game:chat:create', { + gameId: 'game-uuid', + gameName: 'Quiz Game #123', + playerIds: ['player-uuid-1', 'player-uuid-2'] +}); + +// Game chat created +socket.on('game:chat:created', (data: { + chat: { + id: string; + type: 'game'; + name: string; + gameId: string; + users: string[]; + }; +}) => { + // Show game chat +}); +``` + +### Chat History Management + +**Get Chat History:** +```typescript +// Request full history +socket.emit('chat:history', { chatId: 'chat-uuid' }); + +// Receive active chat history +socket.on('chat:history', (data: { + chatId: string; + messages: Message[]; + users: string[]; + type: ChatType; + name: string | null; +}) => { + // Display full history +}); + +// Receive archived chat history +socket.on('chat:history:archived', (data: { + chatId: string; + messages: Message[]; + isGameChat: boolean; + archiveDate: Date; +}) => { + // Display as read-only +}); +``` + +### Message Retention Rules +- **All chats**: Messages older than 2 weeks are deleted +- **Direct & Game chats**: Max 10 messages per user (FIFO) +- **Group chats**: Time limit only (no per-user limit) +- **Archive**: Inactive chats (30 minutes) are archived +- **Cleanup**: Archived messages cleaned after 4 weeks + +### Complete Implementation Example + +```typescript +// hooks/useWebSocket.ts +import { useEffect, useRef, useState } from 'react'; +import io, { Socket } from 'socket.io-client'; + +export const useWebSocket = (token: string | null) => { + const socketRef = useRef(null); + const [isConnected, setIsConnected] = useState(false); + const [chats, setChats] = useState([]); + + useEffect(() => { + if (!token) return; + + socketRef.current = io('http://localhost:3000', { + auth: { token }, + withCredentials: true + }); + + const socket = socketRef.current; + + socket.on('connect', () => setIsConnected(true)); + socket.on('connect_error', () => setIsConnected(false)); + socket.on('disconnect', () => setIsConnected(false)); + + 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 + )); + }); + + return () => { + socket.disconnect(); + }; + }, [token]); + + const sendMessage = (chatId: string, message: string) => { + socketRef.current?.emit('message:send', { chatId, message }); + }; + + const joinChat = (chatId: string) => { + socketRef.current?.emit('chat:join', { chatId }); + }; + + const createDirectChat = (targetUserId: string) => { + socketRef.current?.emit('chat:direct', { targetUserId }); + }; + + const createGroup = (name: string, userIds: string[]) => { + socketRef.current?.emit('group:create', { name, userIds }); + }; + + return { + socket: socketRef.current, + isConnected, + chats, + sendMessage, + joinChat, + createDirectChat, + createGroup + }; +}; +``` + +--- + +This documentation provides a complete reference for all 50+ endpoints available in the SerpentRace backend API, with accurate data structures, validation rules, and implementation examples derived directly from the TypeScript source code. diff --git a/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js b/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js index a90608a2..4b3f30a3 100644 --- a/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js +++ b/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js @@ -1,5 +1,5 @@ -/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.467\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '204df79fd8f170c051d838B1RIBxdTXgwCCQEIAQMHU1MJCltS', mode: 'build'}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.467/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '204df79fd8f170c051d838B1RIBxdTXgwCCQEIAQMHU1MJCltS', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ +/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.467\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '2019ca8297ecdac6edb848AglNURYHAwEBUFBQVVJXXVcOUA4F', mode: 'build'}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.467/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '2019ca8297ecdac6edb848AglNURYHAwEBUFBQVVJXXVcOUA4F', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ /*! * /** * * Copyright (c) Meta Platforms, Inc. and affiliates. diff --git a/SerpentRace_Backend/node_modules/jest/bin/jest.js b/SerpentRace_Backend/node_modules/jest/bin/jest.js index 2bc4849b..1c52155f 100644 --- a/SerpentRace_Backend/node_modules/jest/bin/jest.js +++ b/SerpentRace_Backend/node_modules/jest/bin/jest.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.467\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '204df79fd8f170c051d838B1RIBxdTXgwCCQEIAQMHU1MJCltS', mode: 'build'}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.467/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '204df79fd8f170c051d838B1RIBxdTXgwCCQEIAQMHU1MJCltS', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ +/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.467\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '2019ca8297ecdac6edb848AglNURYHAwEBUFBQVVJXXVcOUA4F', mode: 'build'}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.467/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '2019ca8297ecdac6edb848AglNURYHAwEBUFBQVVJXXVcOUA4F', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ /** * Copyright (c) Meta Platforms, Inc. and affiliates.