Files
shd.build/scripts/admin-bootstrap.mjs
T

205 lines
6.5 KiB
JavaScript

import { connect } from '@tursodatabase/database';
import { randomUUID } from 'node:crypto';
import { resolve } from 'node:path';
import process from 'node:process';
const usage = 'Usage: npm run admin:bootstrap -- <verified-email>';
const suppliedEmails = process.argv.slice(2);
if (suppliedEmails.length !== 1) throw new Error(usage);
const email = suppliedEmails[0]
.normalize('NFKC')
.trim()
.toLocaleLowerCase('en-US');
if (
email.length < 3 ||
email.length > 320 ||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(email) ||
/[\r\n]/u.test(email)
) {
throw new Error('Provide one valid verified email address.');
}
const databasePath = resolve(
process.env.TDR_DATABASE_PATH || './data/shd-build-development.db',
);
const database = await connect(databasePath);
let transactionStarted = false;
try {
const now = new Date().toISOString();
await database.exec('BEGIN IMMEDIATE');
transactionStarted = true;
const matchingAccounts = await (
await database.prepare(`
SELECT users.id
FROM users
JOIN user_emails ON user_emails.user_id=users.id
LEFT JOIN user_status ON user_status.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
WHERE user_emails.email=? COLLATE NOCASE
AND user_emails.verified_at IS NOT NULL
AND deleted_accounts.user_id IS NULL
AND user_status.blocked_at IS NULL
AND (user_status.banned_until IS NULL OR user_status.banned_until<=?)
`)
).all(email, now);
if (matchingAccounts.length !== 1)
throw new Error(
'The email must identify exactly one active verified account.',
);
const targetId = String(matchingAccounts[0].id);
const activeAdministrators = await (
await database.prepare(`
SELECT users.id
FROM users
LEFT JOIN user_status ON user_status.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
WHERE deleted_accounts.user_id IS NULL
AND user_status.blocked_at IS NULL
AND (user_status.banned_until IS NULL OR user_status.banned_until<=?)
AND EXISTS (
SELECT 1 FROM user_roles
WHERE user_roles.user_id=users.id AND user_roles.role_id='role-admin'
)
`)
).all(now);
const targetAlreadyAdministrator = activeAdministrators.some(
(administrator) => String(administrator.id) === targetId,
);
if (activeAdministrators.length > 0 && !targetAlreadyAdministrator)
throw new Error('An active full administrator already exists.');
if (!targetAlreadyAdministrator) {
const administratorRole = await (
await database.prepare(`
SELECT roles.id
FROM roles
JOIN role_permissions ON role_permissions.role_id=roles.id
JOIN permissions ON permissions.id=role_permissions.permission_id
WHERE roles.id='role-admin' AND permissions.name='system.admin'
LIMIT 1
`)
).get();
if (!administratorRole)
throw new Error('The canonical administrator role is unavailable.');
const activeFactor = await (
await database.prepare(
"SELECT id FROM mfa_totp_factors WHERE user_id=? AND state='active' LIMIT 1",
)
).get(targetId);
if (!activeFactor)
throw new Error('The target account must have an active TOTP factor.');
const unusedRecoveryCode = await (
await database.prepare(
'SELECT id FROM mfa_recovery_codes WHERE user_id=? AND consumed_at IS NULL AND revoked_at IS NULL LIMIT 1',
)
).get(targetId);
if (!unusedRecoveryCode)
throw new Error('The target account must have an unused recovery code.');
const before = {
roleIds: (
await (
await database.prepare(
'SELECT role_id FROM user_roles WHERE user_id=? ORDER BY role_id',
)
).all(targetId)
).map((row) => String(row.role_id)),
};
await (
await database.prepare(
"INSERT OR IGNORE INTO user_roles(user_id,role_id) VALUES(?,'role-admin')",
)
).run(targetId);
await (
await database.prepare(
"UPDATE users SET role='user' WHERE id=? AND role='admin'",
)
).run(targetId);
const targetSessions = await (
await database.prepare('SELECT id FROM sessions WHERE user_id=?')
).all(targetId);
const sessionIds = targetSessions.map((row) => String(row.id));
if (sessionIds.length) {
const placeholders = sessionIds.map(() => '?').join(',');
await (
await database.prepare(
`UPDATE login_events SET session_ended_at=?,session_end_reason=? WHERE session_id IN (${placeholders})`,
)
).run(now, 'administrator_bootstrap', ...sessionIds);
await (
await database.prepare(
`DELETE FROM sessions WHERE id IN (${placeholders})`,
)
).run(...sessionIds);
}
await (
await database.prepare(`
INSERT INTO audit_log(
id,actor_id,entity_type,entity_id,action,before_json,after_json,created_at
) VALUES(?,?,?,?,?,?,?,?)
`)
).run(
randomUUID(),
targetId,
'role_management',
targetId,
'administrator.bootstrapped',
JSON.stringify(before),
JSON.stringify({
roleId: 'role-admin',
sessionsRevoked: sessionIds.length,
mfaRequired: true,
}),
now,
);
}
const resultingAdministrators = await (
await database.prepare(`
SELECT COUNT(DISTINCT users.id) AS count
FROM users
LEFT JOIN user_status ON user_status.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
WHERE deleted_accounts.user_id IS NULL
AND user_status.blocked_at IS NULL
AND (user_status.banned_until IS NULL OR user_status.banned_until<=?)
AND EXISTS (
SELECT 1 FROM user_roles
WHERE user_roles.user_id=users.id AND user_roles.role_id='role-admin'
)
`)
).get(now);
if (Number(resultingAdministrators?.count) < 1)
throw new Error('The database must retain an active full administrator.');
await database.exec('COMMIT');
transactionStarted = false;
console.log(
targetAlreadyAdministrator
? 'The target account already has unrestricted administration.'
: 'Initial unrestricted administration granted.',
);
} catch (error) {
if (transactionStarted) {
try {
await database.exec('ROLLBACK');
} catch {
// Preserve the original failure without exposing database details.
}
}
console.error(
error instanceof Error ? error.message : 'Administrator bootstrap failed.',
);
process.exitCode = 1;
} finally {
await database.close();
}