import { argon2id, hash } from 'argon2'; import { randomBytes, randomUUID } from 'node:crypto'; export const defaultUsers = [ { username: 'Aewyn', role: 'user', passwordEnvironmentKey: 'DEFAULT_ADMIN_PASSWORD' }, { username: 'Quartermaster', role: 'user', passwordEnvironmentKey: 'DEFAULT_USER_PASSWORD' } ]; function temporaryPassword() { // 120 bits of entropy encoded without punctuation that is awkward to paste. return randomBytes(15).toString('base64url'); } async function secureHash(password) { return hash(password, { type: argon2id, memoryCost: 19_456, timeCost: 2, parallelism: 1 }); } /** * Creates missing default users. With resetPasswords enabled, existing default * users keep their IDs and related records but receive new password hashes. */ export async function initializeDefaultUsers(database, options = {}) { const resetPasswords = options.resetPasswords === true; const findUser = await database.prepare( 'SELECT id, username, role FROM users WHERE login_username = ? COLLATE NOCASE OR username = ? COLLATE NOCASE' ); const insertUser = await database.prepare( 'INSERT INTO users (id, username, login_username, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?)' ); const updateCredential = await database.prepare( 'UPDATE users SET login_username = ?, password_hash = ?, role = ? WHERE id = ?' ); const credentials = []; for (const definition of defaultUsers) { const existing = await findUser.get(definition.username, definition.username); if (existing && !resetPasswords) { console.log(`${definition.username}: already exists (${existing.role}); left unchanged`); continue; } const configuredPassword = process.env[definition.passwordEnvironmentKey]?.trim(); const password = configuredPassword || temporaryPassword(); if (password.length < 12 || password.length > 128) { throw new Error(`${definition.passwordEnvironmentKey} must be between 12 and 128 characters.`); } const passwordHash = await secureHash(password); if (existing) { await updateCredential.run(definition.username, passwordHash, String(existing.role), existing.id); console.log(`${definition.username}: credentials reset; role and related data preserved`); } else { await insertUser.run( randomUUID(), definition.username, definition.username, passwordHash, definition.role, new Date().toISOString() ); console.log(`${definition.username}: default account created`); } credentials.push({ username: definition.username, role: existing ? String(existing.role) : definition.role, password, generated: !configuredPassword }); } if (credentials.length) { console.log('\nDefault user credentials (shown once):'); for (const credential of credentials) { console.log(`${credential.username} (${credential.role}): ${credential.password}${credential.generated ? ' [generated]' : ' [from environment]'}`); } console.log('Change generated passwords after the first sign-in.\n'); } return credentials; }