Add read-only Noya admin panel with assignments and UX polish.
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>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user