mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-03 22:37:20 +02:00
feat: add admin roles handler factory and Express routes
This commit is contained in:
parent
59fe67c95d
commit
452333ee4d
5 changed files with 305 additions and 0 deletions
|
|
@ -145,6 +145,7 @@ const startServer = async () => {
|
||||||
app.use('/api/admin', routes.adminAuth);
|
app.use('/api/admin', routes.adminAuth);
|
||||||
app.use('/api/admin/config', routes.adminConfig);
|
app.use('/api/admin/config', routes.adminConfig);
|
||||||
app.use('/api/admin/groups', routes.adminGroups);
|
app.use('/api/admin/groups', routes.adminGroups);
|
||||||
|
app.use('/api/admin/roles', routes.adminRoles);
|
||||||
app.use('/api/actions', routes.actions);
|
app.use('/api/actions', routes.actions);
|
||||||
app.use('/api/keys', routes.keys);
|
app.use('/api/keys', routes.keys);
|
||||||
app.use('/api/api-keys', routes.apiKeys);
|
app.use('/api/api-keys', routes.apiKeys);
|
||||||
|
|
|
||||||
54
api/server/routes/admin/roles.js
Normal file
54
api/server/routes/admin/roles.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
const express = require('express');
|
||||||
|
const { createAdminRolesHandlers } = require('@librechat/api');
|
||||||
|
const { SystemCapabilities } = require('@librechat/data-schemas');
|
||||||
|
const { requireCapability } = require('~/server/middleware/roles/capabilities');
|
||||||
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
|
const db = require('~/models');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
|
||||||
|
const requireReadRoles = requireCapability(SystemCapabilities.READ_ROLES);
|
||||||
|
const requireManageRoles = requireCapability(SystemCapabilities.MANAGE_ROLES);
|
||||||
|
|
||||||
|
async function listUsersByRole(roleName) {
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const User = mongoose.models.User;
|
||||||
|
const users = await User.find({ role: roleName })
|
||||||
|
.select('_id name email avatar createdAt')
|
||||||
|
.lean();
|
||||||
|
return users.map((u) => ({
|
||||||
|
userId: String(u._id),
|
||||||
|
name: u.name ?? String(u._id),
|
||||||
|
email: u.email ?? '',
|
||||||
|
avatarUrl: u.avatar,
|
||||||
|
joinedAt: u.createdAt ? u.createdAt.toISOString() : new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = createAdminRolesHandlers({
|
||||||
|
listRoles: db.listRoles,
|
||||||
|
getRoleByName: db.getRoleByName,
|
||||||
|
createRole: db.createRole,
|
||||||
|
updateRoleByName: db.updateRoleByName,
|
||||||
|
updateAccessPermissions: db.updateAccessPermissions,
|
||||||
|
deleteRole: db.deleteRole,
|
||||||
|
findUser: db.findUser,
|
||||||
|
updateUser: db.updateUser,
|
||||||
|
countUsers: db.countUsers,
|
||||||
|
listUsersByRole,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use(requireJwtAuth, requireAdminAccess);
|
||||||
|
|
||||||
|
router.get('/', requireReadRoles, handlers.listRoles);
|
||||||
|
router.post('/', requireManageRoles, handlers.createRole);
|
||||||
|
router.get('/:name', requireReadRoles, handlers.getRole);
|
||||||
|
router.patch('/:name', requireManageRoles, handlers.updateRole);
|
||||||
|
router.delete('/:name', requireManageRoles, handlers.deleteRole);
|
||||||
|
router.patch('/:name/permissions', requireManageRoles, handlers.updateRolePermissions);
|
||||||
|
router.get('/:name/members', requireReadRoles, handlers.getRoleMembers);
|
||||||
|
router.post('/:name/members', requireManageRoles, handlers.addRoleMember);
|
||||||
|
router.delete('/:name/members/:userId', requireManageRoles, handlers.removeRoleMember);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -4,6 +4,7 @@ const categories = require('./categories');
|
||||||
const adminAuth = require('./admin/auth');
|
const adminAuth = require('./admin/auth');
|
||||||
const adminConfig = require('./admin/config');
|
const adminConfig = require('./admin/config');
|
||||||
const adminGroups = require('./admin/groups');
|
const adminGroups = require('./admin/groups');
|
||||||
|
const adminRoles = require('./admin/roles');
|
||||||
const endpoints = require('./endpoints');
|
const endpoints = require('./endpoints');
|
||||||
const staticRoute = require('./static');
|
const staticRoute = require('./static');
|
||||||
const messages = require('./messages');
|
const messages = require('./messages');
|
||||||
|
|
@ -35,6 +36,7 @@ module.exports = {
|
||||||
adminAuth,
|
adminAuth,
|
||||||
adminConfig,
|
adminConfig,
|
||||||
adminGroups,
|
adminGroups,
|
||||||
|
adminRoles,
|
||||||
keys,
|
keys,
|
||||||
apiKeys,
|
apiKeys,
|
||||||
user,
|
user,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
export { createAdminConfigHandlers } from './config';
|
export { createAdminConfigHandlers } from './config';
|
||||||
export { createAdminGroupsHandlers } from './groups';
|
export { createAdminGroupsHandlers } from './groups';
|
||||||
|
export { createAdminRolesHandlers } from './roles';
|
||||||
export type { AdminConfigDeps } from './config';
|
export type { AdminConfigDeps } from './config';
|
||||||
export type { AdminGroupsDeps } from './groups';
|
export type { AdminGroupsDeps } from './groups';
|
||||||
|
export type { AdminRolesDeps } from './roles';
|
||||||
|
|
|
||||||
246
packages/api/src/admin/roles.ts
Normal file
246
packages/api/src/admin/roles.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
import { SystemRoles } from 'librechat-data-provider';
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import type { IRole, IUser } from '@librechat/data-schemas';
|
||||||
|
import type { FilterQuery } from 'mongoose';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import type { ServerRequest } from '~/types/http';
|
||||||
|
|
||||||
|
interface RoleNameParams {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoleMemberParams extends RoleNameParams {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdminMember {
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
joinedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminRolesDeps {
|
||||||
|
listRoles: () => Promise<IRole[]>;
|
||||||
|
getRoleByName: (
|
||||||
|
name: string,
|
||||||
|
fields?: string | string[] | null,
|
||||||
|
) => Promise<IRole | null>;
|
||||||
|
createRole: (roleData: Partial<IRole>) => Promise<IRole>;
|
||||||
|
updateRoleByName: (name: string, updates: Partial<IRole>) => Promise<IRole | null>;
|
||||||
|
updateAccessPermissions: (
|
||||||
|
name: string,
|
||||||
|
perms: Record<string, Record<string, boolean>>,
|
||||||
|
roleData?: IRole,
|
||||||
|
) => Promise<void>;
|
||||||
|
deleteRole: (name: string) => Promise<IRole | null>;
|
||||||
|
findUser: (
|
||||||
|
criteria: FilterQuery<IUser>,
|
||||||
|
fields?: string | string[] | null,
|
||||||
|
) => Promise<IUser | null>;
|
||||||
|
updateUser: (userId: string, data: Partial<IUser>) => Promise<IUser | null>;
|
||||||
|
countUsers: (filter?: FilterQuery<IUser>) => Promise<number>;
|
||||||
|
listUsersByRole: (roleName: string) => Promise<AdminMember[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAdminRolesHandlers(deps: AdminRolesDeps) {
|
||||||
|
const {
|
||||||
|
listRoles,
|
||||||
|
getRoleByName,
|
||||||
|
createRole,
|
||||||
|
updateRoleByName,
|
||||||
|
updateAccessPermissions,
|
||||||
|
deleteRole,
|
||||||
|
findUser,
|
||||||
|
updateUser,
|
||||||
|
countUsers,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function listRolesHandler(_req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const roles = await listRoles();
|
||||||
|
return res.status(200).json({ roles });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] listRoles error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to list roles' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoleHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params as RoleNameParams;
|
||||||
|
const role = await getRoleByName(name);
|
||||||
|
if (!role) {
|
||||||
|
return res.status(404).json({ error: 'Role not found' });
|
||||||
|
}
|
||||||
|
return res.status(200).json({ role });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] getRole error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to get role' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRoleHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name, permissions } = req.body as { name?: string; permissions?: IRole['permissions'] };
|
||||||
|
if (!name || typeof name !== 'string' || !name.trim()) {
|
||||||
|
return res.status(400).json({ error: 'name is required' });
|
||||||
|
}
|
||||||
|
const role = await createRole({
|
||||||
|
name: name.trim(),
|
||||||
|
permissions: permissions || {},
|
||||||
|
});
|
||||||
|
return res.status(201).json({ role });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to create role';
|
||||||
|
logger.error('[adminRoles] createRole error:', error);
|
||||||
|
const status = message.includes('already exists') || message.includes('reserved') ? 409 : 500;
|
||||||
|
return res.status(status).json({ error: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRoleHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params as RoleNameParams;
|
||||||
|
const body = req.body as Partial<IRole>;
|
||||||
|
|
||||||
|
const existing = await getRoleByName(name);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Role not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: Partial<IRole> = {};
|
||||||
|
if (body.name !== undefined) {
|
||||||
|
updates.name = body.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await updateRoleByName(name, updates);
|
||||||
|
return res.status(200).json({ role });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] updateRole error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to update role' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRolePermissionsHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params as RoleNameParams;
|
||||||
|
const { permissions } = req.body as {
|
||||||
|
permissions: Record<string, Record<string, boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!permissions || typeof permissions !== 'object') {
|
||||||
|
return res.status(400).json({ error: 'permissions object is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await getRoleByName(name);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Role not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateAccessPermissions(name, permissions, existing);
|
||||||
|
const updated = await getRoleByName(name);
|
||||||
|
return res.status(200).json({ role: updated });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] updateRolePermissions error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to update role permissions' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRoleHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params as RoleNameParams;
|
||||||
|
if (SystemRoles[name as keyof typeof SystemRoles]) {
|
||||||
|
return res.status(403).json({ error: 'Cannot delete system role' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await getRoleByName(name);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Role not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteRole(name);
|
||||||
|
return res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] deleteRole error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to delete role' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoleMembersHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params as RoleNameParams;
|
||||||
|
const existing = await getRoleByName(name);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Role not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = await deps.listUsersByRole(name);
|
||||||
|
return res.status(200).json({ members });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] getRoleMembers error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to get role members' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRoleMemberHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params as RoleNameParams;
|
||||||
|
const { userId } = req.body as { userId: string };
|
||||||
|
|
||||||
|
if (!userId || typeof userId !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'userId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await getRoleByName(name);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Role not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await findUser({ _id: userId });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUser(userId, { role: name });
|
||||||
|
return res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] addRoleMember error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to add role member' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeRoleMemberHandler(req: ServerRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name, userId } = req.params as RoleMemberParams;
|
||||||
|
|
||||||
|
const user = await findUser({ _id: userId });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== name) {
|
||||||
|
return res.status(400).json({ error: 'User is not a member of this role' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUser(userId, { role: SystemRoles.USER });
|
||||||
|
return res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[adminRoles] removeRoleMember error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to remove role member' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
listRoles: listRolesHandler,
|
||||||
|
getRole: getRoleHandler,
|
||||||
|
createRole: createRoleHandler,
|
||||||
|
updateRole: updateRoleHandler,
|
||||||
|
updateRolePermissions: updateRolePermissionsHandler,
|
||||||
|
deleteRole: deleteRoleHandler,
|
||||||
|
getRoleMembers: getRoleMembersHandler,
|
||||||
|
addRoleMember: addRoleMemberHandler,
|
||||||
|
removeRoleMember: removeRoleMemberHandler,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue