/** * Migration script: auth data from JSON files → PostgreSQL * * Migrates: * - users.json → users table * - passkeys.json → passkeys table (email → userId FK) * - token-blacklist.json → token_blacklist table * * Usage: bun run scripts/migrate-auth-to-pg.ts */ import { join } from 'node:path'; import { db } from 'officerdb/db'; import { users, passkeys, tokenBlacklist } from 'officerdb/schema'; const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data'); const AUTH_DIR = join(DATA_PATH, 'auth'); type OldUser = { id: number; email: string; password: string | null; role: string; status: string; name: string | null; username: string | null; avatar: string | null; passwordChangedAt: number | null; }; type OldPasskey = { id: number; email: string; origin: string | null; credentialId: string | null; publicKey: string | null; counter: number; }; type OldBlacklistEntry = { jti: string; expiresAt: number; }; async function readJson(path: string, fallback: T): Promise { try { const file = Bun.file(path); if (!(await file.exists())) return fallback; return (await file.json()) as T; } catch { return fallback; } } async function migrate() { console.log(`[migrate] Reading JSON files from ${AUTH_DIR}`); const oldUsers = await readJson(join(AUTH_DIR, 'users.json'), []); const oldPasskeys = await readJson(join(AUTH_DIR, 'passkeys.json'), []); const oldBlacklist = await readJson(join(AUTH_DIR, 'token-blacklist.json'), []); console.log(`[migrate] Found: ${oldUsers.length} users, ${oldPasskeys.length} passkeys, ${oldBlacklist.length} blacklisted tokens`); if (oldUsers.length === 0) { console.log('[migrate] No users to migrate. Done.'); process.exit(0); } // Build email → userId map for passkey migration const emailToUserId = new Map(); // Migrate users console.log('[migrate] Migrating users...'); for (const u of oldUsers) { const [inserted] = await db .insert(users) .values({ email: u.email, password: u.password, role: u.role as 'Member' | 'Admin' | 'Owner' | 'Super Admin', status: u.status as 'Unverified' | 'Active' | 'Prospect' | 'Invited' | 'Blocked' | 'Banned' | 'Deleted', name: u.name, username: u.username, avatar: u.avatar, passwordChangedAt: u.passwordChangedAt ? new Date(u.passwordChangedAt) : null, }) .returning(); emailToUserId.set(u.email, inserted!.id); console.log(` [user] ${u.email} (old id=${u.id} → new id=${inserted!.id})`); } // Migrate passkeys if (oldPasskeys.length > 0) { console.log('[migrate] Migrating passkeys...'); for (const p of oldPasskeys) { const userId = emailToUserId.get(p.email); if (!userId) { console.warn(` [passkey] Skipping passkey for unknown email: ${p.email}`); continue; } await db.insert(passkeys).values({ userId, origin: p.origin, credentialId: p.credentialId, publicKey: p.publicKey, counter: p.counter, }); console.log(` [passkey] ${p.email} / ${p.origin}`); } } // Migrate token blacklist if (oldBlacklist.length > 0) { const now = Math.floor(Date.now() / 1000); const active = oldBlacklist.filter((b) => b.expiresAt >= now); console.log(`[migrate] Migrating ${active.length} active blacklisted tokens (${oldBlacklist.length - active.length} expired, skipped)...`); for (const b of active) { await db .insert(tokenBlacklist) .values({ jti: b.jti, expiresAt: new Date(b.expiresAt * 1000), }) .onConflictDoNothing(); } } console.log('[migrate] Done!'); process.exit(0); } migrate().catch((err) => { console.error('[migrate] Failed:', err); process.exit(1); });