diff --git a/docker-compose.yml b/docker-compose.yml index 203c2d1..5859374 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,6 @@ services: volumes: # Persist database and generated PDFs - ./data:/app/data - # Base resume JSON (read-only) - - ./resume-generator/base.json:/app/resume-generator/base.json:ro environment: # Server config - NODE_ENV=production diff --git a/orchestrator/src/server/api/routes/profile.ts b/orchestrator/src/server/api/routes/profile.ts index ad531b9..9c7307d 100644 --- a/orchestrator/src/server/api/routes/profile.ts +++ b/orchestrator/src/server/api/routes/profile.ts @@ -1,8 +1,9 @@ import { Router, Request, Response } from 'express'; -import { access, mkdir, writeFile } from 'fs/promises'; +import { mkdir, stat, writeFile } from 'fs/promises'; import { dirname } from 'path'; import { extractProjectsFromProfile } from '../../services/resumeProjects.js'; import { clearProfileCache, DEFAULT_PROFILE_PATH, getProfile } from '../../services/profile.js'; +import { resumeDataSchema } from '@shared/rxresume-schema.js'; export const profileRouter = Router(); @@ -38,8 +39,9 @@ profileRouter.get('/', async (req: Request, res: Response) => { */ profileRouter.get('/status', async (_req: Request, res: Response) => { try { - await access(DEFAULT_PROFILE_PATH); - res.json({ success: true, data: { exists: true, error: null } }); + const fileInfo = await stat(DEFAULT_PROFILE_PATH); + const exists = fileInfo.isFile() && fileInfo.size > 0; + res.json({ success: true, data: { exists, error: exists ? null : 'Resume file is empty' } }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.json({ success: true, data: { exists: false, error: message } }); @@ -57,13 +59,30 @@ profileRouter.post('/upload', async (req: Request, res: Response) => { throw new Error('Invalid profile payload. Expected a JSON object.'); } + const parsed = resumeDataSchema.safeParse(profile); + if (!parsed.success) { + const details = parsed.error.issues[0]?.message ?? 'Resume JSON does not match the RxResume schema.'; + throw new Error(`Invalid resume JSON: ${details}`); + } + + const existing = await stat(DEFAULT_PROFILE_PATH).catch(() => null); + if (existing && existing.isDirectory()) { + throw new Error('Resume path is a directory. Remove it and upload again.'); + } + await mkdir(dirname(DEFAULT_PROFILE_PATH), { recursive: true }); - await writeFile(DEFAULT_PROFILE_PATH, JSON.stringify(profile, null, 2), 'utf-8'); + await writeFile(DEFAULT_PROFILE_PATH, JSON.stringify(parsed.data, null, 2), 'utf-8'); clearProfileCache(); res.json({ success: true, data: { exists: true, error: null } }); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; + let message = error instanceof Error ? error.message : 'Unknown error'; + if (error && typeof error === 'object' && 'code' in error) { + const code = (error as { code?: string }).code; + if (code === 'EROFS') { + message = 'Resume path is read-only. Remove the bind mount and restart the container.'; + } + } res.status(400).json({ success: false, error: message }); } }); diff --git a/orchestrator/src/server/services/profile.ts b/orchestrator/src/server/services/profile.ts index f108658..b30e0c2 100644 --- a/orchestrator/src/server/services/profile.ts +++ b/orchestrator/src/server/services/profile.ts @@ -1,9 +1,9 @@ import { readFile } from 'fs/promises'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { join } from 'path'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -export const DEFAULT_PROFILE_PATH = process.env.RESUME_PROFILE_PATH || join(__dirname, '../../../../resume-generator/base.json'); +import { getDataDir } from '../config/dataDir.js'; + +export const DEFAULT_PROFILE_PATH = process.env.RESUME_PROFILE_PATH || join(getDataDir(), 'resume.json'); let cachedProfile: any = null; let cachedProfilePath: string | null = null; diff --git a/orchestrator/src/shared/rxresume-schema.ts b/orchestrator/src/shared/rxresume-schema.ts index b487960..e7d7f28 100644 --- a/orchestrator/src/shared/rxresume-schema.ts +++ b/orchestrator/src/shared/rxresume-schema.ts @@ -1,6 +1,7 @@ // combined types from: https://github.com/amruthpillai/reactive-resume/tree/v4.5.5/libs/schema/src import { z } from "zod"; +import { createId } from '@paralleldrive/cuid2'; // --- Shared --- @@ -10,6 +11,7 @@ export type FilterKeys = { export const idSchema = z .string() + .length(24) .cuid2() .describe("Unique identifier for the item (CUID2 format)");