// Login + protected directory listing — replaces index.php. import { readdir, stat } from 'node:fs/promises'; import { join } from 'node:path'; import { ROOT, TITLE, PASSWORD } from '../config.js'; const HIDDEN = new Set(['index.php', '.htaccess', 'node_modules', 'package.json', 'package-lock.json', 'config.js', 'server.js', 'lib', 'routes', 'cron', 'test', '.git']); function esc(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatBytes(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let i = bytes > 0 ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0; if (i >= units.length) i = units.length - 1; return `${(bytes / Math.pow(1024, i)) || 0} ${units[i]}`; } function pad(n) { return n < 10 ? `0${n}` : String(n); } function fmtDate(ms) { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } function loginPage(error = '') { return ` ${esc(TITLE)}

${esc(TITLE)}

${esc(error)}
`; } async function listingPage() { 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()); }); const rows = entries.map((e) => { const href = encodeURIComponent(e.name); return ` ${esc(e.name)}${e.isDir ? 'dossier' : ''} ${e.isDir ? '—' : esc(formatBytes(e.size))} ${esc(fmtDate(e.mtime))} `; }).join(''); return ` ${esc(TITLE)}

${esc(TITLE)}

Se déconnecter
${rows}
NomTailleModifié
`; } export default async function indexRoutes(fastify) { fastify.get('/', async (req, reply) => { reply.header('Content-Type', 'text/html; charset=utf-8'); if (req.query?.logout != null) { req.session.delete(); return reply.redirect('/'); } if (!req.session.get('auth_ok')) { return loginPage(); } return listingPage(); }); fastify.post('/', async (req, reply) => { reply.header('Content-Type', 'text/html; charset=utf-8'); const submitted = req.body?.password ?? ''; if (timingSafeEqual(submitted, PASSWORD)) { req.session.set('auth_ok', true); return reply.redirect('/'); } return loginPage('Mot de passe incorrect.'); }); } function timingSafeEqual(a, b) { if (typeof a !== 'string' || typeof b !== 'string') return false; if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); return diff === 0; }