/** * Promote an existing password user to admin by email. * Does not create users. Idempotent. Does not revoke sessions or mutate other data. * * Usage: npm run admin:promote -- admin@example.com */ import { AuthProvider, UserRole } from '@prisma/client'; import { prisma } from '../src/lib/prisma.ts'; import { AUDIT_ACTIONS } from '../src/modules/admin/auditActions.ts'; import { AuditPersistError, recordAudit } from '../src/modules/admin/audit.service.ts'; import { normalizeEmail } from '../src/modules/admin/adminBootstrap.ts'; const rawEmail = process.argv[2]; if (!rawEmail) { console.error('Usage: npm run admin:promote -- '); process.exit(1); } const email = normalizeEmail(rawEmail); if (!email.includes('@')) { console.error('Invalid email address'); process.exit(1); } const identity = await prisma.authIdentity.findFirst({ where: { provider: AuthProvider.password, OR: [{ email }, { providerSubject: email }], }, include: { user: { select: { id: true, externalId: true, role: true, isGuest: true, updatedAt: true, }, }, }, }); if (!identity?.user) { console.error( JSON.stringify( { ok: false, email, error: 'USER_NOT_FOUND', message: 'No password account found for this email. No user was created.', }, null, 2, ), ); await prisma.$disconnect(); process.exit(1); } const user = identity.user; const alreadyAdmin = user.role === UserRole.admin; if (!alreadyAdmin) { await prisma.user.update({ where: { id: user.id }, data: { role: UserRole.admin }, }); } try { await recordAudit({ actorUserId: user.id, action: AUDIT_ACTIONS.ADMIN_PROMOTE_USER, targetUserId: user.id, targetType: 'user', targetId: user.externalId, metadata: { source: 'admin_promote_script', email, alreadyAdmin, previousRole: user.role, nextRole: UserRole.admin, }, }); } catch (err) { const message = err instanceof AuditPersistError ? err.message : 'audit failed'; console.error( JSON.stringify( { ok: false, email, externalId: user.externalId, role: UserRole.admin, promoted: !alreadyAdmin, alreadyAdmin, auditRecorded: false, error: 'AUDIT_FAILED', message, }, null, 2, ), ); await prisma.$disconnect(); process.exit(1); } console.log( JSON.stringify( { ok: true, email, externalId: user.externalId, role: UserRole.admin, promoted: !alreadyAdmin, alreadyAdmin, auditRecorded: true, sessionsRevoked: false, message: alreadyAdmin ? 'User was already admin; no data changes besides audit log.' : 'User role updated to admin. Other user data unchanged; sessions not revoked.', }, null, 2, ), ); await prisma.$disconnect();