Ship admin auth, user overview, assessments, chats, assignment filters/export, security hardening, and a separate desktop admin shell with clearer assessment answers. Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
2.9 KiB
TypeScript
131 lines
2.9 KiB
TypeScript
/**
|
|
* 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 -- <email>');
|
|
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();
|