133 lines
3.7 KiB
JavaScript
133 lines
3.7 KiB
JavaScript
/**
|
|
* 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}`);
|
|
});
|