/** * Prints a one-time recovery link for an account. Meant for whoever runs the * server when nobody can sign in as an admin anymore: * * bun run recover * * It only talks to the database, the server does not have to be running. The * link works like the ones admins make under Settings → Users: it lets the * account set a new password, deletes its saved API keys, turns two-factor * off and signs every device out. * * Kept free of imports from the app so it also runs in the production image. * The link has to be made the same way as in server/utils/recovery.ts. */ import { createHash, randomBytes, randomUUID } from 'node:crypto'; import { Pool } from 'pg'; const LIFETIME_MS = 24 * 60 * 60 * 1000; const main = async (): Promise => { const email = process.argv[2]?.trim().toLowerCase(); if (!email) { console.error('Usage: bun run recover '); return 1; } const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgres://postgres:password@localhost:5432/veridian', }); const found = await pool.query('SELECT id, name FROM users WHERE lower(email) = $1', [email]); const user = found.rows[0] as { id: string; name: string } | undefined; if (!user) { console.error(`No account with the email ${email}.`); await pool.end(); return 1; } const token = randomBytes(32).toString('base64url'); const tokenHash = createHash('sha256').update(token).digest('hex'); const expiresAt = new Date(Date.now() + LIFETIME_MS); // only one link works at a time await pool.query('DELETE FROM recovery_links WHERE user_id = $1', [user.id]); await pool.query( 'INSERT INTO recovery_links (id, token_hash, user_id, reset_security, expires_at) VALUES ($1, $2, $3, true, $4)', [randomUUID(), tokenHash, user.id, expiresAt], ); await pool.end(); const baseUrl = (process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000').replace(/\/$/, ''); console.log(`Recovery link for ${user.name} <${email}>:`); console.log(''); console.log(` ${baseUrl}/auth/recover/${token}`); console.log(''); console.log('It works once and expires in 24 hours. Using it deletes the saved API keys'); console.log('of this account, turns two-factor off and signs every device out.'); return 0; }; process.exit(await main());