205 lines
6.5 KiB
JavaScript
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();
|
|
}
|