mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🔒 fix: Provider Validation for Social, OpenID, SAML, and LDAP Logins (#8999)
* fix: social login provider crossover * feat: Enhance OpenID login handling and add tests for provider validation * refactor: authentication error handling to use ErrorTypes.AUTH_FAILED enum * refactor: update authentication error handling in LDAP and SAML strategies to use ErrorTypes.AUTH_FAILED enum * ci: Add validation for login with existing email and different provider in SAML strategy chore: Add logging for existing users with different providers in LDAP, SAML, and Social Login strategies
This commit is contained in:
parent
04d74a7e07
commit
1ccac58403
18 changed files with 314 additions and 125 deletions
|
|
@ -1,46 +0,0 @@
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
//handle duplicates
|
|
||||||
const handleDuplicateKeyError = (err, res) => {
|
|
||||||
logger.error('Duplicate key error:', err.keyValue);
|
|
||||||
const field = `${JSON.stringify(Object.keys(err.keyValue))}`;
|
|
||||||
const code = 409;
|
|
||||||
res
|
|
||||||
.status(code)
|
|
||||||
.send({ messages: `An document with that ${field} already exists.`, fields: field });
|
|
||||||
};
|
|
||||||
|
|
||||||
//handle validation errors
|
|
||||||
const handleValidationError = (err, res) => {
|
|
||||||
logger.error('Validation error:', err.errors);
|
|
||||||
let errors = Object.values(err.errors).map((el) => el.message);
|
|
||||||
let fields = `${JSON.stringify(Object.values(err.errors).map((el) => el.path))}`;
|
|
||||||
let code = 400;
|
|
||||||
if (errors.length > 1) {
|
|
||||||
errors = errors.join(' ');
|
|
||||||
res.status(code).send({ messages: `${JSON.stringify(errors)}`, fields: fields });
|
|
||||||
} else {
|
|
||||||
res.status(code).send({ messages: `${JSON.stringify(errors)}`, fields: fields });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = (err, _req, res, _next) => {
|
|
||||||
try {
|
|
||||||
if (err.name === 'ValidationError') {
|
|
||||||
return handleValidationError(err, res);
|
|
||||||
}
|
|
||||||
if (err.code && err.code == 11000) {
|
|
||||||
return handleDuplicateKeyError(err, res);
|
|
||||||
}
|
|
||||||
// Special handling for errors like SyntaxError
|
|
||||||
if (err.statusCode && err.body) {
|
|
||||||
return res.status(err.statusCode).send(err.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('ErrorController => error', err);
|
|
||||||
return res.status(500).send('An unknown error occurred.');
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('ErrorController => processing error', err);
|
|
||||||
return res.status(500).send('Processing error in ErrorController.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -8,14 +8,12 @@ const express = require('express');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const { isEnabled } = require('@librechat/api');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const mongoSanitize = require('express-mongo-sanitize');
|
const mongoSanitize = require('express-mongo-sanitize');
|
||||||
|
const { isEnabled, ErrorController } = require('@librechat/api');
|
||||||
const { connectDb, indexSync } = require('~/db');
|
const { connectDb, indexSync } = require('~/db');
|
||||||
|
|
||||||
const validateImageRequest = require('./middleware/validateImageRequest');
|
const validateImageRequest = require('./middleware/validateImageRequest');
|
||||||
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
|
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
|
||||||
const errorController = require('./controllers/ErrorController');
|
|
||||||
const initializeMCPs = require('./services/initializeMCPs');
|
const initializeMCPs = require('./services/initializeMCPs');
|
||||||
const configureSocialLogins = require('./socialLogins');
|
const configureSocialLogins = require('./socialLogins');
|
||||||
const AppService = require('./services/AppService');
|
const AppService = require('./services/AppService');
|
||||||
|
|
@ -120,8 +118,7 @@ const startServer = async () => {
|
||||||
app.use('/api/tags', routes.tags);
|
app.use('/api/tags', routes.tags);
|
||||||
app.use('/api/mcp', routes.mcp);
|
app.use('/api/mcp', routes.mcp);
|
||||||
|
|
||||||
// Add the error controller one more time after all routes
|
app.use(ErrorController);
|
||||||
app.use(errorController);
|
|
||||||
|
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.set({
|
res.set({
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ async function healthCheckPoll(app, retries = 0) {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return; // App is healthy
|
return; // App is healthy
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore connection errors during polling
|
// Ignore connection errors during polling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
|
const { isEnabled } = require('@librechat/api');
|
||||||
const { randomState } = require('openid-client');
|
const { randomState } = require('openid-client');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { ErrorTypes } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
checkBan,
|
checkBan,
|
||||||
logHeaders,
|
logHeaders,
|
||||||
|
|
@ -10,8 +13,6 @@ const {
|
||||||
checkDomainAllowed,
|
checkDomainAllowed,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
||||||
const { isEnabled } = require('~/server/utils');
|
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -46,13 +47,13 @@ const oauthHandler = async (req, res) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
router.get('/error', (req, res) => {
|
router.get('/error', (req, res) => {
|
||||||
// A single error message is pushed by passport when authentication fails.
|
/** A single error message is pushed by passport when authentication fails. */
|
||||||
|
const errorMessage = req.session?.messages?.pop() || 'Unknown error';
|
||||||
logger.error('Error in OAuth authentication:', {
|
logger.error('Error in OAuth authentication:', {
|
||||||
message: req.session?.messages?.pop() || 'Unknown error',
|
message: errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect to login page with auth_failed parameter to prevent infinite redirect loops
|
res.redirect(`${domains.client}/login?redirect=false&error=${ErrorTypes.AUTH_FAILED}`);
|
||||||
res.redirect(`${domains.client}/login?redirect=false`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { isEnabled } = require('@librechat/api');
|
||||||
const LdapStrategy = require('passport-ldapauth');
|
const LdapStrategy = require('passport-ldapauth');
|
||||||
const { SystemRoles } = require('librechat-data-provider');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { SystemRoles, ErrorTypes } = require('librechat-data-provider');
|
||||||
const { createUser, findUser, updateUser, countUsers } = require('~/models');
|
const { createUser, findUser, updateUser, countUsers } = require('~/models');
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
const { getBalanceConfig } = require('~/server/services/Config');
|
||||||
const { isEnabled } = require('~/server/utils');
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
LDAP_URL,
|
LDAP_URL,
|
||||||
|
|
@ -90,6 +90,14 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
|
||||||
(LDAP_ID && userinfo[LDAP_ID]) || userinfo.uid || userinfo.sAMAccountName || userinfo.mail;
|
(LDAP_ID && userinfo[LDAP_ID]) || userinfo.uid || userinfo.sAMAccountName || userinfo.mail;
|
||||||
|
|
||||||
let user = await findUser({ ldapId });
|
let user = await findUser({ ldapId });
|
||||||
|
if (user && user.provider !== 'ldap') {
|
||||||
|
logger.info(
|
||||||
|
`[ldapStrategy] User ${user.email} already exists with provider ${user.provider}`,
|
||||||
|
);
|
||||||
|
return done(null, false, {
|
||||||
|
message: ErrorTypes.AUTH_FAILED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const fullNameAttributes = LDAP_FULL_NAME && LDAP_FULL_NAME.split(',');
|
const fullNameAttributes = LDAP_FULL_NAME && LDAP_FULL_NAME.split(',');
|
||||||
const fullName =
|
const fullName =
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ const fetch = require('node-fetch');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const client = require('openid-client');
|
const client = require('openid-client');
|
||||||
const jwtDecode = require('jsonwebtoken/decode');
|
const jwtDecode = require('jsonwebtoken/decode');
|
||||||
const { CacheKeys } = require('librechat-data-provider');
|
|
||||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
const { hashToken, logger } = require('@librechat/data-schemas');
|
const { hashToken, logger } = require('@librechat/data-schemas');
|
||||||
|
const { CacheKeys, ErrorTypes } = require('librechat-data-provider');
|
||||||
const { Strategy: OpenIDStrategy } = require('openid-client/passport');
|
const { Strategy: OpenIDStrategy } = require('openid-client/passport');
|
||||||
const { isEnabled, safeStringify, logHeaders } = require('@librechat/api');
|
const { isEnabled, safeStringify, logHeaders } = require('@librechat/api');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
|
|
@ -320,6 +320,14 @@ async function setupOpenId() {
|
||||||
} for openidId: ${claims.sub}`,
|
} for openidId: ${claims.sub}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (user != null && user.provider !== 'openid') {
|
||||||
|
logger.info(
|
||||||
|
`[openidStrategy] Attempted OpenID login by user ${user.email}, was registered with "${user.provider}" provider`,
|
||||||
|
);
|
||||||
|
return done(null, false, {
|
||||||
|
message: ErrorTypes.AUTH_FAILED,
|
||||||
|
});
|
||||||
|
}
|
||||||
const userinfo = {
|
const userinfo = {
|
||||||
...claims,
|
...claims,
|
||||||
...(await getUserInfo(openidConfig, tokenset.access_token, claims.sub)),
|
...(await getUserInfo(openidConfig, tokenset.access_token, claims.sub)),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const jwtDecode = require('jsonwebtoken/decode');
|
const jwtDecode = require('jsonwebtoken/decode');
|
||||||
const { setupOpenId } = require('./openidStrategy');
|
const { ErrorTypes } = require('librechat-data-provider');
|
||||||
const { findUser, createUser, updateUser } = require('~/models');
|
const { findUser, createUser, updateUser } = require('~/models');
|
||||||
|
const { setupOpenId } = require('./openidStrategy');
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
jest.mock('node-fetch');
|
jest.mock('node-fetch');
|
||||||
|
|
@ -50,7 +51,7 @@ jest.mock('openid-client', () => {
|
||||||
issuer: 'https://fake-issuer.com',
|
issuer: 'https://fake-issuer.com',
|
||||||
// Add any other properties needed by the implementation
|
// Add any other properties needed by the implementation
|
||||||
}),
|
}),
|
||||||
fetchUserInfo: jest.fn().mockImplementation((config, accessToken, sub) => {
|
fetchUserInfo: jest.fn().mockImplementation(() => {
|
||||||
// Only return additional properties, but don't override any claims
|
// Only return additional properties, but don't override any claims
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}),
|
}),
|
||||||
|
|
@ -261,17 +262,20 @@ describe('setupOpenId', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update an existing user on login', async () => {
|
it('should update an existing user on login', async () => {
|
||||||
// Arrange – simulate that a user already exists
|
// Arrange – simulate that a user already exists with openid provider
|
||||||
const existingUser = {
|
const existingUser = {
|
||||||
_id: 'existingUserId',
|
_id: 'existingUserId',
|
||||||
provider: 'local',
|
provider: 'openid',
|
||||||
email: tokenset.claims().email,
|
email: tokenset.claims().email,
|
||||||
openidId: '',
|
openidId: '',
|
||||||
username: '',
|
username: '',
|
||||||
name: '',
|
name: '',
|
||||||
};
|
};
|
||||||
findUser.mockImplementation(async (query) => {
|
findUser.mockImplementation(async (query) => {
|
||||||
if (query.openidId === tokenset.claims().sub || query.email === tokenset.claims().email) {
|
if (
|
||||||
|
query.openidId === tokenset.claims().sub ||
|
||||||
|
(query.email === tokenset.claims().email && query.provider === 'openid')
|
||||||
|
) {
|
||||||
return existingUser;
|
return existingUser;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -294,12 +298,38 @@ describe('setupOpenId', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should block login when email exists with different provider', async () => {
|
||||||
|
// Arrange – simulate that a user exists with same email but different provider
|
||||||
|
const existingUser = {
|
||||||
|
_id: 'existingUserId',
|
||||||
|
provider: 'google',
|
||||||
|
email: tokenset.claims().email,
|
||||||
|
googleId: 'some-google-id',
|
||||||
|
username: 'existinguser',
|
||||||
|
name: 'Existing User',
|
||||||
|
};
|
||||||
|
findUser.mockImplementation(async (query) => {
|
||||||
|
if (query.email === tokenset.claims().email && !query.provider) {
|
||||||
|
return existingUser;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await validate(tokenset);
|
||||||
|
|
||||||
|
// Assert – verify that the strategy rejects login
|
||||||
|
expect(result.user).toBe(false);
|
||||||
|
expect(result.details.message).toBe(ErrorTypes.AUTH_FAILED);
|
||||||
|
expect(createUser).not.toHaveBeenCalled();
|
||||||
|
expect(updateUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should enforce the required role and reject login if missing', async () => {
|
it('should enforce the required role and reject login if missing', async () => {
|
||||||
// Arrange – simulate a token without the required role.
|
// Arrange – simulate a token without the required role.
|
||||||
jwtDecode.mockReturnValue({
|
jwtDecode.mockReturnValue({
|
||||||
roles: ['SomeOtherRole'],
|
roles: ['SomeOtherRole'],
|
||||||
});
|
});
|
||||||
const userinfo = tokenset.claims();
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user, details } = await validate(tokenset);
|
const { user, details } = await validate(tokenset);
|
||||||
|
|
@ -310,9 +340,6 @@ describe('setupOpenId', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should attempt to download and save the avatar if picture is provided', async () => {
|
it('should attempt to download and save the avatar if picture is provided', async () => {
|
||||||
// Arrange – ensure userinfo contains a picture URL
|
|
||||||
const userinfo = tokenset.claims();
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset);
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
|
const { ErrorTypes } = require('librechat-data-provider');
|
||||||
const { hashToken, logger } = require('@librechat/data-schemas');
|
const { hashToken, logger } = require('@librechat/data-schemas');
|
||||||
const { Strategy: SamlStrategy } = require('@node-saml/passport-saml');
|
const { Strategy: SamlStrategy } = require('@node-saml/passport-saml');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
|
|
@ -203,6 +204,15 @@ async function setupSaml() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user && user.provider !== 'saml') {
|
||||||
|
logger.info(
|
||||||
|
`[samlStrategy] User ${user.email} already exists with provider ${user.provider}`,
|
||||||
|
);
|
||||||
|
return done(null, false, {
|
||||||
|
message: ErrorTypes.AUTH_FAILED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const fullName = getFullName(profile);
|
const fullName = getFullName(profile);
|
||||||
|
|
||||||
const username = convertToUsername(
|
const username = convertToUsername(
|
||||||
|
|
|
||||||
|
|
@ -378,11 +378,11 @@ u7wlOSk+oFzDIO/UILIA
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update an existing user on login', async () => {
|
it('should update an existing user on login', async () => {
|
||||||
// Set up findUser to return an existing user
|
// Set up findUser to return an existing user with saml provider
|
||||||
const { findUser } = require('~/models');
|
const { findUser } = require('~/models');
|
||||||
const existingUser = {
|
const existingUser = {
|
||||||
_id: 'existing-user-id',
|
_id: 'existing-user-id',
|
||||||
provider: 'local',
|
provider: 'saml',
|
||||||
email: baseProfile.email,
|
email: baseProfile.email,
|
||||||
samlId: '',
|
samlId: '',
|
||||||
username: 'oldusername',
|
username: 'oldusername',
|
||||||
|
|
@ -400,6 +400,26 @@ u7wlOSk+oFzDIO/UILIA
|
||||||
expect(user.email).toBe(baseProfile.email);
|
expect(user.email).toBe(baseProfile.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should block login when email exists with different provider', async () => {
|
||||||
|
// Set up findUser to return a user with different provider
|
||||||
|
const { findUser } = require('~/models');
|
||||||
|
const existingUser = {
|
||||||
|
_id: 'existing-user-id',
|
||||||
|
provider: 'google',
|
||||||
|
email: baseProfile.email,
|
||||||
|
googleId: 'some-google-id',
|
||||||
|
username: 'existinguser',
|
||||||
|
name: 'Existing User',
|
||||||
|
};
|
||||||
|
findUser.mockResolvedValue(existingUser);
|
||||||
|
|
||||||
|
const profile = { ...baseProfile };
|
||||||
|
const result = await validate(profile);
|
||||||
|
|
||||||
|
expect(result.user).toBe(false);
|
||||||
|
expect(result.details.message).toBe(require('librechat-data-provider').ErrorTypes.AUTH_FAILED);
|
||||||
|
});
|
||||||
|
|
||||||
it('should attempt to download and save the avatar if picture is provided', async () => {
|
it('should attempt to download and save the avatar if picture is provided', async () => {
|
||||||
const profile = { ...baseProfile };
|
const profile = { ...baseProfile };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,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 { createSocialUser, handleExistingUser } = require('./process');
|
const { createSocialUser, handleExistingUser } = require('./process');
|
||||||
const { isEnabled } = require('~/server/utils');
|
|
||||||
const { findUser } = require('~/models');
|
const { findUser } = require('~/models');
|
||||||
|
|
||||||
const socialLogin =
|
const socialLogin =
|
||||||
|
|
@ -11,12 +12,20 @@ const socialLogin =
|
||||||
profile,
|
profile,
|
||||||
});
|
});
|
||||||
|
|
||||||
const oldUser = 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 (oldUser) {
|
if (existingUser?.provider === provider) {
|
||||||
await handleExistingUser(oldUser, avatarUrl);
|
await handleExistingUser(existingUser, avatarUrl);
|
||||||
return cb(null, oldUser);
|
return cb(null, existingUser);
|
||||||
|
} else if (existingUser) {
|
||||||
|
logger.info(
|
||||||
|
`[${provider}Login] User ${email} already exists with provider ${existingUser.provider}`,
|
||||||
|
);
|
||||||
|
const error = new Error(ErrorTypes.AUTH_FAILED);
|
||||||
|
error.code = ErrorTypes.AUTH_FAILED;
|
||||||
|
error.provider = existingUser.provider;
|
||||||
|
return cb(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ALLOW_SOCIAL_REGISTRATION) {
|
if (ALLOW_SOCIAL_REGISTRATION) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useOutletContext, useSearchParams } from 'react-router-dom';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { OpenIDIcon } from '@librechat/client';
|
import { ErrorTypes } from 'librechat-data-provider';
|
||||||
|
import { OpenIDIcon, useToastContext } from '@librechat/client';
|
||||||
|
import { useOutletContext, useSearchParams } from 'react-router-dom';
|
||||||
import type { TLoginLayoutContext } from '~/common';
|
import type { TLoginLayoutContext } from '~/common';
|
||||||
import { ErrorMessage } from '~/components/Auth/ErrorMessage';
|
import { ErrorMessage } from '~/components/Auth/ErrorMessage';
|
||||||
import SocialButton from '~/components/Auth/SocialButton';
|
import SocialButton from '~/components/Auth/SocialButton';
|
||||||
|
|
@ -11,6 +12,7 @@ import LoginForm from './LoginForm';
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
const { error, setError, login } = useAuthContext();
|
const { error, setError, login } = useAuthContext();
|
||||||
const { startupConfig } = useOutletContext<TLoginLayoutContext>();
|
const { startupConfig } = useOutletContext<TLoginLayoutContext>();
|
||||||
|
|
||||||
|
|
@ -21,6 +23,19 @@ function Login() {
|
||||||
// Persist the disable flag locally so that once detected, auto-redirect stays disabled.
|
// Persist the disable flag locally so that once detected, auto-redirect stays disabled.
|
||||||
const [isAutoRedirectDisabled, setIsAutoRedirectDisabled] = useState(disableAutoRedirect);
|
const [isAutoRedirectDisabled, setIsAutoRedirectDisabled] = useState(disableAutoRedirect);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const oauthError = searchParams?.get('error');
|
||||||
|
if (oauthError && oauthError === ErrorTypes.AUTH_FAILED) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_auth_error_oauth_failed'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
const newParams = new URLSearchParams(searchParams);
|
||||||
|
newParams.delete('error');
|
||||||
|
setSearchParams(newParams, { replace: true });
|
||||||
|
}
|
||||||
|
}, [searchParams, setSearchParams, showToast, localize]);
|
||||||
|
|
||||||
// Once the disable flag is detected, update local state and remove the parameter from the URL.
|
// Once the disable flag is detected, update local state and remove the parameter from the URL.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disableAutoRedirect) {
|
if (disableAutoRedirect) {
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@
|
||||||
"com_auth_error_login_rl": "Too many login attempts in a short amount of time. Please try again later.",
|
"com_auth_error_login_rl": "Too many login attempts in a short amount of time. Please try again later.",
|
||||||
"com_auth_error_login_server": "There was an internal server error. Please wait a few moments and try again.",
|
"com_auth_error_login_server": "There was an internal server error. Please wait a few moments and try again.",
|
||||||
"com_auth_error_login_unverified": "Your account has not been verified. Please check your email for a verification link.",
|
"com_auth_error_login_unverified": "Your account has not been verified. Please check your email for a verification link.",
|
||||||
|
"com_auth_error_oauth_failed": "Authentication failed. Please check your login method and try again.",
|
||||||
"com_auth_facebook_login": "Continue with Facebook",
|
"com_auth_facebook_login": "Continue with Facebook",
|
||||||
"com_auth_full_name": "Full name",
|
"com_auth_full_name": "Full name",
|
||||||
"com_auth_github_login": "Continue with Github",
|
"com_auth_github_login": "Continue with Github",
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,43 @@
|
||||||
const errorController = require('./ErrorController');
|
import { logger } from '@librechat/data-schemas';
|
||||||
const { logger } = require('~/config');
|
import { ErrorController } from './error';
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import type { ValidationError, MongoServerError, CustomError } from '~/types';
|
||||||
|
|
||||||
// Mock the logger
|
// Mock the logger
|
||||||
jest.mock('~/config', () => ({
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
|
...jest.requireActual('@librechat/data-schemas'),
|
||||||
logger: {
|
logger: {
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('ErrorController', () => {
|
describe('ErrorController', () => {
|
||||||
let mockReq, mockRes, mockNext;
|
let mockReq: Request;
|
||||||
|
let mockRes: Response;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReq = {};
|
mockReq = {
|
||||||
|
originalUrl: '',
|
||||||
|
} as Request;
|
||||||
mockRes = {
|
mockRes = {
|
||||||
status: jest.fn().mockReturnThis(),
|
status: jest.fn().mockReturnThis(),
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
};
|
} as unknown as Response;
|
||||||
mockNext = jest.fn();
|
(logger.error as jest.Mock).mockClear();
|
||||||
logger.error.mockClear();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ValidationError handling', () => {
|
describe('ValidationError handling', () => {
|
||||||
it('should handle ValidationError with single error', () => {
|
it('should handle ValidationError with single error', () => {
|
||||||
const validationError = {
|
const validationError = {
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
|
message: 'Validation error',
|
||||||
errors: {
|
errors: {
|
||||||
email: { message: 'Email is required', path: 'email' },
|
email: { message: 'Email is required', path: 'email' },
|
||||||
},
|
},
|
||||||
};
|
} as ValidationError;
|
||||||
|
|
||||||
errorController(validationError, mockReq, mockRes, mockNext);
|
ErrorController(validationError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith({
|
expect(mockRes.send).toHaveBeenCalledWith({
|
||||||
|
|
@ -43,13 +50,14 @@ describe('ErrorController', () => {
|
||||||
it('should handle ValidationError with multiple errors', () => {
|
it('should handle ValidationError with multiple errors', () => {
|
||||||
const validationError = {
|
const validationError = {
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
|
message: 'Validation error',
|
||||||
errors: {
|
errors: {
|
||||||
email: { message: 'Email is required', path: 'email' },
|
email: { message: 'Email is required', path: 'email' },
|
||||||
password: { message: 'Password is required', path: 'password' },
|
password: { message: 'Password is required', path: 'password' },
|
||||||
},
|
},
|
||||||
};
|
} as ValidationError;
|
||||||
|
|
||||||
errorController(validationError, mockReq, mockRes, mockNext);
|
ErrorController(validationError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith({
|
expect(mockRes.send).toHaveBeenCalledWith({
|
||||||
|
|
@ -63,9 +71,9 @@ describe('ErrorController', () => {
|
||||||
const validationError = {
|
const validationError = {
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
errors: {},
|
errors: {},
|
||||||
};
|
} as ValidationError;
|
||||||
|
|
||||||
errorController(validationError, mockReq, mockRes, mockNext);
|
ErrorController(validationError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith({
|
expect(mockRes.send).toHaveBeenCalledWith({
|
||||||
|
|
@ -78,43 +86,59 @@ describe('ErrorController', () => {
|
||||||
describe('Duplicate key error handling', () => {
|
describe('Duplicate key error handling', () => {
|
||||||
it('should handle duplicate key error (code 11000)', () => {
|
it('should handle duplicate key error (code 11000)', () => {
|
||||||
const duplicateKeyError = {
|
const duplicateKeyError = {
|
||||||
|
name: 'MongoServerError',
|
||||||
|
message: 'Duplicate key error',
|
||||||
code: 11000,
|
code: 11000,
|
||||||
keyValue: { email: 'test@example.com' },
|
keyValue: { email: 'test@example.com' },
|
||||||
};
|
errmsg:
|
||||||
|
'E11000 duplicate key error collection: test.users index: email_1 dup key: { email: "test@example.com" }',
|
||||||
|
} as MongoServerError;
|
||||||
|
|
||||||
errorController(duplicateKeyError, mockReq, mockRes, mockNext);
|
ErrorController(duplicateKeyError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(409);
|
expect(mockRes.status).toHaveBeenCalledWith(409);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith({
|
expect(mockRes.send).toHaveBeenCalledWith({
|
||||||
messages: 'An document with that ["email"] already exists.',
|
messages: 'An document with that ["email"] already exists.',
|
||||||
fields: '["email"]',
|
fields: '["email"]',
|
||||||
});
|
});
|
||||||
expect(logger.error).toHaveBeenCalledWith('Duplicate key error:', duplicateKeyError.keyValue);
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
|
'Duplicate key error: E11000 duplicate key error collection: test.users index: email_1 dup key: { email: "test@example.com" }',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle duplicate key error with multiple fields', () => {
|
it('should handle duplicate key error with multiple fields', () => {
|
||||||
const duplicateKeyError = {
|
const duplicateKeyError = {
|
||||||
|
name: 'MongoServerError',
|
||||||
|
message: 'Duplicate key error',
|
||||||
code: 11000,
|
code: 11000,
|
||||||
keyValue: { email: 'test@example.com', username: 'testuser' },
|
keyValue: { email: 'test@example.com', username: 'testuser' },
|
||||||
};
|
errmsg:
|
||||||
|
'E11000 duplicate key error collection: test.users index: email_1 dup key: { email: "test@example.com" }',
|
||||||
|
} as MongoServerError;
|
||||||
|
|
||||||
errorController(duplicateKeyError, mockReq, mockRes, mockNext);
|
ErrorController(duplicateKeyError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(409);
|
expect(mockRes.status).toHaveBeenCalledWith(409);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith({
|
expect(mockRes.send).toHaveBeenCalledWith({
|
||||||
messages: 'An document with that ["email","username"] already exists.',
|
messages: 'An document with that ["email","username"] already exists.',
|
||||||
fields: '["email","username"]',
|
fields: '["email","username"]',
|
||||||
});
|
});
|
||||||
expect(logger.error).toHaveBeenCalledWith('Duplicate key error:', duplicateKeyError.keyValue);
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
|
'Duplicate key error: E11000 duplicate key error collection: test.users index: email_1 dup key: { email: "test@example.com" }',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle error with code 11000 as string', () => {
|
it('should handle error with code 11000 as string', () => {
|
||||||
const duplicateKeyError = {
|
const duplicateKeyError = {
|
||||||
code: '11000',
|
name: 'MongoServerError',
|
||||||
|
message: 'Duplicate key error',
|
||||||
|
code: 11000,
|
||||||
keyValue: { email: 'test@example.com' },
|
keyValue: { email: 'test@example.com' },
|
||||||
};
|
errmsg:
|
||||||
|
'E11000 duplicate key error collection: test.users index: email_1 dup key: { email: "test@example.com" }',
|
||||||
|
} as MongoServerError;
|
||||||
|
|
||||||
errorController(duplicateKeyError, mockReq, mockRes, mockNext);
|
ErrorController(duplicateKeyError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(409);
|
expect(mockRes.status).toHaveBeenCalledWith(409);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith({
|
expect(mockRes.send).toHaveBeenCalledWith({
|
||||||
|
|
@ -129,9 +153,9 @@ describe('ErrorController', () => {
|
||||||
const syntaxError = {
|
const syntaxError = {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
body: 'Invalid JSON syntax',
|
body: 'Invalid JSON syntax',
|
||||||
};
|
} as CustomError;
|
||||||
|
|
||||||
errorController(syntaxError, mockReq, mockRes, mockNext);
|
ErrorController(syntaxError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith('Invalid JSON syntax');
|
expect(mockRes.send).toHaveBeenCalledWith('Invalid JSON syntax');
|
||||||
|
|
@ -141,9 +165,9 @@ describe('ErrorController', () => {
|
||||||
const customError = {
|
const customError = {
|
||||||
statusCode: 422,
|
statusCode: 422,
|
||||||
body: { error: 'Unprocessable entity' },
|
body: { error: 'Unprocessable entity' },
|
||||||
};
|
} as CustomError;
|
||||||
|
|
||||||
errorController(customError, mockReq, mockRes, mockNext);
|
ErrorController(customError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(422);
|
expect(mockRes.status).toHaveBeenCalledWith(422);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith({ error: 'Unprocessable entity' });
|
expect(mockRes.send).toHaveBeenCalledWith({ error: 'Unprocessable entity' });
|
||||||
|
|
@ -152,9 +176,9 @@ describe('ErrorController', () => {
|
||||||
it('should handle error with statusCode but no body', () => {
|
it('should handle error with statusCode but no body', () => {
|
||||||
const partialError = {
|
const partialError = {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
};
|
} as CustomError;
|
||||||
|
|
||||||
errorController(partialError, mockReq, mockRes, mockNext);
|
ErrorController(partialError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
||||||
|
|
@ -163,9 +187,9 @@ describe('ErrorController', () => {
|
||||||
it('should handle error with body but no statusCode', () => {
|
it('should handle error with body but no statusCode', () => {
|
||||||
const partialError = {
|
const partialError = {
|
||||||
body: 'Some error message',
|
body: 'Some error message',
|
||||||
};
|
} as CustomError;
|
||||||
|
|
||||||
errorController(partialError, mockReq, mockRes, mockNext);
|
ErrorController(partialError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
||||||
|
|
@ -176,7 +200,7 @@ describe('ErrorController', () => {
|
||||||
it('should handle unknown errors', () => {
|
it('should handle unknown errors', () => {
|
||||||
const unknownError = new Error('Some unknown error');
|
const unknownError = new Error('Some unknown error');
|
||||||
|
|
||||||
errorController(unknownError, mockReq, mockRes, mockNext);
|
ErrorController(unknownError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
||||||
|
|
@ -187,32 +211,31 @@ describe('ErrorController', () => {
|
||||||
const mongoError = {
|
const mongoError = {
|
||||||
code: 11100,
|
code: 11100,
|
||||||
message: 'Some MongoDB error',
|
message: 'Some MongoDB error',
|
||||||
};
|
} as MongoServerError;
|
||||||
|
|
||||||
errorController(mongoError, mockReq, mockRes, mockNext);
|
ErrorController(mongoError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
||||||
expect(logger.error).toHaveBeenCalledWith('ErrorController => error', mongoError);
|
expect(logger.error).toHaveBeenCalledWith('ErrorController => error', mongoError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle null/undefined errors', () => {
|
it('should handle generic errors', () => {
|
||||||
errorController(null, mockReq, mockRes, mockNext);
|
const genericError = new Error('Test error');
|
||||||
|
|
||||||
|
ErrorController(genericError, mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||||
expect(mockRes.send).toHaveBeenCalledWith('Processing error in ErrorController.');
|
expect(mockRes.send).toHaveBeenCalledWith('An unknown error occurred.');
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith('ErrorController => error', genericError);
|
||||||
'ErrorController => processing error',
|
|
||||||
expect.any(Error),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Catch block handling', () => {
|
describe('Catch block handling', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Restore logger mock to normal behavior for these tests
|
// Restore logger mock to normal behavior for these tests
|
||||||
logger.error.mockRestore();
|
(logger.error as jest.Mock).mockRestore();
|
||||||
logger.error = jest.fn();
|
(logger.error as jest.Mock) = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle errors when logger.error throws', () => {
|
it('should handle errors when logger.error throws', () => {
|
||||||
|
|
@ -220,10 +243,10 @@ describe('ErrorController', () => {
|
||||||
const freshMockRes = {
|
const freshMockRes = {
|
||||||
status: jest.fn().mockReturnThis(),
|
status: jest.fn().mockReturnThis(),
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
};
|
} as unknown as Response;
|
||||||
|
|
||||||
// Mock logger to throw on the first call, succeed on the second
|
// Mock logger to throw on the first call, succeed on the second
|
||||||
logger.error
|
(logger.error as jest.Mock)
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
throw new Error('Logger error');
|
throw new Error('Logger error');
|
||||||
})
|
})
|
||||||
|
|
@ -231,7 +254,7 @@ describe('ErrorController', () => {
|
||||||
|
|
||||||
const testError = new Error('Test error');
|
const testError = new Error('Test error');
|
||||||
|
|
||||||
errorController(testError, mockReq, freshMockRes, mockNext);
|
ErrorController(testError, mockReq, freshMockRes);
|
||||||
|
|
||||||
expect(freshMockRes.status).toHaveBeenCalledWith(500);
|
expect(freshMockRes.status).toHaveBeenCalledWith(500);
|
||||||
expect(freshMockRes.send).toHaveBeenCalledWith('Processing error in ErrorController.');
|
expect(freshMockRes.send).toHaveBeenCalledWith('Processing error in ErrorController.');
|
||||||
83
packages/api/src/middleware/error.ts
Normal file
83
packages/api/src/middleware/error.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import { ErrorTypes } from 'librechat-data-provider';
|
||||||
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
|
import type { MongoServerError, ValidationError, CustomError } from '~/types';
|
||||||
|
|
||||||
|
const handleDuplicateKeyError = (err: MongoServerError, res: Response) => {
|
||||||
|
logger.warn('Duplicate key error: ' + (err.errmsg || err.message));
|
||||||
|
const field = err.keyValue ? `${JSON.stringify(Object.keys(err.keyValue))}` : 'unknown';
|
||||||
|
const code = 409;
|
||||||
|
res
|
||||||
|
.status(code)
|
||||||
|
.send({ messages: `An document with that ${field} already exists.`, fields: field });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidationError = (err: ValidationError, res: Response) => {
|
||||||
|
logger.error('Validation error:', err.errors);
|
||||||
|
const errorMessages = Object.values(err.errors).map((el) => el.message);
|
||||||
|
const fields = `${JSON.stringify(Object.values(err.errors).map((el) => el.path))}`;
|
||||||
|
const code = 400;
|
||||||
|
const messages =
|
||||||
|
errorMessages.length > 1
|
||||||
|
? `${JSON.stringify(errorMessages.join(' '))}`
|
||||||
|
: `${JSON.stringify(errorMessages)}`;
|
||||||
|
|
||||||
|
res.status(code).send({ messages, fields });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Type guard for ValidationError */
|
||||||
|
function isValidationError(err: unknown): err is ValidationError {
|
||||||
|
return err !== null && typeof err === 'object' && 'name' in err && err.name === 'ValidationError';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type guard for MongoServerError (duplicate key) */
|
||||||
|
function isMongoServerError(err: unknown): err is MongoServerError {
|
||||||
|
return err !== null && typeof err === 'object' && 'code' in err && err.code === 11000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type guard for CustomError with statusCode and body */
|
||||||
|
function isCustomError(err: unknown): err is CustomError {
|
||||||
|
return err !== null && typeof err === 'object' && 'statusCode' in err && 'body' in err;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorController = (
|
||||||
|
err: Error | CustomError,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Response | void => {
|
||||||
|
try {
|
||||||
|
if (!err) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const error = err as CustomError;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(error.message === ErrorTypes.AUTH_FAILED || error.code === ErrorTypes.AUTH_FAILED) &&
|
||||||
|
req.originalUrl &&
|
||||||
|
req.originalUrl.includes('/oauth/') &&
|
||||||
|
req.originalUrl.includes('/callback')
|
||||||
|
) {
|
||||||
|
const domain = process.env.DOMAIN_CLIENT || 'http://localhost:3080';
|
||||||
|
return res.redirect(`${domain}/login?redirect=false&error=${ErrorTypes.AUTH_FAILED}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidationError(error)) {
|
||||||
|
return handleValidationError(error, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMongoServerError(error)) {
|
||||||
|
return handleDuplicateKeyError(error, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCustomError(error) && error.statusCode && error.body) {
|
||||||
|
return res.status(error.statusCode).send(error.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error('ErrorController => error', err);
|
||||||
|
return res.status(500).send('An unknown error occurred.');
|
||||||
|
} catch (processingError) {
|
||||||
|
logger.error('ErrorController => processing error', processingError);
|
||||||
|
return res.status(500).send('Processing error in ErrorController.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './access';
|
export * from './access';
|
||||||
|
export * from './error';
|
||||||
|
|
|
||||||
27
packages/api/src/types/error.ts
Normal file
27
packages/api/src/types/error.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Error as MongooseError } from 'mongoose';
|
||||||
|
|
||||||
|
/** MongoDB duplicate key error interface */
|
||||||
|
export interface MongoServerError extends Error {
|
||||||
|
code: number;
|
||||||
|
keyValue?: Record<string, unknown>;
|
||||||
|
errmsg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mongoose validation error interface */
|
||||||
|
export interface ValidationError extends MongooseError {
|
||||||
|
name: 'ValidationError';
|
||||||
|
errors: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
message: string;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Custom error with status code and body */
|
||||||
|
export interface CustomError extends Error {
|
||||||
|
statusCode?: number;
|
||||||
|
body?: unknown;
|
||||||
|
code?: string | number;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * from './azure';
|
export * from './azure';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
|
export * from './error';
|
||||||
export * from './google';
|
export * from './google';
|
||||||
export * from './mistral';
|
export * from './mistral';
|
||||||
export * from './openai';
|
export * from './openai';
|
||||||
|
|
|
||||||
|
|
@ -1359,6 +1359,10 @@ export enum ErrorTypes {
|
||||||
* Endpoint models not loaded
|
* Endpoint models not loaded
|
||||||
*/
|
*/
|
||||||
ENDPOINT_MODELS_NOT_LOADED = 'endpoint_models_not_loaded',
|
ENDPOINT_MODELS_NOT_LOADED = 'endpoint_models_not_loaded',
|
||||||
|
/**
|
||||||
|
* Generic Authentication failure
|
||||||
|
*/
|
||||||
|
AUTH_FAILED = 'auth_failed',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue