#!/usr/bin/env node /** * Génère un hash Argon2id pour BOOTSTRAP_ADMIN_PASSWORD_HASH. * * cd apps/backend && node scripts/setup/generate-admin-hash.js * * Le mot de passe est saisi sans écho et ne quitte jamais votre poste : ni * argument de ligne de commande (visible dans `ps` et dans l'historique du * shell), ni variable d'environnement, ni fichier temporaire. * * RAPPEL — le mode SANS mot de passe est préférable. * Si votre chaîne SMTP fonctionne, ne renseignez que BOOTSTRAP_ADMIN_EMAIL : * le compte est alors créé sans mot de passe utilisable et vous le définissez * via « mot de passe oublié ». Aucun secret n'existe nulle part, il n'y a donc * rien à faire fuiter. Ce script n'est utile que si vous devez pouvoir vous * connecter avant que l'envoi de courriels ne soit opérationnel. */ 'use strict'; const argon2 = require('argon2'); const readline = require('readline'); // Mêmes paramètres que auth.service.ts : un hash produit ici est vérifiable // par l'application sans aucune adaptation. const ARGON2_OPTIONS = { type: argon2.argon2id, memoryCost: 65536, // 64 Mo timeCost: 3, parallelism: 4, }; const MIN_LENGTH = 16; /** Saisie masquée sur un terminal ; lecture directe si l'entrée est redirigée. */ function readSecret(prompt) { return new Promise((resolve, reject) => { if (!process.stdin.isTTY) { let data = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => (data += chunk)); process.stdin.on('end', () => resolve(data.replace(/\r?\n$/, ''))); process.stdin.on('error', reject); return; } const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const onKeypress = () => { // Réécrit la ligne sans révéler la longueur de la saisie. readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); process.stdout.write(prompt); }; process.stdout.write(prompt); process.stdin.on('data', onKeypress); rl.question('', answer => { process.stdin.removeListener('data', onKeypress); rl.close(); process.stdout.write('\n'); resolve(answer); }); }); } function checkStrength(password) { const problems = []; if (password.length < MIN_LENGTH) { problems.push(`au moins ${MIN_LENGTH} caractères (${password.length} fournis)`); } if (!/[a-z]/.test(password)) problems.push('une minuscule'); if (!/[A-Z]/.test(password)) problems.push('une majuscule'); if (!/[0-9]/.test(password)) problems.push('un chiffre'); if (!/[^A-Za-z0-9]/.test(password)) problems.push('un caractère spécial'); return problems; } async function main() { console.log(''); console.log('Génération du hash Argon2id pour le premier administrateur.'); console.log('La saisie n’est pas affichée.'); console.log(''); const password = await readSecret('Mot de passe : '); if (!password) { console.error('Aucun mot de passe saisi.'); process.exit(1); } if (process.stdin.isTTY) { const confirmation = await readSecret('Confirmation : '); if (confirmation !== password) { console.error('Les deux saisies diffèrent.'); process.exit(1); } } const problems = checkStrength(password); if (problems.length > 0) { console.error(''); console.error('Mot de passe refusé. Il manque : ' + problems.join(', ') + '.'); console.error('Ce compte a tous les droits sur la plateforme : générez plutôt une'); console.error('phrase longue et aléatoire depuis votre gestionnaire de mots de passe.'); process.exit(1); } const hash = await argon2.hash(password, ARGON2_OPTIONS); console.log(''); console.log('Hash à placer dans le Secret Kubernetes (jamais dans le ConfigMap) :'); console.log(''); console.log(' BOOTSTRAP_ADMIN_PASSWORD_HASH: ' + JSON.stringify(hash)); console.log(''); console.log(' cd infra/prod && sops k8s/base/03-secrets.sops.yaml'); console.log(''); console.log('Après votre première connexion :'); console.log(' 1. changez le mot de passe depuis l’interface ;'); console.log(' 2. retirez BOOTSTRAP_ADMIN_PASSWORD_HASH du Secret et réappliquez.'); console.log(''); console.log('Un hash reste attaquable hors ligne : il n’a plus aucune raison'); console.log('de rester stocké une fois le compte opérationnel.'); console.log(''); } main().catch(error => { console.error('Échec :', error.message); process.exit(1); });