Compare commits
18 Commits
main
...
feat/user-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c696b935b8 | ||
|
|
b1147b6409 | ||
|
|
4448c13684 | ||
|
|
2fd04b6d65 | ||
|
|
a8955e51a3 | ||
|
|
d2559ba977 | ||
|
|
b50406ab9e | ||
|
|
acafdb54c2 | ||
|
|
ca3237c7be | ||
|
|
d3764fd9fe | ||
|
|
24fc57fd01 | ||
|
|
e0ab2c666a | ||
|
|
e8702e104d | ||
|
|
dd762f7223 | ||
|
|
4166eac99d | ||
|
|
82a1f554b5 | ||
|
|
0cdccb617c | ||
|
|
39649ce523 |
6
api/models/Group.js
Normal file
6
api/models/Group.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { groupSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Group = mongoose.model('Group', groupSchema);
|
||||
|
||||
module.exports = Group;
|
||||
127
api/models/groupMethods.js
Normal file
127
api/models/groupMethods.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const User = require('./User');
|
||||
const Group = require('~/models/Group');
|
||||
|
||||
/**
|
||||
* Retrieve a group by ID and convert the found group document to a plain object.
|
||||
*
|
||||
* @param {string} groupId - The ID of the group to find and return as a plain object.
|
||||
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
|
||||
* @returns {Promise<Object|null>} A plain object representing the group document, or `null` if no group is found.
|
||||
*/
|
||||
const getGroupById = (groupId, fieldsToSelect = null) => {
|
||||
const query = Group.findById(groupId);
|
||||
if (fieldsToSelect) {
|
||||
query.select(fieldsToSelect);
|
||||
}
|
||||
return query.lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Search for a single group or multiple groups based on partial data and return them as plain objects.
|
||||
*
|
||||
* @param {Partial<Object>} searchCriteria - The partial data to use for searching groups.
|
||||
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned documents.
|
||||
* @returns {Promise<Object[]>} An array of plain objects representing the group documents.
|
||||
*/
|
||||
const findGroup = (searchCriteria, fieldsToSelect = null) => {
|
||||
const query = Group.find(searchCriteria);
|
||||
if (fieldsToSelect) {
|
||||
query.select(fieldsToSelect);
|
||||
}
|
||||
return query.lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a group with new data without overwriting existing properties.
|
||||
*
|
||||
* @param {string} groupId - The ID of the group to update.
|
||||
* @param {Object} updateData - An object containing the properties to update.
|
||||
* @returns {Promise<Object|null>} The updated group document as a plain object, or `null` if no group is found.
|
||||
*/
|
||||
const updateGroup = (groupId, updateData) => {
|
||||
return Group.findByIdAndUpdate(
|
||||
groupId,
|
||||
{ $set: updateData },
|
||||
{ new: true, runValidators: true },
|
||||
).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new group.
|
||||
*
|
||||
* @param {Object} data - The group data to be created.
|
||||
* @returns {Promise<Object>} The created group document.
|
||||
*/
|
||||
const createGroup = async (data) => {
|
||||
return await Group.create(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Count the number of group documents in the collection based on the provided filter.
|
||||
*
|
||||
* @param {Object} [filter={}] - The filter to apply when counting the documents.
|
||||
* @returns {Promise<number>} The count of documents that match the filter.
|
||||
*/
|
||||
const countGroups = (filter = {}) => {
|
||||
return Group.countDocuments(filter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a group by its unique ID only if no user is assigned to it.
|
||||
*
|
||||
* @param {string} groupId - The ID of the group to delete.
|
||||
* @returns {Promise<{ deletedCount: number, message: string }>} An object indicating the number of deleted documents.
|
||||
*/
|
||||
const deleteGroupById = async (groupId) => {
|
||||
// Check if any users reference the group
|
||||
const userCount = await User.countDocuments({ groups: groupId });
|
||||
if (userCount > 0) {
|
||||
return { deletedCount: 0, message: `Cannot delete group; it is assigned to ${userCount} user(s).` };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await Group.deleteOne({ _id: groupId });
|
||||
if (result.deletedCount === 0) {
|
||||
return { deletedCount: 0, message: 'No group found with that ID.' };
|
||||
}
|
||||
return { deletedCount: result.deletedCount, message: 'Group was deleted successfully.' };
|
||||
} catch (error) {
|
||||
throw new Error('Error deleting group: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Override deletion of a group by its unique ID.
|
||||
* This function first removes the group ObjectId from all users' groups arrays,
|
||||
* then proceeds to delete the group document.
|
||||
*
|
||||
* @param {string} groupId - The ID of the group to delete.
|
||||
* @returns {Promise<{ deletedCount: number, message: string }>} An object indicating the deletion result.
|
||||
*/
|
||||
const overrideDeleteGroupById = async (groupId) => {
|
||||
// Remove group references from all users
|
||||
await User.updateMany(
|
||||
{ groups: groupId },
|
||||
{ $pull: { groups: groupId } },
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await Group.deleteOne({ _id: groupId });
|
||||
if (result.deletedCount === 0) {
|
||||
return { deletedCount: 0, message: 'No group found with that ID.' };
|
||||
}
|
||||
return { deletedCount: result.deletedCount, message: 'Group was deleted successfully (override).' };
|
||||
} catch (error) {
|
||||
throw new Error('Error deleting group: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getGroupById,
|
||||
findGroup,
|
||||
updateGroup,
|
||||
createGroup,
|
||||
countGroups,
|
||||
deleteGroupById,
|
||||
overrideDeleteGroupById,
|
||||
};
|
||||
@@ -40,6 +40,7 @@ const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset')
|
||||
const { createToken, findToken, updateToken, deleteTokens } = require('./Token');
|
||||
const Balance = require('./Balance');
|
||||
const User = require('./User');
|
||||
const Group = require('./Group');
|
||||
const Key = require('./Key');
|
||||
|
||||
module.exports = {
|
||||
@@ -92,6 +93,7 @@ module.exports = {
|
||||
countActiveSessions,
|
||||
|
||||
User,
|
||||
Group,
|
||||
Key,
|
||||
Balance,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||
const { findGroup } = require('~/models/groupMethods');
|
||||
const { hashToken } = require('~/server/utils/crypto');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
@@ -105,6 +106,71 @@ function convertToUsername(input, defaultValue = '') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts roles from the specified token using configuration from environment variables.
|
||||
* @param {object} tokenset - The token set returned by the OpenID provider.
|
||||
* @returns {Array} The roles extracted from the token.
|
||||
*/
|
||||
function extractRoles(tokenset) {
|
||||
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
|
||||
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
|
||||
const token =
|
||||
requiredRoleTokenKind === 'access'
|
||||
? jwtDecode(tokenset.access_token)
|
||||
: jwtDecode(tokenset.id_token);
|
||||
const pathParts = requiredRoleParameterPath.split('.');
|
||||
let found = true;
|
||||
const roles = pathParts.reduce((acc, key) => {
|
||||
if (!acc || !(key in acc)) {
|
||||
found = false;
|
||||
return [];
|
||||
}
|
||||
return acc[key];
|
||||
}, token);
|
||||
if (!found) {
|
||||
logger.error(
|
||||
`[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
|
||||
);
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's groups based on the provided roles.
|
||||
* It removes any existing OpenID group references and then adds the groups
|
||||
* that match the roles from the external group collection.
|
||||
*
|
||||
* @param {object} user - The user object.
|
||||
* @param {Array} roles - The roles extracted from the token.
|
||||
* @returns {Promise<Array>} The updated groups array.
|
||||
*/
|
||||
async function updateUserGroups(user, roles) {
|
||||
user.groups = user.groups || [];
|
||||
// Remove existing OpenID group references.
|
||||
const currentOpenIdGroups = await findGroup({
|
||||
_id: { $in: user.groups },
|
||||
provider: 'openid',
|
||||
});
|
||||
const currentOpenIdGroupIds = new Set(
|
||||
currentOpenIdGroups.map((g) => g._id.toString()),
|
||||
);
|
||||
user.groups = user.groups.filter(
|
||||
(id) => !currentOpenIdGroupIds.has(id.toString()),
|
||||
);
|
||||
|
||||
// Look up groups matching the roles.
|
||||
const matchingGroups = await findGroup({
|
||||
provider: 'openid',
|
||||
externalId: { $in: roles },
|
||||
});
|
||||
matchingGroups.forEach((group) => {
|
||||
if (!user.groups.some((id) => id.toString() === group._id.toString())) {
|
||||
user.groups.push(group._id);
|
||||
}
|
||||
});
|
||||
return user.groups;
|
||||
}
|
||||
|
||||
async function setupOpenId() {
|
||||
try {
|
||||
if (process.env.PROXY) {
|
||||
@@ -134,8 +200,6 @@ async function setupOpenId() {
|
||||
}
|
||||
const client = new issuer.Client(clientMetadata);
|
||||
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
|
||||
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
|
||||
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
|
||||
const openidLogin = new OpenIDStrategy(
|
||||
{
|
||||
client,
|
||||
@@ -145,8 +209,13 @@ async function setupOpenId() {
|
||||
},
|
||||
async (tokenset, userinfo, done) => {
|
||||
try {
|
||||
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
|
||||
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
|
||||
logger.info(
|
||||
`[openidStrategy] verify login openidId: ${userinfo.sub}`,
|
||||
);
|
||||
logger.debug('[openidStrategy] verify login tokenset and userinfo', {
|
||||
tokenset,
|
||||
userinfo,
|
||||
});
|
||||
|
||||
let user = await findUser({ openidId: userinfo.sub });
|
||||
logger.info(
|
||||
@@ -164,29 +233,10 @@ async function setupOpenId() {
|
||||
|
||||
const fullName = getFullName(userinfo);
|
||||
|
||||
// Check for the required role using extracted roles.
|
||||
let roles = [];
|
||||
if (requiredRole) {
|
||||
let decodedToken = '';
|
||||
if (requiredRoleTokenKind === 'access') {
|
||||
decodedToken = jwtDecode(tokenset.access_token);
|
||||
} else if (requiredRoleTokenKind === 'id') {
|
||||
decodedToken = jwtDecode(tokenset.id_token);
|
||||
}
|
||||
const pathParts = requiredRoleParameterPath.split('.');
|
||||
let found = true;
|
||||
let roles = pathParts.reduce((o, key) => {
|
||||
if (o === null || o === undefined || !(key in o)) {
|
||||
found = false;
|
||||
return [];
|
||||
}
|
||||
return o[key];
|
||||
}, decodedToken);
|
||||
|
||||
if (!found) {
|
||||
logger.error(
|
||||
`[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
|
||||
);
|
||||
}
|
||||
|
||||
roles = extractRoles(tokenset);
|
||||
if (!roles.includes(requiredRole)) {
|
||||
return done(null, false, {
|
||||
message: `You must have the "${requiredRole}" role to log in.`,
|
||||
@@ -243,6 +293,10 @@ async function setupOpenId() {
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredRole) {
|
||||
await updateUserGroups(user, roles);
|
||||
}
|
||||
|
||||
user = await updateUser(user._id, user);
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -19,6 +19,9 @@ jest.mock('~/models/userMethods', () => ({
|
||||
createUser: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
}));
|
||||
jest.mock('~/models/groupMethods', () => ({
|
||||
findGroup: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
jest.mock('~/server/utils/crypto', () => ({
|
||||
hashToken: jest.fn().mockResolvedValue('hashed-token'),
|
||||
}));
|
||||
|
||||
@@ -76,6 +76,19 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||
<div>
|
||||
{title}
|
||||
<div className="text-text-secondary">{description}</div>
|
||||
{spec.badges && spec.badges.length > 0 && (
|
||||
<div className="mt-1 flex gap-2">
|
||||
{spec.badges.map((badge, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold shadow-sm"
|
||||
style={{ backgroundColor: badge.color, color: '#fff' }}
|
||||
>
|
||||
{badge.text}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,9 +19,22 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
||||
const localize = useLocalize();
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const modularChat = useRecoilValue(store.modularChat);
|
||||
const user = useRecoilValue(store.user);
|
||||
const getDefaultConversation = useDefaultConvo();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
|
||||
const allowedModelSpecs = useMemo(() => {
|
||||
if (!modelSpecs) {return [];}
|
||||
return modelSpecs.filter(spec => {
|
||||
// If no groups defined for spec, allow it.
|
||||
if (!spec.groups || spec.groups.length === 0) {return true;}
|
||||
// Otherwise, check if the user exists and has groups.
|
||||
if (!user || !user.groups || user.groups.length === 0) {return false;}
|
||||
// Check if at least one of the spec's groups is in the user's groups.
|
||||
return spec.groups.some(groupId => user.groups.includes(groupId));
|
||||
});
|
||||
}, [modelSpecs, user]);
|
||||
|
||||
const onSelectSpec = (spec: TModelSpec) => {
|
||||
const { preset } = spec;
|
||||
preset.iconURL = getModelSpecIconURL(spec);
|
||||
@@ -82,21 +95,15 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
||||
};
|
||||
|
||||
const selected = useMemo(() => {
|
||||
const spec = modelSpecs?.find((spec) => spec.name === conversation?.spec);
|
||||
if (!spec) {
|
||||
return undefined;
|
||||
}
|
||||
return spec;
|
||||
}, [modelSpecs, conversation?.spec]);
|
||||
const spec = allowedModelSpecs.find((spec) => spec.name === conversation?.spec);
|
||||
return spec || undefined;
|
||||
}, [allowedModelSpecs, conversation?.spec]);
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
const menuItems = menuRef.current?.querySelectorAll('[role="option"]');
|
||||
if (!menuItems) {
|
||||
return;
|
||||
}
|
||||
if (!menuItems.length) {
|
||||
if (!menuItems || !menuItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +139,7 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
||||
endpointsConfig={endpointsConfig}
|
||||
/>
|
||||
<Portal>
|
||||
{modelSpecs && modelSpecs.length && (
|
||||
{allowedModelSpecs && allowedModelSpecs.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
@@ -154,7 +161,7 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
||||
className="models-scrollbar mt-2 max-h-[65vh] min-w-[340px] max-w-xs overflow-y-auto rounded-lg border border-gray-100 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
|
||||
>
|
||||
<ModelSpecs
|
||||
specs={modelSpecs}
|
||||
specs={allowedModelSpecs}
|
||||
selected={selected}
|
||||
setSelected={onSelectSpec}
|
||||
endpointsConfig={endpointsConfig}
|
||||
|
||||
69
config/assign-group.js
Normal file
69
config/assign-group.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const path = require('path');
|
||||
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
||||
const { askQuestion, silentExit } = require('./helpers');
|
||||
const User = require('~/models/User');
|
||||
const Group = require('~/models/Group');
|
||||
const connect = require('./connect');
|
||||
|
||||
(async () => {
|
||||
await connect();
|
||||
|
||||
console.purple('---------------------------------------');
|
||||
console.purple('Assign a Group to a User');
|
||||
console.purple('---------------------------------------');
|
||||
|
||||
// Read arguments from CLI or prompt the user
|
||||
const userEmail = process.argv[2] || (await askQuestion('User email: '));
|
||||
const groupName = process.argv[3] || (await askQuestion('Group name to assign: '));
|
||||
|
||||
// Validate email format
|
||||
if (!userEmail.includes('@')) {
|
||||
console.red('Error: Invalid email address!');
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
// Find the group by name
|
||||
const group = await Group.findOne({ name: groupName });
|
||||
if (!group) {
|
||||
console.red('Error: No group with that name was found!');
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
// Find the user by email
|
||||
const user = await User.findOne({ email: userEmail });
|
||||
if (!user) {
|
||||
console.red('Error: No user with that email was found!');
|
||||
silentExit(1);
|
||||
}
|
||||
console.purple(`Found user: ${user.email}`);
|
||||
|
||||
// Assign the group to the user if not already assigned
|
||||
try {
|
||||
if (!Array.isArray(user.groups)) {
|
||||
user.groups = [];
|
||||
}
|
||||
|
||||
// Convert both user group IDs and the target group ID to strings for comparison
|
||||
const groupIdStr = group._id.toString();
|
||||
const userGroupIds = user.groups.map(id => id.toString());
|
||||
|
||||
if (!userGroupIds.includes(groupIdStr)) {
|
||||
user.groups.push(group._id);
|
||||
await user.save();
|
||||
console.green(`User ${user.email} successfully assigned to group ${group.name}!`);
|
||||
} else {
|
||||
console.yellow(`User ${user.email} is already assigned to group ${group.name}.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.red('Error assigning group to user: ' + error.message);
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
silentExit(0);
|
||||
})();
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('There was an uncaught error:');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
62
config/create-group.js
Normal file
62
config/create-group.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const path = require('path');
|
||||
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
||||
const { askQuestion, silentExit } = require('./helpers');
|
||||
const Group = require('~/models/Group');
|
||||
const connect = require('./connect');
|
||||
|
||||
(async () => {
|
||||
await connect();
|
||||
|
||||
console.purple('---------------------------------------');
|
||||
console.purple('Create a New Group');
|
||||
console.purple('---------------------------------------');
|
||||
|
||||
// Prompt for basic group info.
|
||||
const groupName = process.argv[2] || (await askQuestion('Group name: '));
|
||||
const groupDescription =
|
||||
process.argv[3] || (await askQuestion('Group description (optional): '));
|
||||
|
||||
// Ask for the group type (local or openid; defaults to local)
|
||||
let groupType =
|
||||
process.argv[4] ||
|
||||
(await askQuestion('Group type (local/openid, default is local): '));
|
||||
groupType = groupType.trim().toLowerCase() || 'local';
|
||||
|
||||
let groupData;
|
||||
if (groupType === 'openid') {
|
||||
// For OpenID groups, prompt for an external ID.
|
||||
const externalId =
|
||||
process.argv[5] ||
|
||||
(await askQuestion('External ID for OpenID group: '));
|
||||
groupData = {
|
||||
name: groupName,
|
||||
description: groupDescription,
|
||||
provider: 'openid',
|
||||
externalId: externalId.trim(),
|
||||
};
|
||||
} else {
|
||||
// For local groups, we only need name and description.
|
||||
groupData = {
|
||||
name: groupName,
|
||||
description: groupDescription,
|
||||
provider: 'local',
|
||||
};
|
||||
}
|
||||
|
||||
// Create the group document
|
||||
let group;
|
||||
try {
|
||||
group = await Group.create(groupData);
|
||||
} catch (error) {
|
||||
console.red('Error creating group: ' + error.message);
|
||||
silentExit(1);
|
||||
}
|
||||
console.green(`Group created successfully with id: ${group._id}`);
|
||||
silentExit(0);
|
||||
})();
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('There was an uncaught error:');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -104,6 +104,31 @@ registration:
|
||||
# userMax: 50
|
||||
# userWindowInMinutes: 60 # Rate limit window for conversation imports per user
|
||||
|
||||
|
||||
# Example Model Specifications
|
||||
#modelSpecs:
|
||||
# enforce: true
|
||||
# prioritize: true
|
||||
# list:
|
||||
# - name: "4o-mini"
|
||||
# label: "4o-mini"
|
||||
# groups:
|
||||
# - "67b9a5c64165f31925e9b25a"
|
||||
# - "67b9a5c64165f31925e9b25f"
|
||||
# description: "The most advanced frontier model from Azure OpenAI, suitable to solve complex multi-step problems."
|
||||
# iconURL: "https://www.librechat.ai/librechat.png"
|
||||
# badges:
|
||||
# - text: "Test"
|
||||
# color: "#FF0000"
|
||||
# - text: "Beta"
|
||||
# color: "#00FF00"
|
||||
# preset:
|
||||
# default: true
|
||||
# endpoint: "azureOpenAI"
|
||||
# model: "gpt-4o-mini"
|
||||
# modelLabel: "4o-mini"
|
||||
|
||||
|
||||
# Example Actions Object Structure
|
||||
actions:
|
||||
allowedDomains:
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"reset-password": "node config/reset-password.js",
|
||||
"ban-user": "node config/ban-user.js",
|
||||
"delete-user": "node config/delete-user.js",
|
||||
"create-group": "node config/create-group.js",
|
||||
"assign-group": "node config/assign-group.js",
|
||||
"update-banner": "node config/update-banner.js",
|
||||
"delete-banner": "node config/delete-banner.js",
|
||||
"backend": "cross-env NODE_ENV=production node api/server/index.js",
|
||||
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
authTypeSchema,
|
||||
} from './schemas';
|
||||
|
||||
export type TBadge = {
|
||||
text: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type TModelSpec = {
|
||||
name: string;
|
||||
label: string;
|
||||
@@ -19,8 +24,15 @@ export type TModelSpec = {
|
||||
showIconInHeader?: boolean;
|
||||
iconURL?: string | EModelEndpoint; // Allow using project-included icons
|
||||
authType?: AuthType;
|
||||
groups?: string[];
|
||||
badges?: TBadge[];
|
||||
};
|
||||
|
||||
export const tBadgeSchema = z.object({
|
||||
text: z.string(),
|
||||
color: z.string(),
|
||||
});
|
||||
|
||||
export const tModelSpecSchema = z.object({
|
||||
name: z.string(),
|
||||
label: z.string(),
|
||||
@@ -32,6 +44,8 @@ export const tModelSpecSchema = z.object({
|
||||
showIconInHeader: z.boolean().optional(),
|
||||
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
|
||||
authType: authTypeSchema.optional(),
|
||||
groups: z.array(z.string()).optional(),
|
||||
badges: z.array(tBadgeSchema).optional(),
|
||||
});
|
||||
|
||||
export const specsConfigSchema = z.object({
|
||||
|
||||
@@ -106,6 +106,16 @@ export type TBackupCode = {
|
||||
usedAt: Date | null;
|
||||
};
|
||||
|
||||
export type TGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
externalId?: string;
|
||||
provider: 'local' | 'openid';
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type TUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
@@ -117,6 +127,7 @@ export type TUser = {
|
||||
plugins?: string[];
|
||||
twoFactorEnabled?: boolean;
|
||||
backupCodes?: TBackupCode[];
|
||||
groups: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import categoriesSchema from './schema/categories';
|
||||
import conversationTagSchema from './schema/conversationTag';
|
||||
import convoSchema from './schema/convo';
|
||||
import fileSchema from './schema/file';
|
||||
import groupSchema from './schema/group';
|
||||
import keySchema from './schema/key';
|
||||
import messageSchema from './schema/message';
|
||||
import pluginAuthSchema from './schema/pluginAuth';
|
||||
@@ -32,6 +33,7 @@ export {
|
||||
conversationTagSchema,
|
||||
convoSchema,
|
||||
fileSchema,
|
||||
groupSchema,
|
||||
keySchema,
|
||||
messageSchema,
|
||||
pluginAuthSchema,
|
||||
|
||||
39
packages/data-schemas/src/schema/group.ts
Normal file
39
packages/data-schemas/src/schema/group.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Schema, Document } from 'mongoose';
|
||||
|
||||
export interface IGroup extends Document {
|
||||
name: string;
|
||||
description?: string;
|
||||
externalId?: string;
|
||||
provider: 'local' | 'openid';
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
const groupSchema = new Schema<IGroup>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
externalId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: function (this: IGroup) {
|
||||
return this.provider !== 'local';
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'local',
|
||||
enum: ['local', 'openid'],
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
export default groupSchema;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Schema, Document } from 'mongoose';
|
||||
import { Schema, Document, Types } from 'mongoose';
|
||||
import { SystemRoles } from 'librechat-data-provider';
|
||||
|
||||
export interface IUser extends Document {
|
||||
@@ -18,6 +18,7 @@ export interface IUser extends Document {
|
||||
discordId?: string;
|
||||
appleId?: string;
|
||||
plugins?: unknown[];
|
||||
groups?: Types.ObjectId[];
|
||||
twoFactorEnabled?: boolean;
|
||||
totpSecret?: string;
|
||||
backupCodes?: Array<{
|
||||
@@ -135,6 +136,11 @@ const User = new Schema<IUser>(
|
||||
plugins: {
|
||||
type: Array,
|
||||
},
|
||||
groups: {
|
||||
type: [Schema.Types.ObjectId],
|
||||
ref: 'Group',
|
||||
default: [],
|
||||
},
|
||||
twoFactorEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
Reference in New Issue
Block a user