mirror of
https://github.com/R0m1k3/Presencia.git
synced 2026-10-11 17:27:42 +02:00
Sécurité - JWT : un JWT_SECRET d'exemple ou trop court est ignoré au profit d'un secret aléatoire conservé en base (avant, la valeur publique de docker-compose.yml permettait de forger un jeton admin). - Le compte est relu en base à chaque requête : désactivation, changement de rôle et réinitialisation du mot de passe prennent effet immédiatement. - Connexion : 10 échecs max par e-mail / 15 min, temps constant que l'e-mail existe ou non, mot de passe admin retiré des logs. - Validation des entrées (ids, dates, mois, longueurs, e-mail) : 400 au lieu de 500. Un admin ne peut plus se désactiver ni se rétrograder. - Mots de passe : 8 caractères minimum ; changement en libre-service. - Export : nom de fichier conforme RFC 5987 (un nom de société avec « — » faisait planter l'export). - esc() échappe aussi les guillemets (injection d'attributs HTML). - En-têtes CSP / X-Frame-Options / nosniff (Nginx + API), API et Postgres publiés sur 127.0.0.1 seulement, image backend non-root via npm ci. Optimisations - Export : une requête pour tout le mois au lieu d'une par cadre. - Saisie groupée : un seul INSERT (unnest), doublons dédupliqués. - Dates renvoyées en chaînes (plus de décalage d'un jour selon le TZ). - Planning : chaque clic met à jour l'affichage localement au lieu de recharger le mois ; les réponses de mois périmées sont ignorées. - Recherche Utilisateurs / Plannings filtrée localement (plus une requête par touche) ; statuts des sociétés chargés en parallèle. UI/UX - Mobile : toutes les destinations dans la barre basse (Sociétés et Ma saisie étaient inaccessibles à l'admin) ; tableaux affichés en cartes (les boutons d'action étaient hors écran). - Boutons désactivés pendant l'envoi, focus et Échap dans les dialogues, retour à l'écran de connexion quand la session est révoquée, erreurs d'export affichées au lieu d'un fichier JSON téléchargé, actions destructives signalées, confirmation avant désactivation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5Bdayziw6tybgqETZoNSt
81 lines
2.5 KiB
JavaScript
81 lines
2.5 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const bcrypt = require('bcryptjs');
|
|
const db = require('./db');
|
|
const { initJwtSecret } = require('./middleware/auth');
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function waitForDb(retries = 30, delayMs = 2000) {
|
|
let lastErr = null;
|
|
for (let i = 0; i < retries; i++) {
|
|
try {
|
|
await db.query('SELECT 1');
|
|
return;
|
|
} catch (err) {
|
|
lastErr = err;
|
|
console.log(`En attente de la base de données... (${i + 1}/${retries}) — ${err.message}`);
|
|
await sleep(delayMs);
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Impossible de se connecter à la base de données : ${lastErr ? lastErr.message : 'raison inconnue'}`
|
|
);
|
|
}
|
|
|
|
async function runMigrations() {
|
|
const { rows } = await db.query(
|
|
`SELECT EXISTS (
|
|
SELECT FROM information_schema.tables WHERE table_name = 'companies'
|
|
) AS exists`
|
|
);
|
|
if (rows[0].exists) {
|
|
console.log('Schéma déjà initialisé.');
|
|
} else {
|
|
console.log('Initialisation du schéma de base de données...');
|
|
await db.query(readMigration('001_init.sql'));
|
|
console.log('Schéma créé.');
|
|
}
|
|
// Idempotent, so safe on every start: brings existing databases up to date.
|
|
await db.query(readMigration('002_security.sql'));
|
|
}
|
|
|
|
function readMigration(name) {
|
|
return fs.readFileSync(path.join(__dirname, '..', 'migrations', name), 'utf8');
|
|
}
|
|
|
|
async function seedAdmin() {
|
|
const { rows } = await db.query(
|
|
"SELECT id FROM users WHERE role = 'admin' LIMIT 1"
|
|
);
|
|
if (rows.length > 0) return;
|
|
|
|
const email = process.env.ADMIN_EMAIL || 'admin@presencia.local';
|
|
const password = process.env.ADMIN_PASSWORD || 'ChangeMe123!';
|
|
const fullName = process.env.ADMIN_NAME || 'Administrateur';
|
|
|
|
const hash = await bcrypt.hash(password, 10);
|
|
await db.query(
|
|
`INSERT INTO users (full_name, email, password_hash, role, company_id, active)
|
|
VALUES ($1, $2, $3, 'admin', NULL, true)`,
|
|
[fullName, email, hash]
|
|
);
|
|
console.log('========================================================');
|
|
console.log(' Compte administrateur créé :');
|
|
console.log(` Email : ${email}`);
|
|
console.log(' Mot de passe : celui de ADMIN_PASSWORD');
|
|
console.log(' Merci de le changer après la première connexion.');
|
|
console.log('========================================================');
|
|
}
|
|
|
|
async function bootstrap() {
|
|
await waitForDb();
|
|
await runMigrations();
|
|
await initJwtSecret();
|
|
await seedAdmin();
|
|
}
|
|
|
|
module.exports = bootstrap;
|