🛂 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:
Danny Avila 2025-09-05 11:09:32 -04:00 committed by GitHub
parent eef93024d5
commit 75dd6fb28b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 37 additions and 13 deletions

View file

@ -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 () => {

View file

@ -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(),

View file

@ -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) {

View file

@ -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(),

View file

@ -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);
} }