| import { Types } from 'mongoose'; |
| import { PrincipalType } from 'librechat-data-provider'; |
| import type { TUser, TPrincipalSearchResult } from 'librechat-data-provider'; |
| import type { Model, ClientSession } from 'mongoose'; |
| import type { IGroup, IRole, IUser } from '~/types'; |
|
|
| export function createUserGroupMethods(mongoose: typeof import('mongoose')) { |
| |
| |
| |
| |
| |
| |
| |
| async function findGroupById( |
| groupId: string | Types.ObjectId, |
| projection: Record<string, unknown> = {}, |
| session?: ClientSession, |
| ): Promise<IGroup | null> { |
| const Group = mongoose.models.Group as Model<IGroup>; |
| const query = Group.findOne({ _id: groupId }, projection); |
| if (session) { |
| query.session(session); |
| } |
| return await query.lean(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async function findGroupByExternalId( |
| idOnTheSource: string, |
| source: 'entra' | 'local' = 'entra', |
| projection: Record<string, unknown> = {}, |
| session?: ClientSession, |
| ): Promise<IGroup | null> { |
| const Group = mongoose.models.Group as Model<IGroup>; |
| const query = Group.findOne({ idOnTheSource, source }, projection); |
| if (session) { |
| query.session(session); |
| } |
| return await query.lean(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async function findGroupsByNamePattern( |
| namePattern: string, |
| source: 'entra' | 'local' | null = null, |
| limit: number = 20, |
| session?: ClientSession, |
| ): Promise<IGroup[]> { |
| const Group = mongoose.models.Group as Model<IGroup>; |
| const regex = new RegExp(namePattern, 'i'); |
| const query: Record<string, unknown> = { |
| $or: [{ name: regex }, { email: regex }, { description: regex }], |
| }; |
|
|
| if (source) { |
| query.source = source; |
| } |
|
|
| const dbQuery = Group.find(query).limit(limit); |
| if (session) { |
| dbQuery.session(session); |
| } |
| return await dbQuery.lean(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function findGroupsByMemberId( |
| userId: string | Types.ObjectId, |
| session?: ClientSession, |
| ): Promise<IGroup[]> { |
| const User = mongoose.models.User as Model<IUser>; |
| const Group = mongoose.models.Group as Model<IGroup>; |
|
|
| const userQuery = User.findById(userId, 'idOnTheSource'); |
| if (session) { |
| userQuery.session(session); |
| } |
| const user = (await userQuery.lean()) as { idOnTheSource?: string } | null; |
|
|
| if (!user) { |
| return []; |
| } |
|
|
| const userIdOnTheSource = user.idOnTheSource || userId.toString(); |
|
|
| const query = Group.find({ memberIds: userIdOnTheSource }); |
| if (session) { |
| query.session(session); |
| } |
| return await query.lean(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function createGroup(groupData: Partial<IGroup>, session?: ClientSession): Promise<IGroup> { |
| const Group = mongoose.models.Group as Model<IGroup>; |
| const options = session ? { session } : {}; |
| return await Group.create([groupData], options).then((groups) => groups[0]); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async function upsertGroupByExternalId( |
| idOnTheSource: string, |
| source: 'entra' | 'local', |
| updateData: Partial<IGroup>, |
| session?: ClientSession, |
| ): Promise<IGroup | null> { |
| const Group = mongoose.models.Group as Model<IGroup>; |
| const options = { |
| new: true, |
| upsert: true, |
| ...(session ? { session } : {}), |
| }; |
|
|
| return await Group.findOneAndUpdate({ idOnTheSource, source }, { $set: updateData }, options); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function addUserToGroup( |
| userId: string | Types.ObjectId, |
| groupId: string | Types.ObjectId, |
| session?: ClientSession, |
| ): Promise<{ user: IUser; group: IGroup | null }> { |
| const User = mongoose.models.User as Model<IUser>; |
| const Group = mongoose.models.Group as Model<IGroup>; |
|
|
| const options = { new: true, ...(session ? { session } : {}) }; |
|
|
| const user = (await User.findById(userId, 'idOnTheSource', options).lean()) as { |
| idOnTheSource?: string; |
| _id: Types.ObjectId; |
| } | null; |
| if (!user) { |
| throw new Error(`User not found: ${userId}`); |
| } |
|
|
| const userIdOnTheSource = user.idOnTheSource || userId.toString(); |
| const updatedGroup = await Group.findByIdAndUpdate( |
| groupId, |
| { $addToSet: { memberIds: userIdOnTheSource } }, |
| options, |
| ).lean(); |
|
|
| return { user: user as IUser, group: updatedGroup }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function removeUserFromGroup( |
| userId: string | Types.ObjectId, |
| groupId: string | Types.ObjectId, |
| session?: ClientSession, |
| ): Promise<{ user: IUser; group: IGroup | null }> { |
| const User = mongoose.models.User as Model<IUser>; |
| const Group = mongoose.models.Group as Model<IGroup>; |
|
|
| const options = { new: true, ...(session ? { session } : {}) }; |
|
|
| const user = (await User.findById(userId, 'idOnTheSource', options).lean()) as { |
| idOnTheSource?: string; |
| _id: Types.ObjectId; |
| } | null; |
| if (!user) { |
| throw new Error(`User not found: ${userId}`); |
| } |
|
|
| const userIdOnTheSource = user.idOnTheSource || userId.toString(); |
| const updatedGroup = await Group.findByIdAndUpdate( |
| groupId, |
| { $pull: { memberIds: userIdOnTheSource } }, |
| options, |
| ).lean(); |
|
|
| return { user: user as IUser, group: updatedGroup }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function getUserGroups( |
| userId: string | Types.ObjectId, |
| session?: ClientSession, |
| ): Promise<IGroup[]> { |
| return await findGroupsByMemberId(userId, session); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function getUserPrincipals( |
| params: { |
| userId: string | Types.ObjectId; |
| role?: string | null; |
| }, |
| session?: ClientSession, |
| ): Promise<Array<{ principalType: string; principalId?: string | Types.ObjectId }>> { |
| const { userId, role } = params; |
| |
| const userObjectId = typeof userId === 'string' ? new Types.ObjectId(userId) : userId; |
| const principals: Array<{ principalType: string; principalId?: string | Types.ObjectId }> = [ |
| { principalType: PrincipalType.USER, principalId: userObjectId }, |
| ]; |
|
|
| |
| let userRole = role; |
| if (userRole === undefined) { |
| const User = mongoose.models.User as Model<IUser>; |
| const query = User.findById(userId).select('role'); |
| if (session) { |
| query.session(session); |
| } |
| const user = await query.lean(); |
| userRole = user?.role; |
| } |
|
|
| |
| if (userRole && userRole.trim()) { |
| principals.push({ principalType: PrincipalType.ROLE, principalId: userRole }); |
| } |
|
|
| const userGroups = await getUserGroups(userId, session); |
| if (userGroups && userGroups.length > 0) { |
| userGroups.forEach((group) => { |
| principals.push({ principalType: PrincipalType.GROUP, principalId: group._id }); |
| }); |
| } |
|
|
| principals.push({ principalType: PrincipalType.PUBLIC }); |
|
|
| return principals; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function syncUserEntraGroups( |
| userId: string | Types.ObjectId, |
| entraGroups: Array<{ id: string; name: string; description?: string; email?: string }>, |
| session?: ClientSession, |
| ): Promise<{ |
| user: IUser; |
| addedGroups: IGroup[]; |
| removedGroups: IGroup[]; |
| }> { |
| const User = mongoose.models.User as Model<IUser>; |
| const Group = mongoose.models.Group as Model<IGroup>; |
|
|
| const query = User.findById(userId, { idOnTheSource: 1 }); |
| if (session) { |
| query.session(session); |
| } |
| const user = (await query.lean()) as { idOnTheSource?: string; _id: Types.ObjectId } | null; |
|
|
| if (!user) { |
| throw new Error(`User not found: ${userId}`); |
| } |
|
|
| |
| const userIdOnTheSource = user.idOnTheSource || userId.toString(); |
|
|
| const entraIdMap = new Map<string, boolean>(); |
| const addedGroups: IGroup[] = []; |
| const removedGroups: IGroup[] = []; |
|
|
| for (const entraGroup of entraGroups) { |
| entraIdMap.set(entraGroup.id, true); |
|
|
| let group = await findGroupByExternalId(entraGroup.id, 'entra', {}, session); |
|
|
| if (!group) { |
| group = await createGroup( |
| { |
| name: entraGroup.name, |
| description: entraGroup.description, |
| email: entraGroup.email, |
| idOnTheSource: entraGroup.id, |
| source: 'entra', |
| memberIds: [userIdOnTheSource], |
| }, |
| session, |
| ); |
|
|
| addedGroups.push(group); |
| } else if (!group.memberIds?.includes(userIdOnTheSource)) { |
| const { group: updatedGroup } = await addUserToGroup(userId, group._id, session); |
| if (updatedGroup) { |
| addedGroups.push(updatedGroup); |
| } |
| } |
| } |
|
|
| const groupsQuery = Group.find( |
| { source: 'entra', memberIds: userIdOnTheSource }, |
| { _id: 1, idOnTheSource: 1 }, |
| ); |
| if (session) { |
| groupsQuery.session(session); |
| } |
| const existingGroups = (await groupsQuery.lean()) as Array<{ |
| _id: Types.ObjectId; |
| idOnTheSource?: string; |
| }>; |
|
|
| for (const group of existingGroups) { |
| if (group.idOnTheSource && !entraIdMap.has(group.idOnTheSource)) { |
| const { group: removedGroup } = await removeUserFromGroup(userId, group._id, session); |
| if (removedGroup) { |
| removedGroups.push(removedGroup); |
| } |
| } |
| } |
|
|
| const userQuery = User.findById(userId); |
| if (session) { |
| userQuery.session(session); |
| } |
| const updatedUser = await userQuery.lean(); |
|
|
| if (!updatedUser) { |
| throw new Error(`User not found after update: ${userId}`); |
| } |
|
|
| return { |
| user: updatedUser, |
| addedGroups, |
| removedGroups, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function calculateRelevanceScore(item: TPrincipalSearchResult, searchPattern: string): number { |
| const exactRegex = new RegExp(`^${searchPattern}$`, 'i'); |
| const startsWithPattern = searchPattern.toLowerCase(); |
|
|
| |
| const searchableFields = |
| item.type === PrincipalType.USER |
| ? [item.name, item.email, item.username].filter(Boolean) |
| : [item.name, item.email, item.description].filter(Boolean); |
|
|
| let maxScore = 0; |
|
|
| for (const field of searchableFields) { |
| if (!field) continue; |
| const fieldLower = field.toLowerCase(); |
| let score = 0; |
|
|
| |
| if (exactRegex.test(field)) { |
| score = 100; |
| } else if (fieldLower.startsWith(startsWithPattern)) { |
| |
| score = 80; |
| } else if (fieldLower.includes(startsWithPattern)) { |
| |
| score = 50; |
| } else { |
| |
| score = 10; |
| } |
|
|
| maxScore = Math.max(maxScore, score); |
| } |
|
|
| return maxScore; |
| } |
|
|
| |
| |
| |
| |
| |
| function sortPrincipalsByRelevance< |
| T extends { _searchScore?: number; type: string; name?: string; email?: string }, |
| >(results: T[]): T[] { |
| return results.sort((a, b) => { |
| if (b._searchScore !== a._searchScore) { |
| return (b._searchScore || 0) - (a._searchScore || 0); |
| } |
| if (a.type !== b.type) { |
| return a.type === PrincipalType.USER ? -1 : 1; |
| } |
| const aName = a.name || a.email || ''; |
| const bName = b.name || b.email || ''; |
| return aName.localeCompare(bName); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| function transformUserToTPrincipalSearchResult(user: TUser): TPrincipalSearchResult { |
| return { |
| id: user.id, |
| type: PrincipalType.USER, |
| name: user.name || user.email, |
| email: user.email, |
| username: user.username, |
| avatar: user.avatar, |
| provider: user.provider, |
| source: 'local', |
| idOnTheSource: (user as TUser & { idOnTheSource?: string }).idOnTheSource || user.id, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| function transformGroupToTPrincipalSearchResult(group: IGroup): TPrincipalSearchResult { |
| return { |
| id: group._id?.toString(), |
| type: PrincipalType.GROUP, |
| name: group.name, |
| email: group.email, |
| avatar: group.avatar, |
| description: group.description, |
| source: group.source || 'local', |
| memberCount: group.memberIds ? group.memberIds.length : 0, |
| idOnTheSource: group.idOnTheSource || group._id?.toString(), |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function searchPrincipals( |
| searchPattern: string, |
| limitPerType: number = 10, |
| typeFilter: Array<PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE> | null = null, |
| session?: ClientSession, |
| ): Promise<TPrincipalSearchResult[]> { |
| if (!searchPattern || searchPattern.trim().length === 0) { |
| return []; |
| } |
|
|
| const trimmedPattern = searchPattern.trim(); |
| const promises: Promise<TPrincipalSearchResult[]>[] = []; |
|
|
| if (!typeFilter || typeFilter.includes(PrincipalType.USER)) { |
| |
| const userFields = 'name email username avatar provider idOnTheSource'; |
| |
| const User = mongoose.models.User as Model<IUser>; |
| const regex = new RegExp(trimmedPattern, 'i'); |
| const userQuery = User.find({ |
| $or: [{ name: regex }, { email: regex }, { username: regex }], |
| }) |
| .select(userFields) |
| .limit(limitPerType); |
|
|
| if (session) { |
| userQuery.session(session); |
| } |
|
|
| promises.push( |
| userQuery.lean().then((users) => |
| users.map((user) => { |
| const userWithId = user as IUser & { idOnTheSource?: string }; |
| return transformUserToTPrincipalSearchResult({ |
| id: userWithId._id?.toString() || '', |
| name: userWithId.name, |
| email: userWithId.email, |
| username: userWithId.username, |
| avatar: userWithId.avatar, |
| provider: userWithId.provider, |
| } as TUser); |
| }), |
| ), |
| ); |
| } else { |
| promises.push(Promise.resolve([])); |
| } |
|
|
| if (!typeFilter || typeFilter.includes(PrincipalType.GROUP)) { |
| promises.push( |
| findGroupsByNamePattern(trimmedPattern, null, limitPerType, session).then((groups) => |
| groups.map(transformGroupToTPrincipalSearchResult), |
| ), |
| ); |
| } else { |
| promises.push(Promise.resolve([])); |
| } |
|
|
| if (!typeFilter || typeFilter.includes(PrincipalType.ROLE)) { |
| const Role = mongoose.models.Role as Model<IRole>; |
| if (Role) { |
| const regex = new RegExp(trimmedPattern, 'i'); |
| const roleQuery = Role.find({ name: regex }).select('name').limit(limitPerType); |
|
|
| if (session) { |
| roleQuery.session(session); |
| } |
|
|
| promises.push( |
| roleQuery.lean().then((roles) => |
| roles.map((role) => ({ |
| |
| id: role.name, |
| type: PrincipalType.ROLE, |
| name: role.name, |
| source: 'local' as const, |
| idOnTheSource: role.name, |
| })), |
| ), |
| ); |
| } |
| } else { |
| promises.push(Promise.resolve([])); |
| } |
|
|
| const results = await Promise.all(promises); |
| const combined = results.flat(); |
| return combined; |
| } |
|
|
| return { |
| findGroupById, |
| findGroupByExternalId, |
| findGroupsByNamePattern, |
| findGroupsByMemberId, |
| createGroup, |
| upsertGroupByExternalId, |
| addUserToGroup, |
| removeUserFromGroup, |
| getUserGroups, |
| getUserPrincipals, |
| syncUserEntraGroups, |
| searchPrincipals, |
| calculateRelevanceScore, |
| sortPrincipalsByRelevance, |
| }; |
| } |
|
|
| export type UserGroupMethods = ReturnType<typeof createUserGroupMethods>; |
|
|