mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-25 04:40:15 +01:00
Move AuthService to package-auth
This commit is contained in:
parent
f9d40784f0
commit
e77aa92a7b
30 changed files with 1520 additions and 285 deletions
|
|
@ -4,18 +4,22 @@ const openIdClient = require('openid-client');
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
registerUser,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
setAuthTokens,
|
||||
requestPasswordReset,
|
||||
setOpenIDAuthTokens,
|
||||
} = require('~/server/services/AuthService');
|
||||
} = require('@librechat/auth');
|
||||
const { findUser, getUserById, deleteAllUserSessions, findSession } = require('~/models');
|
||||
const { getOpenIdConfig } = require('~/strategies');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||
const { getBalanceConfig } = require('~/server/services/Config');
|
||||
|
||||
const registrationController = async (req, res) => {
|
||||
try {
|
||||
const response = await registerUser(req.body);
|
||||
const isEmailDomAllowed = await isEmailDomainAllowed(req.body.email);
|
||||
const balanceConfig = await getBalanceConfig();
|
||||
const response = await registerUser(req.body, {}, isEmailDomAllowed, balanceConfig);
|
||||
const { status, message } = response;
|
||||
res.status(status).send({ message });
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const {
|
|||
} = require('~/models');
|
||||
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
||||
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
|
||||
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
|
||||
const { verifyEmail, resendVerificationEmail } = require('@librechat/auth');
|
||||
const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud');
|
||||
const { processDeleteRequest } = require('~/server/services/Files/process');
|
||||
const { Transaction, Balance, User } = require('~/db/models');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { generate2FATempToken } = require('~/server/services/twoFactorService');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const { setAuthTokens } = require('@librechat/auth');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const loginController = async (req, res) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const cookies = require('cookie');
|
||||
const { getOpenIdConfig } = require('~/strategies');
|
||||
const { logoutUser } = require('~/server/services/AuthService');
|
||||
const { logoutUser } = require('@librechat/auth');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const {
|
|||
getTOTPSecret,
|
||||
verifyBackupCode,
|
||||
} = require('~/server/services/twoFactorService');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const { setAuthTokens } = require('@librechat/auth');
|
||||
const { getUserById } = require('~/models');
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const cookieParser = require('cookie-parser');
|
|||
const { connectDb, indexSync } = require('~/db');
|
||||
|
||||
const { jwtLogin, passportLogin } = require('~/strategies');
|
||||
const { initAuthModels } = require('@librechat/auth');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { ldapLogin } = require('~/strategies');
|
||||
const { logger } = require('~/config');
|
||||
|
|
@ -36,7 +37,8 @@ const startServer = async () => {
|
|||
if (typeof Bun !== 'undefined') {
|
||||
axios.defaults.headers.common['Accept-Encoding'] = 'gzip';
|
||||
}
|
||||
await connectDb();
|
||||
const mongooseInstance = await connectDb();
|
||||
initAuthModels(mongooseInstance);
|
||||
|
||||
logger.info('Connected to MongoDB');
|
||||
await indexSync();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const {
|
|||
setBalanceConfig,
|
||||
checkDomainAllowed,
|
||||
} = require('~/server/middleware');
|
||||
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
||||
const { setAuthTokens, setOpenIDAuthTokens } = require('@librechat/auth');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ const {
|
|||
conflictingAzureVariables,
|
||||
extractVariableName,
|
||||
} = require('librechat-data-provider');
|
||||
const { isEnabled, checkEmailConfig } = require('~/server/utils');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { checkEmailConfig } = require('@librechat/auth');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const secretDefaults = {
|
||||
|
|
|
|||
|
|
@ -8,22 +8,8 @@ const queue = require('./queue');
|
|||
const files = require('./files');
|
||||
const math = require('./math');
|
||||
|
||||
/**
|
||||
* Check if email configuration is set
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function checkEmailConfig() {
|
||||
return (
|
||||
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
|
||||
!!process.env.EMAIL_USERNAME &&
|
||||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
...streamResponse,
|
||||
checkEmailConfig,
|
||||
...cryptoUtils,
|
||||
...handleText,
|
||||
countTokens,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const { errorsToString } = require('librechat-data-provider');
|
||||
const { Strategy: PassportLocalStrategy } = require('passport-local');
|
||||
const { isEnabled, checkEmailConfig } = require('~/server/utils');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { checkEmailConfig } = require('@librechat/auth');
|
||||
const { findUser, comparePassword, updateUser } = require('~/models');
|
||||
const { loginSchema } = require('./validators');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const path = require('path');
|
|||
const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose'));
|
||||
const { User } = require('@librechat/data-schemas').createModels(mongoose);
|
||||
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
||||
const { registerUser } = require('~/server/services/AuthService');
|
||||
const { registerUser } = require('@librechat/auth');
|
||||
const { askQuestion, silentExit } = require('./helpers');
|
||||
const connect = require('./connect');
|
||||
|
||||
|
|
@ -102,7 +102,9 @@ or the user will need to attempt logging in to have a verification link sent to
|
|||
const user = { email, password, name, username, confirm_password: password };
|
||||
let result;
|
||||
try {
|
||||
result = await registerUser(user, { emailVerified });
|
||||
const isEmailDomAllowed = await isEmailDomAllowed(user.email);
|
||||
const balanceConfig = await getBalanceConfig();
|
||||
result = await registerUser(user, { emailVerified }, isEmailDomAllowed, balanceConfig);
|
||||
} catch (error) {
|
||||
console.red('Error: ' + error.message);
|
||||
silentExit(1);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ const path = require('path');
|
|||
const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose'));
|
||||
const { User } = require('@librechat/data-schemas').createModels(mongoose);
|
||||
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
||||
const { sendEmail, checkEmailConfig } = require('~/server/utils');
|
||||
const { sendEmail } = require('~/server/utils');
|
||||
const { checkEmailConfig } = require('@librechat/auth');
|
||||
const { askQuestion, silentExit } = require('./helpers');
|
||||
const { createInvite } = require('~/models/inviteUser');
|
||||
const connect = require('./connect');
|
||||
|
|
|
|||
334
package-lock.json
generated
334
package-lock.json
generated
|
|
@ -2200,15 +2200,6 @@
|
|||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"api/node_modules/jose": {
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
|
||||
"integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"api/node_modules/keyv-file": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/keyv-file/-/keyv-file-5.1.2.tgz",
|
||||
|
|
@ -2418,19 +2409,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api/node_modules/openid-client": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.5.0.tgz",
|
||||
"integrity": "sha512-fAfYaTnOYE2kQCqEJGX9KDObW2aw7IQy4jWpU/+3D3WoCFLbix5Hg6qIPQ6Js9r7f8jDUmsnnguRNCSw4wU/IQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jose": "^6.0.10",
|
||||
"oauth4webapi": "^3.5.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"api/node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
|
|
@ -19271,6 +19249,10 @@
|
|||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@librechat/auth": {
|
||||
"resolved": "packages/auth",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@librechat/backend": {
|
||||
"resolved": "api",
|
||||
"link": true
|
||||
|
|
@ -24648,6 +24630,15 @@
|
|||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcrypt": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
|
||||
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
|
|
@ -36359,11 +36350,55 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz",
|
||||
"integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
"bson": "^6.10.3",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.2.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||
|
|
@ -36373,7 +36408,6 @@
|
|||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
|
|
@ -36385,7 +36419,6 @@
|
|||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
|
|
@ -36394,6 +36427,27 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.15.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.1.tgz",
|
||||
"integrity": "sha512-RhQ4DzmBi5BNGcS0w4u1vdMRIKcteXTCNzDt1j7XRcdWYBz1MjMjulBhPaeC5jBCHOD1yinuOFTTSOWLLGexWw==",
|
||||
"dependencies": {
|
||||
"bson": "^6.10.3",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.16.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "17.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/moo-color": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
|
||||
|
|
@ -37132,6 +37186,26 @@
|
|||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.5.0.tgz",
|
||||
"integrity": "sha512-fAfYaTnOYE2kQCqEJGX9KDObW2aw7IQy4jWpU/+3D3WoCFLbix5Hg6qIPQ6Js9r7f8jDUmsnnguRNCSw4wU/IQ==",
|
||||
"dependencies": {
|
||||
"jose": "^6.0.10",
|
||||
"oauth4webapi": "^3.5.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/jose": {
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
|
||||
"integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||
|
|
@ -43233,7 +43307,6 @@
|
|||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz",
|
||||
"integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -45428,18 +45501,21 @@
|
|||
"packages/auth": {
|
||||
"name": "@librechat/auth",
|
||||
"version": "0.0.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"@librechat/data-schemas": "^0.0.7",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"klona": "^2.0.6",
|
||||
"mongoose": "^8.12.1",
|
||||
"nodemailer": "^7.0.3",
|
||||
"openid-client": "^6.5.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-facebook": "^3.0.0"
|
||||
"traverse": "^0.6.11",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@librechat/data-schemas": "^0.0.7",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
|
|
@ -45447,10 +45523,12 @@
|
|||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.0",
|
||||
"@types/traverse": "^0.6.37",
|
||||
"jest": "^29.5.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"rimraf": "^5.0.1",
|
||||
|
|
@ -45465,6 +45543,200 @@
|
|||
"keyv": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/bcryptjs": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/nodemailer": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
|
||||
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/rimraf": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob": "^10.3.7"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/traverse": {
|
||||
"version": "0.6.11",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz",
|
||||
"integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==",
|
||||
"dependencies": {
|
||||
"gopd": "^1.2.0",
|
||||
"typedarray.prototype.slice": "^1.0.5",
|
||||
"which-typed-array": "^1.1.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/winston": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/winston-daily-rotate-file": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz",
|
||||
"integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==",
|
||||
"dependencies": {
|
||||
"file-stream-rotator": "^0.6.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"triple-beam": "^1.4.1",
|
||||
"winston-transport": "^4.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"winston": "^3"
|
||||
}
|
||||
},
|
||||
"packages/auth/node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"packages/data-provider": {
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.7.86",
|
||||
|
|
|
|||
2
packages/auth/.gitignore
vendored
Normal file
2
packages/auth/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
test_bundle/
|
||||
21
packages/auth/LICENSE
Normal file
21
packages/auth/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 LibreChat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
114
packages/auth/README.md
Normal file
114
packages/auth/README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# `@librechat/data-schemas`
|
||||
|
||||
Mongoose schemas and models for LibreChat. This package provides a comprehensive collection of Mongoose schemas used across the LibreChat project, enabling robust data modeling and validation for various entities such as actions, agents, messages, users, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Modular Schemas:** Includes schemas for actions, agents, assistants, balance, banners, categories, conversation tags, conversations, files, keys, messages, plugin authentication, presets, projects, prompts, prompt groups, roles, sessions, shared links, tokens, tool calls, transactions, and users.
|
||||
- **TypeScript Support:** Provides TypeScript definitions for type-safe development.
|
||||
- **Ready for Mongoose Integration:** Easily integrate with Mongoose to create models and interact with your MongoDB database.
|
||||
- **Flexible & Extensible:** Designed to support the evolving needs of LibreChat while being adaptable to other projects.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package via npm or yarn:
|
||||
|
||||
```bash
|
||||
npm install @librechat/auth
|
||||
```
|
||||
|
||||
Or with yarn:
|
||||
|
||||
```bash
|
||||
yarn add @librechat/auth
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, you can import and use the schemas in your project. For example, to create a Mongoose model for a user:
|
||||
|
||||
```js
|
||||
import mongoose from 'mongoose';
|
||||
import { userSchema } from '@librechat/data-schemas';
|
||||
|
||||
const UserModel = mongoose.model('User', userSchema);
|
||||
|
||||
// Now you can use UserModel to create, read, update, and delete user documents.
|
||||
```
|
||||
|
||||
You can also import other schemas as needed:
|
||||
|
||||
```js
|
||||
import { actionSchema, agentSchema, messageSchema } from '@librechat/data-schemas';
|
||||
```
|
||||
|
||||
Each schema is designed to integrate seamlessly with Mongoose and provides indexes, timestamps, and validations tailored for LibreChat’s use cases.
|
||||
|
||||
## Development
|
||||
|
||||
This package uses Rollup and TypeScript for building and bundling.
|
||||
|
||||
### Available Scripts
|
||||
|
||||
- **Build:**
|
||||
Cleans the `dist` directory and builds the package.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
- **Build Watch:**
|
||||
Rebuilds automatically on file changes.
|
||||
|
||||
```bash
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
- **Test:**
|
||||
Runs tests with coverage in watch mode.
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
- **Test (CI):**
|
||||
Runs tests with coverage for CI environments.
|
||||
|
||||
```bash
|
||||
npm run test:ci
|
||||
```
|
||||
|
||||
- **Verify:**
|
||||
Runs tests in CI mode to verify code integrity.
|
||||
|
||||
```bash
|
||||
npm run verify
|
||||
```
|
||||
|
||||
- **Clean:**
|
||||
Removes the `dist` directory.
|
||||
```bash
|
||||
npm run clean
|
||||
```
|
||||
|
||||
For those using Bun, equivalent scripts are available:
|
||||
|
||||
- **Bun Clean:** `bun run b:clean`
|
||||
- **Bun Build:** `bun run b:build`
|
||||
|
||||
## Repository & Issues
|
||||
|
||||
The source code is maintained on GitHub.
|
||||
|
||||
- **Repository:** [LibreChat Repository](https://github.com/danny-avila/LibreChat.git)
|
||||
- **Issues & Bug Reports:** [LibreChat Issues](https://github.com/danny-avila/LibreChat/issues)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to improve and expand the data schemas are welcome. If you have suggestions, improvements, or bug fixes, please open an issue or submit a pull request on the [GitHub repository](https://github.com/danny-avila/LibreChat/issues).
|
||||
|
||||
For more detailed documentation on each schema and model, please refer to the source code or visit the [LibreChat website](https://librechat.ai).
|
||||
4
packages/auth/babel.config.cjs
Normal file
4
packages/auth/babel.config.cjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
|
||||
plugins: ['babel-plugin-replace-ts-export-assignment'],
|
||||
};
|
||||
19
packages/auth/jest.config.mjs
Normal file
19
packages/auth/jest.config.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export default {
|
||||
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!<rootDir>/node_modules/'],
|
||||
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
|
||||
coverageReporters: ['text', 'cobertura'],
|
||||
testResultsProcessor: 'jest-junit',
|
||||
moduleNameMapper: {
|
||||
'^@src/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
// coverageThreshold: {
|
||||
// global: {
|
||||
// statements: 58,
|
||||
// branches: 49,
|
||||
// functions: 50,
|
||||
// lines: 57,
|
||||
// },
|
||||
// },
|
||||
restoreMocks: true,
|
||||
testTimeout: 15000,
|
||||
};
|
||||
89
packages/auth/package.json
Normal file
89
packages/auth/package.json
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"name": "@librechat/auth",
|
||||
"version": "0.0.1",
|
||||
"description": "Librechat auth functionality",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.es.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/types/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && rollup -c --silent --bundleConfigAsCjs",
|
||||
"build:watch": "rollup -c -w",
|
||||
"test": "jest --coverage --watch",
|
||||
"test:ci": "jest --coverage --ci",
|
||||
"verify": "npm run test:ci",
|
||||
"b:clean": "bun run rimraf dist",
|
||||
"b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/danny-avila/LibreChat.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/danny-avila/LibreChat/issues"
|
||||
},
|
||||
"homepage": "https://librechat.ai",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.1.0",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.0",
|
||||
"@types/traverse": "^0.6.37",
|
||||
"jest": "^29.5.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-generate-package-json": "^3.2.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-typescript2": "^0.35.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@librechat/data-schemas": "^0.0.7",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"klona": "^2.0.6",
|
||||
"mongoose": "^8.12.1",
|
||||
"nodemailer": "^7.0.3",
|
||||
"openid-client": "^6.5.0",
|
||||
"traverse": "^0.6.11",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"keyv": "^5.3.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"mongoose",
|
||||
"schema",
|
||||
"typescript",
|
||||
"librechat"
|
||||
]
|
||||
}
|
||||
40
packages/auth/rollup.config.js
Normal file
40
packages/auth/rollup.config.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import json from '@rollup/plugin-json';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.es.js',
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
// Allow importing JSON files
|
||||
json(),
|
||||
// Automatically externalize peer dependencies
|
||||
peerDepsExternal(),
|
||||
// Resolve modules from node_modules
|
||||
nodeResolve(),
|
||||
// Convert CommonJS modules to ES6
|
||||
commonjs(),
|
||||
// Compile TypeScript files and generate type declarations
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
declaration: true,
|
||||
declarationDir: 'dist/types',
|
||||
rootDir: 'src',
|
||||
}),
|
||||
],
|
||||
// Do not bundle these external dependencies
|
||||
external: ['mongoose'],
|
||||
};
|
||||
241
packages/auth/src/config/parsers.ts
Normal file
241
packages/auth/src/config/parsers.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import { klona } from 'klona';
|
||||
import winston from 'winston';
|
||||
import traverse from 'traverse';
|
||||
|
||||
const SPLAT_SYMBOL = Symbol.for('splat');
|
||||
const MESSAGE_SYMBOL = Symbol.for('message');
|
||||
const CONSOLE_JSON_STRING_LENGTH: number =
|
||||
parseInt(process.env.CONSOLE_JSON_STRING_LENGTH || '', 10) || 255;
|
||||
|
||||
const sensitiveKeys: RegExp[] = [
|
||||
/^(sk-)[^\s]+/, // OpenAI API key pattern
|
||||
/(Bearer )[^\s]+/, // Header: Bearer token pattern
|
||||
/(api-key:? )[^\s]+/, // Header: API key pattern
|
||||
/(key=)[^\s]+/, // URL query param: sensitive key pattern (Google)
|
||||
];
|
||||
|
||||
/**
|
||||
* Determines if a given value string is sensitive and returns matching regex patterns.
|
||||
*
|
||||
* @param valueStr - The value string to check.
|
||||
* @returns An array of regex patterns that match the value string.
|
||||
*/
|
||||
function getMatchingSensitivePatterns(valueStr: string): RegExp[] {
|
||||
if (valueStr) {
|
||||
// Filter and return all regex patterns that match the value string
|
||||
return sensitiveKeys.filter((regex) => regex.test(valueStr));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Redacts sensitive information from a console message and trims it to a specified length if provided.
|
||||
* @param str - The console message to be redacted.
|
||||
* @param trimLength - The optional length at which to trim the redacted message.
|
||||
* @returns The redacted and optionally trimmed console message.
|
||||
*/
|
||||
function redactMessage(str: string, trimLength?: number): string {
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const patterns = getMatchingSensitivePatterns(str);
|
||||
patterns.forEach((pattern) => {
|
||||
str = str.replace(pattern, '$1[REDACTED]');
|
||||
});
|
||||
|
||||
if (trimLength !== undefined && str.length > trimLength) {
|
||||
return `${str.substring(0, trimLength)}...`;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redacts sensitive information from log messages if the log level is 'error'.
|
||||
* Note: Intentionally mutates the object.
|
||||
* @param info - The log information object.
|
||||
* @returns The modified log information object.
|
||||
*/
|
||||
const redactFormat = winston.format((info: winston.Logform.TransformableInfo) => {
|
||||
if (info.level === 'error') {
|
||||
// Type guard to ensure message is a string
|
||||
if (typeof info.message === 'string') {
|
||||
info.message = redactMessage(info.message);
|
||||
}
|
||||
|
||||
// Handle MESSAGE_SYMBOL with type safety
|
||||
const symbolValue = (info as Record<string | symbol, unknown>)[MESSAGE_SYMBOL];
|
||||
if (typeof symbolValue === 'string') {
|
||||
(info as Record<string | symbol, unknown>)[MESSAGE_SYMBOL] = redactMessage(symbolValue);
|
||||
}
|
||||
}
|
||||
return info;
|
||||
});
|
||||
|
||||
/**
|
||||
* Truncates long strings, especially base64 image data, within log messages.
|
||||
*
|
||||
* @param value - The value to be inspected and potentially truncated.
|
||||
* @param length - The length at which to truncate the value. Default: 100.
|
||||
* @returns The truncated or original value.
|
||||
*/
|
||||
const truncateLongStrings = (value: unknown, length = 100): unknown => {
|
||||
if (typeof value === 'string') {
|
||||
return value.length > length ? value.substring(0, length) + '... [truncated]' : value;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* An array mapping function that truncates long strings (objects converted to JSON strings).
|
||||
* @param item - The item to be condensed.
|
||||
* @returns The condensed item.
|
||||
*/
|
||||
const condenseArray = (item: unknown): string | unknown => {
|
||||
if (typeof item === 'string') {
|
||||
return truncateLongStrings(JSON.stringify(item));
|
||||
} else if (typeof item === 'object') {
|
||||
return truncateLongStrings(JSON.stringify(item));
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats log messages for debugging purposes.
|
||||
* - Truncates long strings within log messages.
|
||||
* - Condenses arrays by truncating long strings and objects as strings within array items.
|
||||
* - Redacts sensitive information from log messages if the log level is 'error'.
|
||||
* - Converts log information object to a formatted string.
|
||||
*
|
||||
* @param options - The options for formatting log messages.
|
||||
* @returns The formatted log message.
|
||||
*/
|
||||
const debugTraverse = winston.format.printf(
|
||||
({ level, message, timestamp, ...metadata }: Record<string, unknown>) => {
|
||||
if (!message) {
|
||||
return `${timestamp} ${level}`;
|
||||
}
|
||||
|
||||
// Type-safe version of the CJS logic: !message?.trim || typeof message !== 'string'
|
||||
if (typeof message !== 'string' || !message.trim) {
|
||||
return `${timestamp} ${level}: ${JSON.stringify(message)}`;
|
||||
}
|
||||
|
||||
let msg = `${timestamp} ${level}: ${truncateLongStrings(message.trim(), 150)}`;
|
||||
|
||||
try {
|
||||
if (level !== 'debug') {
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Type-safe access to SPLAT_SYMBOL using bracket notation
|
||||
const metadataRecord = metadata as Record<string | symbol, unknown>;
|
||||
const splatArray = metadataRecord[SPLAT_SYMBOL];
|
||||
const debugValue = Array.isArray(splatArray) ? splatArray[0] : undefined;
|
||||
|
||||
if (!debugValue) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (debugValue && Array.isArray(debugValue)) {
|
||||
msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`;
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (typeof debugValue !== 'object') {
|
||||
return (msg += ` ${debugValue}`);
|
||||
}
|
||||
|
||||
msg += '\n{';
|
||||
|
||||
const copy = klona(metadata);
|
||||
|
||||
traverse(copy).forEach(function (this: traverse.TraverseContext, value: unknown) {
|
||||
if (typeof this?.key === 'symbol') {
|
||||
return;
|
||||
}
|
||||
|
||||
let _parentKey = '';
|
||||
const parent = this.parent;
|
||||
|
||||
if (typeof parent?.key !== 'symbol' && parent?.key) {
|
||||
_parentKey = parent.key;
|
||||
}
|
||||
|
||||
const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`;
|
||||
const tabs = `${parent && parent.notRoot ? ' ' : ' '}`;
|
||||
const currentKey = this?.key ?? 'unknown';
|
||||
|
||||
if (this.isLeaf && typeof value === 'string') {
|
||||
const truncatedText = truncateLongStrings(value);
|
||||
msg += `\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`;
|
||||
} else if (this.notLeaf && Array.isArray(value) && value.length > 0) {
|
||||
const currentMessage = `\n${tabs}// ${value.length} ${currentKey.replace(/s$/, '')}(s)`;
|
||||
this.update(currentMessage, true);
|
||||
msg += currentMessage;
|
||||
const stringifiedArray = value.map(condenseArray);
|
||||
msg += `\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`;
|
||||
} else if (this.isLeaf && typeof value === 'function') {
|
||||
msg += `\n${tabs}${parentKey}${currentKey}: function,`;
|
||||
} else if (this.isLeaf) {
|
||||
msg += `\n${tabs}${parentKey}${currentKey}: ${value},`;
|
||||
}
|
||||
});
|
||||
|
||||
msg += '\n}';
|
||||
return msg;
|
||||
} catch (e: unknown) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||
return (msg += `\n[LOGGER PARSING ERROR] ${errorMessage}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Truncates long string values in JSON log objects.
|
||||
* Prevents outputting extremely long values (e.g., base64, blobs).
|
||||
*/
|
||||
const jsonTruncateFormat = winston.format((info: winston.Logform.TransformableInfo) => {
|
||||
const truncateLongStrings = (str: string, maxLength: number): string =>
|
||||
str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
|
||||
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
const truncateObject = (obj: unknown): unknown => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Handle circular references - now with proper object type
|
||||
if (seen.has(obj)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(obj);
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => truncateObject(item));
|
||||
}
|
||||
|
||||
// We know this is an object at this point
|
||||
const objectRecord = obj as Record<string, unknown>;
|
||||
const newObj: Record<string, unknown> = {};
|
||||
Object.entries(objectRecord).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
newObj[key] = truncateLongStrings(value, CONSOLE_JSON_STRING_LENGTH);
|
||||
} else {
|
||||
newObj[key] = truncateObject(value);
|
||||
}
|
||||
});
|
||||
return newObj;
|
||||
};
|
||||
|
||||
return truncateObject(info) as winston.Logform.TransformableInfo;
|
||||
});
|
||||
|
||||
export { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat };
|
||||
123
packages/auth/src/config/winston.ts
Normal file
123
packages/auth/src/config/winston.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import path from 'path';
|
||||
import winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
import { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat } from './parsers';
|
||||
|
||||
// Define log directory
|
||||
const logDir = path.join(__dirname, '..', 'logs');
|
||||
|
||||
// Type-safe environment variables
|
||||
const { NODE_ENV, DEBUG_LOGGING, CONSOLE_JSON, DEBUG_CONSOLE } = process.env;
|
||||
|
||||
const useConsoleJson = typeof CONSOLE_JSON === 'string' && CONSOLE_JSON.toLowerCase() === 'true';
|
||||
|
||||
const useDebugConsole = typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE.toLowerCase() === 'true';
|
||||
|
||||
const useDebugLogging = typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING.toLowerCase() === 'true';
|
||||
|
||||
// Define custom log levels
|
||||
const levels: winston.config.AbstractConfigSetLevels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
verbose: 4,
|
||||
debug: 5,
|
||||
activity: 6,
|
||||
silly: 7,
|
||||
};
|
||||
|
||||
winston.addColors({
|
||||
info: 'green',
|
||||
warn: 'italic yellow',
|
||||
error: 'red',
|
||||
debug: 'blue',
|
||||
});
|
||||
|
||||
const level = (): string => {
|
||||
const env = NODE_ENV || 'development';
|
||||
return env === 'development' ? 'debug' : 'warn';
|
||||
};
|
||||
|
||||
const fileFormat = winston.format.combine(
|
||||
redactFormat(),
|
||||
winston.format.timestamp({ format: () => new Date().toISOString() }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
);
|
||||
|
||||
const transports: winston.transport[] = [
|
||||
new winston.transports.DailyRotateFile({
|
||||
level: 'error',
|
||||
filename: `${logDir}/error-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
format: fileFormat,
|
||||
}),
|
||||
];
|
||||
|
||||
if (useDebugLogging) {
|
||||
transports.push(
|
||||
new winston.transports.DailyRotateFile({
|
||||
level: 'debug',
|
||||
filename: `${logDir}/debug-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
format: winston.format.combine(fileFormat, debugTraverse),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const consoleFormat = winston.format.combine(
|
||||
redactFormat(),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf((info) => {
|
||||
const message = `${info.timestamp} ${info.level}: ${info.message}`;
|
||||
return info.level.includes('error') ? redactMessage(message) : message;
|
||||
}),
|
||||
);
|
||||
|
||||
let consoleLogLevel: string = 'info';
|
||||
if (useDebugConsole) {
|
||||
consoleLogLevel = 'debug';
|
||||
}
|
||||
|
||||
// Add console transport
|
||||
if (useDebugConsole) {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
level: consoleLogLevel,
|
||||
format: useConsoleJson
|
||||
? winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json())
|
||||
: winston.format.combine(fileFormat, debugTraverse),
|
||||
}),
|
||||
);
|
||||
} else if (useConsoleJson) {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
level: consoleLogLevel,
|
||||
format: winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json()),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
level: consoleLogLevel,
|
||||
format: consoleFormat,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Create logger
|
||||
const logger = winston.createLogger({
|
||||
level: level(),
|
||||
levels,
|
||||
transports,
|
||||
});
|
||||
|
||||
export default logger;
|
||||
|
|
@ -1,46 +1,47 @@
|
|||
const bcrypt = require('bcryptjs');
|
||||
const { webcrypto } = require('node:crypto');
|
||||
const { SystemRoles, errorsToString } = require('librechat-data-provider');
|
||||
const {
|
||||
findUser,
|
||||
createUser,
|
||||
updateUser,
|
||||
findToken,
|
||||
countUsers,
|
||||
getUserById,
|
||||
findSession,
|
||||
createToken,
|
||||
deleteTokens,
|
||||
deleteSession,
|
||||
createSession,
|
||||
generateToken,
|
||||
deleteUserById,
|
||||
generateRefreshToken,
|
||||
} = require('~/models');
|
||||
const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils');
|
||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||
const { getBalanceConfig } = require('~/server/services/Config');
|
||||
const { registerSchema } = require('~/strategies/validators');
|
||||
const { logger } = require('~/config');
|
||||
import { Request, Response } from 'express';
|
||||
import { TokenEndpointResponse } from 'openid-client';
|
||||
import { errorsToString, SystemRoles } from 'librechat-data-provider';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { IUser } from '@librechat/data-schemas';
|
||||
import { registerSchema } from './strategies/validators';
|
||||
import { webcrypto } from 'node:crypto';
|
||||
import { sendEmail } from './utils/sendEmail';
|
||||
import logger from './config/winston';
|
||||
import { ObjectId } from 'mongoose';
|
||||
import { checkEmailConfig, isEnabled } from './utils';
|
||||
import { initAuthModels, getMethods } from './init';
|
||||
|
||||
const genericVerificationMessage = 'Please check your email to verify your email address.';
|
||||
const domains = {
|
||||
client: process.env.DOMAIN_CLIENT,
|
||||
server: process.env.DOMAIN_SERVER,
|
||||
};
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const genericVerificationMessage = 'Please check your email to verify your email address.';
|
||||
interface LogoutResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: { _id: string };
|
||||
session?: {
|
||||
destroy: (callback?: (err?: any) => void) => void;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*
|
||||
* @param {ServerRequest} req
|
||||
* @param req
|
||||
* @param {string} refreshToken
|
||||
* @returns
|
||||
*/
|
||||
const logoutUser = async (req, refreshToken) => {
|
||||
const logoutUser = async (
|
||||
req: AuthenticatedRequest,
|
||||
refreshToken: string | null,
|
||||
): Promise<LogoutResponse> => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { findSession, deleteSession } = getMethods();
|
||||
const userId: string | null = req.user?._id ?? null;
|
||||
const session = await findSession({ userId: userId, refreshToken });
|
||||
|
||||
if (session) {
|
||||
|
|
@ -53,24 +54,116 @@ const logoutUser = async (req, refreshToken) => {
|
|||
}
|
||||
|
||||
try {
|
||||
req.session.destroy();
|
||||
req.session?.destroy();
|
||||
} catch (destroyErr) {
|
||||
logger.debug('[logoutUser] Failed to destroy session.', destroyErr);
|
||||
}
|
||||
|
||||
return { status: 200, message: 'Logout successful' };
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
return { status: 500, message: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
* @param {MongoUser} user <email, password, name, username>
|
||||
* @param {Partial<MongoUser>} [additionalData={}]
|
||||
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
||||
*/
|
||||
const registerUser = async (
|
||||
user: IUser,
|
||||
additionalData: Partial<IUser> = {},
|
||||
isEmailDomainAllowed: boolean = true,
|
||||
balanceConfig: Record<string, any>,
|
||||
) => {
|
||||
const { error } = registerSchema.safeParse(user);
|
||||
const { findUser, countUsers, createUser, updateUser, deleteUserById } = getMethods();
|
||||
if (error) {
|
||||
const errorMessage = errorsToString(error.errors);
|
||||
logger.info(
|
||||
'Route: register - Validation Error',
|
||||
{ name: 'Request params:', value: user },
|
||||
{ name: 'Validation error:', value: errorMessage },
|
||||
);
|
||||
|
||||
return { status: 404, message: errorMessage };
|
||||
}
|
||||
|
||||
const { email, password, name, username } = user;
|
||||
|
||||
let newUserId;
|
||||
try {
|
||||
const existingUser = await findUser({ email }, 'email _id');
|
||||
if (existingUser) {
|
||||
logger.info(
|
||||
'Register User - Email in use',
|
||||
{ name: 'Request params:', value: user },
|
||||
{ name: 'Existing user:', value: existingUser },
|
||||
);
|
||||
|
||||
// Sleep for 1 second
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
}
|
||||
|
||||
if (!isEmailDomainAllowed) {
|
||||
const errorMessage =
|
||||
'The email address provided cannot be used. Please use a different email address.';
|
||||
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
||||
return { status: 403, message: errorMessage };
|
||||
}
|
||||
|
||||
//determine if this is the first registered user (not counting anonymous_user)
|
||||
const isFirstRegisteredUser = (await countUsers()) === 0;
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const newUserData: Partial<IUser> = {
|
||||
provider: 'local',
|
||||
email,
|
||||
username,
|
||||
name,
|
||||
avatar: '',
|
||||
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
|
||||
password: bcrypt.hashSync(password ?? '', salt),
|
||||
...additionalData,
|
||||
};
|
||||
|
||||
const emailEnabled = checkEmailConfig();
|
||||
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN ?? '');
|
||||
|
||||
const newUser = await createUser(newUserData, balanceConfig, disableTTL, true);
|
||||
newUserId = newUser._id;
|
||||
if (emailEnabled && !newUser.emailVerified) {
|
||||
await sendVerificationEmail({
|
||||
_id: newUserId,
|
||||
email,
|
||||
name,
|
||||
});
|
||||
} else {
|
||||
await updateUser(newUserId, { emailVerified: true });
|
||||
}
|
||||
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
} catch (err) {
|
||||
logger.error('[registerUser] Error in registering user:', err);
|
||||
if (newUserId) {
|
||||
const result = await deleteUserById(newUserId);
|
||||
logger.warn(
|
||||
`[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`,
|
||||
);
|
||||
}
|
||||
return { status: 500, message: 'Something went wrong' };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates Token and corresponding Hash for verification
|
||||
* @returns {[string, string]}
|
||||
*/
|
||||
const createTokenHash = () => {
|
||||
const token = Buffer.from(webcrypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
||||
const hash = bcrypt.hashSync(token, 10);
|
||||
const createTokenHash = (): [string, string] => {
|
||||
const token: string = Buffer.from(webcrypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
||||
const hash: string = bcrypt.hashSync(token, 10);
|
||||
return [token, hash];
|
||||
};
|
||||
|
||||
|
|
@ -79,9 +172,9 @@ const createTokenHash = () => {
|
|||
* @param {Partial<MongoUser> & { _id: ObjectId, email: string, name: string}} user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const sendVerificationEmail = async (user) => {
|
||||
const sendVerificationEmail = async (user: Partial<IUser> & { _id: ObjectId; email: string }) => {
|
||||
const [verifyToken, hash] = createTokenHash();
|
||||
|
||||
const { createToken } = getMethods();
|
||||
const verificationLink = `${
|
||||
domains.client
|
||||
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
||||
|
|
@ -112,9 +205,10 @@ const sendVerificationEmail = async (user) => {
|
|||
* Verify Email
|
||||
* @param {Express.Request} req
|
||||
*/
|
||||
const verifyEmail = async (req) => {
|
||||
const verifyEmail = async (req: Request) => {
|
||||
const { email, token } = req.body;
|
||||
const decodedEmail = decodeURIComponent(email);
|
||||
const { findUser, findToken, updateUser, deleteTokens } = getMethods();
|
||||
|
||||
const user = await findUser({ email: decodedEmail }, 'email _id emailVerified');
|
||||
|
||||
|
|
@ -157,99 +251,117 @@ const verifyEmail = async (req) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
* @param {MongoUser} user <email, password, name, username>
|
||||
* @param {Partial<MongoUser>} [additionalData={}]
|
||||
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
||||
* Resend Verification Email
|
||||
* @param {Object} req
|
||||
* @param {Object} req.body
|
||||
* @param {String} req.body.email
|
||||
* @returns {Promise<{status: number, message: string}>}
|
||||
*/
|
||||
const registerUser = async (user, additionalData = {}) => {
|
||||
const { error } = registerSchema.safeParse(user);
|
||||
if (error) {
|
||||
const errorMessage = errorsToString(error.errors);
|
||||
logger.info(
|
||||
'Route: register - Validation Error',
|
||||
{ name: 'Request params:', value: user },
|
||||
{ name: 'Validation error:', value: errorMessage },
|
||||
);
|
||||
|
||||
return { status: 404, message: errorMessage };
|
||||
}
|
||||
|
||||
const { email, password, name, username } = user;
|
||||
|
||||
let newUserId;
|
||||
const resendVerificationEmail = async (req: Request) => {
|
||||
try {
|
||||
const existingUser = await findUser({ email }, 'email _id');
|
||||
const { deleteTokens, findUser, createToken } = getMethods();
|
||||
const { email } = req.body;
|
||||
await deleteTokens(email);
|
||||
const user = await findUser({ email }, 'email _id name');
|
||||
|
||||
if (existingUser) {
|
||||
logger.info(
|
||||
'Register User - Email in use',
|
||||
{ name: 'Request params:', value: user },
|
||||
{ name: 'Existing user:', value: existingUser },
|
||||
);
|
||||
|
||||
// Sleep for 1 second
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
if (!user) {
|
||||
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
}
|
||||
|
||||
if (!(await isEmailDomainAllowed(email))) {
|
||||
const errorMessage =
|
||||
'The email address provided cannot be used. Please use a different email address.';
|
||||
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
||||
return { status: 403, message: errorMessage };
|
||||
}
|
||||
const [verifyToken, hash] = createTokenHash();
|
||||
|
||||
//determine if this is the first registered user (not counting anonymous_user)
|
||||
const isFirstRegisteredUser = (await countUsers()) === 0;
|
||||
const verificationLink = `${
|
||||
domains.client
|
||||
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const newUserData = {
|
||||
provider: 'local',
|
||||
email,
|
||||
username,
|
||||
name,
|
||||
avatar: null,
|
||||
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
|
||||
password: bcrypt.hashSync(password, salt),
|
||||
...additionalData,
|
||||
await sendEmail({
|
||||
email: user.email,
|
||||
subject: 'Verify your email',
|
||||
payload: {
|
||||
appName: process.env.APP_TITLE || 'LibreChat',
|
||||
name: user.name || user.username || user.email,
|
||||
verificationLink: verificationLink,
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
template: 'verifyEmail.handlebars',
|
||||
});
|
||||
|
||||
await createToken({
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
token: hash,
|
||||
createdAt: Date.now(),
|
||||
expiresIn: 900,
|
||||
});
|
||||
|
||||
logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
message: genericVerificationMessage,
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.error(`[resendVerificationEmail] Error resending verification email: ${error.message}`);
|
||||
return {
|
||||
status: 500,
|
||||
message: 'Something went wrong.',
|
||||
};
|
||||
|
||||
const emailEnabled = checkEmailConfig();
|
||||
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN);
|
||||
const balanceConfig = await getBalanceConfig();
|
||||
|
||||
const newUser = await createUser(newUserData, balanceConfig, disableTTL, true);
|
||||
newUserId = newUser._id;
|
||||
if (emailEnabled && !newUser.emailVerified) {
|
||||
await sendVerificationEmail({
|
||||
_id: newUserId,
|
||||
email,
|
||||
name,
|
||||
});
|
||||
} else {
|
||||
await updateUser(newUserId, { emailVerified: true });
|
||||
}
|
||||
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
} catch (err) {
|
||||
logger.error('[registerUser] Error in registering user:', err);
|
||||
if (newUserId) {
|
||||
const result = await deleteUserById(newUserId);
|
||||
logger.warn(
|
||||
`[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`,
|
||||
);
|
||||
}
|
||||
return { status: 500, message: 'Something went wrong' };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset Password
|
||||
*
|
||||
* @param {*} userId
|
||||
* @param {String} token
|
||||
* @param {String} password
|
||||
* @returns
|
||||
*/
|
||||
const resetPassword = async (userId: string | ObjectId, token: string, password: string) => {
|
||||
const { findToken, updateUser, deleteTokens } = getMethods();
|
||||
let passwordResetToken = await findToken({
|
||||
userId,
|
||||
});
|
||||
|
||||
if (!passwordResetToken) {
|
||||
return new Error('Invalid or expired password reset token');
|
||||
}
|
||||
|
||||
const isValid = bcrypt.compareSync(token, passwordResetToken.token);
|
||||
|
||||
if (!isValid) {
|
||||
return new Error('Invalid or expired password reset token');
|
||||
}
|
||||
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const user = await updateUser(userId, { password: hash });
|
||||
|
||||
if (checkEmailConfig()) {
|
||||
await sendEmail({
|
||||
email: user.email,
|
||||
subject: 'Password Reset Successfully',
|
||||
payload: {
|
||||
appName: process.env.APP_TITLE || 'LibreChat',
|
||||
name: user.name || user.username || user.email,
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
template: 'passwordReset.handlebars',
|
||||
});
|
||||
}
|
||||
|
||||
await deleteTokens({ token: passwordResetToken.token });
|
||||
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
|
||||
return { message: 'Password reset was successful' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
* @param {Express.Request} req
|
||||
*/
|
||||
const requestPasswordReset = async (req) => {
|
||||
const requestPasswordReset = async (req: Request) => {
|
||||
const { email } = req.body;
|
||||
const { findUser, createToken, deleteTokens } = getMethods();
|
||||
const user = await findUser({ email }, 'email _id');
|
||||
const emailEnabled = checkEmailConfig();
|
||||
|
||||
|
|
@ -302,50 +414,7 @@ const requestPasswordReset = async (req) => {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset Password
|
||||
*
|
||||
* @param {*} userId
|
||||
* @param {String} token
|
||||
* @param {String} password
|
||||
* @returns
|
||||
*/
|
||||
const resetPassword = async (userId, token, password) => {
|
||||
let passwordResetToken = await findToken({
|
||||
userId,
|
||||
});
|
||||
|
||||
if (!passwordResetToken) {
|
||||
return new Error('Invalid or expired password reset token');
|
||||
}
|
||||
|
||||
const isValid = bcrypt.compareSync(token, passwordResetToken.token);
|
||||
|
||||
if (!isValid) {
|
||||
return new Error('Invalid or expired password reset token');
|
||||
}
|
||||
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const user = await updateUser(userId, { password: hash });
|
||||
|
||||
if (checkEmailConfig()) {
|
||||
await sendEmail({
|
||||
email: user.email,
|
||||
subject: 'Password Reset Successfully',
|
||||
payload: {
|
||||
appName: process.env.APP_TITLE || 'LibreChat',
|
||||
name: user.name || user.username || user.email,
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
template: 'passwordReset.handlebars',
|
||||
});
|
||||
}
|
||||
|
||||
await deleteTokens({ token: passwordResetToken.token });
|
||||
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
|
||||
return { message: 'Password reset was successful' };
|
||||
};
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
/**
|
||||
* Set Auth Tokens
|
||||
*
|
||||
|
|
@ -354,8 +423,14 @@ const resetPassword = async (userId, token, password) => {
|
|||
* @param {String} sessionId
|
||||
* @returns
|
||||
*/
|
||||
const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||
const setAuthTokens = async (
|
||||
userId: string | ObjectId,
|
||||
res: Response,
|
||||
sessionId: string | null = null,
|
||||
) => {
|
||||
try {
|
||||
const { getUserById, generateToken, findSession, generateRefreshToken, createSession } =
|
||||
getMethods();
|
||||
const user = await getUserById(userId);
|
||||
const token = await generateToken(user);
|
||||
|
||||
|
|
@ -402,14 +477,14 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
|||
* @param {Object} res - response object
|
||||
* @returns {String} - access token
|
||||
*/
|
||||
const setOpenIDAuthTokens = (tokenset, res) => {
|
||||
const setOpenIDAuthTokens = (tokenset: TokenEndpointResponse, res: Response) => {
|
||||
try {
|
||||
if (!tokenset) {
|
||||
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
|
||||
return;
|
||||
}
|
||||
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
||||
const expiryInMilliseconds = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||
const expiryInMilliseconds = eval(REFRESH_TOKEN_EXPIRY ?? '') ?? 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||
const expirationDate = new Date(Date.now() + expiryInMilliseconds);
|
||||
if (tokenset == null) {
|
||||
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
|
||||
|
|
@ -437,73 +512,15 @@ const setOpenIDAuthTokens = (tokenset, res) => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resend Verification Email
|
||||
* @param {Object} req
|
||||
* @param {Object} req.body
|
||||
* @param {String} req.body.email
|
||||
* @returns {Promise<{status: number, message: string}>}
|
||||
*/
|
||||
const resendVerificationEmail = async (req) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
await deleteTokens(email);
|
||||
const user = await findUser({ email }, 'email _id name');
|
||||
|
||||
if (!user) {
|
||||
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
}
|
||||
|
||||
const [verifyToken, hash] = createTokenHash();
|
||||
|
||||
const verificationLink = `${
|
||||
domains.client
|
||||
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
||||
|
||||
await sendEmail({
|
||||
email: user.email,
|
||||
subject: 'Verify your email',
|
||||
payload: {
|
||||
appName: process.env.APP_TITLE || 'LibreChat',
|
||||
name: user.name || user.username || user.email,
|
||||
verificationLink: verificationLink,
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
template: 'verifyEmail.handlebars',
|
||||
});
|
||||
|
||||
await createToken({
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
token: hash,
|
||||
createdAt: Date.now(),
|
||||
expiresIn: 900,
|
||||
});
|
||||
|
||||
logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
message: genericVerificationMessage,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`[resendVerificationEmail] Error resending verification email: ${error.message}`);
|
||||
return {
|
||||
status: 500,
|
||||
message: 'Something went wrong.',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
logoutUser,
|
||||
verifyEmail,
|
||||
registerUser,
|
||||
export {
|
||||
setOpenIDAuthTokens,
|
||||
setAuthTokens,
|
||||
logoutUser,
|
||||
registerUser,
|
||||
verifyEmail,
|
||||
resendVerificationEmail,
|
||||
resetPassword,
|
||||
requestPasswordReset,
|
||||
resendVerificationEmail,
|
||||
setOpenIDAuthTokens,
|
||||
checkEmailConfig,
|
||||
initAuthModels,
|
||||
};
|
||||
28
packages/auth/src/init.ts
Normal file
28
packages/auth/src/init.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { createMethods, createModels } from '@librechat/data-schemas';
|
||||
import type { Mongoose } from 'mongoose';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
let models: any = null;
|
||||
let methods: any = {};
|
||||
|
||||
export function initAuthModels(mongoose: Mongoose) {
|
||||
if (initialized) return;
|
||||
models = createModels(mongoose);
|
||||
methods = createMethods(mongoose);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
export function getModels() {
|
||||
if (!models) {
|
||||
throw new Error('Auth models have not been initialized. Call initAuthModels() first.');
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
export function getMethods() {
|
||||
if (!methods) {
|
||||
throw new Error('Auth methods have not been initialized. Call initAuthModels() first.');
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
76
packages/auth/src/strategies/validators.ts
Normal file
76
packages/auth/src/strategies/validators.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const allowedCharactersRegex = new RegExp(
|
||||
'^[' +
|
||||
'a-zA-Z0-9_.@#$%&*()' + // Basic Latin characters and symbols
|
||||
'\\p{Script=Latin}' + // Latin script characters
|
||||
'\\p{Script=Common}' + // Characters common across scripts
|
||||
'\\p{Script=Cyrillic}' + // Cyrillic script
|
||||
'\\p{Script=Devanagari}' + // Devanagari script
|
||||
'\\p{Script=Han}' + // Han script
|
||||
'\\p{Script=Arabic}' + // Arabic script
|
||||
'\\p{Script=Hiragana}' + // Hiragana
|
||||
'\\p{Script=Katakana}' + // Katakana
|
||||
'\\p{Script=Hangul}' + // Hangul
|
||||
']+$', // End
|
||||
'u', // Unicode mode
|
||||
);
|
||||
|
||||
const injectionPatternsRegex = /('|--|\$ne|\$gt|\$lt|\$or|\{|\}|\*|;|<|>|\/|=)/i;
|
||||
|
||||
const usernameSchema = z
|
||||
.string()
|
||||
.min(2)
|
||||
.max(80)
|
||||
.refine((value) => allowedCharactersRegex.test(value), {
|
||||
message: 'Invalid characters in username',
|
||||
})
|
||||
.refine((value) => !injectionPatternsRegex.test(value), {
|
||||
message: 'Potential injection attack detected',
|
||||
});
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z
|
||||
.string()
|
||||
.min(8)
|
||||
.max(128)
|
||||
.refine((value) => value.trim().length > 0, {
|
||||
message: 'Password cannot be only spaces',
|
||||
}),
|
||||
});
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(3).max(80),
|
||||
username: z
|
||||
.union([z.literal(''), usernameSchema])
|
||||
.transform((value) => (value === '' ? null : value))
|
||||
.optional()
|
||||
.nullable(),
|
||||
email: z.string().email(),
|
||||
password: z
|
||||
.string()
|
||||
.min(8)
|
||||
.max(128)
|
||||
.refine((value) => value.trim().length > 0, {
|
||||
message: 'Password cannot be only spaces',
|
||||
}),
|
||||
confirm_password: z
|
||||
.string()
|
||||
.min(8)
|
||||
.max(128)
|
||||
.refine((value) => value.trim().length > 0, {
|
||||
message: 'Password cannot be only spaces',
|
||||
}),
|
||||
})
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
if (confirm_password !== password) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'The passwords did not match',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export { usernameSchema, loginSchema, registerSchema };
|
||||
38
packages/auth/src/utils/index.ts
Normal file
38
packages/auth/src/utils/index.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
export * from './schemaMethods';
|
||||
|
||||
/**
|
||||
* Checks if the given value is truthy by being either the boolean `true` or a string
|
||||
* that case-insensitively matches 'true'.
|
||||
*
|
||||
* @function
|
||||
* @param {string|boolean|null|undefined} value - The value to check.
|
||||
* @returns {boolean} Returns `true` if the value is the boolean `true` or a case-insensitive
|
||||
* match for the string 'true', otherwise returns `false`.
|
||||
* @example
|
||||
*
|
||||
* isEnabled("True"); // returns true
|
||||
* isEnabled("TRUE"); // returns true
|
||||
* isEnabled(true); // returns true
|
||||
* isEnabled("false"); // returns false
|
||||
* isEnabled(false); // returns false
|
||||
* isEnabled(null); // returns false
|
||||
* isEnabled(); // returns false
|
||||
*/
|
||||
export function isEnabled(value: boolean | string) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase().trim() === 'true';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function checkEmailConfig() {
|
||||
return (
|
||||
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
|
||||
!!process.env.EMAIL_USERNAME &&
|
||||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM
|
||||
);
|
||||
}
|
||||
39
packages/auth/src/utils/schemaMethods.ts
Normal file
39
packages/auth/src/utils/schemaMethods.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { createModels } from '@librechat/data-schemas';
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const { createMethods } = require('@librechat/data-schemas');
|
||||
const methods = createMethods(mongoose);
|
||||
|
||||
const {
|
||||
findSession,
|
||||
deleteSession,
|
||||
createSession,
|
||||
findUser,
|
||||
countUsers,
|
||||
deleteUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
createToken,
|
||||
findToken,
|
||||
deleteTokens,
|
||||
generateToken,
|
||||
generateRefreshToken,
|
||||
getUserById,
|
||||
} = methods;
|
||||
|
||||
export {
|
||||
findSession,
|
||||
deleteSession,
|
||||
createSession,
|
||||
findUser,
|
||||
countUsers,
|
||||
deleteUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
createToken,
|
||||
findToken,
|
||||
deleteTokens,
|
||||
generateToken,
|
||||
generateRefreshToken,
|
||||
getUserById,
|
||||
};
|
||||
83
packages/auth/src/utils/sendEmail.ts
Normal file
83
packages/auth/src/utils/sendEmail.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import nodemailer, { TransportOptions } from 'nodemailer';
|
||||
import handlebars from 'handlebars';
|
||||
import logger from '../config/winston';
|
||||
import { isEnabled } from '.';
|
||||
|
||||
interface SendEmailParams {
|
||||
email: string;
|
||||
subject: string;
|
||||
payload: Record<string, string | number>;
|
||||
template: string;
|
||||
throwError?: boolean;
|
||||
}
|
||||
|
||||
interface SendEmailResponse {
|
||||
accepted: string[];
|
||||
rejected: string[];
|
||||
response: string;
|
||||
envelope: { from: string; to: string[] };
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
export const sendEmail = async ({
|
||||
email,
|
||||
subject,
|
||||
payload,
|
||||
template,
|
||||
throwError = true,
|
||||
}: SendEmailParams): Promise<SendEmailResponse | Error> => {
|
||||
try {
|
||||
const transporterOptions: TransportOptions = {
|
||||
secure: process.env.EMAIL_ENCRYPTION === 'tls',
|
||||
requireTLS: process.env.EMAIL_ENCRYPTION === 'starttls',
|
||||
tls: {
|
||||
rejectUnauthorized: !isEnabled(process.env.EMAIL_ALLOW_SELFSIGNED ?? ''),
|
||||
},
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
};
|
||||
|
||||
if (process.env.EMAIL_ENCRYPTION_HOSTNAME) {
|
||||
transporterOptions.tls = {
|
||||
...transporterOptions.tls,
|
||||
servername: process.env.EMAIL_ENCRYPTION_HOSTNAME,
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.EMAIL_SERVICE) {
|
||||
transporterOptions.service = process.env.EMAIL_SERVICE;
|
||||
} else {
|
||||
transporterOptions.host = process.env.EMAIL_HOST;
|
||||
transporterOptions.port = Number(process.env.EMAIL_PORT ?? 25);
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(transporterOptions);
|
||||
|
||||
const templatePath = path.join(__dirname, 'emails', template);
|
||||
const source = fs.readFileSync(templatePath, 'utf8');
|
||||
const compiledTemplate = handlebars.compile(source);
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${process.env.EMAIL_FROM_NAME || process.env.APP_TITLE}" <${process.env.EMAIL_FROM}>`,
|
||||
to: `"${payload.name}" <${email}>`,
|
||||
envelope: {
|
||||
from: process.env.EMAIL_FROM!,
|
||||
to: email,
|
||||
},
|
||||
subject,
|
||||
html: compiledTemplate(payload),
|
||||
};
|
||||
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error: any) {
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('[sendEmail]', error);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
22
packages/auth/tsconfig.json
Normal file
22
packages/auth/tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"declarationDir": "dist/types",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"paths": {
|
||||
"@librechat/data-schemas/*": ["./packages/data-schemas/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
10
packages/auth/tsconfig.spec.json
Normal file
10
packages/auth/tsconfig.spec.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"outDir": "./dist/tests",
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["specs/**/*", "src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue