File size: 2,506 Bytes
57a889c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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.
 */
@Controller('api/trips/:tripId/files')
export class FilesDownloadController {
  constructor(private readonly files: FilesService) {}

  @Get(':id/download')
  download(@Req() req: Request, @Res() res: Response, @Param('tripId') tripId: string, @Param('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) });
  }
}