56 lines
2 KiB
JavaScript
56 lines
2 KiB
JavaScript
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}`);
|
|
}
|