mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
📧 feat: email verification (#2344)
* feat: verification email * chore: email verification invalid; localize: update * fix: redirect to login when signup: fix: save emailVerified correctly * docs: update ALLOW_UNVERIFIED_EMAIL_LOGIN; fix: don't accept login only when ALLOW_UNVERIFIED_EMAIL_LOGIN = true * fix: user needs to be authenticated * style: update * fix: registration success message and redirect logic * refactor: use `isEnabled` in ALLOW_UNVERIFIED_EMAIL_LOGIN * refactor: move checkEmailConfig to server/utils * refactor: use req as param for verifyEmail function * chore: jsdoc * chore: remove console log * refactor: rename `createNewUser` to `createSocialUser` * refactor: update typing and add expiresAt field to userSchema * refactor: begin use of user methods over direct model access for User * refactor: initial email verification rewrite * chore: typing * refactor: registration flow rewrite * chore: remove help center text * refactor: update getUser to getUserById and add findUser methods. general fixes from recent changes * refactor: Update updateUser method to remove expiresAt field and use $set and $unset operations, createUser now returns Id only * refactor: Update openidStrategy to use optional chaining for avatar check, move saveBuffer init to buffer condition * refactor: logout on deleteUser mutatation * refactor: Update openidStrategy login success message format * refactor: Add emailVerified field to Discord and Facebook profile details * refactor: move limiters to separate middleware dir * refactor: Add limiters for email verification and password reset * refactor: Remove getUserController and update routes and controllers accordingly * refactor: Update getUserById method to exclude password and version fields * refactor: move verification to user route, add resend verification option * refactor: Improve email verification process and resend option * refactor: remove more direct model access of User and remove unused code * refactor: replace user authentication methods and token generation * fix: add user.id to jwt user * refactor: Update AuthContext to include setError function, add resend link to Login Form, make registration redirect shorter * fix(updateUserPluginsService): ensure userPlugins variable is defined * refactor: Delete all shared links for a specific user * fix: remove use of direct User.save() in handleExistingUser * fix(importLibreChatConvo): handle missing createdAt field in messages --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
b7fef6958b
commit
ee673d682e
67 changed files with 1863 additions and 1117 deletions
|
|
@ -17,6 +17,7 @@ const getProfileDetails = (profile) => {
|
|||
avatarUrl,
|
||||
username: profile.username,
|
||||
name: profile.global_name,
|
||||
emailVerified: true,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const getProfileDetails = (profile) => ({
|
|||
avatarUrl: profile.photos[0]?.value,
|
||||
username: profile.displayName,
|
||||
name: profile.name?.givenName + ' ' + profile.name?.familyName,
|
||||
emailVerified: true,
|
||||
});
|
||||
|
||||
const facebookLogin = socialLogin('facebook', getProfileDetails);
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
const jose = require('jose');
|
||||
const { logger } = require('~/config');
|
||||
// No longer using this strategy as Bun now supports JWTs natively.
|
||||
|
||||
const passportCustom = require('passport-custom');
|
||||
const CustomStrategy = passportCustom.Strategy;
|
||||
const User = require('~/models/User');
|
||||
|
||||
const joseLogin = async () =>
|
||||
new CustomStrategy(async (req, done) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return done(null, false, { message: 'No auth token' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||
const { payload } = await jose.jwtVerify(token, secret);
|
||||
|
||||
const user = await User.findById(payload.id);
|
||||
if (user) {
|
||||
done(null, user);
|
||||
} else {
|
||||
logger.debug('JoseJwtStrategy => no user found');
|
||||
done(null, false, { message: 'No user found' });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.code === 'ERR_JWT_EXPIRED') {
|
||||
logger.error('JoseJwtStrategy => token expired');
|
||||
} else {
|
||||
logger.error('JoseJwtStrategy => error');
|
||||
logger.error(err);
|
||||
}
|
||||
done(null, false, { message: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = joseLogin;
|
||||
*/
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
|
||||
const { getUserById } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
const User = require('~/models/User');
|
||||
|
||||
// JWT strategy
|
||||
const jwtLogin = async () =>
|
||||
|
|
@ -11,7 +11,8 @@ const jwtLogin = async () =>
|
|||
},
|
||||
async (payload, done) => {
|
||||
try {
|
||||
const user = await User.findById(payload?.id);
|
||||
const user = await getUserById(payload?.id);
|
||||
user.id = user._id.toString();
|
||||
if (user) {
|
||||
done(null, user);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const LdapStrategy = require('passport-ldapauth');
|
||||
const User = require('~/models/User');
|
||||
const { findUser, createUser } = require('~/models/userMethods');
|
||||
const fs = require('fs');
|
||||
|
||||
const ldapOptions = {
|
||||
|
|
@ -36,19 +36,19 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
|
|||
userinfo.mail;
|
||||
|
||||
const username = userinfo.givenName || userinfo.mail;
|
||||
let user = await User.findOne({ email: userinfo.mail });
|
||||
let user = await findUser({ email: userinfo.mail });
|
||||
if (user && user.provider !== 'ldap') {
|
||||
return done(null, false, { message: 'Invalid credentials' });
|
||||
}
|
||||
if (!user) {
|
||||
user = new User({
|
||||
user = {
|
||||
provider: 'ldap',
|
||||
ldapId: userinfo.uid,
|
||||
username,
|
||||
email: userinfo.mail || '',
|
||||
emailVerified: true,
|
||||
name: fullName,
|
||||
});
|
||||
};
|
||||
} else {
|
||||
user.provider = 'ldap';
|
||||
user.ldapId = userinfo.uid;
|
||||
|
|
@ -56,7 +56,7 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
|
|||
user.name = fullName;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
await createUser(user);
|
||||
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,15 @@
|
|||
const { errorsToString } = require('librechat-data-provider');
|
||||
const { Strategy: PassportLocalStrategy } = require('passport-local');
|
||||
const { findUser, comparePassword } = require('~/models');
|
||||
const { loginSchema } = require('./validators');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const logger = require('~/utils/logger');
|
||||
const User = require('~/models/User');
|
||||
|
||||
async function validateLoginRequest(req) {
|
||||
const { error } = loginSchema.safeParse(req.body);
|
||||
return error ? errorsToString(error.errors) : null;
|
||||
}
|
||||
|
||||
async function findUserByEmail(email) {
|
||||
return User.findOne({ email: email.trim() });
|
||||
}
|
||||
|
||||
async function comparePassword(user, password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
user.comparePassword(password, function (err, isMatch) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(isMatch);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function passportLogin(req, email, password, done) {
|
||||
try {
|
||||
const validationError = await validateLoginRequest(req);
|
||||
|
|
@ -33,7 +19,7 @@ async function passportLogin(req, email, password, done) {
|
|||
return done(null, false, { message: validationError });
|
||||
}
|
||||
|
||||
const user = await findUserByEmail(email);
|
||||
const user = await findUser({ email: email.trim() });
|
||||
if (!user) {
|
||||
logError('Passport Local Strategy - User Not Found', { email });
|
||||
logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`);
|
||||
|
|
@ -47,6 +33,12 @@ async function passportLogin(req, email, password, done) {
|
|||
return done(null, false, { message: 'Incorrect password.' });
|
||||
}
|
||||
|
||||
if (!user.emailVerified && !isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN)) {
|
||||
logError('Passport Local Strategy - Email not verified', { email });
|
||||
logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`);
|
||||
return done(null, user, { message: 'Email not verified.' });
|
||||
}
|
||||
|
||||
logger.info(`[Login] [Login successful] [Username: ${email}] [Request-IP: ${req.ip}]`);
|
||||
return done(null, user);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ const passport = require('passport');
|
|||
const jwtDecode = require('jsonwebtoken/decode');
|
||||
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||
const { logger } = require('~/config');
|
||||
const User = require('~/models/User');
|
||||
|
||||
let crypto;
|
||||
try {
|
||||
|
|
@ -88,13 +88,13 @@ async function setupOpenId() {
|
|||
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
|
||||
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
|
||||
|
||||
let user = await User.findOne({ openidId: userinfo.sub });
|
||||
let user = await findUser({ openidId: userinfo.sub });
|
||||
logger.info(
|
||||
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${userinfo.sub}`,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
user = await User.findOne({ email: userinfo.email });
|
||||
user = await findUser({ email: userinfo.email });
|
||||
logger.info(
|
||||
`[openidStrategy] user ${user ? 'found' : 'not found'} with email: ${
|
||||
userinfo.email
|
||||
|
|
@ -148,14 +148,16 @@ async function setupOpenId() {
|
|||
);
|
||||
|
||||
if (!user) {
|
||||
user = new User({
|
||||
user = {
|
||||
provider: 'openid',
|
||||
openidId: userinfo.sub,
|
||||
username,
|
||||
email: userinfo.email || '',
|
||||
emailVerified: userinfo.email_verified || false,
|
||||
name: fullName,
|
||||
});
|
||||
};
|
||||
const userId = await createUser();
|
||||
user._id = userId;
|
||||
} else {
|
||||
user.provider = 'openid';
|
||||
user.openidId = userinfo.sub;
|
||||
|
|
@ -163,7 +165,7 @@ async function setupOpenId() {
|
|||
user.name = fullName;
|
||||
}
|
||||
|
||||
if (userinfo.picture) {
|
||||
if (userinfo.picture && !user.avatar?.includes('manual=true')) {
|
||||
/** @type {string | undefined} */
|
||||
const imageUrl = userinfo.picture;
|
||||
|
||||
|
|
@ -177,25 +179,21 @@ async function setupOpenId() {
|
|||
}
|
||||
|
||||
const imageBuffer = await downloadImage(imageUrl, tokenset.access_token);
|
||||
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
||||
if (imageBuffer) {
|
||||
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
||||
const imagePath = await saveBuffer({
|
||||
fileName,
|
||||
userId: user._id.toString(),
|
||||
buffer: imageBuffer,
|
||||
});
|
||||
user.avatar = imagePath ?? '';
|
||||
} else {
|
||||
user.avatar = '';
|
||||
}
|
||||
} else {
|
||||
user.avatar = '';
|
||||
}
|
||||
|
||||
await user.save();
|
||||
user = await updateUser(user._id, user);
|
||||
|
||||
logger.info(
|
||||
`[openidStrategy] login success openidId: ${user.openidId} username: ${user.username} email: ${user.email}`,
|
||||
`[openidStrategy] login success openidId: ${user.openidId} | email: ${user.email} | username: ${user.username} `,
|
||||
{
|
||||
user: {
|
||||
openidId: user.openidId,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
const { FileSources } = require('librechat-data-provider');
|
||||
const { createUser, updateUser, getUserById } = require('~/models/userMethods');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
||||
const User = require('~/models/User');
|
||||
|
||||
/**
|
||||
* Updates the avatar URL of an existing user. If the user's avatar URL does not include the query parameter
|
||||
* '?manual=true', it updates the user's avatar with the provided URL. For local file storage, it directly updates
|
||||
* the avatar URL, while for other storage types, it processes the avatar URL using the specified file strategy.
|
||||
*
|
||||
* @param {User} oldUser - The existing user object that needs to be updated.
|
||||
* @param {MongoUser} oldUser - The existing user object that needs to be updated.
|
||||
* @param {string} avatarUrl - The new avatar URL to be set for the user.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
|
|
@ -20,9 +20,9 @@ const handleExistingUser = async (oldUser, avatarUrl) => {
|
|||
const fileStrategy = process.env.CDN_PROVIDER;
|
||||
const isLocal = fileStrategy === FileSources.local;
|
||||
|
||||
let updatedAvatar = false;
|
||||
if (isLocal && (oldUser.avatar === null || !oldUser.avatar.includes('?manual=true'))) {
|
||||
oldUser.avatar = avatarUrl;
|
||||
await oldUser.save();
|
||||
updatedAvatar = avatarUrl;
|
||||
} else if (!isLocal && (oldUser.avatar === null || !oldUser.avatar.includes('?manual=true'))) {
|
||||
const userId = oldUser._id;
|
||||
const resizedBuffer = await resizeAvatar({
|
||||
|
|
@ -30,8 +30,11 @@ const handleExistingUser = async (oldUser, avatarUrl) => {
|
|||
input: avatarUrl,
|
||||
});
|
||||
const { processAvatar } = getStrategyFunctions(fileStrategy);
|
||||
oldUser.avatar = await processAvatar({ buffer: resizedBuffer, userId });
|
||||
await oldUser.save();
|
||||
updatedAvatar = await processAvatar({ buffer: resizedBuffer, userId });
|
||||
}
|
||||
|
||||
if (updatedAvatar) {
|
||||
await updateUser(oldUser._id, { avatar: updatedAvatar });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ const handleExistingUser = async (oldUser, avatarUrl) => {
|
|||
*
|
||||
* @throws {Error} Throws an error if there's an issue creating or saving the new user object.
|
||||
*/
|
||||
const createNewUser = async ({
|
||||
const createSocialUser = async ({
|
||||
email,
|
||||
avatarUrl,
|
||||
provider,
|
||||
|
|
@ -75,27 +78,24 @@ const createNewUser = async ({
|
|||
emailVerified,
|
||||
};
|
||||
|
||||
// TODO: remove direct access of User model
|
||||
const newUser = await new User(update).save();
|
||||
|
||||
const newUserId = await createUser(update);
|
||||
const fileStrategy = process.env.CDN_PROVIDER;
|
||||
const isLocal = fileStrategy === FileSources.local;
|
||||
|
||||
if (!isLocal) {
|
||||
const userId = newUser._id;
|
||||
const resizedBuffer = await resizeAvatar({
|
||||
userId,
|
||||
userId: newUserId,
|
||||
input: avatarUrl,
|
||||
});
|
||||
const { processAvatar } = getStrategyFunctions(fileStrategy);
|
||||
newUser.avatar = await processAvatar({ buffer: resizedBuffer, userId });
|
||||
await newUser.save();
|
||||
const avatar = await processAvatar({ buffer: resizedBuffer, userId: newUserId });
|
||||
await updateUser(newUserId, { avatar });
|
||||
}
|
||||
|
||||
return newUser;
|
||||
return await getUserById(newUserId);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
handleExistingUser,
|
||||
createNewUser,
|
||||
createSocialUser,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
const { createNewUser, handleExistingUser } = require('./process');
|
||||
const { logger } = require('~/config');
|
||||
const { findUserByEmail } = require('~/server/services/UserService');
|
||||
const { createSocialUser, handleExistingUser } = require('./process');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { findUser } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const socialLogin =
|
||||
(provider, getProfileDetails) => async (accessToken, refreshToken, profile, cb) => {
|
||||
try {
|
||||
const { email, id, avatarUrl, username, name, emailVerified } = getProfileDetails(profile);
|
||||
|
||||
const oldUser = await findUserByEmail(email);
|
||||
const oldUser = await findUser({ email: email.trim() });
|
||||
const ALLOW_SOCIAL_REGISTRATION = isEnabled(process.env.ALLOW_SOCIAL_REGISTRATION);
|
||||
|
||||
if (oldUser) {
|
||||
|
|
@ -17,7 +17,7 @@ const socialLogin =
|
|||
}
|
||||
|
||||
if (ALLOW_SOCIAL_REGISTRATION) {
|
||||
const newUser = await createNewUser({
|
||||
const newUser = await createSocialUser({
|
||||
email,
|
||||
avatarUrl,
|
||||
provider,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue