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
168 lines
5.9 KiB
JavaScript
168 lines
5.9 KiB
JavaScript
#!/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);
|
||
});
|