chore: update documents
This commit is contained in:
@@ -99,7 +99,6 @@ export class StatisticsService {
|
||||
// Continue with 0 count
|
||||
}
|
||||
|
||||
// Statistics will be returned
|
||||
const statistics: Statistics = {
|
||||
totalGames,
|
||||
totalAnimations,
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* Authentication and Authorization Types
|
||||
*/
|
||||
|
||||
// ===================================
|
||||
// User Types
|
||||
// ===================================
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
@@ -44,9 +42,7 @@ export interface UpdateUserData {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Authentication Types
|
||||
// ===================================
|
||||
|
||||
export interface LoginCredentials {
|
||||
username: string;
|
||||
@@ -65,9 +61,7 @@ export interface AuthTokens {
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Role and Permission Types
|
||||
// ===================================
|
||||
|
||||
export interface Permission {
|
||||
id: number;
|
||||
@@ -86,9 +80,7 @@ export interface Role {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Activity Log Types
|
||||
// ===================================
|
||||
|
||||
export interface ActivityLog {
|
||||
id: number;
|
||||
@@ -114,9 +106,7 @@ export interface LogActivityData {
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Auth Settings Types
|
||||
// ===================================
|
||||
|
||||
export interface AuthSettings {
|
||||
guestAccessEnabled: boolean;
|
||||
|
||||
@@ -11,9 +11,9 @@ Flashpoint Web uses JWT-based authentication with role-based access control
|
||||
graph TB
|
||||
subgraph "Frontend"
|
||||
LoginForm[Login Form]
|
||||
AuthStore[Zustand Auth Store<br/>Access Token Only]
|
||||
AuthStore[Zustand Auth Store<br/>User Info Only]
|
||||
APIClient[Axios API Client]
|
||||
Cookies[HTTP-only Cookies<br/>fp_refresh]
|
||||
Cookies[HTTP-only Cookies<br/>fp_access + fp_refresh]
|
||||
end
|
||||
|
||||
subgraph "Backend"
|
||||
@@ -30,15 +30,14 @@ graph TB
|
||||
end
|
||||
|
||||
LoginForm --> AuthStore
|
||||
AuthRoute -.Set Cookie.-> Cookies
|
||||
AuthRoute -.accessToken.-> APIClient
|
||||
AuthRoute -.Set Cookies.-> Cookies
|
||||
AuthStore --> APIClient
|
||||
APIClient --> AuthRoute
|
||||
AuthRoute --> AuthService
|
||||
AuthService --> UserDB
|
||||
UserDB -.return user.-> AuthService
|
||||
AuthService -.JWT tokens.-> AuthRoute
|
||||
AuthRoute -.accessToken only.-> APIClient
|
||||
AuthRoute -.user info + expiresIn.-> APIClient
|
||||
APIClient --> AuthStore
|
||||
|
||||
APIClient -.JWT Bearer.-> GameRoutes
|
||||
@@ -89,9 +88,9 @@ sequenceDiagram
|
||||
|
||||
AuthService->>AuthService: Generate JWT tokens
|
||||
AuthService-->>Backend: {user, accessToken, refreshToken}
|
||||
Backend->>Backend: Set fp_refresh HTTP-only cookie
|
||||
Backend-->>API: 201 Created (only accessToken in body)
|
||||
API->>AuthStore: setAuth(user, accessToken)
|
||||
Backend->>Backend: Set fp_access + fp_refresh HTTP-only cookies
|
||||
Backend-->>API: 201 Created (user info + expiresIn only)
|
||||
API->>AuthStore: setAuth(user)
|
||||
AuthStore-->>RegisterForm: Success
|
||||
RegisterForm->>User: Redirect to dashboard
|
||||
end
|
||||
@@ -142,13 +141,13 @@ sequenceDiagram
|
||||
API-->>LoginForm: Show error
|
||||
else Valid credentials
|
||||
AuthService->>UserDB: SELECT user permissions
|
||||
AuthService->>AuthService: Generate JWT access token (15m expiry)
|
||||
AuthService->>AuthService: Generate JWT access token (1h expiry, configurable)
|
||||
AuthService->>AuthService: Generate JWT refresh token (30d expiry)
|
||||
AuthService-->>Backend: {user, accessToken, refreshToken}
|
||||
Backend->>Backend: Set fp_refresh HTTP-only cookie
|
||||
Backend-->>API: 200 OK (only accessToken in body)
|
||||
Backend->>Backend: Set fp_access + fp_refresh HTTP-only cookies
|
||||
Backend-->>API: 200 OK (user info + expiresIn only)
|
||||
|
||||
API->>AuthStore: setAuth(user, accessToken)
|
||||
API->>AuthStore: setAuth(user)
|
||||
AuthStore->>ThemeStore: loadThemeFromServer()
|
||||
AuthStore-->>LoginForm: Success
|
||||
LoginForm->>User: Redirect to /browse
|
||||
@@ -158,7 +157,7 @@ sequenceDiagram
|
||||
**JWT Token Structure**:
|
||||
|
||||
```typescript
|
||||
// Access Token
|
||||
// Access Token (stored in HTTP-only cookie fp_access, path: /api)
|
||||
{
|
||||
userId: 1,
|
||||
username: "user",
|
||||
@@ -166,10 +165,10 @@ sequenceDiagram
|
||||
role: "user",
|
||||
permissions: ["games.read", "games.play"],
|
||||
iat: 1705579200,
|
||||
exp: 1705580100 // 15 minutes
|
||||
exp: 1705582800 // 1 hour (configurable via JWT_EXPIRES_IN)
|
||||
}
|
||||
|
||||
// Refresh Token (stored in HTTP-only cookie)
|
||||
// Refresh Token (stored in HTTP-only cookie fp_refresh, path: /api/auth)
|
||||
{
|
||||
userId: 1,
|
||||
type: "refresh",
|
||||
@@ -473,7 +472,8 @@ export const ProtectedRoute = ({ children, requiredPermission }) => {
|
||||
## 7. Guest Mode
|
||||
|
||||
When `auth.guest_access_enabled` is true in system settings, unauthenticated
|
||||
users can browse games with limited permissions.
|
||||
users can browse games with limited permissions. **Note:** Guests cannot play
|
||||
games — only authenticated users have the `games.play` permission.
|
||||
|
||||
```typescript
|
||||
// Frontend
|
||||
@@ -481,7 +481,7 @@ const guestUser: User = {
|
||||
id: 0,
|
||||
username: 'Guest',
|
||||
role: 'guest',
|
||||
permissions: ['games.read', 'games.play'],
|
||||
permissions: ['games.read', 'playlists.read'], // No games.play
|
||||
};
|
||||
|
||||
// Backend
|
||||
@@ -501,7 +501,7 @@ export const optionalAuth = async (req, res, next) => {
|
||||
id: 0,
|
||||
username: 'guest',
|
||||
role: 'guest',
|
||||
permissions: ['games.read'],
|
||||
permissions: ['games.read', 'playlists.read'], // Read-only access
|
||||
};
|
||||
}
|
||||
|
||||
@@ -511,12 +511,13 @@ export const optionalAuth = async (req, res, next) => {
|
||||
|
||||
## Security Measures
|
||||
|
||||
**Password Security**: Bcrypt with 10 rounds
|
||||
**Password Security**: Bcrypt with 10 rounds (configurable via `BCRYPT_SALT_ROUNDS`)
|
||||
|
||||
**JWT Security**:
|
||||
|
||||
- Short-lived access tokens (15 minutes) - held in memory only
|
||||
- Long-lived refresh tokens (30 days) - stored in HTTP-only cookies
|
||||
- Access tokens (1 hour default, configurable via `JWT_EXPIRES_IN`) - stored in HTTP-only cookie `fp_access` with path `/api`
|
||||
- Long-lived refresh tokens (30 days) - stored in HTTP-only cookie `fp_refresh` with path `/api/auth`
|
||||
- Both tokens in HTTP-only cookies prevents XSS token theft
|
||||
- Refresh tokens are rotated on each use to prevent token reuse attacks
|
||||
- Signed with random 256-bit JWT_SECRET
|
||||
- Verify algorithm: HS256
|
||||
|
||||
@@ -13,20 +13,14 @@ import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
// Server configuration
|
||||
// Server configuration (hardcoded)
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
port: parseInt(process.env.PORT || '3100', 10),
|
||||
host: process.env.HOST || '0.0.0.0',
|
||||
port: 3100, // Hardcoded, not configurable
|
||||
host: '0.0.0.0', // Hardcoded, not configurable
|
||||
|
||||
// Flashpoint paths (only FLASHPOINT_PATH is needed)
|
||||
flashpointPath: process.env.FLASHPOINT_PATH || 'D:/Flashpoint',
|
||||
// All other paths are derived automatically:
|
||||
// - flashpointDbPath: ${flashpointPath}/Data/flashpoint.sqlite
|
||||
// - htdocsPath: ${flashpointPath}/Legacy/htdocs
|
||||
// - imagesPath: ${flashpointPath}/Data/Images
|
||||
// - logosPath: ${flashpointPath}/Data/Logos
|
||||
// - playlistsPath: ${flashpointPath}/Data/Playlists
|
||||
// - gamesPath: ${flashpointPath}/Data/Games
|
||||
// All other paths are derived automatically
|
||||
// ... more config
|
||||
};
|
||||
```
|
||||
@@ -46,26 +40,8 @@ export const config = {
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
#### `PORT`
|
||||
|
||||
- **Type**: number
|
||||
- **Default**: `3100`
|
||||
- **Description**: HTTP server port
|
||||
- **Usage**:
|
||||
```bash
|
||||
PORT=3100
|
||||
```
|
||||
|
||||
#### `HOST`
|
||||
|
||||
- **Type**: string
|
||||
- **Default**: `0.0.0.0`
|
||||
- **Description**: Server bind address
|
||||
- **Usage**:
|
||||
```bash
|
||||
HOST=0.0.0.0 # Listen on all interfaces
|
||||
HOST=localhost # Listen on localhost only
|
||||
```
|
||||
**Note:** `PORT` and `HOST` are hardcoded to `3100` and `0.0.0.0` respectively
|
||||
and cannot be changed via environment variables.
|
||||
|
||||
### Flashpoint Paths
|
||||
|
||||
|
||||
@@ -13,9 +13,14 @@ hot-reload.
|
||||
- **Singleton pattern**: Single database connection per process
|
||||
- **File watching**: Detects when Flashpoint Launcher updates the database
|
||||
- **Hot-reload**: Automatically reconnects when database changes
|
||||
- **Read/Write access**: Allows play statistics updates
|
||||
- **Read-only access**: Flashpoint database is read-only (use UserDatabaseService
|
||||
for writes)
|
||||
- **Local copy support**: Optional local database copy for network storage
|
||||
performance
|
||||
- **Synchronous API**: Uses better-sqlite3 for better performance than async
|
||||
wrappers
|
||||
- **Cache invalidation**: Clears GameSearchCache and filter caches on database
|
||||
reload
|
||||
|
||||
## Public API
|
||||
|
||||
@@ -42,24 +47,27 @@ const result = db.prepare('SELECT * FROM game WHERE id = ?').get(gameId);
|
||||
|
||||
---
|
||||
|
||||
### `get(sql: string, params: any[]): any | null`
|
||||
### `get<T>(sql: string, params: unknown[]): T | undefined`
|
||||
|
||||
Execute a query and return the first result row.
|
||||
|
||||
```typescript
|
||||
const game = DatabaseService.get('SELECT * FROM game WHERE id = ?', [gameId]);
|
||||
const game = DatabaseService.get<GameRow>(
|
||||
'SELECT * FROM game WHERE id = ?',
|
||||
[gameId]
|
||||
);
|
||||
```
|
||||
|
||||
**Returns**: First result row or null if no results
|
||||
**Returns**: First result row or undefined if no results
|
||||
|
||||
---
|
||||
|
||||
### `all(sql: string, params: any[]): any[]`
|
||||
### `all<T>(sql: string, params: unknown[]): T[]`
|
||||
|
||||
Execute a query and return all result rows.
|
||||
|
||||
```typescript
|
||||
const games = DatabaseService.all(
|
||||
const games = DatabaseService.all<GameRow>(
|
||||
'SELECT * FROM game WHERE platformName = ? LIMIT ?',
|
||||
['Flash', 100]
|
||||
);
|
||||
@@ -69,19 +77,42 @@ const games = DatabaseService.all(
|
||||
|
||||
---
|
||||
|
||||
### `run(sql: string, params: any[]): void`
|
||||
### `exec<T>(sql: string, params: unknown[]): T[]`
|
||||
|
||||
Execute an INSERT, UPDATE, or DELETE query.
|
||||
Execute a query and return results as objects. Same as `all()` but with
|
||||
different naming convention.
|
||||
|
||||
```typescript
|
||||
DatabaseService.run(
|
||||
'UPDATE game SET playtime = ?, playCounter = ? WHERE id = ?',
|
||||
[3600, 5, gameId]
|
||||
const results = DatabaseService.exec<{ id: number; name: string }>(
|
||||
'SELECT id, name FROM game',
|
||||
[]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `getTableColumns(tableName: string): Set<string>`
|
||||
|
||||
Get column names for a table using PRAGMA table_info. Used for runtime schema
|
||||
introspection.
|
||||
|
||||
```typescript
|
||||
const columns = DatabaseService.getTableColumns('game');
|
||||
// Returns Set {'id', 'title', 'developer', ...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `getStatus(): DatabaseStatus`
|
||||
|
||||
Get current database connection status.
|
||||
|
||||
```typescript
|
||||
const status = DatabaseService.getStatus();
|
||||
// { connected: true, sourcePath: '...', activePath: '...', usingLocalCopy: false, lastModified: Date }
|
||||
|
||||
---
|
||||
|
||||
### `isConnected(): boolean`
|
||||
|
||||
Check if database is connected.
|
||||
|
||||
@@ -138,6 +138,55 @@ const permissions = ['games.read', 'games.play'];
|
||||
PermissionCache.setUserPermissions(5, permissions);
|
||||
```
|
||||
|
||||
### getRolePermissions(roleId: number): string[] | null
|
||||
|
||||
Get role permissions from cache.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `roleId` (number): Role ID to lookup
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `string[]`: Array of permission strings if cache hit
|
||||
- `null`: If cache miss or entry expired
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const cached = PermissionCache.getRolePermissions(2);
|
||||
if (cached) {
|
||||
// Use cached permissions
|
||||
return cached;
|
||||
} else {
|
||||
// Query database
|
||||
const permissions = await queryRolePermissions(2);
|
||||
PermissionCache.setRolePermissions(2, permissions);
|
||||
return permissions;
|
||||
}
|
||||
```
|
||||
|
||||
### setRolePermissions(roleId: number, permissions: string[]): void
|
||||
|
||||
Store role permissions in cache.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `roleId` (number): Role ID
|
||||
- `permissions` (string[]): Array of permission strings
|
||||
|
||||
**Side Effects:**
|
||||
|
||||
- Creates cache entry with 10-minute TTL
|
||||
- Logs debug message to console
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const permissions = ['games.read', 'games.play', 'playlists.create'];
|
||||
PermissionCache.setRolePermissions(2, permissions);
|
||||
```
|
||||
|
||||
### invalidateUser(userId: number): void
|
||||
|
||||
Remove user's permission cache entry.
|
||||
@@ -235,7 +284,41 @@ for (const user of usersWithRole) {
|
||||
But for now, the simpler approach of clearing all user cache is more
|
||||
reliable.
|
||||
|
||||
### invalidateAllUsers(): void
|
||||
|
||||
Clear all user permission cache entries.
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
- Bulk user permission changes
|
||||
- Role structure changes
|
||||
- System-wide permission refresh
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// After bulk permission update
|
||||
await permissionService.bulkUpdate(userIds);
|
||||
PermissionCache.invalidateAllUsers();
|
||||
```
|
||||
|
||||
### invalidateAllRoles(): void
|
||||
|
||||
Clear all role permission cache entries.
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
- Bulk role permission changes
|
||||
- Permission structure changes
|
||||
- System-wide role refresh
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// After updating permission definitions
|
||||
await permissionService.rebuildPermissions();
|
||||
PermissionCache.invalidateAllRoles();
|
||||
```
|
||||
|
||||
### clearAll(): void
|
||||
|
||||
@@ -255,6 +338,27 @@ PermissionCache.clearAll();
|
||||
logger.info('All permission caches cleared');
|
||||
```
|
||||
|
||||
### cleanup(): void
|
||||
|
||||
Remove expired cache entries from both user and role caches.
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Iterates through all cache entries
|
||||
- Removes entries where `expiresAt < Date.now()`
|
||||
- Logs count of removed entries if any were expired
|
||||
- Called automatically by the cleanup interval
|
||||
|
||||
**Note:** This is called automatically every 5 minutes when `startCleanup()` has
|
||||
been invoked. Manual calls are typically not needed.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Manual cleanup (rarely needed)
|
||||
PermissionCache.cleanup();
|
||||
```
|
||||
|
||||
### getStats(): CacheStats
|
||||
|
||||
Get cache statistics.
|
||||
|
||||
@@ -31,8 +31,9 @@ static async initialize(): Promise<void> {
|
||||
// Open database connection
|
||||
this.db = new BetterSqlite3(config.userDbPath);
|
||||
|
||||
// Enable foreign keys
|
||||
// Enable foreign keys and WAL mode
|
||||
this.db.pragma('foreign_keys = ON');
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
|
||||
// Create tables from schema file
|
||||
await this.createTables();
|
||||
@@ -40,11 +41,15 @@ static async initialize(): Promise<void> {
|
||||
// Run migrations
|
||||
await this.runMigrations();
|
||||
|
||||
// Create default admin user
|
||||
await this.createDefaultAdmin();
|
||||
// Check if initial setup is needed (first user becomes admin)
|
||||
this.needsInitialSetup();
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** UserDatabaseService uses WAL (Write-Ahead Logging) journal mode for
|
||||
better concurrent access, while DatabaseService (flashpoint.sqlite) uses DELETE
|
||||
mode.
|
||||
|
||||
### 2. Schema Management
|
||||
|
||||
Loads schema from SQL file:
|
||||
|
||||
@@ -8,7 +8,32 @@ endpoints.
|
||||
The API client is a centralized Axios instance with request/response
|
||||
interceptors for authentication and error handling.
|
||||
|
||||
**Location:** `frontend/src/lib/api.ts`
|
||||
**Location:** `frontend/src/lib/api/`
|
||||
|
||||
```
|
||||
lib/api/
|
||||
├── client.ts # Axios instance with interceptors
|
||||
├── index.ts # Re-exports all API modules
|
||||
├── activities.ts # Activity log endpoints
|
||||
├── auth.ts # Authentication endpoints
|
||||
├── domains.ts # Domain management
|
||||
├── favorites.ts # User favorites
|
||||
├── games.ts # Game search/details
|
||||
├── github.ts # GitHub integration
|
||||
├── jobs.ts # Background jobs
|
||||
├── platforms.ts # Platform list
|
||||
├── playTracking.ts # Play session tracking
|
||||
├── playlists.ts # Playlist management
|
||||
├── roles.ts # Role/permission management
|
||||
├── ruffle.ts # Ruffle emulator settings
|
||||
├── settings.ts # System settings
|
||||
├── sharedPlaylists.ts # Shared playlist access
|
||||
├── statistics.ts # User statistics
|
||||
├── tags.ts # Tag list
|
||||
├── updates.ts # Update checking
|
||||
├── userPlaylists.ts # User playlist operations
|
||||
└── users.ts # User management
|
||||
```
|
||||
|
||||
## Client Configuration
|
||||
|
||||
|
||||
@@ -9,21 +9,26 @@ Components are organized by domain and functionality:
|
||||
|
||||
```
|
||||
components/
|
||||
├── auth/ # Authentication components (4 components)
|
||||
├── common/ # Shared/utility components (11 components)
|
||||
├── activities/ # Activity log components (2 components)
|
||||
├── auth/ # Authentication components (8 components)
|
||||
├── common/ # Shared/utility components (14 components)
|
||||
├── dialogs/ # Modal dialogs (1 component)
|
||||
├── error/ # Error handling components (8 components)
|
||||
├── game/ # Game-specific components (1 component)
|
||||
├── layout/ # Layout structure (3 components)
|
||||
├── library/ # Game browsing/display (9 components)
|
||||
├── player/ # Game players (2 components)
|
||||
├── playlist/ # Playlist management (3 components)
|
||||
├── github/ # GitHub integration (1 component)
|
||||
├── home/ # Home page components (1 component)
|
||||
├── jobs/ # Job management (4 components)
|
||||
├── layout/ # Layout structure (9 components)
|
||||
├── library/ # Game browsing/display (10 components)
|
||||
├── player/ # Game players (3 components)
|
||||
├── playlist/ # Playlist management (10 components)
|
||||
│ └── SharePlaylistDialog includes domain selector for admins
|
||||
├── roles/ # Role management (2 components)
|
||||
├── search/ # Search and filtering (2 components)
|
||||
├── settings/ # Settings tabs including AppSettingsTab with Domain Settings
|
||||
├── stats/ # Statistics/charts (4 components)
|
||||
├── theme/ # Theme controls (2 components)
|
||||
├── ui/ # Shadcn UI primitives (40+ components)
|
||||
├── roles/ # Role management (3 components)
|
||||
├── search/ # Search and filtering (5 components)
|
||||
├── settings/ # Settings tabs (6 components)
|
||||
├── stats/ # Statistics/charts (5 components)
|
||||
├── theme/ # Theme controls (1 component)
|
||||
├── ui/ # Shadcn UI primitives (36 components)
|
||||
└── users/ # User management (3 components)
|
||||
```
|
||||
|
||||
|
||||
@@ -5,16 +5,18 @@ frontend.
|
||||
|
||||
## Overview
|
||||
|
||||
The application uses 18+ custom hooks organized by functionality:
|
||||
The application uses 30 custom hooks organized by functionality:
|
||||
|
||||
- **Data Fetching** - useGames, usePlaylists, useUsers, useRoles, useActivities
|
||||
- **Authentication** - useAuth
|
||||
- **Data Fetching** - useGames, usePlaylists, useUserPlaylists, useUsers,
|
||||
useRoles, useActivities, useFavorites
|
||||
- **Authentication** - useAuth, usePublicSettings
|
||||
- **UI** - useDebounce, useSwipeGesture, useMountEffect, useToast,
|
||||
useDateTimeFormat
|
||||
- **Game Data** - useFilterOptions, useTags, usePlatforms, useStatistics,
|
||||
useDownload
|
||||
- **Play Tracking** - usePlayTracking
|
||||
useDateTimeFormat, useMediaQuery, useFeatureFlags
|
||||
- **Game Data** - useFilterOptions, useFilterDropdowns, useTags, usePlatforms,
|
||||
useStatistics, useDownload, useMostPlayedGames, useRecentGames
|
||||
- **Play Tracking** - usePlayTracking, useActivityFilters
|
||||
- **Community** - useCommunityPlaylists
|
||||
- **Shared Access** - useSharedAccessToken, useSharedPlaylistAccess, useDomains
|
||||
|
||||
## Data Fetching Hooks
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ Return null if not found in any ZIP
|
||||
**Purpose**: Read Flashpoint preferences.json and expose gameDataSources for
|
||||
auto-downloading game ZIPs.
|
||||
|
||||
**Location**: `backend/src/game/services/PreferencesService.ts`
|
||||
**Location**: `backend/src/services/PreferencesService.ts`
|
||||
|
||||
**Architecture**:
|
||||
|
||||
@@ -820,66 +820,6 @@ Future improvements:
|
||||
- Local cache: Sequential writes
|
||||
- Solution: SSD recommended, RAM disk for cache, bounded resource cleanup
|
||||
|
||||
## Code Quality & Security Improvements (Second Pass Review)
|
||||
|
||||
The following medium and low severity fixes were implemented to improve code
|
||||
quality and maintainability:
|
||||
|
||||
### HTML Injection & Logging
|
||||
- `htmlInjector.ts` now uses project logger instead of `console.log`
|
||||
- Dead code branch removed from polyfill injection logic
|
||||
- Improves consistency with logging across the module
|
||||
|
||||
### PreferencesService Initialization
|
||||
- Logs warning on re-initialization with different path
|
||||
- Prevents silent configuration changes that could affect game downloads
|
||||
- Helps identify configuration issues during development
|
||||
|
||||
### Schema Validation
|
||||
- Hostname schema minimum length corrected to 2 (matches regex requirement)
|
||||
- Enforces consistency between validation rules and runtime expectations
|
||||
|
||||
### Regex Pattern Fixes
|
||||
- `pathSecurity.ts` regex patterns no longer use `/g` flag with `test()`
|
||||
- Prevents stateful matching bugs that could skip validation on consecutive calls
|
||||
- Single-flag patterns are checked per call, not maintained across calls
|
||||
|
||||
### Code Cleanup
|
||||
- Unused exports removed from:
|
||||
- `mimeTypes.ts` - Cleaning up unused MIME type exports
|
||||
- `validation/schemas.ts` - Removing unused Zod schema exports
|
||||
- `utils/cors.ts` - Removing unused utility functions
|
||||
- `utils/pathSecurity.ts` - Removing unused security utilities
|
||||
- Reduces module surface area and maintenance burden
|
||||
|
||||
### Download Validation
|
||||
- `GameDataDownloader.getFilename()` now validates timestamps
|
||||
- Rejects NaN timestamps to prevent corrupted filenames
|
||||
- Ensures valid download filename format
|
||||
|
||||
### Resource Leak Prevention
|
||||
- `GameDataDownloader` destroys response stream on size limit
|
||||
- Prevents resource leaks when downloads exceed maximum size
|
||||
- Proper cleanup when download constraints are violated
|
||||
|
||||
### DRY Principle
|
||||
- `GameDataService` uses `GameDataDownloader.getFilename()` instead of duplicating logic
|
||||
- Centralizes filename generation, preventing consistency issues
|
||||
|
||||
### Additional Low-Priority Improvements (2026-02-07)
|
||||
|
||||
**Error Handling & Streaming Safety**:
|
||||
- `proxy-request-handler` `sendError()` now guards against `headersSent` to prevent write-after-headers errors in streaming scenarios
|
||||
- `collectRequestBody` now uses `settled` flag to prevent double resolve/reject when both `end` and `error` events fire
|
||||
|
||||
**Visibility & Encapsulation**:
|
||||
- `findActiveDownload` in `gamezipserver.ts` is now private (was publicly accessible but only used internally)
|
||||
- `PRIVATE_IP_RANGES` in `GameDataDownloader.ts` is now `static readonly`
|
||||
|
||||
**Module Boundaries**:
|
||||
- Removed unused imports and convenience re-exports from service index files for cleaner module boundaries
|
||||
- Internal-only constants like `CUSTOM_MIME_TYPES` no longer exported, reducing public API surface
|
||||
|
||||
## Security Hardening
|
||||
|
||||
The game content module implements multiple defense-in-depth security measures:
|
||||
@@ -1057,82 +997,35 @@ private sendError(res: Response, status: number, message: string) {
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Request Body Size Limits
|
||||
|
||||
**DoS Protection**: The GameZip server limits request body sizes to prevent
|
||||
memory exhaustion attacks:
|
||||
|
||||
```typescript
|
||||
// gamezipserver.ts
|
||||
private readBody(req: http.IncomingMessage, maxSize: number = 1024 * 1024): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
if (body.length > maxSize) {
|
||||
req.destroy(); // Immediately terminate connection
|
||||
reject(new Error('Request body too large'));
|
||||
}
|
||||
});
|
||||
req.on('end', () => resolve(body));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Protection**:
|
||||
|
||||
- 1MB maximum request size by default
|
||||
- Connection destroyed immediately on overflow
|
||||
- Prevents memory exhaustion attacks
|
||||
- Configurable per endpoint if needed
|
||||
|
||||
### Path Traversal Prevention
|
||||
|
||||
All file paths are normalized:
|
||||
**ZIP Mount ID Validation**: Uses Zod schema to validate game IDs:
|
||||
|
||||
```typescript
|
||||
path.normalize(filePath).toLowerCase();
|
||||
// validation/schemas.ts
|
||||
const mountGameSchema = z.object({
|
||||
gameId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.regex(/^[a-zA-Z0-9\-_]+$/), // Only alphanumeric, hyphens, underscores
|
||||
});
|
||||
```
|
||||
|
||||
**ZIP Mount ID Validation**:
|
||||
**ZIP Path Validation**: Resolved paths must stay within the allowed games
|
||||
directory:
|
||||
|
||||
```typescript
|
||||
// Reject IDs with path traversal sequences
|
||||
if (
|
||||
id.includes('/') ||
|
||||
id.includes('\\') ||
|
||||
id.includes('..') ||
|
||||
id.includes('\0')
|
||||
) {
|
||||
this.sendError(res, 400, 'Invalid mount ID');
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**ZIP Path Validation**:
|
||||
|
||||
```typescript
|
||||
// Ensure ZIP files are within allowed directory
|
||||
const resolvedZipPath = path.resolve(normalizedZipPath);
|
||||
const resolvedGamesPath = path.resolve(allowedGamesPath);
|
||||
|
||||
if (!resolvedZipPath.startsWith(resolvedGamesPath)) {
|
||||
this.sendError(
|
||||
res,
|
||||
403,
|
||||
'Forbidden: ZIP file must be within games directory'
|
||||
);
|
||||
return;
|
||||
if (!resolvedZipPath.startsWith(resolvedGamesPath + path.sep)) {
|
||||
return { success: false, statusCode: 403 };
|
||||
}
|
||||
```
|
||||
|
||||
Prevents attacks like:
|
||||
|
||||
- `../../../etc/passwd`
|
||||
- `..\\..\\windows\\system32`
|
||||
- Null byte injection (`file.txt\0.jpg`)
|
||||
- Directory escape via mount IDs
|
||||
Prevents attacks like path traversal (`../../../etc/passwd`), backslash
|
||||
escapes, and directory escape via mount IDs.
|
||||
|
||||
### ZIP Access Control
|
||||
|
||||
|
||||
@@ -38,13 +38,14 @@ Instead of HTTP endpoints, GameZip Server provides direct method calls:
|
||||
|
||||
```typescript
|
||||
// Called from backend
|
||||
await gameZipServer.mountZip(id: string, params: MountParams)
|
||||
const result = await gameZipServer.mountZip(params: MountParams)
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
```typescript
|
||||
interface MountParams {
|
||||
id: string; // Required: Mount ID (typically game UUID)
|
||||
zipPath: string; // Required: Absolute path to ZIP file
|
||||
gameId?: string; // Optional: Game UUID (enables auto-download)
|
||||
dateAdded?: string; // Optional: ISO date (used for filename)
|
||||
@@ -54,20 +55,32 @@ interface MountParams {
|
||||
|
||||
**Response:**
|
||||
|
||||
Throws on error, returns void on success.
|
||||
```typescript
|
||||
interface MountResult {
|
||||
success: boolean; // True if mounted or download started
|
||||
downloading?: boolean; // True if download in progress (HTTP 202)
|
||||
statusCode: number; // HTTP status code (200, 202, 400, 403, 404, 500, 503)
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Example usage
|
||||
try {
|
||||
await gameZipServer.mountZip('game-abc123', {
|
||||
zipPath: 'D:/Flashpoint/Data/Games/Flash/G/game.zip',
|
||||
gameId: 'game-abc123',
|
||||
dateAdded: '2024-01-15T10:30:00.000Z',
|
||||
sha256: 'ABC123...'
|
||||
});
|
||||
console.log('ZIP mounted successfully');
|
||||
} catch (error) {
|
||||
console.error('Mount failed:', error.message);
|
||||
const result = await gameZipServer.mountZip({
|
||||
id: 'game-abc123',
|
||||
zipPath: 'D:/Flashpoint/Data/Games/Flash/G/game.zip',
|
||||
gameId: 'game-abc123',
|
||||
dateAdded: '2024-01-15T10:30:00.000Z',
|
||||
sha256: 'ABC123...'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
if (result.downloading) {
|
||||
console.log('Download in progress, poll for completion');
|
||||
} else {
|
||||
console.log('ZIP mounted successfully');
|
||||
}
|
||||
} else {
|
||||
console.error(`Mount failed with status ${result.statusCode}`);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -290,7 +303,7 @@ Connection: keep-alive
|
||||
|
||||
```http
|
||||
Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
|
||||
Access-Control-Allow-Headers: *
|
||||
```
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ private sendError(res: ServerResponse, statusCode: number, message: string) {
|
||||
// CRITICAL: Set CORS headers even for errors
|
||||
if (settings.allowCrossDomain) {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', '*');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ Search, browse, and retrieve game information.
|
||||
|
||||
Query params: `search` (title/developer/publisher), `platform`, `series`,
|
||||
`developers`, `publishers`, `playModes`, `languages`, `library`
|
||||
(arcade|theatre), `tags`, `yearFrom`, `yearTo`, `sortBy`
|
||||
(title|releaseDate|dateAdded|developer, default: title), `sortOrder` (asc|desc,
|
||||
default: asc), `page` (default: 1), `limit` (default: 50, max: 100),
|
||||
`showBroken` (default: false), `showExtreme` (default: false)
|
||||
(arcade|theatre), `tags`, `yearFrom`, `yearTo`, `dateAddedSince` (ISO datetime),
|
||||
`dateModifiedSince` (ISO datetime), `sortBy`
|
||||
(title|releaseDate|dateAdded|dateModified|developer, default: title),
|
||||
`sortOrder` (asc|desc, default: asc), `page` (default: 1), `limit` (default: 50,
|
||||
max: 100), `showBroken` (default: false), `showExtreme` (default: false)
|
||||
|
||||
Returns paginated array with essential game fields (id, title, developer,
|
||||
platformName, library, tags, orderTitle).
|
||||
|
||||
@@ -20,7 +20,7 @@ Error: `404 Not Found`
|
||||
|
||||
## Create Playlist
|
||||
|
||||
`POST /api/playlists` - Optional auth
|
||||
`POST /api/playlists` - Requires auth + `playlists.create` permission
|
||||
|
||||
Body:
|
||||
`{ "title": "string (required)", "description": "string (optional)", "author": "string (optional)", "icon": "string (optional)", "library": "arcade"|"theatre" (optional), "extreme": boolean (optional) }`
|
||||
@@ -29,7 +29,7 @@ Returns `201 Created` with new playlist (empty games array).
|
||||
|
||||
## Add Games to Playlist
|
||||
|
||||
`POST /api/playlists/:id/games` - Optional auth
|
||||
`POST /api/playlists/:id/games` - Requires auth + `playlists.update` permission
|
||||
|
||||
Body: `{ "gameIds": ["uuid1", "uuid2", ...] }`
|
||||
|
||||
@@ -39,7 +39,7 @@ Error: `404 Not Found` if playlist not found
|
||||
|
||||
## Remove Games from Playlist
|
||||
|
||||
`DELETE /api/playlists/:id/games` - Optional auth
|
||||
`DELETE /api/playlists/:id/games` - Requires auth + `playlists.update` permission
|
||||
|
||||
Body: `{ "gameIds": ["uuid1", "uuid2", ...] }`
|
||||
|
||||
@@ -47,7 +47,7 @@ Returns updated playlist with remaining games.
|
||||
|
||||
## Delete Playlist
|
||||
|
||||
`DELETE /api/playlists/:id` - Optional auth
|
||||
`DELETE /api/playlists/:id` - Requires auth + `playlists.delete` permission
|
||||
|
||||
Returns `{ "success": true }`
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ Manage system-wide configuration settings.
|
||||
|
||||
## Get All Settings
|
||||
|
||||
`GET /api/settings` - Requires `settings.read` permission
|
||||
`GET /api/settings` - Requires `settings.update` permission
|
||||
|
||||
Returns grouped settings by category (auth, app, metadata, features, game).
|
||||
|
||||
**Note:** This endpoint requires `settings.update` (not `settings.read`) because it returns all settings including sensitive values.
|
||||
|
||||
## Get Category Settings
|
||||
|
||||
`GET /api/settings/:category` - Requires `settings.read` permission
|
||||
|
||||
@@ -41,10 +41,16 @@ backend/
|
||||
│ │ └── ... (other routes)
|
||||
│ │
|
||||
│ ├── game/ # Game service (merged from game-service/)
|
||||
│ │ ├── handlers/ # Request handlers
|
||||
│ │ ├── services/ # Game-related services
|
||||
│ │ ├── utils/ # Game utilities
|
||||
│ │ └── ... (game-specific code)
|
||||
│ │ ├── gamezipserver.ts # ZIP mount/serve manager
|
||||
│ │ ├── legacy-server.ts # Legacy content fallback chain
|
||||
│ │ ├── zip-manager.ts # ZIP file handling with LRU cache
|
||||
│ │ ├── proxy-request-handler.ts # Proxy request routing
|
||||
│ │ ├── config.ts # Game service configuration
|
||||
│ │ ├── mimeTypes.ts # MIME type mappings
|
||||
│ │ ├── cgi/ # CGI script execution
|
||||
│ │ ├── services/ # Game-related services
|
||||
│ │ ├── utils/ # Game utilities (CORS, path security)
|
||||
│ │ └── validation/ # Zod schemas for validation
|
||||
│ │
|
||||
│ ├── services/ # Business logic layer
|
||||
│ │ ├── DatabaseService.ts # Flashpoint DB (read-only)
|
||||
@@ -124,8 +130,8 @@ frontend/
|
||||
│ ├── store/ # Zustand stores
|
||||
│ │ ├── auth.ts # User & auth state
|
||||
│ │ ├── theme.ts # Theme preferences
|
||||
│ │ ├── ui.ts # UI state
|
||||
│ │ └── preferences.ts # User preferences
|
||||
│ │ ├── ui.ts # UI state (sidebar, etc.)
|
||||
│ │ └── sharedAccess.ts # Shared playlist access tokens
|
||||
│ │
|
||||
│ ├── lib/ # Library code
|
||||
│ │ ├── api.ts # Axios client (authenticated)
|
||||
|
||||
@@ -280,6 +280,22 @@ Aggregated play statistics per user per game.
|
||||
|
||||
**Indexes:** user_id, game_id, total_plays, total_playtime_seconds
|
||||
|
||||
### user_stats
|
||||
|
||||
Aggregate user statistics (overall totals across all games).
|
||||
|
||||
| Column | Type | Default | Description |
|
||||
| -------------------- | ------- | ------- | ----------------------------- |
|
||||
| user_id | INTEGER | - | Primary key, foreign key to users.id |
|
||||
| total_games_played | INTEGER | 0 | Total unique games played |
|
||||
| total_playtime_seconds | INTEGER | 0 | Total playtime across all games |
|
||||
| total_sessions | INTEGER | 0 | Total play sessions |
|
||||
| first_play_at | TEXT | NULL | First ever play timestamp |
|
||||
| last_play_at | TEXT | NULL | Most recent play timestamp |
|
||||
| updated_at | TEXT | now() | ISO 8601 |
|
||||
|
||||
**Foreign Key:** user_id references users(id) ON DELETE CASCADE
|
||||
|
||||
### user_settings
|
||||
|
||||
Flexible user preference storage (key-value).
|
||||
|
||||
+6
-4
@@ -70,12 +70,14 @@ web application for browsing and playing games from the Flashpoint Archive.
|
||||
- [HTML5 Player](04-frontend/player-implementation/html5-player.md) - HTML5
|
||||
games
|
||||
|
||||
### 05. Game Service
|
||||
### 05. Game Service (Integrated into Backend)
|
||||
|
||||
- [Game Service Overview](05-game-service/README.md) - Game service introduction
|
||||
- [Architecture](05-game-service/architecture.md) - Dual-server design
|
||||
- [Proxy Server](05-game-service/proxy-server.md) - HTTP proxy (port 22500)
|
||||
- [GameZip Server](05-game-service/gamezip-server.md) - ZIP server (port 22501)
|
||||
- [Architecture](05-game-service/architecture.md) - Integrated single-service
|
||||
design
|
||||
- [Proxy Server](05-game-service/proxy-server.md) - HTTP proxy (/game-proxy/\*)
|
||||
- [GameZip Server](05-game-service/gamezip-server.md) - ZIP server
|
||||
(/game-zip/\*)
|
||||
- [Legacy Server](05-game-service/legacy-server.md) - Legacy content serving
|
||||
- [ZIP Manager](05-game-service/zip-manager.md) - ZIP mounting
|
||||
- [MIME Types](05-game-service/mime-types.md) - MIME type handling
|
||||
|
||||
Reference in New Issue
Block a user