// 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)}
`;
}
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)}
`;
}
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;
}