import { createHash, randomBytes, randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { pathToFileURL } from "node:url"; import Database from "better-sqlite3"; import { databasePathFromUrl } from "./databasePath.mjs"; const PASSWORD_HASH_KEY = "admin_password_hash"; const RESET_TOKEN_KEY = "admin_password_reset"; const RESET_TOKEN_LIFETIME_MS = 15 * 60 * 1000; function activeRecord(value) { try { const record = JSON.parse(value); return new Date(record.expiresAt).getTime() > Date.now(); } catch { return false; } } export function createResetToken(databasePath) { if (!existsSync(databasePath)) throw new Error(`Database does not exist: ${databasePath}`); const database = new Database(databasePath, { fileMustExist: true }); try { if (!database.prepare('SELECT 1 FROM "Setting" WHERE "key" = ?').get(PASSWORD_HASH_KEY)) { throw new Error("Password recovery is unavailable before initial setup"); } const existing = database .prepare('SELECT "value" FROM "Setting" WHERE "key" = ?') .get(RESET_TOKEN_KEY); if (existing && activeRecord(existing.value)) { throw new Error("An unexpired reset token already exists; it was not replaced"); } const token = randomBytes(24).toString("base64url"); const expiresAt = new Date(Date.now() + RESET_TOKEN_LIFETIME_MS).toISOString(); const value = JSON.stringify({ digest: createHash("sha256").update(token, "utf8").digest("hex"), nonce: randomUUID(), expiresAt, }); database .prepare( 'INSERT INTO "Setting" ("key", "value") VALUES (?, ?) ON CONFLICT("key") DO UPDATE SET "value" = excluded."value"' ) .run(RESET_TOKEN_KEY, value); return { token, expiresAt }; } finally { database.close(); } } if (import.meta.url === pathToFileURL(process.argv[1]).href) { const result = createResetToken(databasePathFromUrl(process.env.DATABASE_URL)); console.info(`Password reset token: ${result.token}`); console.info(`Expires: ${result.expiresAt}`); }