Spaces:
Sleeping
Sleeping
File size: 3,452 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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | import {
Controller,
Post,
Body,
Param,
Headers,
HttpException,
UseGuards,
UseInterceptors,
UploadedFiles,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import type { User } from '../../types';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { BookingImportService } from './booking-import.service';
import type { BookingImportPreviewItem, BookingImportPreviewResponse, BookingImportConfirmResponse } from '@trek/shared';
const ACCEPTED_EXTS = new Set(['.eml', '.pdf', '.pkpass', '.html', '.htm', '.txt']);
const MAX_FILE_BYTES = 10 * 1024 * 1024;
const MAX_FILES = 5;
const UPLOAD = {
storage: memoryStorage(),
limits: { fileSize: MAX_FILE_BYTES, files: MAX_FILES },
};
@Controller('api/trips/:tripId/reservations/import')
@UseGuards(JwtAuthGuard)
export class BookingImportController {
constructor(private readonly bookingImport: BookingImportService) {}
private requireTrip(tripId: string, user: User) {
const trip = this.bookingImport.verifyTripAccess(tripId, user.id);
if (!trip) throw new HttpException({ error: 'Trip not found' }, 404);
return trip;
}
private requireEdit(trip: ReturnType<BookingImportService['verifyTripAccess']>, user: User): void {
if (!this.bookingImport.canEdit(trip!, user)) {
throw new HttpException({ error: 'No permission' }, 403);
}
}
/**
* POST /api/trips/:tripId/reservations/import/booking
* Accepts up to 5 booking confirmation files (EML, PDF, PKPass, HTML, TXT).
* Returns a preview list without persisting anything.
*/
@Post('booking')
@UseInterceptors(FilesInterceptor('files', MAX_FILES, UPLOAD))
async preview(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@UploadedFiles() files: Express.Multer.File[] | undefined,
) {
const trip = this.requireTrip(tripId, user);
this.requireEdit(trip, user);
if (!this.bookingImport.isAvailable()) {
throw new HttpException({ error: 'KItinerary extractor is not available on this server' }, 503);
}
if (!files || files.length === 0) {
throw new HttpException({ error: 'No files uploaded' }, 400);
}
// Validate extensions
for (const f of files) {
const ext = f.originalname.toLowerCase().slice(f.originalname.lastIndexOf('.'));
if (!ACCEPTED_EXTS.has(ext)) {
throw new HttpException({ error: `Unsupported file type: ${f.originalname}. Accepted: EML, PDF, PKPass, HTML, TXT` }, 400);
}
}
const result: BookingImportPreviewResponse = await this.bookingImport.preview(files);
return result;
}
/**
* POST /api/trips/:tripId/reservations/import/booking/confirm
* Persists the user-confirmed subset of parsed items.
*/
@Post('booking/confirm')
async confirm(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Body() body: { items?: BookingImportPreviewItem[] },
@Headers('x-socket-id') socketId?: string,
): Promise<BookingImportConfirmResponse> {
const trip = this.requireTrip(tripId, user);
this.requireEdit(trip, user);
const items = body?.items;
if (!Array.isArray(items) || items.length === 0) {
throw new HttpException({ error: 'items must be a non-empty array' }, 400);
}
return this.bookingImport.confirm(tripId, items, socketId);
}
}
|