feat: add super-admin role with admin impersonation support

Add a new super-admin role that can impersonate other admins. Regular
admins retain all existing permissions but cannot impersonate other
admins or promote users to super-admin.

Backend changes:
- Add isSuperAdmin field to users table with default false
- Add isSuperAdmin() check function to auth service
- Update JWT tokens to include isSuperAdmin claim
- Allow super-admins to impersonate other admins
- Add security rules for super-admin role changes

Frontend changes:
- Display "Super Admin" badge with gradient styling
- Add "Super Admin" option to role change modal
- Enable impersonate button for super-admins targeting admins
- Add "Super Admins Only" filter option

Security rules:
- Only super-admins can promote/demote super-admins
- Regular admins cannot promote users to super-admin
- Super-admins cannot demote themselves
- Cannot demote the last super-admin

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-23 13:32:55 +01:00
parent a5f0e3b96f
commit ed433902d9
9 changed files with 1022 additions and 50 deletions

View File

@@ -225,6 +225,7 @@ const app = new Elysia()
email: payload.email,
callsign: payload.callsign,
isAdmin: payload.isAdmin,
isSuperAdmin: payload.isSuperAdmin,
impersonatedBy: payload.impersonatedBy, // Admin ID if impersonating
},
isImpersonation,
@@ -360,6 +361,8 @@ const app = new Elysia()
userId: user.id,
email: user.email,
callsign: user.callsign,
isAdmin: user.isAdmin,
isSuperAdmin: user.isSuperAdmin,
exp,
});
@@ -429,6 +432,7 @@ const app = new Elysia()
email: user.email,
callsign: user.callsign,
isAdmin: user.isAdmin,
isSuperAdmin: user.isSuperAdmin,
exp,
});
@@ -1209,7 +1213,7 @@ const app = new Elysia()
/**
* POST /api/admin/users/:userId/role
* Update user admin status (admin only)
* Update user role (admin only)
*/
.post('/api/admin/users/:userId/role', async ({ user, params, body, set }) => {
if (!user || !user.isAdmin) {
@@ -1223,21 +1227,27 @@ const app = new Elysia()
return { success: false, error: 'Invalid user ID' };
}
const { isAdmin } = body;
const { role } = body;
if (typeof isAdmin !== 'boolean') {
if (typeof role !== 'string') {
set.status = 400;
return { success: false, error: 'isAdmin (boolean) is required' };
return { success: false, error: 'role (string) is required' };
}
const validRoles = ['user', 'admin', 'super-admin'];
if (!validRoles.includes(role)) {
set.status = 400;
return { success: false, error: `Invalid role. Must be one of: ${validRoles.join(', ')}` };
}
try {
await changeUserRole(user.id, targetUserId, isAdmin);
await changeUserRole(user.id, targetUserId, role);
return {
success: true,
message: 'User admin status updated successfully',
message: 'User role updated successfully',
};
} catch (error) {
logger.error('Error updating user admin status', { error: error.message, userId: user.id });
logger.error('Error updating user role', { error: error.message, userId: user.id });
set.status = 400;
return {
success: false,
@@ -1304,6 +1314,7 @@ const app = new Elysia()
email: targetUser.email,
callsign: targetUser.callsign,
isAdmin: targetUser.isAdmin,
isSuperAdmin: targetUser.isSuperAdmin,
impersonatedBy: user.id, // Admin ID who started impersonation
exp,
});
@@ -1364,6 +1375,7 @@ const app = new Elysia()
email: adminUser.email,
callsign: adminUser.callsign,
isAdmin: adminUser.isAdmin,
isSuperAdmin: adminUser.isSuperAdmin,
exp,
});