import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto'; import type { PrismaClient } from '@prisma/client'; import { SYSTEM_ROLES } from '@sport/types'; /** * Password hashing for scripts. * * Deliberately duplicated from `common/security/password.service.ts` rather * than imported: these scripts run under `tsx` outside the Nest container, and * booting the DI graph to hash one string would be the more fragile choice. * * The hash FORMAT is the contract between the two, and it is self-describing — * so a drift shows up as a failed login on the very next attempt, not as silent * corruption. If a third caller ever appears, extract it to a package. */ const PARAMS = { N: 16_384, r: 8, p: 1 } as const; const KEY_LENGTH = 64; const MAX_MEM = 64 * 1024 * 1024; export function hashPassword(plaintext: string): Promise { const salt = randomBytes(16); return new Promise((resolve, reject) => { scrypt( plaintext.normalize('NFKC'), salt, KEY_LENGTH, { ...PARAMS, maxmem: MAX_MEM }, (error, derived) => { if (error) return reject(error); resolve( [ 'scrypt', PARAMS.N, PARAMS.r, PARAMS.p, salt.toString('base64'), derived.toString('base64'), ].join('$'), ); }, ); }); } /** Used by the smoke test below to prove the format round-trips. */ export function verifyPassword(plaintext: string, stored: string): Promise { const parts = stored.split('$'); if (parts.length !== 6 || parts[0] !== 'scrypt') return Promise.resolve(false); const [, n, r, p, salt, hash] = parts; return new Promise((resolve) => { scrypt( plaintext.normalize('NFKC'), Buffer.from(salt ?? '', 'base64'), KEY_LENGTH, { N: Number(n), r: Number(r), p: Number(p), maxmem: MAX_MEM }, (error, derived) => { if (error) return resolve(false); const expected = Buffer.from(hash ?? '', 'base64'); resolve(derived.length === expected.length && timingSafeEqual(derived, expected)); }, ); }); } /** A readable, high-entropy password for generated accounts. */ export function generatePassword(): string { // Base64url of 18 bytes ≈ 24 characters, ~144 bits. Suffixed to guarantee the // policy's uppercase/lowercase/digit requirements regardless of the draw. return `${randomBytes(18).toString('base64url')}aA1`; } export interface SeededAccount { email: string; password: string; role: string; created: boolean; } /** * Development sign-in accounts. * * Guarded twice — by NODE_ENV and by an explicit opt-out — because a known * password reaching production is the single worst thing a seed can do. The * generated password is printed once and never stored anywhere else. * * Existing accounts are left completely alone: re-running the seed must not * reset a password someone has already changed. */ export async function seedDevAccounts(prisma: PrismaClient): Promise { const accounts: SeededAccount[] = []; const definitions = [ { email: 'admin@sport.local', type: 'SUPER_ADMIN' as const, firstName: 'Demo', lastName: 'Admin', roleKey: SYSTEM_ROLES.SUPER_ADMIN, }, { email: 'staff@sport.local', type: 'STAFF' as const, firstName: 'Demo', lastName: 'Staff', roleKey: SYSTEM_ROLES.CATALOG_MANAGER, }, { email: 'customer@sport.local', type: 'CUSTOMER' as const, firstName: 'Demo', lastName: 'Customer', roleKey: SYSTEM_ROLES.CUSTOMER, }, ]; for (const definition of definitions) { const existing = await prisma.user.findUnique({ where: { email: definition.email } }); if (existing) { accounts.push({ email: definition.email, password: '(unchanged)', role: definition.roleKey, created: false, }); continue; } const password = generatePassword(); const role = await prisma.role.findUnique({ where: { key: definition.roleKey } }); const user = await prisma.user.create({ data: { email: definition.email, passwordHash: await hashPassword(password), type: definition.type, status: 'ACTIVE', firstName: definition.firstName, lastName: definition.lastName, emailVerifiedAt: new Date(), ...(role ? { roles: { create: { roleId: role.id } } } : {}), }, select: { id: true }, }); // A customer account needs its shopper profile, or /account has nothing to // hang addresses and orders off later. if (definition.type === 'CUSTOMER') { await prisma.customer.create({ data: { userId: user.id } }); } accounts.push({ email: definition.email, password, role: definition.roleKey, created: true, }); } return accounts; }