111 lines
3.3 KiB
JavaScript
111 lines
3.3 KiB
JavaScript
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;
|
|
}
|