120 lines
5.7 KiB
JavaScript
120 lines
5.7 KiB
JavaScript
'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;
|