- Bloc GEO/IA (resume IA, FAQ, points cles, entites) + rendu public "En bref"/"A retenir"/FAQ + JSON-LD Article & FAQPage. - Corbeille: soft-delete (deleted_at), filtres Tous/Publies/Brouillons/ Planifies/Corbeille, restauration + suppression definitive. - Duplication d'un article (nouveau brouillon, slug unique). - Drag & drop de l'image de couverture. - Recadrage auto de la couverture en 16:9 (sharp) via endpoint dedie /admin/blog/cover-images. - Migration AddGeoAndTrashToBlogPosts (ai_summary, faq, key_takeaways, ai_entities, deleted_at). Ajout dependance sharp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
249 lines
9.1 KiB
TypeScript
249 lines
9.1 KiB
TypeScript
import { Controller, Get, Post, Param, Query, Body, Res, StreamableFile } from '@nestjs/common';
|
|
import { Response } from 'express';
|
|
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery, ApiBody } from '@nestjs/swagger';
|
|
import { Public } from '../decorators/public.decorator';
|
|
import { CsvBookingService } from '../services/csv-booking.service';
|
|
import {
|
|
CarrierDocumentsResponseDto,
|
|
VerifyDocumentAccessDto,
|
|
DocumentAccessRequirementsDto,
|
|
} from '../dto/carrier-documents.dto';
|
|
|
|
/**
|
|
* CSV Booking Actions Controller (Public Routes)
|
|
*
|
|
* Handles public accept/reject actions from carrier emails
|
|
* Separated from main controller to avoid routing conflicts
|
|
*/
|
|
@ApiTags('CSV Booking Actions')
|
|
@Controller('csv-booking-actions')
|
|
export class CsvBookingActionsController {
|
|
constructor(private readonly csvBookingService: CsvBookingService) {}
|
|
|
|
/**
|
|
* Accept a booking request (PUBLIC - token-based)
|
|
*
|
|
* GET /api/v1/csv-booking-actions/accept/:token
|
|
*/
|
|
@Public()
|
|
@Get('accept/:token')
|
|
@ApiOperation({
|
|
summary: 'Accept booking request (public)',
|
|
description:
|
|
'Public endpoint for carriers to accept a booking via email link. Updates booking status and notifies the user.',
|
|
})
|
|
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Booking accepted successfully.',
|
|
})
|
|
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
|
|
@ApiResponse({
|
|
status: 400,
|
|
description: 'Booking cannot be accepted (invalid status or expired)',
|
|
})
|
|
async acceptBooking(@Param('token') token: string) {
|
|
// Accept the booking
|
|
const booking = await this.csvBookingService.acceptBooking(token);
|
|
|
|
// Return simple success response
|
|
return {
|
|
success: true,
|
|
bookingId: booking.id,
|
|
action: 'accepted',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reject a booking request (PUBLIC - token-based)
|
|
*
|
|
* GET /api/v1/csv-booking-actions/reject/:token
|
|
*/
|
|
@Public()
|
|
@Get('reject/:token')
|
|
@ApiOperation({
|
|
summary: 'Reject booking request (public)',
|
|
description:
|
|
'Public endpoint for carriers to reject a booking via email link. Updates booking status and notifies the user.',
|
|
})
|
|
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
|
@ApiQuery({
|
|
name: 'reason',
|
|
required: false,
|
|
description: 'Rejection reason',
|
|
example: 'No capacity available',
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Booking rejected successfully.',
|
|
})
|
|
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
|
|
@ApiResponse({
|
|
status: 400,
|
|
description: 'Booking cannot be rejected (invalid status or expired)',
|
|
})
|
|
async rejectBooking(@Param('token') token: string, @Query('reason') reason: string) {
|
|
// Reject the booking
|
|
const booking = await this.csvBookingService.rejectBooking(token, reason);
|
|
|
|
// Return simple success response
|
|
return {
|
|
success: true,
|
|
bookingId: booking.id,
|
|
action: 'rejected',
|
|
reason: reason || null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check document access requirements (PUBLIC - token-based)
|
|
*
|
|
* GET /api/v1/csv-booking-actions/documents/:token/requirements
|
|
*/
|
|
@Public()
|
|
@Get('documents/:token/requirements')
|
|
@ApiOperation({
|
|
summary: 'Check document access requirements (public)',
|
|
description:
|
|
'Check if a password is required to access booking documents. Use this before showing the password form.',
|
|
})
|
|
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Access requirements retrieved successfully.',
|
|
type: DocumentAccessRequirementsDto,
|
|
})
|
|
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
|
|
async getDocumentAccessRequirements(
|
|
@Param('token') token: string
|
|
): Promise<DocumentAccessRequirementsDto> {
|
|
return this.csvBookingService.checkDocumentAccessRequirements(token);
|
|
}
|
|
|
|
/**
|
|
* Get booking documents for carrier with password verification (PUBLIC - token-based)
|
|
*
|
|
* POST /api/v1/csv-booking-actions/documents/:token
|
|
*/
|
|
@Public()
|
|
@Post('documents/:token')
|
|
@ApiOperation({
|
|
summary: 'Get booking documents with password (public)',
|
|
description:
|
|
'Public endpoint for carriers to access booking documents after acceptance. Requires password verification. Returns booking summary and documents with signed download URLs.',
|
|
})
|
|
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
|
@ApiBody({ type: VerifyDocumentAccessDto })
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Booking documents retrieved successfully.',
|
|
type: CarrierDocumentsResponseDto,
|
|
})
|
|
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
|
|
@ApiResponse({ status: 400, description: 'Booking has not been accepted yet' })
|
|
@ApiResponse({ status: 401, description: 'Invalid password' })
|
|
async getBookingDocumentsWithPassword(
|
|
@Param('token') token: string,
|
|
@Body() dto: VerifyDocumentAccessDto
|
|
): Promise<CarrierDocumentsResponseDto> {
|
|
return this.csvBookingService.getDocumentsForCarrier(token, dto.password);
|
|
}
|
|
|
|
/**
|
|
* Get booking documents for carrier (PUBLIC - token-based) - Legacy without password
|
|
* Kept for backward compatibility with bookings created before password protection
|
|
*
|
|
* GET /api/v1/csv-booking-actions/documents/:token
|
|
*/
|
|
@Public()
|
|
@Get('documents/:token')
|
|
@ApiOperation({
|
|
summary: 'Get booking documents (public) - Legacy',
|
|
description:
|
|
'Public endpoint for carriers to access booking documents. For new bookings, use POST with password instead.',
|
|
})
|
|
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: 'Booking documents retrieved successfully.',
|
|
type: CarrierDocumentsResponseDto,
|
|
})
|
|
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
|
|
@ApiResponse({ status: 400, description: 'Booking has not been accepted yet' })
|
|
@ApiResponse({ status: 401, description: 'Password required for this booking' })
|
|
async getBookingDocuments(@Param('token') token: string): Promise<CarrierDocumentsResponseDto> {
|
|
return this.csvBookingService.getDocumentsForCarrier(token);
|
|
}
|
|
|
|
/**
|
|
* Download a single booking document, streamed through the API (PUBLIC - token-based).
|
|
*
|
|
* Password-protected bookings must use POST with the password in the body.
|
|
*
|
|
* POST /api/v1/csv-booking-actions/documents/:token/:documentId/download
|
|
*/
|
|
@Public()
|
|
@Post('documents/:token/:documentId/download')
|
|
@ApiOperation({
|
|
summary: 'Download a booking document with password (public)',
|
|
description:
|
|
'Streams a single booking document to the carrier. Applies the same access rules as the documents list (booking accepted + password when protected).',
|
|
})
|
|
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
|
@ApiParam({ name: 'documentId', description: 'Document ID' })
|
|
@ApiBody({ type: VerifyDocumentAccessDto })
|
|
@ApiResponse({ status: 200, description: 'Document streamed successfully.' })
|
|
@ApiResponse({ status: 401, description: 'Invalid or missing password' })
|
|
@ApiResponse({ status: 404, description: 'Booking or document not found' })
|
|
async downloadBookingDocumentWithPassword(
|
|
@Param('token') token: string,
|
|
@Param('documentId') documentId: string,
|
|
@Body() dto: VerifyDocumentAccessDto,
|
|
@Res({ passthrough: true }) res: Response
|
|
): Promise<StreamableFile> {
|
|
return this.streamDocument(token, documentId, dto?.password, res);
|
|
}
|
|
|
|
/**
|
|
* Download a single booking document (PUBLIC - token-based) - Legacy without password.
|
|
*
|
|
* GET /api/v1/csv-booking-actions/documents/:token/:documentId/download
|
|
*/
|
|
@Public()
|
|
@Get('documents/:token/:documentId/download')
|
|
@ApiOperation({
|
|
summary: 'Download a booking document (public) - Legacy',
|
|
description:
|
|
'Streams a single booking document for bookings without password protection. Protected bookings must use the POST variant.',
|
|
})
|
|
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
|
@ApiParam({ name: 'documentId', description: 'Document ID' })
|
|
@ApiResponse({ status: 200, description: 'Document streamed successfully.' })
|
|
@ApiResponse({ status: 401, description: 'Password required for this booking' })
|
|
@ApiResponse({ status: 404, description: 'Booking or document not found' })
|
|
async downloadBookingDocument(
|
|
@Param('token') token: string,
|
|
@Param('documentId') documentId: string,
|
|
@Res({ passthrough: true }) res: Response
|
|
): Promise<StreamableFile> {
|
|
return this.streamDocument(token, documentId, undefined, res);
|
|
}
|
|
|
|
private async streamDocument(
|
|
token: string,
|
|
documentId: string,
|
|
password: string | undefined,
|
|
res: Response
|
|
): Promise<StreamableFile> {
|
|
const { buffer, fileName, mimeType } = await this.csvBookingService.streamDocumentForCarrier(
|
|
token,
|
|
documentId,
|
|
password
|
|
);
|
|
|
|
res.setHeader('Content-Type', mimeType);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
|
|
return new StreamableFile(buffer);
|
|
}
|
|
}
|