86211923db
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
907 lines
22 KiB
Markdown
907 lines
22 KiB
Markdown
# 🎮 SerpentRace Frontend Developer Guide
|
|
|
|
## 📋 Table of Contents
|
|
|
|
1. [Quick Start](#-quick-start)
|
|
2. [Authentication System](#-authentication-system)
|
|
3. [Game Integration](#-game-integration)
|
|
4. [API Reference](#-api-reference)
|
|
5. [WebSocket Events](#-websocket-events)
|
|
6. [Data Models](#-data-models)
|
|
7. [Error Handling](#-error-handling)
|
|
8. [Performance Tips](#-performance-tips)
|
|
9. [Security Guidelines](#-security-guidelines)
|
|
10. [Troubleshooting](#-troubleshooting)
|
|
|
|
---
|
|
|
|
## 🚀 Quick Start
|
|
|
|
### **Base Configuration**
|
|
|
|
```typescript
|
|
// config.ts
|
|
export const API_CONFIG = {
|
|
baseURL: 'http://localhost:3000/api',
|
|
wsURL: 'http://localhost:3000',
|
|
timeout: 10000,
|
|
retryAttempts: 3
|
|
};
|
|
```
|
|
|
|
### **API Client Setup**
|
|
|
|
```typescript
|
|
// apiClient.ts
|
|
import axios from 'axios';
|
|
|
|
const apiClient = axios.create({
|
|
baseURL: API_CONFIG.baseURL,
|
|
timeout: API_CONFIG.timeout,
|
|
withCredentials: true, // Important for cookie-based auth
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
// Request interceptor for auth token
|
|
apiClient.interceptors.request.use((config) => {
|
|
const token = localStorage.getItem('auth_token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// Response interceptor for token refresh
|
|
apiClient.interceptors.response.use(
|
|
(response) => response,
|
|
async (error) => {
|
|
if (error.response?.status === 401) {
|
|
// Handle token expiration
|
|
localStorage.removeItem('auth_token');
|
|
window.location.href = '/login';
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 Authentication System
|
|
|
|
### **1. User Registration**
|
|
|
|
```typescript
|
|
interface RegisterRequest {
|
|
username: string;
|
|
email: string;
|
|
password: string;
|
|
fname?: string;
|
|
lname?: string;
|
|
phone?: string;
|
|
}
|
|
|
|
async function registerUser(userData: RegisterRequest) {
|
|
const response = await apiClient.post('/users/create', userData);
|
|
return response.data; // Returns user data without password
|
|
}
|
|
```
|
|
|
|
### **2. User Login**
|
|
|
|
```typescript
|
|
interface LoginRequest {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
interface LoginResponse {
|
|
token: string;
|
|
user: {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
state: number; // 0=NOT_VERIFIED, 1=VERIFIED_REGULAR, 2=VERIFIED_PREMIUM, 3=ADMIN
|
|
orgId?: string;
|
|
};
|
|
}
|
|
|
|
async function loginUser(credentials: LoginRequest): Promise<LoginResponse> {
|
|
const response = await apiClient.post('/users/login', credentials);
|
|
|
|
// Store token for future requests
|
|
localStorage.setItem('auth_token', response.data.token);
|
|
|
|
return response.data;
|
|
}
|
|
```
|
|
|
|
### **3. Token Management**
|
|
|
|
```typescript
|
|
class AuthManager {
|
|
private token: string | null = null;
|
|
|
|
setToken(token: string) {
|
|
this.token = token;
|
|
localStorage.setItem('auth_token', token);
|
|
}
|
|
|
|
getToken(): string | null {
|
|
return this.token || localStorage.getItem('auth_token');
|
|
}
|
|
|
|
clearToken() {
|
|
this.token = null;
|
|
localStorage.removeItem('auth_token');
|
|
}
|
|
|
|
isAuthenticated(): boolean {
|
|
return !!this.getToken();
|
|
}
|
|
}
|
|
|
|
export const authManager = new AuthManager();
|
|
```
|
|
|
|
---
|
|
|
|
## 🎮 Game Integration
|
|
|
|
### **1. Create Game**
|
|
|
|
```typescript
|
|
interface CreateGameRequest {
|
|
deckids: string[]; // Array of deck UUIDs
|
|
maxplayers: number; // 2-8 players
|
|
logintype: number; // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
|
|
}
|
|
|
|
interface GameResponse {
|
|
id: string;
|
|
gamecode: string; // 6-character join code
|
|
maxplayers: number;
|
|
state: number; // 0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED
|
|
players: string[];
|
|
gameToken?: string; // For immediate joining
|
|
}
|
|
|
|
async function createGame(gameData: CreateGameRequest): Promise<GameResponse> {
|
|
const response = await apiClient.post('/games/start', gameData);
|
|
return response.data;
|
|
}
|
|
```
|
|
|
|
### **2. Join Game**
|
|
|
|
```typescript
|
|
interface JoinGameRequest {
|
|
gameCode: string; // 6-character code
|
|
playerName?: string; // Required for public games, optional for authenticated
|
|
}
|
|
|
|
interface JoinGameResponse extends GameResponse {
|
|
gameToken: string; // Use this for WebSocket authentication
|
|
playerName: string;
|
|
isGamemaster: boolean;
|
|
pendingApproval?: boolean; // True for private games awaiting approval
|
|
}
|
|
|
|
async function joinGame(joinData: JoinGameRequest): Promise<JoinGameResponse> {
|
|
const response = await apiClient.post('/games/join', joinData);
|
|
return response.data;
|
|
}
|
|
```
|
|
|
|
### **3. WebSocket Game Connection**
|
|
|
|
```typescript
|
|
import io, { Socket } from 'socket.io-client';
|
|
|
|
class GameClient {
|
|
private gameSocket: Socket | null = null;
|
|
private gameToken: string = '';
|
|
private eventListeners = new Map<string, Function>();
|
|
|
|
async connectToGame(gameToken: string): Promise<void> {
|
|
this.gameToken = gameToken;
|
|
|
|
// Connect to game namespace
|
|
this.gameSocket = io('/game', {
|
|
transports: ['websocket']
|
|
});
|
|
|
|
this.setupEventHandlers();
|
|
|
|
// Join specific game with token
|
|
this.gameSocket.emit('game:join', { gameToken });
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.gameSocket!.once('game:joined', (data) => {
|
|
console.log('Successfully joined game:', data);
|
|
resolve();
|
|
});
|
|
|
|
this.gameSocket!.once('game:error', (error) => {
|
|
console.error('Game connection error:', error);
|
|
reject(new Error(error.message));
|
|
});
|
|
});
|
|
}
|
|
|
|
private setupEventHandlers() {
|
|
if (!this.gameSocket) return;
|
|
|
|
// Game state updates
|
|
this.addListener('game:state-update', (gameState) => {
|
|
console.log('Game state updated:', gameState);
|
|
// Update UI with new game state
|
|
});
|
|
|
|
// Player movements
|
|
this.addListener('game:player-moved', (moveData) => {
|
|
console.log('Player moved:', moveData);
|
|
// Update board visualization
|
|
});
|
|
|
|
// Field effects
|
|
this.addListener('game:field-effect', (effectData) => {
|
|
console.log('Field effect triggered:', effectData);
|
|
// Show effect animation/notification
|
|
});
|
|
|
|
// Chat messages
|
|
this.addListener('game:chat-message', (chatData) => {
|
|
console.log('Game chat:', chatData);
|
|
// Display chat message
|
|
});
|
|
}
|
|
|
|
addListener(event: string, handler: Function) {
|
|
if (!this.gameSocket) return;
|
|
|
|
this.gameSocket.on(event, handler);
|
|
this.eventListeners.set(event, handler);
|
|
}
|
|
|
|
removeAllListeners() {
|
|
this.eventListeners.forEach((handler, event) => {
|
|
this.gameSocket?.off(event, handler);
|
|
});
|
|
this.eventListeners.clear();
|
|
}
|
|
|
|
rollDice(diceValue: number) {
|
|
if (!this.gameSocket) return;
|
|
|
|
this.gameSocket.emit('game:dice-roll', {
|
|
gameCode: this.gameCode, // Extract from gameToken
|
|
diceValue
|
|
});
|
|
}
|
|
|
|
sendChatMessage(message: string) {
|
|
if (!this.gameSocket) return;
|
|
|
|
this.gameSocket.emit('game:chat', {
|
|
gameCode: this.gameCode,
|
|
message
|
|
});
|
|
}
|
|
|
|
disconnect() {
|
|
this.removeAllListeners();
|
|
this.gameSocket?.disconnect();
|
|
this.gameSocket = null;
|
|
}
|
|
}
|
|
```
|
|
|
|
### **4. Private Game Approval Flow**
|
|
|
|
```typescript
|
|
// For gamemaster - handle approval requests
|
|
gameSocket.on('game:player-requesting-join', (data) => {
|
|
console.log('Player requesting to join:', data);
|
|
// Show approval UI with player name
|
|
showApprovalDialog(data.playerName, data.gameCode);
|
|
});
|
|
|
|
function approvePlayer(gameCode: string, playerName: string) {
|
|
gameSocket.emit('game:approve-player', { gameCode, playerName });
|
|
}
|
|
|
|
function rejectPlayer(gameCode: string, playerName: string, reason?: string) {
|
|
gameSocket.emit('game:reject-player', { gameCode, playerName, reason });
|
|
}
|
|
|
|
// For joining player - handle approval response
|
|
gameSocket.on('game:pending-approval', (data) => {
|
|
console.log('Waiting for gamemaster approval:', data);
|
|
// Show waiting message
|
|
});
|
|
|
|
gameSocket.on('game:approval-granted', (data) => {
|
|
console.log('Approved! Joining game:', data);
|
|
// Automatically join game rooms
|
|
gameSocket.emit('game:join-approved', { gameToken });
|
|
});
|
|
|
|
gameSocket.on('game:approval-denied', (data) => {
|
|
console.log('Join request denied:', data);
|
|
// Show rejection message and reason
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📡 API Reference
|
|
|
|
### **User Endpoints**
|
|
|
|
| Endpoint | Method | Auth | Description |
|
|
|----------|---------|------|-------------|
|
|
| `/users/login` | POST | No | User authentication |
|
|
| `/users/create` | POST | No | User registration |
|
|
| `/users/logout` | POST | Yes | User logout |
|
|
| `/users/profile` | GET | Yes | Get user profile |
|
|
| `/users/profile` | PATCH | Yes | Update user profile |
|
|
| `/users/verify-email` | POST | No | Verify email token |
|
|
| `/users/request-password-reset` | POST | No | Request password reset |
|
|
| `/users/reset-password` | POST | No | Reset password with token |
|
|
|
|
### **Game Endpoints**
|
|
|
|
| Endpoint | Method | Auth | Description |
|
|
|----------|---------|------|-------------|
|
|
| `/games/start` | POST | Yes | Create new game |
|
|
| `/games/join` | POST | Optional* | Join existing game |
|
|
| `/games/{gameId}/start` | POST | Yes | Start game (gamemaster only) |
|
|
| `/games/my-games` | GET | Yes | Get user's games |
|
|
| `/games/active` | GET | No | Get active public games |
|
|
|
|
*Auth required for private/organization games
|
|
|
|
### **Deck Endpoints**
|
|
|
|
| Endpoint | Method | Auth | Description |
|
|
|----------|---------|------|-------------|
|
|
| `/decks` | GET | Optional | Get available decks |
|
|
| `/decks` | POST | Yes | Create new deck |
|
|
| `/decks/{id}` | GET | Optional | Get deck details |
|
|
| `/decks/{id}` | PUT | Yes | Update deck (owner only) |
|
|
| `/decks/{id}` | DELETE | Yes | Delete deck (owner only) |
|
|
|
|
### **Organization Endpoints**
|
|
|
|
| Endpoint | Method | Auth | Description |
|
|
|----------|---------|------|-------------|
|
|
| `/organizations` | GET | Yes | Get user's organization |
|
|
| `/organizations/{id}/join` | POST | Yes | Request to join organization |
|
|
|
|
---
|
|
|
|
## 🔌 WebSocket Events
|
|
|
|
### **Connection Events**
|
|
|
|
```typescript
|
|
// Connect to main chat namespace
|
|
const chatSocket = io('/', {
|
|
auth: { token: authToken },
|
|
transports: ['websocket']
|
|
});
|
|
|
|
// Connect to game namespace
|
|
const gameSocket = io('/game', {
|
|
transports: ['websocket']
|
|
});
|
|
```
|
|
|
|
### **Game Events (Client → Server)**
|
|
|
|
| Event | Data | Description |
|
|
|-------|------|-------------|
|
|
| `game:join` | `{ gameToken: string }` | Join game with token |
|
|
| `game:leave` | `{ gameCode: string }` | Leave current game |
|
|
| `game:dice-roll` | `{ gameCode: string, diceValue: number }` | Roll dice (1-6) |
|
|
| `game:chat` | `{ gameCode: string, message: string }` | Send chat message |
|
|
| `game:ready` | `{ gameCode: string, ready: boolean }` | Toggle ready status |
|
|
| `game:approve-player` | `{ gameCode: string, playerName: string }` | Approve join request |
|
|
| `game:reject-player` | `{ gameCode: string, playerName: string, reason?: string }` | Reject join request |
|
|
|
|
### **Game Events (Server → Client)**
|
|
|
|
| Event | Data | Description |
|
|
|-------|------|-------------|
|
|
| `game:joined` | `GameJoinedData` | Successfully joined game |
|
|
| `game:left` | `GameLeftData` | Successfully left game |
|
|
| `game:player-moved` | `PlayerMoveData` | Player moved on board |
|
|
| `game:field-effect` | `FieldEffectData` | Field effect triggered |
|
|
| `game:chat-message` | `ChatMessageData` | Game chat message |
|
|
| `game:state-update` | `GameStateData` | Game state changed |
|
|
| `game:player-joined` | `PlayerJoinedData` | New player joined |
|
|
| `game:player-left` | `PlayerLeftData` | Player left game |
|
|
| `game:game-started` | `GameStartedData` | Game started |
|
|
| `game:game-ended` | `GameEndedData` | Game finished |
|
|
| `game:error` | `{ message: string }` | Game-related error |
|
|
|
|
---
|
|
|
|
## 📊 Data Models
|
|
|
|
### **Game State Model**
|
|
|
|
```typescript
|
|
interface GameState {
|
|
gameId: string;
|
|
gameCode: string;
|
|
state: GameStateEnum; // 0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED
|
|
maxPlayers: number;
|
|
currentPlayers: PlayerState[];
|
|
gamemaster: string; // User ID
|
|
board: BoardField[];
|
|
currentTurn?: string; // Player ID whose turn it is
|
|
turnOrder: string[]; // Player IDs in turn sequence
|
|
startedAt?: Date;
|
|
finishedAt?: Date;
|
|
winner?: string; // Player ID
|
|
}
|
|
|
|
interface PlayerState {
|
|
playerId: string;
|
|
playerName: string;
|
|
boardPosition: number; // 0-101 (0=start, 101=finish)
|
|
isReady: boolean;
|
|
isOnline: boolean;
|
|
joinedAt: Date;
|
|
turnOrder: number;
|
|
}
|
|
|
|
interface BoardField {
|
|
position: number; // 1-100
|
|
type: 'regular' | 'positive' | 'negative' | 'luck';
|
|
effect?: string; // Description of field effect
|
|
}
|
|
```
|
|
|
|
### **Move Data Model**
|
|
|
|
```typescript
|
|
interface PlayerMoveData {
|
|
playerId: string;
|
|
playerName: string;
|
|
diceValue: number;
|
|
oldPosition: number;
|
|
newPosition: number;
|
|
hasWon: boolean;
|
|
cardEffect?: {
|
|
applied: boolean;
|
|
description: string;
|
|
positionChange: number;
|
|
extraTurn: boolean;
|
|
turnEffect?: 'LOSE_TURN' | 'EXTRA_TURN';
|
|
effects: string[];
|
|
};
|
|
timestamp: string;
|
|
}
|
|
```
|
|
|
|
### **Field Effect Model**
|
|
|
|
```typescript
|
|
interface FieldEffectData {
|
|
playerId: string;
|
|
playerName: string;
|
|
fieldNumber: number;
|
|
card?: GameCard;
|
|
consequence?: {
|
|
type: ConsequenceType;
|
|
value?: number;
|
|
description: string;
|
|
};
|
|
newPosition?: number;
|
|
turnEffect?: 'LOSE_TURN' | 'EXTRA_TURN';
|
|
requiresInput?: boolean;
|
|
inputPrompt?: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
interface GameCard {
|
|
id: string;
|
|
text: string; // Question or content
|
|
type: CardType; // 0=QUIZ, 1=SENTENCE_PAIRING, 2=OWN_ANSWER, 3=TRUE_FALSE, 4=CLOSER
|
|
answer?: string;
|
|
consequence?: {
|
|
type: ConsequenceType; // 0=MOVE_FORWARD, 1=MOVE_BACKWARD, 2=LOSE_TURN, 3=EXTRA_TURN, 5=GO_TO_START
|
|
value?: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ⚠️ Error Handling
|
|
|
|
### **API Error Response Format**
|
|
|
|
```typescript
|
|
interface APIError {
|
|
error: string;
|
|
details?: string;
|
|
code?: string;
|
|
timestamp?: string;
|
|
}
|
|
|
|
// Common HTTP Status Codes
|
|
// 400 - Bad Request (validation errors)
|
|
// 401 - Unauthorized (authentication required)
|
|
// 403 - Forbidden (insufficient permissions)
|
|
// 404 - Not Found
|
|
// 409 - Conflict (duplicate data)
|
|
// 500 - Internal Server Error
|
|
```
|
|
|
|
### **Error Handling Pattern**
|
|
|
|
```typescript
|
|
async function handleAPICall<T>(apiCall: () => Promise<T>): Promise<T> {
|
|
try {
|
|
return await apiCall();
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
const response = error.response;
|
|
|
|
switch (response?.status) {
|
|
case 400:
|
|
throw new Error(`Validation Error: ${response.data.error}`);
|
|
case 401:
|
|
// Handle authentication error
|
|
authManager.clearToken();
|
|
window.location.href = '/login';
|
|
throw new Error('Authentication required');
|
|
case 403:
|
|
throw new Error(`Access Denied: ${response.data.error}`);
|
|
case 404:
|
|
throw new Error('Resource not found');
|
|
case 409:
|
|
throw new Error(`Conflict: ${response.data.error}`);
|
|
case 500:
|
|
throw new Error('Server error. Please try again later.');
|
|
default:
|
|
throw new Error(`Network error: ${error.message}`);
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
try {
|
|
const user = await handleAPICall(() => loginUser(credentials));
|
|
console.log('Login successful:', user);
|
|
} catch (error) {
|
|
console.error('Login failed:', error.message);
|
|
showErrorMessage(error.message);
|
|
}
|
|
```
|
|
|
|
### **WebSocket Error Handling**
|
|
|
|
```typescript
|
|
gameSocket.on('game:error', (error) => {
|
|
console.error('Game error:', error);
|
|
|
|
switch (error.message) {
|
|
case 'Game not found':
|
|
showError('The game you\'re trying to join no longer exists.');
|
|
break;
|
|
case 'Game is full':
|
|
showError('This game is full. Please try another game.');
|
|
break;
|
|
case 'Invalid or expired game token':
|
|
showError('Your game session has expired. Please rejoin.');
|
|
break;
|
|
default:
|
|
showError(`Game error: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
gameSocket.on('disconnect', (reason) => {
|
|
console.log('Disconnected from game:', reason);
|
|
|
|
if (reason === 'io server disconnect') {
|
|
// Server disconnected the client
|
|
showError('Disconnected from game server');
|
|
} else {
|
|
// Client disconnected or network issue
|
|
showWarning('Connection lost. Attempting to reconnect...');
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 Performance Optimization
|
|
|
|
### **1. Connection Management**
|
|
|
|
```typescript
|
|
class ConnectionManager {
|
|
private static chatSocket: Socket | null = null;
|
|
private static gameSocket: Socket | null = null;
|
|
|
|
static getChatSocket(): Socket {
|
|
if (!this.chatSocket) {
|
|
this.chatSocket = io('/', {
|
|
auth: { token: authManager.getToken() },
|
|
transports: ['websocket']
|
|
});
|
|
}
|
|
return this.chatSocket;
|
|
}
|
|
|
|
static getGameSocket(): Socket {
|
|
if (!this.gameSocket) {
|
|
this.gameSocket = io('/game', {
|
|
transports: ['websocket']
|
|
});
|
|
}
|
|
return this.gameSocket;
|
|
}
|
|
|
|
static disconnect() {
|
|
this.chatSocket?.disconnect();
|
|
this.gameSocket?.disconnect();
|
|
this.chatSocket = null;
|
|
this.gameSocket = null;
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Event Listener Cleanup**
|
|
|
|
```typescript
|
|
class GameComponent {
|
|
private eventCleanup: (() => void)[] = [];
|
|
|
|
componentDidMount() {
|
|
const gameSocket = ConnectionManager.getGameSocket();
|
|
|
|
// Track listeners for cleanup
|
|
const addListener = (event: string, handler: Function) => {
|
|
gameSocket.on(event, handler);
|
|
this.eventCleanup.push(() => gameSocket.off(event, handler));
|
|
};
|
|
|
|
addListener('game:player-moved', this.handlePlayerMove);
|
|
addListener('game:state-update', this.handleStateUpdate);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
// Clean up all event listeners
|
|
this.eventCleanup.forEach(cleanup => cleanup());
|
|
this.eventCleanup = [];
|
|
}
|
|
}
|
|
```
|
|
|
|
### **3. API Caching**
|
|
|
|
```typescript
|
|
class APICache {
|
|
private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();
|
|
|
|
async get<T>(key: string, fetcher: () => Promise<T>, ttl = 300000): Promise<T> {
|
|
const cached = this.cache.get(key);
|
|
|
|
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
return cached.data;
|
|
}
|
|
|
|
const data = await fetcher();
|
|
this.cache.set(key, { data, timestamp: Date.now(), ttl });
|
|
return data;
|
|
}
|
|
|
|
invalidate(pattern?: string) {
|
|
if (pattern) {
|
|
for (const key of this.cache.keys()) {
|
|
if (key.includes(pattern)) {
|
|
this.cache.delete(key);
|
|
}
|
|
}
|
|
} else {
|
|
this.cache.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
const apiCache = new APICache();
|
|
|
|
// Usage
|
|
const decks = await apiCache.get(
|
|
'available-decks',
|
|
() => apiClient.get('/decks').then(res => res.data),
|
|
300000 // 5 minutes
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔒 Security Guidelines
|
|
|
|
### **1. Token Security**
|
|
|
|
```typescript
|
|
// ❌ DON'T: Store tokens in localStorage for sensitive apps
|
|
localStorage.setItem('auth_token', token);
|
|
|
|
// ✅ DO: Use secure, httpOnly cookies when possible
|
|
// This requires server-side cookie configuration
|
|
|
|
// ✅ DO: Clear tokens on logout
|
|
function logout() {
|
|
localStorage.removeItem('auth_token');
|
|
apiCache.invalidate();
|
|
ConnectionManager.disconnect();
|
|
window.location.href = '/login';
|
|
}
|
|
```
|
|
|
|
### **2. Input Validation**
|
|
|
|
```typescript
|
|
function validateGameCode(gameCode: string): boolean {
|
|
// Game codes are exactly 6 alphanumeric characters
|
|
return /^[A-Z0-9]{6}$/.test(gameCode);
|
|
}
|
|
|
|
function validatePlayerName(playerName: string): boolean {
|
|
// Player names: 3-50 characters, alphanumeric + spaces
|
|
return /^[a-zA-Z0-9\s]{3,50}$/.test(playerName.trim());
|
|
}
|
|
|
|
function sanitizeMessage(message: string): string {
|
|
// Remove HTML tags and limit length
|
|
return message
|
|
.replace(/<[^>]*>/g, '')
|
|
.substring(0, 500)
|
|
.trim();
|
|
}
|
|
```
|
|
|
|
### **3. Error Message Security**
|
|
|
|
```typescript
|
|
// ❌ DON'T: Expose sensitive information
|
|
console.error('Database error:', fullErrorDetails);
|
|
|
|
// ✅ DO: Log safely and show user-friendly messages
|
|
function handleError(error: any) {
|
|
console.error('API Error:', error.response?.status || 'Unknown');
|
|
|
|
const userMessage = error.response?.data?.error || 'An unexpected error occurred';
|
|
showUserMessage(userMessage);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 Troubleshooting
|
|
|
|
### **Common Issues & Solutions**
|
|
|
|
#### **1. WebSocket Connection Failed**
|
|
|
|
```typescript
|
|
// Problem: Cannot connect to WebSocket
|
|
// Solution: Check URL and add reconnection logic
|
|
|
|
const gameSocket = io('/game', {
|
|
transports: ['websocket'],
|
|
timeout: 10000,
|
|
forceNew: true,
|
|
reconnection: true,
|
|
reconnectionAttempts: 5,
|
|
reconnectionDelay: 1000
|
|
});
|
|
|
|
gameSocket.on('connect_error', (error) => {
|
|
console.error('Connection failed:', error);
|
|
showError('Unable to connect to game server. Please check your connection.');
|
|
});
|
|
```
|
|
|
|
#### **2. Authentication Token Expired**
|
|
|
|
```typescript
|
|
// Problem: 401 errors on API calls
|
|
// Solution: Implement token refresh or redirect to login
|
|
|
|
apiClient.interceptors.response.use(
|
|
(response) => response,
|
|
async (error) => {
|
|
if (error.response?.status === 401) {
|
|
console.log('Token expired, redirecting to login');
|
|
authManager.clearToken();
|
|
window.location.href = '/login';
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
```
|
|
|
|
#### **3. Game State Out of Sync**
|
|
|
|
```typescript
|
|
// Problem: Game state doesn't match server
|
|
// Solution: Request fresh game state
|
|
|
|
function requestGameStateRefresh(gameCode: string) {
|
|
gameSocket.emit('game:request-state', { gameCode });
|
|
}
|
|
|
|
gameSocket.on('game:state-refresh', (gameState) => {
|
|
console.log('Received fresh game state:', gameState);
|
|
updateGameUI(gameState);
|
|
});
|
|
```
|
|
|
|
#### **4. Memory Leaks in Game Component**
|
|
|
|
```typescript
|
|
// Problem: Event listeners not cleaned up
|
|
// Solution: Proper cleanup pattern
|
|
|
|
useEffect(() => {
|
|
const gameSocket = ConnectionManager.getGameSocket();
|
|
|
|
const handlers = {
|
|
'game:player-moved': handlePlayerMove,
|
|
'game:state-update': handleStateUpdate,
|
|
'game:chat-message': handleChatMessage
|
|
};
|
|
|
|
// Add listeners
|
|
Object.entries(handlers).forEach(([event, handler]) => {
|
|
gameSocket.on(event, handler);
|
|
});
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
Object.entries(handlers).forEach(([event, handler]) => {
|
|
gameSocket.off(event, handler);
|
|
});
|
|
};
|
|
}, []);
|
|
```
|
|
|
|
---
|
|
|
|
## 📞 Support & Documentation
|
|
|
|
### **Additional Resources**
|
|
- **API Documentation**: Available at `/api-docs` (Swagger UI)
|
|
- **WebSocket Events**: Complete event reference in game-websocket-examples.ts
|
|
- **Backend Repository**: Full source code and additional documentation
|
|
|
|
### **Development Tips**
|
|
1. Use browser dev tools Network tab to debug API calls
|
|
2. Enable WebSocket debugging: `localStorage.debug = 'socket.io-client:socket'`
|
|
3. Check server logs for detailed error information
|
|
4. Use the included Postman collection for API testing
|
|
|
|
### **Performance Monitoring**
|
|
- Monitor WebSocket connection status
|
|
- Track API response times
|
|
- Watch for memory leaks in game components
|
|
- Monitor token refresh frequency
|
|
|
|
---
|
|
|
|
*Last updated: September 21, 2025*
|
|
*Backend Version: 1.0.0*
|
|
*API Version: v1* |