mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 18:00:15 +01:00
🛂 refactor: Centralize fileStrategy Resolution for OpenID, SAML, and Social Logins (#9468)
* 🔑 refactor: `fileStrategy` for OpenID, SAML, and Social logins
* ci: Update Apple strategy tests to use correct isEnabled import and enhance handleExistingUser call
This commit is contained in:
parent
eef93024d5
commit
75dd6fb28b
5 changed files with 37 additions and 13 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
const { isEnabled } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { Strategy: AppleStrategy } = require('passport-apple');
|
const { Strategy: AppleStrategy } = require('passport-apple');
|
||||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
const { createSocialUser, handleExistingUser } = require('./process');
|
const { createSocialUser, handleExistingUser } = require('./process');
|
||||||
const { isEnabled } = require('~/server/utils');
|
|
||||||
const socialLogin = require('./socialLogin');
|
const socialLogin = require('./socialLogin');
|
||||||
const { findUser } = require('~/models');
|
const { findUser } = require('~/models');
|
||||||
const { User } = require('~/db/models');
|
const { User } = require('~/db/models');
|
||||||
|
|
@ -17,6 +17,8 @@ jest.mock('@librechat/data-schemas', () => {
|
||||||
logger: {
|
logger: {
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -24,12 +26,19 @@ jest.mock('./process', () => ({
|
||||||
createSocialUser: jest.fn(),
|
createSocialUser: jest.fn(),
|
||||||
handleExistingUser: jest.fn(),
|
handleExistingUser: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('~/server/utils', () => ({
|
jest.mock('@librechat/api', () => ({
|
||||||
|
...jest.requireActual('@librechat/api'),
|
||||||
isEnabled: jest.fn(),
|
isEnabled: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('~/models', () => ({
|
jest.mock('~/models', () => ({
|
||||||
findUser: jest.fn(),
|
findUser: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
fileStrategy: 'local',
|
||||||
|
balance: { enabled: false },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Apple Login Strategy', () => {
|
describe('Apple Login Strategy', () => {
|
||||||
let mongoServer;
|
let mongoServer;
|
||||||
|
|
@ -288,7 +297,14 @@ describe('Apple Login Strategy', () => {
|
||||||
|
|
||||||
expect(mockVerifyCallback).toHaveBeenCalledWith(null, existingUser);
|
expect(mockVerifyCallback).toHaveBeenCalledWith(null, existingUser);
|
||||||
expect(existingUser.avatarUrl).toBeNull(); // As per getProfileDetails
|
expect(existingUser.avatarUrl).toBeNull(); // As per getProfileDetails
|
||||||
expect(handleExistingUser).toHaveBeenCalledWith(existingUser, null);
|
expect(handleExistingUser).toHaveBeenCalledWith(
|
||||||
|
existingUser,
|
||||||
|
null,
|
||||||
|
expect.objectContaining({
|
||||||
|
fileStrategy: 'local',
|
||||||
|
balance: { enabled: false },
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing idToken gracefully', async () => {
|
it('should handle missing idToken gracefully', async () => {
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,7 @@ async function setupOpenId() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = {
|
user = {
|
||||||
provider: 'openid',
|
provider: 'openid',
|
||||||
|
|
@ -409,7 +410,6 @@ async function setupOpenId() {
|
||||||
idOnTheSource: userinfo.oid,
|
idOnTheSource: userinfo.oid,
|
||||||
};
|
};
|
||||||
|
|
||||||
const appConfig = await getAppConfig();
|
|
||||||
const balanceConfig = getBalanceConfig(appConfig);
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
user = await createUser(user, balanceConfig, true, true);
|
user = await createUser(user, balanceConfig, true, true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -438,7 +438,9 @@ async function setupOpenId() {
|
||||||
userinfo.sub,
|
userinfo.sub,
|
||||||
);
|
);
|
||||||
if (imageBuffer) {
|
if (imageBuffer) {
|
||||||
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
const { saveBuffer } = getStrategyFunctions(
|
||||||
|
appConfig?.fileStrategy ?? process.env.CDN_PROVIDER,
|
||||||
|
);
|
||||||
const imagePath = await saveBuffer({
|
const imagePath = await saveBuffer({
|
||||||
fileName,
|
fileName,
|
||||||
userId: user._id.toString(),
|
userId: user._id.toString(),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ const { FileSources } = require('librechat-data-provider');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
||||||
const { updateUser, createUser, getUserById } = require('~/models');
|
const { updateUser, createUser, getUserById } = require('~/models');
|
||||||
const { getAppConfig } = require('~/server/services/Config');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the avatar URL of an existing user. If the user's avatar URL does not include the query parameter
|
* Updates the avatar URL of an existing user. If the user's avatar URL does not include the query parameter
|
||||||
|
|
@ -12,14 +11,15 @@ const { getAppConfig } = require('~/server/services/Config');
|
||||||
*
|
*
|
||||||
* @param {IUser} oldUser - The existing user object that needs to be updated.
|
* @param {IUser} oldUser - The existing user object that needs to be updated.
|
||||||
* @param {string} avatarUrl - The new avatar URL to be set for the user.
|
* @param {string} avatarUrl - The new avatar URL to be set for the user.
|
||||||
|
* @param {AppConfig} appConfig - The application configuration object.
|
||||||
*
|
*
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* The function updates the user's avatar and saves the user object. It does not return any value.
|
* The function updates the user's avatar and saves the user object. It does not return any value.
|
||||||
*
|
*
|
||||||
* @throws {Error} Throws an error if there's an issue saving the updated user object.
|
* @throws {Error} Throws an error if there's an issue saving the updated user object.
|
||||||
*/
|
*/
|
||||||
const handleExistingUser = async (oldUser, avatarUrl) => {
|
const handleExistingUser = async (oldUser, avatarUrl, appConfig) => {
|
||||||
const fileStrategy = process.env.CDN_PROVIDER;
|
const fileStrategy = appConfig?.fileStrategy ?? process.env.CDN_PROVIDER;
|
||||||
const isLocal = fileStrategy === FileSources.local;
|
const isLocal = fileStrategy === FileSources.local;
|
||||||
|
|
||||||
let updatedAvatar = false;
|
let updatedAvatar = false;
|
||||||
|
|
@ -56,6 +56,7 @@ const handleExistingUser = async (oldUser, avatarUrl) => {
|
||||||
* @param {string} params.providerId - The provider-specific ID of the user.
|
* @param {string} params.providerId - The provider-specific ID of the user.
|
||||||
* @param {string} params.username - The username of the new user.
|
* @param {string} params.username - The username of the new user.
|
||||||
* @param {string} params.name - The name of the new user.
|
* @param {string} params.name - The name of the new user.
|
||||||
|
* @param {AppConfig} appConfig - The application configuration object.
|
||||||
* @param {boolean} [params.emailVerified=false] - Optional. Indicates whether the user's email is verified. Defaults to false.
|
* @param {boolean} [params.emailVerified=false] - Optional. Indicates whether the user's email is verified. Defaults to false.
|
||||||
*
|
*
|
||||||
* @returns {Promise<User>}
|
* @returns {Promise<User>}
|
||||||
|
|
@ -71,6 +72,7 @@ const createSocialUser = async ({
|
||||||
providerId,
|
providerId,
|
||||||
username,
|
username,
|
||||||
name,
|
name,
|
||||||
|
appConfig,
|
||||||
emailVerified,
|
emailVerified,
|
||||||
}) => {
|
}) => {
|
||||||
const update = {
|
const update = {
|
||||||
|
|
@ -83,10 +85,9 @@ const createSocialUser = async ({
|
||||||
emailVerified,
|
emailVerified,
|
||||||
};
|
};
|
||||||
|
|
||||||
const appConfig = await getAppConfig();
|
|
||||||
const balanceConfig = getBalanceConfig(appConfig);
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
const newUserId = await createUser(update, balanceConfig);
|
const newUserId = await createUser(update, balanceConfig);
|
||||||
const fileStrategy = process.env.CDN_PROVIDER;
|
const fileStrategy = appConfig?.fileStrategy ?? process.env.CDN_PROVIDER;
|
||||||
const isLocal = fileStrategy === FileSources.local;
|
const isLocal = fileStrategy === FileSources.local;
|
||||||
|
|
||||||
if (!isLocal) {
|
if (!isLocal) {
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,7 @@ async function setupSaml() {
|
||||||
getUserName(profile) || getGivenName(profile) || getEmail(profile),
|
getUserName(profile) || getGivenName(profile) || getEmail(profile),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = {
|
user = {
|
||||||
provider: 'saml',
|
provider: 'saml',
|
||||||
|
|
@ -229,7 +230,6 @@ async function setupSaml() {
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
name: fullName,
|
name: fullName,
|
||||||
};
|
};
|
||||||
const appConfig = await getAppConfig();
|
|
||||||
const balanceConfig = getBalanceConfig(appConfig);
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
user = await createUser(user, balanceConfig, true, true);
|
user = await createUser(user, balanceConfig, true, true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -250,7 +250,9 @@ async function setupSaml() {
|
||||||
fileName = profile.nameID + '.png';
|
fileName = profile.nameID + '.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
const { saveBuffer } = getStrategyFunctions(
|
||||||
|
appConfig?.fileStrategy ?? process.env.CDN_PROVIDER,
|
||||||
|
);
|
||||||
const imagePath = await saveBuffer({
|
const imagePath = await saveBuffer({
|
||||||
fileName,
|
fileName,
|
||||||
userId: user._id.toString(),
|
userId: user._id.toString(),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const { isEnabled } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { ErrorTypes } = require('librechat-data-provider');
|
const { ErrorTypes } = require('librechat-data-provider');
|
||||||
const { createSocialUser, handleExistingUser } = require('./process');
|
const { createSocialUser, handleExistingUser } = require('./process');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { findUser } = require('~/models');
|
const { findUser } = require('~/models');
|
||||||
|
|
||||||
const socialLogin =
|
const socialLogin =
|
||||||
|
|
@ -12,11 +13,12 @@ const socialLogin =
|
||||||
profile,
|
profile,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig();
|
||||||
const existingUser = await findUser({ email: email.trim() });
|
const existingUser = await findUser({ email: email.trim() });
|
||||||
const ALLOW_SOCIAL_REGISTRATION = isEnabled(process.env.ALLOW_SOCIAL_REGISTRATION);
|
const ALLOW_SOCIAL_REGISTRATION = isEnabled(process.env.ALLOW_SOCIAL_REGISTRATION);
|
||||||
|
|
||||||
if (existingUser?.provider === provider) {
|
if (existingUser?.provider === provider) {
|
||||||
await handleExistingUser(existingUser, avatarUrl);
|
await handleExistingUser(existingUser, avatarUrl, appConfig);
|
||||||
return cb(null, existingUser);
|
return cb(null, existingUser);
|
||||||
} else if (existingUser) {
|
} else if (existingUser) {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -38,6 +40,7 @@ const socialLogin =
|
||||||
username,
|
username,
|
||||||
name,
|
name,
|
||||||
emailVerified,
|
emailVerified,
|
||||||
|
appConfig,
|
||||||
});
|
});
|
||||||
return cb(null, newUser);
|
return cb(null, newUser);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue