/** * Deletes what has expired and seeds the first admin, each time connection.ts * opens the database. * * @module */ import { randomBytes, scryptSync } from "node:crypto"; import type { DatabaseSync } from "node:sqlite"; import { ATTACHMENTS_DIR, ORPHANED_ATTACHMENTS_RETENTION_HOURS, UNUSED_INVITES_RETENTION_DAYS, } from "../config.ts"; /** * Runs after the migrations, each time the server starts. It deletes used or * expired reset tokens, invites past their validity and attachments nothing * links to. On an instance with no users, it creates the user `admin` with the * password `admin`, which whoever installs it must change. */ export async function runStartupTasks(db: DatabaseSync): Promise { db.prepare( `DELETE FROM password_reset_tokens WHERE expires_at < datetime('now') OR used_at IS NOT NULL;`, ).run(); db.prepare( `DELETE FROM invites WHERE used_at IS NULL AND created_at < datetime('now', '-${UNUSED_INVITES_RETENTION_DAYS} days');`, ).run(); // An image is uploaded as soon as it's pasted, and linked only when the text // it's pasted into is saved, so a fresh one is left alone. const orphanedAttachments = db.prepare( `SELECT id FROM attachments WHERE resource_id IS NULL AND created_at < datetime('now', '-${ORPHANED_ATTACHMENTS_RETENTION_HOURS} hour');`, ).all() as { id: string }[]; if (orphanedAttachments.length > 0) { for (const { id } of orphanedAttachments) { await Deno.remove(`${ATTACHMENTS_DIR}/${id}`).catch(() => {}); } db.prepare( `DELETE FROM attachments WHERE resource_id IS NULL AND created_at < datetime('now', '-${ORPHANED_ATTACHMENTS_RETENTION_HOURS} hour');`, ).run(); } // Registration needs an invite, so the first user can only come from here. const userCount = db.prepare(`SELECT COUNT(*) as count FROM users`).get() as { count: number; }; if (userCount.count === 0) { // The format of hashPassword in lib/jwt.ts, which verifies it. const salt = randomBytes(16).toString("hex"); const hash = scryptSync("admin", salt, 64).toString("hex"); const passwordHash = `${hash}.${salt}`; db.prepare( `INSERT INTO users (id, username, password_hash, role, created_at, email) VALUES (?, 'admin', ?, 'admin', datetime('now'), 'admin@localhost')`, ).run(crypto.randomUUID(), passwordHash); console.log( "Created default admin user (username: admin, password: admin)", ); } }