'use strict'; const express = require('express'); const router = express.Router(); const path = require('path'); const fs = require('fs'); const multer = require('multer'); const { verifyToken, hasStepPerm } = require('../middleware/auth'); const store = () => require('../services/projectStore'); // ── file upload (multer) ────────────────────────────────────────────────────── // uploads/ is resolved from the CWD (the app root — every start script cd's // there first), NOT __dirname: after esbuild bundling __dirname becomes the // bundle's own folder, which would break the ../uploads hop. const UPLOADS_ROOT = path.resolve('uploads'); const storage = multer.diskStorage({ destination(req, file, cb) { const dir = path.join(UPLOADS_ROOT, 'projects', String(req.params.id)); fs.mkdirSync(dir, { recursive: true }); cb(null, dir); }, filename(req, file, cb) { const safe = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_'); cb(null, Date.now() + '-' + safe); }, }); const upload = multer({ storage, limits: { fileSize: 20 * 1024 * 1024 } }); // ── next code preview ───────────────────────────────────────────────────────── router.get('/next-code', verifyToken, async (req, res) => { try { res.json({ success: true, code: await store().nextCode() }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ── CRUD ────────────────────────────────────────────────────────────────────── router.get('/', verifyToken, async (req, res) => { try { res.json({ success: true, data: await store().list(req.query) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.post('/', verifyToken, async (req, res) => { try { if (!req.body.project_name) return res.status(400).json({ success: false, message: 'Project name is required' }); const result = await store().create({ ...req.body, submitted_by: req.user.username }); res.json({ success: true, ...result }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.get('/:id', verifyToken, async (req, res) => { try { const p = await store().findById(req.params.id); if (!p) return res.status(404).json({ success: false, message: 'Not found' }); res.json({ success: true, data: p }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.patch('/:id', verifyToken, async (req, res) => { try { await store().update(req.params.id, req.body); res.json({ success: true, message: 'Updated' }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.post('/:id/approve', verifyToken, async (req, res) => { try { const step = req.body?.step; if (!step || !hasStepPerm(req.user, `project:${step}`, 'approve')) return res.status(403).json({ success: false, message: `You are not assigned "approve" on approval step: project:${step || '?'}` }); await store().stepApprove(req.params.id, req.body); res.json({ success: true }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ── file upload ─────────────────────────────────────────────────────────────── router.post('/:id/upload', verifyToken, upload.array('files', 10), async (req, res) => { try { const p = await store().findById(req.params.id); if (!p) return res.status(404).json({ success: false, message: 'Project not found' }); const existing = Array.isArray(p.attachments) ? p.attachments : []; const newFiles = (req.files || []).map(f => ({ name: f.filename, originalName: f.originalname, size: f.size, url: `/uploads/projects/${req.params.id}/${f.filename}`, })); await store().update(req.params.id, { attachments: [...existing, ...newFiles] }); res.json({ success: true, files: newFiles }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.delete('/:id/attachments/:filename', verifyToken, async (req, res) => { try { const p = await store().findById(req.params.id); if (!p) return res.status(404).json({ success: false, message: 'Project not found' }); const existing = Array.isArray(p.attachments) ? p.attachments : []; const updated = existing.filter(f => f.name !== req.params.filename); await store().update(req.params.id, { attachments: updated }); // delete file from disk const filePath = path.join(UPLOADS_ROOT, 'projects', String(req.params.id), req.params.filename); fs.unlink(filePath, () => {}); res.json({ success: true }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.delete('/:id', verifyToken, async (req, res) => { try { const p = await store().findById(req.params.id); if (!p) return res.status(404).json({ success: false, message: 'Not found' }); if (p.status !== 'DRAFT' && req.user.role !== 'admin') return res.status(403).json({ success: false, message: 'Only DRAFT projects can be deleted' }); await store().remove(req.params.id); res.json({ success: true }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); module.exports = router;