because I got bored of customising my CV for every job
at main 100 lines 2.8 kB view raw
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}