xpeditis2.0/apps/backend/scripts/setup/admin-password.js
David 10b69f3b2b
All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m32s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m20s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Unit Tests (push) Successful in 10m45s
Dev CI / Notify Failure (push) Has been skipped
CD Preprod / Backend — Lint (push) Successful in 10m24s
CD Preprod / Frontend — Lint & Type-check (push) Successful in 11m18s
CD Preprod / Backend — Unit Tests (push) Successful in 10m16s
CD Preprod / Frontend — Unit Tests (push) Successful in 10m45s
CD Preprod / Backend — Integration Tests (push) Successful in 9m56s
CD Preprod / Build Frontend (push) Successful in 56s
CD Preprod / Build Log Exporter (push) Successful in 32s
CD Preprod / Build Backend (push) Successful in 6m48s
CD Preprod / Deploy to Preprod (push) Successful in 23s
CD Preprod / Notify Failure (push) Has been skipped
CD Preprod / Notify Success (push) Successful in 2s
fix admin monter
2026-09-15 15:21:05 +02:00

168 lines
5.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <conteneur-backend> 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 <email-administrateur>');
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);
});