Spaces:
Sleeping
Sleeping
| import { Controller, Get, HttpException, Param, Req, Res } from '@nestjs/common'; | |
| import type { Request, Response } from 'express'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| import { FilesService } from './files.service'; | |
| /** | |
| * GET /api/trips/:tripId/files/:id/download — authenticated file download. | |
| * | |
| * Deliberately NOT behind the JwtAuthGuard: it accepts a cookie, a Bearer header | |
| * OR a one-shot `?token=` query param (so links can be opened directly), all via | |
| * the legacy authenticateDownload helper. Byte-identical to the legacy route: | |
| * 401 token, 404 trip/file, 403 path traversal, .pkpass served inline for Wallet. | |
| */ | |
| ('api/trips/:tripId/files') | |
| export class FilesDownloadController { | |
| constructor(private readonly files: FilesService) {} | |
| (':id/download') | |
| download(() req: Request, () res: Response, ('tripId') tripId: string, ('id') id: string): void { | |
| const auth = this.files.authenticateDownload(req); | |
| if ('error' in auth) { | |
| throw new HttpException({ error: auth.error }, auth.status); | |
| } | |
| const trip = this.files.verifyTripAccess(tripId, auth.userId); | |
| if (!trip) { | |
| throw new HttpException({ error: 'Trip not found' }, 404); | |
| } | |
| const file = this.files.getFileById(id, tripId); | |
| if (!file) { | |
| throw new HttpException({ error: 'File not found' }, 404); | |
| } | |
| const { resolved, safe } = this.files.resolveFilePath(file.filename); | |
| if (!safe) { | |
| throw new HttpException({ error: 'Forbidden' }, 403); | |
| } | |
| if (!fs.existsSync(resolved)) { | |
| throw new HttpException({ error: 'File not found' }, 404); | |
| } | |
| // Serve Apple Wallet passes inline with the canonical MIME type so Safari | |
| // (iOS/macOS) hands them to Wallet instead of downloading as a blob. | |
| if (path.extname(resolved).toLowerCase() === '.pkpass') { | |
| res.setHeader('Content-Type', 'application/vnd.apple.pkpass'); | |
| res.setHeader('Content-Disposition', `inline; filename="${path.basename(file.original_name || resolved)}"`); | |
| } | |
| // Serve with an explicit { root } + basename rather than an absolute path: | |
| // under the Nest ExpressAdapter, res.sendFile(absolutePath) resolves the | |
| // file relative to the (rewritten) req.url and fails with a spurious | |
| // "Not Found", whereas the root-relative form streams correctly. The | |
| // resolveFilePath guard above already pins this to the uploads dir. | |
| res.sendFile(path.basename(resolved), { root: path.dirname(resolved) }); | |
| } | |
| } | |