/** * Biohazard VFX Micro-CMS API * * Installation: * npm i express multer dotenv * * Usage: * PORT=4000 node admin/api.js * # or use PM2 for process management * * Reverse Proxy: * Proxy /api/* to http://localhost:4000 via Nginx Proxy Manager. * * Security: * Protect /admin/* and /api/* with basic-auth or Cloudflare Zero-Trust. * * SMB Share: * Ensure new files are world-readable (0644) for Nginx. */ const express = require('express'); const multer = require('multer'); const fs = require('fs'); const fsp = require('fs/promises'); const path = require('path'); const dotenv = require('dotenv'); dotenv.config(); const API_SECRET = process.env.API_SECRET; const PORT = process.env.PORT || 4000; const PROJECTS_JSON = path.resolve(__dirname, '../projects/projects.json'); const PROJECTS_DIR = path.resolve(__dirname, '../projects'); const TMP_DIR = path.resolve(__dirname, 'tmp'); if (!API_SECRET) { console.error('Missing API_SECRET in environment.'); process.exit(1); } if (!fs.existsSync(TMP_DIR)) fs.mkdirSync(TMP_DIR, { recursive: true }); const app = express(); app.use(express.json()); const upload = multer({ dest: TMP_DIR }); function slugify(str) { return str .toLowerCase() .replace(/[^a-z0-9]+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 30); } function safeExt(filename) { const ext = path.extname(filename).toLowerCase(); return ext.match(/\.(jpg|jpeg|png|webp|gif)$/) ? ext : '.jpg'; } function requireApiKey(req, res, next) { if (req.headers['x-api-key'] !== API_SECRET) { return res.status(401).json({ ok: false, error: 'Unauthorized' }); } next(); } app.get('/api/projects', async (req, res) => { try { const data = await fsp.readFile(PROJECTS_JSON, 'utf8'); res.type('json').send(data); } catch (e) { res.status(500).json({ ok: false, error: 'Cannot read manifest.' }); } }); app.post('/api/projects', requireApiKey, upload.single('thumb'), async (req, res) => { try { const { title, size = 'small', embed, credits = '', info = '' } = req.body; const file = req.file; if (!title || !embed || !file) { if (file) fs.unlinkSync(file.path); return res.status(400).json({ ok: false, error: 'Missing required fields.' }); } // Load manifest let manifest = []; try { manifest = JSON.parse(await fsp.readFile(PROJECTS_JSON, 'utf8')); } catch (e) {} const index = String(manifest.length + 1).padStart(2, '0'); const slug = slugify(title); const folderName = `${index}_${slug}`; const folderPath = path.join(PROJECTS_DIR, folderName); await fsp.mkdir(folderPath, { recursive: true }); // Move thumbnail const ext = safeExt(file.originalname); const thumbName = `thumbnail${ext}`; const thumbDest = path.join(folderPath, thumbName); await fsp.rename(file.path, thumbDest); await fsp.chmod(thumbDest, 0o644); // Write info.txt and credits.txt with provided content await fsp.writeFile(path.join(folderPath, 'info.txt'), info, { mode: 0o644 }); await fsp.writeFile(path.join(folderPath, 'credits.txt'), credits, { mode: 0o644 }); // Build new project object const newObj = { id: folderName, title, thumbnail: `projects/${folderName}/${thumbName}`, size, embed, credits: `projects/${folderName}/credits.txt`, info: `projects/${folderName}/info.txt` }; manifest.push(newObj); await fsp.writeFile(PROJECTS_JSON, JSON.stringify(manifest, null, 2), { mode: 0o644 }); res.json({ ok: true }); } catch (err) { if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path); res.status(500).json({ ok: false, error: 'Server error.' }); } }); app.listen(PORT, () => { console.log(`Biohazard VFX micro-CMS running on port ${PORT}`); });