Add admin config screen (issue #14)
Deploy to Dev / Deploy & Smoke Test (push) Successful in 23s
Deploy to Dev / Deploy & Smoke Test (push) Successful in 23s
Lets the photographer edit config/site.json from /dashboard instead of SSHing in and hand-editing the file. Two tiers: - Quick-edit form for the fields actually touched day to day (site title/tagline/hero, photographer bio, contact/social, Venmo username, session pricing, theme colors) - Raw JSON textarea for everything else (portfolio categories, locations, session types, Immich settings) - the form's fields are a subset of this, not a separate source of truth Backend: GET/PUT /api/admin/config, gated by the existing requireAdmin middleware (OIDC admin session or legacy ADMIN_SECRET). PUT validates the body is an object with the required top-level sections, writes atomically (temp file + rename), and keeps one prior version as site.json.bak before overwriting. Required a docker-compose.yml change: the api service had no volume mount for config/ at all before this (only portfolio/nginx did, and read-only) - added a read-write mount so the API can actually write the file the live site reads.
This commit is contained in:
@@ -12,6 +12,8 @@ const PORT = process.env.PORT || 3001;
|
||||
|
||||
const CONTRACTS_DIR = path.join(__dirname, 'contracts');
|
||||
const SIGNED_DIR = path.join(__dirname, 'signed-contracts');
|
||||
const CONFIG_PATH = path.join(__dirname, 'config', 'site.json');
|
||||
const CONFIG_BACKUP_PATH = path.join(__dirname, 'config', 'site.json.bak');
|
||||
|
||||
app.use(cors({ origin: process.env.CORS_ORIGIN || '*', credentials: true }));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
@@ -374,4 +376,39 @@ app.get('/api/admin/payments/export', requireAdmin, (req, res) => {
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
// Site config (issue #14) — lets the photographer edit config/site.json from
|
||||
// the browser instead of SSHing in. Requires the api service to have a
|
||||
// read-write mount for ./config (see docker-compose.yml); the portfolio/nginx
|
||||
// service's own mount of the same host directory is read-only.
|
||||
app.get('/api/admin/config', requireAdmin, (req, res) => {
|
||||
try {
|
||||
res.type('application/json').send(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: `Could not read config: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/admin/config', requireAdmin, (req, res) => {
|
||||
const next = req.body;
|
||||
if (!next || typeof next !== 'object' || Array.isArray(next)) {
|
||||
return res.status(400).json({ error: 'Body must be a JSON object' });
|
||||
}
|
||||
for (const key of ['site', 'photographer', 'contact']) {
|
||||
if (!next[key] || typeof next[key] !== 'object') {
|
||||
return res.status(400).json({ error: `Missing or invalid required section: "${key}"` });
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Keep one prior version so a bad save can be undone by hand - not a
|
||||
// full history, just a safety net against fat-fingering the editor.
|
||||
if (fs.existsSync(CONFIG_PATH)) fs.copyFileSync(CONFIG_PATH, CONFIG_BACKUP_PATH);
|
||||
const tmpPath = `${CONFIG_PATH}.tmp`;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(next, null, 2) + '\n');
|
||||
fs.renameSync(tmpPath, CONFIG_PATH);
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: `Could not write config: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`API listening on port ${PORT}`));
|
||||
|
||||
Reference in New Issue
Block a user