import bcrypt from "bcryptjs"; import { getSettings, updateSettings } from "@/lib/db/settings"; const BCRYPT_HASH_PATTERN = /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/; const MANAGEMENT_PASSWORD_SALT_ROUNDS = 12; type JsonRecord = Record; type MigrationSource = "stored_hash" | "stored_plaintext" | "env" | "missing"; interface EnsureManagementPasswordOptions { initialPassword?: string | null; logger?: Pick; settings?: JsonRecord; source?: string; } export interface EnsuredManagementPassword { hash: string | null; migrated: boolean; settings: JsonRecord; source: MigrationSource; } function getInitialPasswordValue(value: string | null | undefined) { return typeof value === "string" && value.length > 0 ? value : null; } export function getStoredManagementPassword(settings: JsonRecord | null | undefined) { return typeof settings?.password === "string" ? settings.password : ""; } export function hasManagementPasswordConfigured(settings: JsonRecord | null | undefined) { return ( getStoredManagementPassword(settings).length > 0 || getInitialPasswordValue(process.env.INITIAL_PASSWORD) !== null ); } export function isBcryptHash(value: unknown): value is string { return typeof value === "string" && BCRYPT_HASH_PATTERN.test(value); } export async function hashManagementPassword(password: string) { return bcrypt.hash(password, MANAGEMENT_PASSWORD_SALT_ROUNDS); } export async function verifyManagementPassword(password: string, hash: string) { if (!isBcryptHash(hash)) return false; return bcrypt.compare(password, hash); } export async function ensurePersistentManagementPasswordHash( options: EnsureManagementPasswordOptions = {} ): Promise { const settings = options.settings ?? ((await getSettings()) as JsonRecord); const storedPassword = getStoredManagementPassword(settings); if (isBcryptHash(storedPassword)) { return { hash: storedPassword, migrated: false, settings, source: "stored_hash", }; } const bootstrapPassword = storedPassword || getInitialPasswordValue(options.initialPassword ?? process.env.INITIAL_PASSWORD); if (!bootstrapPassword) { return { hash: null, migrated: false, settings, source: "missing", }; } const passwordHash = await hashManagementPassword(bootstrapPassword); const updates: JsonRecord = { password: passwordHash }; if (settings.setupComplete !== true) { updates.setupComplete = true; } if (!storedPassword) { updates.requireLogin = true; } const nextSettings = (await updateSettings(updates)) as JsonRecord; if (options.logger) { const context = options.source ? ` during ${options.source}` : ""; const migrationSource = storedPassword ? "stored plaintext password" : "INITIAL_PASSWORD"; options.logger.log(`[AUTH] Migrated ${migrationSource} to bcrypt hash${context}`); } return { hash: getStoredManagementPassword(nextSettings) || passwordHash, migrated: true, settings: nextSettings, source: storedPassword ? "stored_plaintext" : "env", }; }