From 20aa0be85d0371e224228a6031ced4a166c54d3b Mon Sep 17 00:00:00 2001 From: Igor <31377238+TonyMahoney@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:56:44 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Add=20Missing=20"Bal?= =?UTF-8?q?ance"=20Localization=20For=20All=20Languages=20(#5594)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update AccountSettings.tsx --------- Co-authored-by: Danny Avila --- client/src/components/Nav/AccountSettings.tsx | 2 +- client/src/localization/languages/Ar.ts | 1 + client/src/localization/languages/Br.ts | 1 + client/src/localization/languages/De.ts | 1 + client/src/localization/languages/Eng.ts | 1 + client/src/localization/languages/Es.ts | 1 + client/src/localization/languages/Fi.ts | 1 + client/src/localization/languages/Fr.ts | 1 + client/src/localization/languages/He.ts | 1 + client/src/localization/languages/Id.ts | 1 + client/src/localization/languages/It.ts | 1 + client/src/localization/languages/Jp.ts | 1 + client/src/localization/languages/Ko.ts | 1 + client/src/localization/languages/Nl.ts | 1 + client/src/localization/languages/Pl.ts | 1 + client/src/localization/languages/Ru.ts | 1 + client/src/localization/languages/Sv.ts | 1 + client/src/localization/languages/Tr.ts | 1 + client/src/localization/languages/Vi.ts | 1 + client/src/localization/languages/Zh.ts | 1 + client/src/localization/languages/ZhTraditional.ts | 1 + 21 files changed, 21 insertions(+), 1 deletion(-) diff --git a/client/src/components/Nav/AccountSettings.tsx b/client/src/components/Nav/AccountSettings.tsx index 1bedbfd22fe..968add12c03 100644 --- a/client/src/components/Nav/AccountSettings.tsx +++ b/client/src/components/Nav/AccountSettings.tsx @@ -80,7 +80,7 @@ function AccountSettings() { !isNaN(parseFloat(balanceQuery.data)) && ( <>
- {`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`} + {localize('com_nav_balance')}: ${parseFloat(balanceQuery.data).toFixed(2)}
diff --git a/client/src/localization/languages/Ar.ts b/client/src/localization/languages/Ar.ts index 56da6cfbe18..ad5d0c61342 100644 --- a/client/src/localization/languages/Ar.ts +++ b/client/src/localization/languages/Ar.ts @@ -914,4 +914,5 @@ export default { com_ui_collapse_chat: 'طي الدردشة', com_endpoint_message_new: 'رسالة {0}', com_ui_speech_while_submitting: 'لا يمكن إرسال الكلام أثناء إنشاء الرد', + com_nav_balance: 'توازن', }; diff --git a/client/src/localization/languages/Br.ts b/client/src/localization/languages/Br.ts index 268d5845411..aafeedae6cd 100644 --- a/client/src/localization/languages/Br.ts +++ b/client/src/localization/languages/Br.ts @@ -816,4 +816,5 @@ export default { com_ui_decline: 'Eu não aceito', com_ui_terms_and_conditions: 'Termos e Condições', com_ui_no_terms_content: 'Nenhum conteúdo de termos e condições para exibir', + com_nav_balance: 'Equilíbrio', }; diff --git a/client/src/localization/languages/De.ts b/client/src/localization/languages/De.ts index 0d82a70be14..39ccff1953a 100644 --- a/client/src/localization/languages/De.ts +++ b/client/src/localization/languages/De.ts @@ -946,4 +946,5 @@ export default { com_ui_collapse_chat: 'Chat einklappen', com_ui_speech_while_submitting: 'Spracheingabe nicht möglich während eine Antwort generiert wird', com_endpoint_message_new: 'Nachricht {0}', + com_nav_balance: 'Gleichgewicht', }; diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index cb7dcc9e3de..5da1f46d6c3 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -917,4 +917,5 @@ export default { com_ui_terms_and_conditions: 'Terms and Conditions', com_ui_no_terms_content: 'No terms and conditions content to display', com_ui_speech_while_submitting: 'Can\'t submit speech while a response is being generated', + com_nav_balance: 'Balance', }; diff --git a/client/src/localization/languages/Es.ts b/client/src/localization/languages/Es.ts index 33121ea9e8f..28b86778b93 100644 --- a/client/src/localization/languages/Es.ts +++ b/client/src/localization/languages/Es.ts @@ -1203,4 +1203,5 @@ export default { com_endpoint_message_new: 'Mensaje {0}', com_ui_speech_while_submitting: 'No se puede enviar un mensaje de voz mientras se está generando una respuesta', + com_nav_balance: 'Balance', }; diff --git a/client/src/localization/languages/Fi.ts b/client/src/localization/languages/Fi.ts index 9c66993cb7d..ddb05ce6daf 100644 --- a/client/src/localization/languages/Fi.ts +++ b/client/src/localization/languages/Fi.ts @@ -711,4 +711,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'Saldo', }; diff --git a/client/src/localization/languages/Fr.ts b/client/src/localization/languages/Fr.ts index 0c9801fbdc2..20d448cfd44 100644 --- a/client/src/localization/languages/Fr.ts +++ b/client/src/localization/languages/Fr.ts @@ -965,4 +965,5 @@ export default { com_ui_speech_while_submitting: 'Impossible de soumettre un message vocal pendant la génération d\'une réponse', com_endpoint_message_new: 'Message {0}', + com_nav_balance: 'Équilibre', }; diff --git a/client/src/localization/languages/He.ts b/client/src/localization/languages/He.ts index a73e890fd78..c1400e6ddbf 100644 --- a/client/src/localization/languages/He.ts +++ b/client/src/localization/languages/He.ts @@ -440,4 +440,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'לְאַזֵן', }; diff --git a/client/src/localization/languages/Id.ts b/client/src/localization/languages/Id.ts index fb4f580cee4..4f37ff0f18a 100644 --- a/client/src/localization/languages/Id.ts +++ b/client/src/localization/languages/Id.ts @@ -397,4 +397,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'Keseimbangan', }; diff --git a/client/src/localization/languages/It.ts b/client/src/localization/languages/It.ts index fb8c9661b00..a3f3d4d8af9 100644 --- a/client/src/localization/languages/It.ts +++ b/client/src/localization/languages/It.ts @@ -959,4 +959,5 @@ export default { com_endpoint_message_new: 'Messaggio {0}', com_ui_speech_while_submitting: 'Impossibile inviare il messaggio mentre è in corso la generazione di una risposta', + com_nav_balance: 'Bilancia', }; diff --git a/client/src/localization/languages/Jp.ts b/client/src/localization/languages/Jp.ts index 5b2f17a23db..b36061213eb 100644 --- a/client/src/localization/languages/Jp.ts +++ b/client/src/localization/languages/Jp.ts @@ -912,4 +912,5 @@ export default { com_ui_collapse_chat: 'チャットを折りたたむ', com_endpoint_message_new: 'メッセージ {0}', com_ui_speech_while_submitting: '応答の生成中は音声を送信できません', + com_nav_balance: 'バランス', }; diff --git a/client/src/localization/languages/Ko.ts b/client/src/localization/languages/Ko.ts index 63029d895ba..d6c190d9c6e 100644 --- a/client/src/localization/languages/Ko.ts +++ b/client/src/localization/languages/Ko.ts @@ -1150,4 +1150,5 @@ export default { com_ui_collapse_chat: '채팅 접기', com_endpoint_message_new: '메시지 {0}', com_ui_speech_while_submitting: '응답 생성 중에는 음성을 전송할 수 없습니다', + com_nav_balance: '균형', }; diff --git a/client/src/localization/languages/Nl.ts b/client/src/localization/languages/Nl.ts index a31b9bf14fa..0c99e12e16d 100644 --- a/client/src/localization/languages/Nl.ts +++ b/client/src/localization/languages/Nl.ts @@ -350,4 +350,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'Evenwicht', }; diff --git a/client/src/localization/languages/Pl.ts b/client/src/localization/languages/Pl.ts index 814be45bb6b..87de49d9212 100644 --- a/client/src/localization/languages/Pl.ts +++ b/client/src/localization/languages/Pl.ts @@ -284,4 +284,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'Balansować', }; diff --git a/client/src/localization/languages/Ru.ts b/client/src/localization/languages/Ru.ts index 8b031746981..3285d003756 100644 --- a/client/src/localization/languages/Ru.ts +++ b/client/src/localization/languages/Ru.ts @@ -1176,4 +1176,5 @@ export default { com_ui_collapse_chat: 'Свернуть чат', com_endpoint_message_new: 'Сообщение {0}', com_ui_speech_while_submitting: 'Невозможно отправить голосовой ввод во время генерации ответа', + com_nav_balance: 'Баланс', }; diff --git a/client/src/localization/languages/Sv.ts b/client/src/localization/languages/Sv.ts index bb39216312f..57237980e31 100644 --- a/client/src/localization/languages/Sv.ts +++ b/client/src/localization/languages/Sv.ts @@ -337,4 +337,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'Balans', }; diff --git a/client/src/localization/languages/Tr.ts b/client/src/localization/languages/Tr.ts index 8731c7f38a9..57f285ba481 100644 --- a/client/src/localization/languages/Tr.ts +++ b/client/src/localization/languages/Tr.ts @@ -644,4 +644,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'Denge', }; diff --git a/client/src/localization/languages/Vi.ts b/client/src/localization/languages/Vi.ts index 32065ebc946..e399e9d9e02 100644 --- a/client/src/localization/languages/Vi.ts +++ b/client/src/localization/languages/Vi.ts @@ -335,4 +335,5 @@ export default { com_nav_lang_indonesia: 'Indonesia', com_nav_lang_hebrew: 'עברית', com_nav_lang_finnish: 'Suomi', + com_nav_balance: 'Sự cân bằng', }; diff --git a/client/src/localization/languages/Zh.ts b/client/src/localization/languages/Zh.ts index 3c23ca36bea..7984b6115f1 100644 --- a/client/src/localization/languages/Zh.ts +++ b/client/src/localization/languages/Zh.ts @@ -904,4 +904,5 @@ export default { com_ui_collapse_chat: '收起聊天', com_endpoint_message_new: '消息 {0}', com_ui_speech_while_submitting: '正在生成回复时无法提交语音', + com_nav_balance: '平衡', }; diff --git a/client/src/localization/languages/ZhTraditional.ts b/client/src/localization/languages/ZhTraditional.ts index 71d8bcb8059..8734a09d9be 100644 --- a/client/src/localization/languages/ZhTraditional.ts +++ b/client/src/localization/languages/ZhTraditional.ts @@ -881,4 +881,5 @@ export default { com_ui_collapse_chat: '收合對話', com_endpoint_message_new: '訊息 {0}', com_ui_speech_while_submitting: '正在產生回應時無法送出語音', + com_nav_balance: '平衡', }; From 93f5713c74709c81cfb85c4ae6e6bddd6df38182 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Mon, 3 Feb 2025 16:57:49 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=9B=9C=20ci:=20OpenID=20Strategy=20Te?= =?UTF-8?q?st=20Async=20Handling=20(#5613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/strategies/openidStrategy.spec.js | 396 +++++++++++++++++--------- 1 file changed, 261 insertions(+), 135 deletions(-) diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js index e2e7af87a65..7b8a3107a06 100644 --- a/api/strategies/openidStrategy.spec.js +++ b/api/strategies/openidStrategy.spec.js @@ -1,175 +1,301 @@ +const fetch = require('node-fetch'); const jwtDecode = require('jsonwebtoken/decode'); const { Issuer, Strategy: OpenIDStrategy } = require('openid-client'); -const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const User = require('~/models/User'); +const { findUser, createUser, updateUser } = require('~/models/userMethods'); const setupOpenId = require('./openidStrategy'); -jest.mock('jsonwebtoken/decode'); +// --- Mocks --- +jest.mock('node-fetch'); jest.mock('openid-client'); - +jest.mock('jsonwebtoken/decode'); jest.mock('~/server/services/Files/strategies', () => ({ getStrategyFunctions: jest.fn(() => ({ - saveBuffer: jest.fn(), + // You can modify this mock as needed (here returning a dummy function) + saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'), })), })); +jest.mock('~/models/userMethods', () => ({ + findUser: jest.fn(), + createUser: jest.fn(), + updateUser: jest.fn(), +})); +jest.mock('~/server/utils/crypto', () => ({ + hashToken: jest.fn().mockResolvedValue('hashed-token'), +})); +jest.mock('~/server/utils', () => ({ + isEnabled: jest.fn(() => false), // default to false, override per test if needed +})); +jest.mock('~/config', () => ({ + logger: { + info: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, +})); +// Mock Issuer.discover so that setupOpenId gets a fake issuer and client Issuer.discover = jest.fn().mockResolvedValue({ - Client: jest.fn(), + id_token_signing_alg_values_supported: ['RS256'], + Client: jest.fn().mockImplementation((clientMetadata) => { + return { + metadata: clientMetadata, + }; + }), +}); + +// To capture the verify callback from the strategy, we grab it from the mock constructor +let verifyCallback; +OpenIDStrategy.mockImplementation((options, verify) => { + verifyCallback = verify; + return { name: 'openid', options, verify }; }); describe('setupOpenId', () => { - const OLD_ENV = process.env; - describe('OpenIDStrategy', () => { - let validateFn, mongoServer; - - beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - await mongoose.connect(mongoUri); + // Helper to wrap the verify callback in a promise + const validate = (tokenset, userinfo) => + new Promise((resolve, reject) => { + verifyCallback(tokenset, userinfo, (err, user, details) => { + if (err) { + reject(err); + } else { + resolve({ user, details }); + } + }); }); - afterAll(async () => { - process.env = OLD_ENV; - await mongoose.disconnect(); - await mongoServer.stop(); - }); + const tokenset = { + id_token: 'fake_id_token', + access_token: 'fake_access_token', + }; - beforeEach(async () => { - jest.clearAllMocks(); - await User.deleteMany({}); - process.env = { - ...OLD_ENV, - OPENID_ISSUER: 'https://fake-issuer.com', - OPENID_CLIENT_ID: 'fake_client_id', - OPENID_CLIENT_SECRET: 'fake_client_secret', - DOMAIN_SERVER: 'https://example.com', - OPENID_CALLBACK_URL: '/callback', - OPENID_SCOPE: 'openid profile email', - OPENID_REQUIRED_ROLE: 'requiredRole', - OPENID_REQUIRED_ROLE_PARAMETER_PATH: 'roles', - OPENID_REQUIRED_ROLE_TOKEN_KIND: 'id', - }; - - jwtDecode.mockReturnValue({ - roles: ['requiredRole'], - }); + const baseUserinfo = { + sub: '1234', + email: 'test@example.com', + email_verified: true, + given_name: 'First', + family_name: 'Last', + name: 'My Full', + username: 'flast', + picture: 'https://example.com/avatar.png', + }; + + beforeEach(async () => { + // Clear previous mock calls and reset implementations + jest.clearAllMocks(); - //call setup so we can grab a reference to the validate function - await setupOpenId(); - validateFn = OpenIDStrategy.mock.calls[0][1]; + // Reset environment variables needed by the strategy + process.env.OPENID_ISSUER = 'https://fake-issuer.com'; + process.env.OPENID_CLIENT_ID = 'fake_client_id'; + process.env.OPENID_CLIENT_SECRET = 'fake_client_secret'; + process.env.DOMAIN_SERVER = 'https://example.com'; + process.env.OPENID_CALLBACK_URL = '/callback'; + process.env.OPENID_SCOPE = 'openid profile email'; + process.env.OPENID_REQUIRED_ROLE = 'requiredRole'; + process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'roles'; + process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id'; + delete process.env.OPENID_USERNAME_CLAIM; + delete process.env.OPENID_NAME_CLAIM; + delete process.env.PROXY; + + // Default jwtDecode mock returns a token that includes the required role. + jwtDecode.mockReturnValue({ + roles: ['requiredRole'], }); - const tokenset = { - id_token: 'fake_id_token', - }; + // By default, assume that no user is found, so createUser will be called + findUser.mockResolvedValue(null); + createUser.mockImplementation(async (userData) => { + // simulate created user with an _id property + return { _id: 'newUserId', ...userData }; + }); + updateUser.mockImplementation(async (id, userData) => { + return { _id: id, ...userData }; + }); - const userinfo = { - sub: '1234', - email: 'test@example.com', - email_verified: true, - given_name: 'First', - family_name: 'Last', - name: 'My Full', - username: 'flast', + // For image download, simulate a successful response + const fakeBuffer = Buffer.from('fake image'); + const fakeResponse = { + ok: true, + buffer: jest.fn().mockResolvedValue(fakeBuffer), }; + fetch.mockResolvedValue(fakeResponse); - const userModel = { - openidId: userinfo.sub, - email: userinfo.email, - }; + // Finally, call the setup function so that passport.use gets called + await setupOpenId(); + }); - it('should set username correctly for a new user when username claim exists', async () => { - const expectUsername = userinfo.username.toLowerCase(); - await validateFn(tokenset, userinfo, (err, user) => { - expect(err).toBe(null); - expect(user.username).toBe(expectUsername); - }); + it('should create a new user with correct username when username claim exists', async () => { + // Arrange – our userinfo already has username 'flast' + const userinfo = { ...baseUserinfo }; - await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); - }); + // Act + const { user } = await validate(tokenset, userinfo); - it('should set username correctly for a new user when given_name claim exists, but username does not', async () => { - let userinfo_modified = { ...userinfo }; - delete userinfo_modified.username; - const expectUsername = userinfo.given_name.toLowerCase(); + // Assert + expect(user.username).toBe(userinfo.username); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ + provider: 'openid', + openidId: userinfo.sub, + username: userinfo.username, + email: userinfo.email, + name: `${userinfo.given_name} ${userinfo.family_name}`, + }), + true, + true, + ); + }); - await validateFn(tokenset, userinfo_modified, (err, user) => { - expect(err).toBe(null); - expect(user.username).toBe(expectUsername); - }); - await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); - }); + it('should use given_name as username when username claim is missing', async () => { + // Arrange – remove username from userinfo + const userinfo = { ...baseUserinfo }; + delete userinfo.username; + // Expect the username to be the given name (unchanged case) + const expectUsername = userinfo.given_name; - it('should set username correctly for a new user when email claim exists, but username and given_name do not', async () => { - let userinfo_modified = { ...userinfo }; - delete userinfo_modified.username; - delete userinfo_modified.given_name; - const expectUsername = userinfo.email.toLowerCase(); + // Act + const { user } = await validate(tokenset, userinfo); - await validateFn(tokenset, userinfo_modified, (err, user) => { - expect(err).toBe(null); - expect(user.username).toBe(expectUsername); - }); - await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); - }); + // Assert + expect(user.username).toBe(expectUsername); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ username: expectUsername }), + true, + true, + ); + }); - it('should set username correctly for a new user when using OPENID_USERNAME_CLAIM', async () => { - process.env.OPENID_USERNAME_CLAIM = 'sub'; - const expectUsername = userinfo.sub.toLowerCase(); + it('should use email as username when username and given_name are missing', async () => { + // Arrange – remove username and given_name + const userinfo = { ...baseUserinfo }; + delete userinfo.username; + delete userinfo.given_name; + const expectUsername = userinfo.email; - await validateFn(tokenset, userinfo, (err, user) => { - expect(err).toBe(null); - expect(user.username).toBe(expectUsername); - }); - await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); - }); + // Act + const { user } = await validate(tokenset, userinfo); - it('should set name correctly for a new user with first and last names', async () => { - const expectName = userinfo.given_name + ' ' + userinfo.family_name; - await validateFn(tokenset, userinfo, (err, user) => { - expect(err).toBe(null); - expect(user.name).toBe(expectName); - }); - await expect(User.exists({ name: expectName })).resolves.not.toBeNull(); - }); + // Assert + expect(user.username).toBe(expectUsername); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ username: expectUsername }), + true, + true, + ); + }); - it('should set name correctly for a new user using OPENID_NAME_CLAIM', async () => { - const expectName = 'Custom Name'; - process.env.OPENID_NAME_CLAIM = 'name'; - let userinfo_modified = { ...userinfo, name: expectName }; + it('should override username with OPENID_USERNAME_CLAIM when set', async () => { + // Arrange – set OPENID_USERNAME_CLAIM so that the sub claim is used + process.env.OPENID_USERNAME_CLAIM = 'sub'; + const userinfo = { ...baseUserinfo }; - await validateFn(tokenset, userinfo_modified, (err, user) => { - expect(err).toBe(null); - expect(user.name).toBe(expectName); - }); - await expect(User.exists({ name: expectName })).resolves.not.toBeNull(); - }); + // Act + const { user } = await validate(tokenset, userinfo); + + // Assert – username should equal the sub (converted as-is) + expect(user.username).toBe(userinfo.sub); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ username: userinfo.sub }), + true, + true, + ); + }); - it('should should update existing user after login', async () => { - const expectUsername = userinfo.username.toLowerCase(); - await User.create(userModel); + it('should set the full name correctly when given_name and family_name exist', async () => { + // Arrange + const userinfo = { ...baseUserinfo }; + const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`; - await validateFn(tokenset, userinfo, (err) => { - expect(err).toBe(null); - }); - const newUser = await User.findOne({ openidId: userModel.openidId }); - await expect(newUser.provider).toBe('openid'); - await expect(newUser.username).toBe(expectUsername); - await expect(newUser.name).toBe(userinfo.given_name + ' ' + userinfo.family_name); + // Act + const { user } = await validate(tokenset, userinfo); + + // Assert + expect(user.name).toBe(expectedFullName); + }); + + it('should override full name with OPENID_NAME_CLAIM when set', async () => { + // Arrange – use the name claim as the full name + process.env.OPENID_NAME_CLAIM = 'name'; + const userinfo = { ...baseUserinfo, name: 'Custom Name' }; + + // Act + const { user } = await validate(tokenset, userinfo); + + // Assert + expect(user.name).toBe('Custom Name'); + }); + + it('should update an existing user on login', async () => { + // Arrange – simulate that a user already exists + const existingUser = { + _id: 'existingUserId', + provider: 'local', + email: baseUserinfo.email, + openidId: '', + username: '', + name: '', + }; + findUser.mockImplementation(async (query) => { + if (query.openidId === baseUserinfo.sub || query.email === baseUserinfo.email) { + return existingUser; + } + return null; }); - it('should should enforce required role', async () => { - jwtDecode.mockReturnValue({ - roles: ['SomeOtherRole'], - }); - await validateFn(tokenset, userinfo, (err, user, details) => { - expect(err).toBe(null); - expect(user).toBe(false); - expect(details.message).toBe( - 'You must have the "' + process.env.OPENID_REQUIRED_ROLE + '" role to log in.', - ); - }); + const userinfo = { ...baseUserinfo }; + + // Act + const { user } = await validate(tokenset, userinfo); + + // Assert – updateUser should be called and the user object updated + expect(updateUser).toHaveBeenCalledWith(existingUser._id, expect.objectContaining({ + provider: 'openid', + openidId: baseUserinfo.sub, + username: baseUserinfo.username, + name: `${baseUserinfo.given_name} ${baseUserinfo.family_name}`, + })); + }); + + it('should enforce the required role and reject login if missing', async () => { + // Arrange – simulate a token without the required role. + jwtDecode.mockReturnValue({ + roles: ['SomeOtherRole'], }); + const userinfo = { ...baseUserinfo }; + + // Act + const { user, details } = await validate(tokenset, userinfo); + + // Assert – verify that the strategy rejects login + expect(user).toBe(false); + expect(details.message).toBe( + 'You must have the "requiredRole" role to log in.', + ); }); -}); + + it('should attempt to download and save the avatar if picture is provided', async () => { + // Arrange – ensure userinfo contains a picture URL + const userinfo = { ...baseUserinfo }; + + // Act + const { user } = await validate(tokenset, userinfo); + + // Assert – verify that download was attempted and the avatar field was set via updateUser + expect(fetch).toHaveBeenCalled(); + // Our mock getStrategyFunctions.saveBuffer returns '/fake/path/to/avatar.png' + expect(user.avatar).toBe('/fake/path/to/avatar.png'); + }); + + it('should not attempt to download avatar if picture is not provided', async () => { + // Arrange – remove picture + const userinfo = { ...baseUserinfo }; + delete userinfo.picture; + + // Act + const { user } = await validate(tokenset, userinfo); + + // Assert – fetch should not be called and avatar should remain undefined or empty + expect(fetch).not.toHaveBeenCalled(); + // Depending on your implementation, user.avatar may be undefined or an empty string. + }); +}); \ No newline at end of file From 7c8a930061f5dcf6b416ca9706bda86a1a49254a Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Mon, 3 Feb 2025 21:30:02 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20feat:=20added=20Github=20Enterp?= =?UTF-8?q?rise=20SSO=20login=20(#5621)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * https://github.com/danny-avila/LibreChat/issues/2812 * refactored the code to simplify it. * removed unneeded code * removed unneeded code --- .env.example | 3 +++ api/server/socialLogins.js | 2 +- api/strategies/githubStrategy.js | 9 +++++++++ api/strategies/index.js | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 93408df7bba..d87021ea4b3 100644 --- a/.env.example +++ b/.env.example @@ -389,6 +389,9 @@ FACEBOOK_CALLBACK_URL=/oauth/facebook/callback GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= GITHUB_CALLBACK_URL=/oauth/github/callback +# GitHub Eenterprise +# GITHUB_ENTERPRISE_BASE_URL= +# GITHUB_ENTERPRISE_USER_AGENT= # Google GOOGLE_CLIENT_ID= diff --git a/api/server/socialLogins.js b/api/server/socialLogins.js index ec3a73e0ac0..f39d1da5963 100644 --- a/api/server/socialLogins.js +++ b/api/server/socialLogins.js @@ -64,4 +64,4 @@ const configureSocialLogins = (app) => { } }; -module.exports = configureSocialLogins; +module.exports = configureSocialLogins; \ No newline at end of file diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js index bb3712eeba0..1c3937381ed 100644 --- a/api/strategies/githubStrategy.js +++ b/api/strategies/githubStrategy.js @@ -20,6 +20,15 @@ module.exports = () => callbackURL: `${process.env.DOMAIN_SERVER}${process.env.GITHUB_CALLBACK_URL}`, proxy: false, scope: ['user:email'], + ...(process.env.GITHUB_ENTERPRISE_BASE_URL && { + authorizationURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/login/oauth/authorize`, + tokenURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/login/oauth/access_token`, + userProfileURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/api/v3/user`, + userEmailURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/api/v3/user/emails`, + ...(process.env.GITHUB_ENTERPRISE_USER_AGENT && { + userAgent: process.env.GITHUB_ENTERPRISE_USER_AGENT, + }), + }), }, githubLogin, ); diff --git a/api/strategies/index.js b/api/strategies/index.js index cac8460914a..242984beaff 100644 --- a/api/strategies/index.js +++ b/api/strategies/index.js @@ -18,4 +18,4 @@ module.exports = { facebookLogin, setupOpenId, ldapLogin, -}; +}; \ No newline at end of file From 0312d4f4f479f6ffde65c6d617d380ed55d8fec5 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 3 Feb 2025 16:08:34 -0500 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A7=20refactor:=20Revamp=20Model?= =?UTF-8?q?=20and=20Tool=20Filtering=20Logic=20(#5637)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 fix: Update regex to correctly match OpenAI model identifiers * 🔧 fix: Enhance tool filtering logic in ToolService to handle inclusion and exclusion criteria for basic tools and toolkits * feat: support o3-mini Azure streaming * chore: Update model filtering logic to exclude audio and realtime models * ci: linting error --- api/app/clients/OpenAIClient.js | 1 + api/server/services/ModelService.js | 5 +++-- api/server/services/ToolService.js | 11 +++++++++++ api/strategies/openidStrategy.spec.js | 25 +++++++++++++------------ 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 93fbe3c5bb5..9334f1c28b1 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -1282,6 +1282,7 @@ ${convo} if ( this.isOmni === true && (this.azure || /o1(?!-(?:mini|preview)).*$/.test(modelOptions.model)) && + !/o3-.*$/.test(this.modelOptions.model) && modelOptions.stream ) { delete modelOptions.stream; diff --git a/api/server/services/ModelService.js b/api/server/services/ModelService.js index 0547d03187e..1394a5d6977 100644 --- a/api/server/services/ModelService.js +++ b/api/server/services/ModelService.js @@ -159,8 +159,9 @@ const fetchOpenAIModels = async (opts, _models = []) => { } if (baseURL === openaiBaseURL) { - const regex = /(text-davinci-003|gpt-|o1-)/; - models = models.filter((model) => regex.test(model)); + const regex = /(text-davinci-003|gpt-|o\d+-)/; + const excludeRegex = /audio|realtime/; + models = models.filter((model) => regex.test(model) && !excludeRegex.test(model)); const instructModels = models.filter((model) => model.includes('instruct')); const otherModels = models.filter((model) => !model.includes('instruct')); models = otherModels.concat(instructModels); diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 08891c99b25..cf88c0b199b 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -101,6 +101,17 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) const basicToolInstances = [new Calculator(), ...createYouTubeTools({ override: true })]; for (const toolInstance of basicToolInstances) { const formattedTool = formatToOpenAIAssistantTool(toolInstance); + let toolName = formattedTool[Tools.function].name; + toolName = toolkits.some((toolkit) => toolName.startsWith(toolkit.pluginKey)) + ? toolName.split('_')[0] + : toolName; + if (filter.has(toolName) && included.size === 0) { + continue; + } + + if (included.size > 0 && !included.has(toolName)) { + continue; + } tools.push(formattedTool); } diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js index 7b8a3107a06..cea7c5e4a64 100644 --- a/api/strategies/openidStrategy.spec.js +++ b/api/strategies/openidStrategy.spec.js @@ -245,15 +245,18 @@ describe('setupOpenId', () => { const userinfo = { ...baseUserinfo }; // Act - const { user } = await validate(tokenset, userinfo); + await validate(tokenset, userinfo); // Assert – updateUser should be called and the user object updated - expect(updateUser).toHaveBeenCalledWith(existingUser._id, expect.objectContaining({ - provider: 'openid', - openidId: baseUserinfo.sub, - username: baseUserinfo.username, - name: `${baseUserinfo.given_name} ${baseUserinfo.family_name}`, - })); + expect(updateUser).toHaveBeenCalledWith( + existingUser._id, + expect.objectContaining({ + provider: 'openid', + openidId: baseUserinfo.sub, + username: baseUserinfo.username, + name: `${baseUserinfo.given_name} ${baseUserinfo.family_name}`, + }), + ); }); it('should enforce the required role and reject login if missing', async () => { @@ -268,9 +271,7 @@ describe('setupOpenId', () => { // Assert – verify that the strategy rejects login expect(user).toBe(false); - expect(details.message).toBe( - 'You must have the "requiredRole" role to log in.', - ); + expect(details.message).toBe('You must have the "requiredRole" role to log in.'); }); it('should attempt to download and save the avatar if picture is provided', async () => { @@ -292,10 +293,10 @@ describe('setupOpenId', () => { delete userinfo.picture; // Act - const { user } = await validate(tokenset, userinfo); + await validate(tokenset, userinfo); // Assert – fetch should not be called and avatar should remain undefined or empty expect(fetch).not.toHaveBeenCalled(); // Depending on your implementation, user.avatar may be undefined or an empty string. }); -}); \ No newline at end of file +});