Skip to content
This repository was archived by the owner on Apr 8, 2026. It is now read-only.

Commit d22fb87

Browse files
committed
feat(games): implement HEAD endpoint for game download with access control and header management
1 parent 90e02c1 commit d22fb87

1 file changed

Lines changed: 39 additions & 17 deletions

File tree

src/controllers/GameController.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -444,31 +444,53 @@ export class Games {
444444
}
445445
}
446446

447-
@httpHead('/:gameId')
448-
public async headGame(req: Request, res: Response) {
449-
if (!(await validateOr400(gameIdParamSchema, req.params, res))) {
450-
await this.createLog(req, 'headGame', 'games', 400);
451-
return;
452-
}
447+
@httpHead('/:gameId/download', LoggedCheck.middleware)
448+
public async headDownloadGame(req: AuthenticatedRequest, res: Response) {
449+
const { gameId } = req.params;
450+
const userId = req.user.user_id;
453451
try {
454-
const { gameId } = req.params;
455452
const game = await this.gameService.getGame(gameId);
456453
if (!game) {
457-
await this.createLog(req, 'headGame', 'games', 404);
458454
return res.status(404).send({ message: 'Game not found' });
459455
}
456+
const owns = (await this.gameService.userOwnsGame(gameId, userId)) || game.owner_id === userId;
457+
if (!owns) {
458+
return res.status(403).send({ message: 'Access denied' });
459+
}
460+
const link = game.download_link;
461+
if (!link) {
462+
return res.status(404).send({ message: 'Download link not available' });
463+
}
464+
465+
const headers: any = {};
466+
if (req.headers.range) {
467+
headers.Range = req.headers.range;
468+
}
460469

461-
const headers = {
462-
'Content-Type': 'application/json',
463-
'ETag': generateETag(Buffer.from(JSON.stringify(game))),
464-
};
470+
const fileRes = await fetch(link, { method: 'HEAD', headers });
471+
if (!fileRes.ok) {
472+
return res.status(fileRes.status).send({ message: 'Error fetching file headers' });
473+
}
474+
475+
res.setHeader('Content-Disposition', `attachment; filename="${game.name}.zip"`);
476+
res.setHeader('Content-Type', fileRes.headers.get('content-type') || 'application/octet-stream');
477+
478+
const contentLength = fileRes.headers.get('content-length');
479+
if (contentLength !== null) {
480+
res.setHeader('Content-Length', contentLength);
481+
}
482+
const acceptRanges = fileRes.headers.get('accept-ranges');
483+
if (acceptRanges !== null) {
484+
res.setHeader('Accept-Ranges', acceptRanges);
485+
}
486+
const contentRange = fileRes.headers.get('content-range');
487+
if (contentRange !== null) {
488+
res.setHeader('Content-Range', contentRange);
489+
}
465490

466-
res.set(headers);
467-
await this.createLog(req, 'headGame', 'games', 200);
468-
res.status(200).end();
491+
res.status(fileRes.status).end();
469492
} catch (error) {
470-
await this.createLog(req, 'headGame', 'games', 500);
471-
handleError(res, error, 'Error fetching game headers');
493+
handleError(res, error, 'Error fetching file headers');
472494
}
473495
}
474496
}

0 commit comments

Comments
 (0)