because I got bored of customising my CV for every job
1import { createHash } from "node:crypto";
2import { readFile, unlink } from "node:fs/promises";
3import type { User as DomainUser } from "@cv/auth";
4import { JwtAuthGuard } from "@cv/auth";
5import { validateFile } from "@cv/file-upload";
6import {
7 BadRequestException,
8 Body,
9 Controller,
10 Logger,
11 Post,
12 Req,
13 UploadedFile,
14 UseGuards,
15 UseInterceptors,
16} from "@nestjs/common";
17import { FileInterceptor } from "@nestjs/platform-express";
18import { FileImportSource } from "@/modules/data-import/sources/file-import-source";
19import { ImportService } from "@/modules/data-import/import.service";
20
21@Controller("api/cv-parser")
22export class FileUploadController {
23 private readonly logger = new Logger(FileUploadController.name);
24
25 constructor(
26 private readonly importService: ImportService,
27 private readonly fileImportSource: FileImportSource,
28 ) {}
29
30 @Post("upload")
31 @UseGuards(JwtAuthGuard)
32 @UseInterceptors(FileInterceptor("file"))
33 async upload(
34 @Req() req: { user: DomainUser },
35 @Body("profileId") profileId: string,
36 @UploadedFile() file?: Express.Multer.File,
37 ) {
38 if (!file) {
39 throw new BadRequestException("No file provided");
40 }
41
42 if (!profileId) {
43 throw new BadRequestException("profileId is required");
44 }
45
46 const filePath = file.path;
47
48 try {
49 const buffer = file.buffer ?? (await readFile(filePath));
50
51 const validation = validateFile({
52 buffer,
53 mimeType: file.mimetype,
54 originalName: file.originalname,
55 sizeBytes: file.size,
56 });
57
58 if (!validation.valid) {
59 throw new Error(`File validation failed: ${validation.error}`);
60 }
61
62 const fingerprint = createHash("sha256").update(buffer).digest("hex");
63
64 const userFile = await this.importService.createImport(
65 req.user,
66 profileId,
67 this.fileImportSource,
68 {
69 fileName: file.originalname,
70 mimeType: file.mimetype,
71 sizeBytes: file.size,
72 fingerprint,
73 },
74 { buffer, mimeType: file.mimetype },
75 );
76
77 return {
78 userFileId: userFile.id,
79 status: userFile.status,
80 fileName: userFile.fileName,
81 sizeBytes: userFile.sizeBytes,
82 };
83 } catch (error) {
84 this.logger.error(`File upload failed: ${error instanceof Error ? error.message : error}`, error instanceof Error ? error.stack : undefined);
85
86 const message =
87 error instanceof Error ? error.message : "Failed to process file";
88
89 throw new BadRequestException(message);
90 } finally {
91 if (filePath) {
92 try {
93 await unlink(filePath);
94 } catch (err) {
95 this.logger.warn(`Failed to clean up temporary file ${filePath}: ${err instanceof Error ? err.message : err}`);
96 }
97 }
98 }
99 }
100}