Files
proxy_tmdb/routes/admin.js

151 lines
4.5 KiB
JavaScript

// Admin — dashboard, stats API, metrics UI, and the legacy file listing.
//
// All protected by session cookie (argon2id password). The login form is
// rendered inline; everything else is served as static HTML from public/admin/
// once authenticated.
import { readdir, readFile, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { ADMIN_PASSWORD_HASH, ROOT, TITLE } from '../config.js';
import { verifyPassword } from '../lib/password.js';
import { getDiskUsage, getMetricsForUI, getStats } from '../lib/stats.js';
const HIDDEN = new Set([
'node_modules',
'package.json',
'package-lock.json',
'config.js',
'server.js',
'lib',
'routes',
'cron',
'tools',
'test',
'.git',
'.claude',
'.specstory',
'biome.json',
'public',
'.env',
'.env.example',
'.gitignore',
'README.md',
'.cron.lock',
]);
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function loginPage(error = '') {
return `<!doctype html>
<html lang="fr"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin · ${esc(TITLE)}</title>
<link rel="stylesheet" href="/style.css">
<style>
body{display:flex;align-items:center;justify-content:center;min-height:100vh}
.card{background:var(--bg-3);padding:24px;border-radius:12px;width:100%;max-width:360px;box-shadow:0 10px 30px rgba(0,0,0,.35)}
h1{font-size:18px;margin:0 0 16px;color:#f9fafb}
label{display:block;font-size:14px;margin-bottom:8px;color:#cbd5e1}
input[type=password]{width:100%;padding:10px 12px;border:1px solid #334155;border-radius:8px;background:#0f172a;color:#e5e7eb;outline:none}
input[type=password]:focus{border-color:var(--accent-2)}
.btn-login{margin-top:12px;width:100%;padding:10px 12px;border:0;border-radius:8px;background:#2563eb;color:white;font-weight:600;cursor:pointer}
.err{color:var(--danger);font-size:14px;margin-top:10px;min-height:18px}
</style></head>
<body>
<form class="card" method="post" autocomplete="off">
<h1>Admin · ${esc(TITLE)}</h1>
<label for="pw">Mot de passe</label>
<input id="pw" name="password" type="password" required autofocus>
<button class="btn-login" type="submit">Entrer</button>
<div class="err">${esc(error)}</div>
</form>
</body></html>`;
}
async function renderFilesList() {
const names = await readdir(ROOT);
const entries = [];
for (const name of names) {
if (HIDDEN.has(name)) continue;
if (name.startsWith('.')) continue;
let st;
try {
st = await stat(join(ROOT, name));
} catch {
continue;
}
entries.push({
name,
isDir: st.isDirectory(),
size: st.isFile() ? st.size : 0,
mtime: st.mtimeMs,
});
}
entries.sort((a, b) => {
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
return entries;
}
export default async function adminRoutes(fastify) {
// ---- Login / logout / dashboard shell ----
fastify.get('/admin', async (req, reply) => {
reply.header('Content-Type', 'text/html; charset=utf-8');
if (req.query?.logout != null) {
req.session.delete();
return reply.redirect('/admin');
}
if (!req.session.get('auth_ok')) return loginPage();
// Serve the dashboard SPA
return reply.send(await readFile(join(ROOT, 'public', 'admin.html')));
});
fastify.post('/admin', async (req, reply) => {
reply.header('Content-Type', 'text/html; charset=utf-8');
const ok = await verifyPassword(ADMIN_PASSWORD_HASH, req.body?.password ?? '');
if (ok) {
req.session.set('auth_ok', true);
return reply.redirect('/admin');
}
return loginPage('Mot de passe incorrect.');
});
// ---- JSON APIs (auth required) ----
function requireAuth(req, reply) {
if (!req.session.get('auth_ok')) {
reply.code(401).send({ error: 'auth required' });
return false;
}
return true;
}
fastify.get('/admin/api/stats', async (req, reply) => {
if (!requireAuth(req, reply)) return;
return getStats();
});
fastify.get('/admin/api/disk', async (req, reply) => {
if (!requireAuth(req, reply)) return;
return getDiskUsage();
});
fastify.get('/admin/api/metrics', async (req, reply) => {
if (!requireAuth(req, reply)) return;
return getMetricsForUI();
});
fastify.get('/admin/api/files', async (req, reply) => {
if (!requireAuth(req, reply)) return;
return { entries: await renderFilesList() };
});
}