Skip to content

Commit 762dbe3

Browse files
authored
Merge pull request #6 from darkraise/feature/code-review
Feature/code review
2 parents 74cce9f + 410287d commit 762dbe3

191 files changed

Lines changed: 2686 additions & 3031 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ OTEL_SERVICE_NAME=flashpoint-web
4747
OTEL_TRACES_ENABLED=true
4848
OTEL_METRICS_ENABLED=true
4949

50+
# ---------------------------------------------------------------------------
51+
# Container User Permissions
52+
# ---------------------------------------------------------------------------
53+
# Match your host user to avoid bind-mount permission issues
54+
# Run 'id -u' and 'id -g' on your host to find your values
55+
# PUID=1000
56+
# PGID=1000
57+
5058
# ---------------------------------------------------------------------------
5159
# Application settings
5260
# ---------------------------------------------------------------------------

‎.gitignore‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,6 @@ vite.config.js.timestamp-*
139139
vite.config.ts.timestamp-*
140140
.claude/**/*.*
141141

142-
user.db
142+
user.db
143+
*.db-shm
144+
*.db-wal

‎DOCUMENTATION_UPDATES.md‎

Lines changed: 0 additions & 120 deletions
This file was deleted.

‎backend/Dockerfile‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,7 @@ WORKDIR /app/backend
7676
# Expose port
7777
EXPOSE 3100
7878

79-
# Health check
80-
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
81-
CMD curl --fail http://localhost:3100/health || exit 1
79+
# Health check is configured in docker-compose.yml for flexibility
8280

8381
# Environment variables
8482
ENV NODE_ENV=production

‎backend/src/config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const config = {
107107
// Auto-detected from version.txt; affects metadata sync and image path availability
108108
flashpointEdition: flashpointVersion.edition,
109109
flashpointVersionString: flashpointVersion.versionString,
110-
};
110+
} as const;
111111

112112
/** Resolves external image CDN URLs from Flashpoint preferences, with hardcoded fallbacks. */
113113
export async function getExternalImageUrls(): Promise<string[]> {

‎backend/src/game/gamezipserver.ts‎

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { setCorsHeaders } from './utils/cors';
1010
import { sanitizeUrlPath, sanitizeErrorMessage } from './utils/pathSecurity';
1111
import { validateGameId, validateHostname } from './validation/schemas';
1212
import { gameDataDownloader } from './services';
13+
import { DownloadRegistry } from '../services/DownloadRegistry';
1314

1415
/** Escape HTML special characters to prevent XSS */
1516
function escapeHtml(str: string): string {
@@ -104,18 +105,40 @@ export class GameZipServer {
104105
}
105106

106107
if (!zipExists && gameId && dateAdded) {
108+
// Check shared registry first (prevents duplicate if DownloadManager already downloading)
109+
if (DownloadRegistry.isActive(gameId)) {
110+
logger.info(
111+
`[GameZipServer] Download already in progress for game ${gameId} (via DownloadManager)`
112+
);
113+
return { success: true, downloading: true, statusCode: 202 };
114+
}
115+
107116
if (downloadsInProgress.has(gameId)) {
108117
logger.info(`[GameZipServer] Download already in progress for game ${gameId}`);
109118
return { success: true, downloading: true, statusCode: 202 };
110119
}
111120

112-
if (downloadsInProgress.size >= MAX_CONCURRENT_DOWNLOADS) {
121+
const totalActive = downloadsInProgress.size + DownloadRegistry.getActiveCount();
122+
if (totalActive >= MAX_CONCURRENT_DOWNLOADS) {
113123
logger.warn(
114-
`[GameZipServer] Too many concurrent downloads (${downloadsInProgress.size}), rejecting ${gameId}`
124+
`[GameZipServer] Too many concurrent downloads (${totalActive}), rejecting ${gameId}`
115125
);
116126
return { success: false, statusCode: 503 };
117127
}
118128

129+
// Register with shared download registry
130+
const registered = DownloadRegistry.register(gameId, {
131+
gameId,
132+
source: 'game-zip-server',
133+
});
134+
135+
if (!registered) {
136+
logger.info(
137+
`[GameZipServer] Download already in progress for game ${gameId} (detected by registry)`
138+
);
139+
return { success: true, downloading: true, statusCode: 202 };
140+
}
141+
119142
logger.info(
120143
`[GameZipServer] ZIP not found locally, starting background download for game ${gameId}`
121144
);
@@ -280,15 +303,17 @@ export class GameZipServer {
280303
let fileData = result.data;
281304
if (ext === 'html' || ext === 'htm') {
282305
fileData = injectPolyfills(result.data);
283-
logger.info(`[GameZipServer] Injected polyfills into HTML file: ${relPath}`);
306+
logger.debug(`[GameZipServer] Injected polyfills into HTML file: ${relPath}`);
284307
}
285308

286-
logger.info(
309+
logger.debug(
287310
`[GameZipServer] ✓ Serving from ZIP ${result.mountId}: ${relPath} (${fileData.length} bytes)`
288311
);
289312

290313
const settings = ConfigManager.getSettings();
291314
setCorsHeaders(res, settings);
315+
res.setHeader('X-Content-Type-Options', 'nosniff');
316+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
292317

293318
res.setHeader('Content-Type', contentType);
294319
res.setHeader('Content-Length', fileData.length);
@@ -351,22 +376,27 @@ export class GameZipServer {
351376
logger.error(
352377
`[GameZipServer] Downloaded file path outside allowed directory: ${result.filePath}`
353378
);
379+
DownloadRegistry.fail(gameId);
354380
return;
355381
}
356382

357383
try {
358384
await zipManager.mount(mountId, result.filePath);
359385
logger.info(`[GameZipServer] ✓ ZIP downloaded and mounted for game ${gameId}`);
386+
DownloadRegistry.complete(gameId);
360387
} catch (mountError) {
361388
const errorMessage = mountError instanceof Error ? mountError.message : 'Mount failed';
362389
logger.error(`[GameZipServer] Failed to mount downloaded ZIP: ${errorMessage}`);
390+
DownloadRegistry.fail(gameId);
363391
}
364392
} else {
365393
logger.error(`[GameZipServer] Download failed for ${gameId}: ${result.error}`);
394+
DownloadRegistry.fail(gameId);
366395
}
367396
} catch (error) {
368397
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
369398
logger.error(`[GameZipServer] Background download error for ${gameId}: ${errorMsg}`);
399+
DownloadRegistry.fail(gameId);
370400
} finally {
371401
downloadsInProgress.delete(gameId);
372402
}
@@ -377,6 +407,7 @@ export class GameZipServer {
377407
for (const [gameId, progress] of downloadsInProgress.entries()) {
378408
if (now - progress.startTime > DOWNLOAD_STALE_MS) {
379409
logger.warn(`[GameZipServer] Removing stale download for ${gameId}`);
410+
DownloadRegistry.fail(gameId);
380411
downloadsInProgress.delete(gameId);
381412
}
382413
}
@@ -529,6 +560,7 @@ export class GameZipServer {
529560

530561
const settings = ConfigManager.getSettings();
531562
setCorsHeaders(res, settings);
563+
res.setHeader('X-Content-Type-Options', 'nosniff');
532564

533565
res.setHeader('Content-Type', 'text/plain');
534566
res.writeHead(statusCode);

‎backend/src/game/legacy-server.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,7 @@ export class LegacyServer {
162162
const host = hostname.split(':')[0];
163163
const relPath = path.posix.join(host, urlPath);
164164

165-
logger.debug(`[LegacyServer] Serving: ${host}${urlPath}`);
166-
logger.debug(`[LegacyServer] Relative path: ${relPath}`);
165+
logger.debug(`[LegacyServer] Serving: ${host}${urlPath} (relPath: ${relPath})`);
167166

168167
const pathCandidates = this.buildPathCandidates(relPath, urlPath, settings);
169168

‎backend/src/game/proxy-request-handler.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ProxyRequestHandler {
3636
/** Replicates the request processing flow from Go's goproxy */
3737
async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
3838
try {
39-
logger.info(`[ProxyHandler] ${req.method} ${req.url}`);
39+
logger.debug(`[ProxyHandler] ${req.method} ${req.url}`);
4040

4141
if (!req.url) {
4242
this.sendError(res, 400, 'Bad Request: No URL');
@@ -139,6 +139,7 @@ export class ProxyRequestHandler {
139139
const statusCode = result.statusCode || 200;
140140

141141
setCorsHeaders(res, settings);
142+
res.setHeader('X-Content-Type-Options', 'nosniff');
142143

143144
res.setHeader('Cache-Control', `public, max-age=${CACHE_MAX_AGE_SECONDS}`);
144145
if (process.env.NODE_ENV !== 'production') {
@@ -233,6 +234,7 @@ export class ProxyRequestHandler {
233234

234235
const settings = ConfigManager.getSettings();
235236
setCorsHeaders(res, settings);
237+
res.setHeader('X-Content-Type-Options', 'nosniff');
236238

237239
res.setHeader('Content-Type', 'text/plain');
238240
res.writeHead(statusCode);

‎backend/src/game/services/GameDataDownloader.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import net from 'net';
88
import dns from 'dns';
99
import { promisify } from 'util';
1010
import { logger } from '../../utils/logger';
11-
import { PreferencesService, GameDataSource } from './PreferencesService';
11+
import { PreferencesService, GameDataSource } from '../../services/PreferencesService';
1212

1313
const dnsLookup = promisify(dns.lookup);
1414

@@ -103,8 +103,7 @@ export class GameDataDownloader {
103103

104104
logger.info(`[GameDataDownloader] Starting download for ${filename}`);
105105

106-
const prefsService = PreferencesService.getInstance();
107-
const sources = await prefsService.getGameDataSources();
106+
const sources = await PreferencesService.getGameDataSources();
108107

109108
if (sources.length === 0) {
110109
logger.error('[GameDataDownloader] No game data sources configured in preferences.json');
@@ -114,7 +113,7 @@ export class GameDataDownloader {
114113
};
115114
}
116115

117-
const targetDir = params.targetPath || (await prefsService.getDataPacksFolderPath());
116+
const targetDir = params.targetPath || (await PreferencesService.getDataPacksFolderPath());
118117
const targetPath = path.join(targetDir, filename);
119118
const tempPath = `${targetPath}.temp`;
120119

@@ -544,8 +543,7 @@ export class GameDataDownloader {
544543
async exists(gameId: string, dateAdded: string, targetDir?: string): Promise<boolean> {
545544
try {
546545
const filename = GameDataDownloader.getFilename(gameId, dateAdded);
547-
const prefsService = PreferencesService.getInstance();
548-
const dir = targetDir || (await prefsService.getDataPacksFolderPath());
546+
const dir = targetDir || (await PreferencesService.getDataPacksFolderPath());
549547
const filePath = path.join(dir, filename);
550548

551549
try {
@@ -563,8 +561,7 @@ export class GameDataDownloader {
563561
async getFilePath(gameId: string, dateAdded: string, targetDir?: string): Promise<string> {
564562
try {
565563
const filename = GameDataDownloader.getFilename(gameId, dateAdded);
566-
const prefsService = PreferencesService.getInstance();
567-
const dir = targetDir || (await prefsService.getDataPacksFolderPath());
564+
const dir = targetDir || (await PreferencesService.getDataPacksFolderPath());
568565
return path.join(dir, filename);
569566
} catch (error) {
570567
// getFilename() can throw on invalid dateAdded

0 commit comments

Comments
 (0)