#!/usr/bin/env node /** * Commande de secours : définit le mot de passe d'un administrateur, sans SMTP. * * Dans le conteneur backend (le script est copié dans l'image) : * docker exec -it node admin-password.js admin@xpeditis.com * En local : * cd apps/backend && node scripts/setup/admin-password.js admin@xpeditis.com * * Le compte est créé s'il n'existe pas, sinon promu ADMIN et réactivé, puis son * mot de passe est remplacé. Le mot de passe est saisi sans écho : ni argument * de ligne de commande (visible dans `ps` et l'historique du shell), ni * variable d'environnement. Il peut aussi être passé sur l'entrée standard * pour une exécution scriptée. * * Connexion à la base : variables DATABASE_* déjà présentes dans le conteneur. */ 'use strict'; const readline = require('readline'); const argon2 = require('argon2'); const { Client } = require('pg'); // Mêmes paramètres que auth.service.ts. const ARGON2_OPTIONS = { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 4 }; const MIN_LENGTH = 12; const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; /** 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 = () => { 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 strengthProblems(password) { const problems = []; if (password.length < MIN_LENGTH) problems.push(`au moins ${MIN_LENGTH} caractères`); 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'); return problems; } function env(name, fallback) { const value = (process.env[name] || '').trim(); return value || fallback; } async function main() { const email = (process.argv[2] || '').trim().toLowerCase(); if (!EMAIL_PATTERN.test(email)) { console.error('Usage : node admin-password.js '); process.exit(1); } const password = await readSecret(`Nouveau mot de passe pour ${email} : `); if (process.stdin.isTTY) { const confirmation = await readSecret('Confirmation : '); if (confirmation !== password) { console.error('Les deux saisies diffèrent.'); process.exit(1); } } const problems = strengthProblems(password); if (problems.length > 0) { console.error(`Mot de passe refusé. Il manque : ${problems.join(', ')}.`); process.exit(1); } const passwordHash = await argon2.hash(password, ARGON2_OPTIONS); const client = new Client({ host: env('DATABASE_HOST', 'localhost'), port: Number(env('DATABASE_PORT', '5432')), user: env('DATABASE_USER', 'xpeditis'), password: process.env.DATABASE_PASSWORD, database: env('DATABASE_NAME', 'xpeditis_dev'), // Même règle que data-source.ts : en production, pg_hba n'accepte que SSL. ssl: process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false, }); await client.connect(); try { await client.query('BEGIN'); const existing = await client.query('SELECT "id" FROM "users" WHERE "email" = $1', [email]); if (existing.rows.length > 0) { // Réactiver ou promouvoir n'est jamais bloqué par le déclencheur // « au moins un administrateur actif ». await client.query( `UPDATE "users" SET "password_hash" = $2, "role" = 'ADMIN', "is_active" = true, "updated_at" = NOW() WHERE "id" = $1`, [existing.rows[0].id, passwordHash] ); console.log(`Compte ${email} : mot de passe défini, rôle ADMIN, compte actif.`); } else { const organization = await client.query( `INSERT INTO "organizations" ("name", "type", "address_street", "address_city", "address_postal_code", "address_country") VALUES ($1, 'FREIGHT_FORWARDER', $2, $3, $4, $5) ON CONFLICT ("name") DO UPDATE SET "updated_at" = NOW() RETURNING "id"`, [ env('BOOTSTRAP_ADMIN_ORG_NAME', 'Xpeditis'), env('BOOTSTRAP_ADMIN_ORG_STREET', 'A completer'), env('BOOTSTRAP_ADMIN_ORG_CITY', 'A completer'), env('BOOTSTRAP_ADMIN_ORG_POSTAL_CODE', '00000'), env('BOOTSTRAP_ADMIN_ORG_COUNTRY', 'FR').toUpperCase(), ] ); await client.query( `INSERT INTO "users" ("organization_id", "email", "password_hash", "role", "first_name", "last_name", "is_email_verified", "is_active") VALUES ($1, $2, $3, 'ADMIN', $4, $5, true, true)`, [ organization.rows[0].id, email, passwordHash, env('BOOTSTRAP_ADMIN_FIRST_NAME', 'Admin'), env('BOOTSTRAP_ADMIN_LAST_NAME', 'Xpeditis'), ] ); console.log(`Administrateur ${email} créé et actif.`); } await client.query('COMMIT'); console.log('Vous pouvez vous connecter. Changez ce mot de passe depuis l’interface si besoin.'); } catch (error) { await client.query('ROLLBACK').catch(() => undefined); throw error; } finally { await client.end(); } } main().catch(error => { console.error('Échec :', error.message); process.exit(1); });