mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
📩 feat: invite user (#3012)
* feat: basic invite-user script * feat: add invite user functionality and registration validation middleware * fix: invite user fixes * refactor: consolidate direct model access to a central place of functions * style(Registration): add spinner to continue button * refactor: import ordrer * feat: improve invite user script and error handling * fix: merge conflict * refactor: remove `console.log` and use `logger` * fix: token operation and checkinvite issues * bring back comment and remove console log * fix: return invalid token when token is not found * fix: getInvite fix * refactor: Update Token.js to use async/await syntax for update and delete operations * feat: Refactor Token.js to use async/await syntax for createToken and findToken functions * refactor(inviteUser): define functions outside of module.exports * Update AuthService.js --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
a45b384bbc
commit
bbb9324447
16 changed files with 695 additions and 61 deletions
117
api/models/Token.js
Normal file
117
api/models/Token.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
const tokenSchema = require('./schema/tokenSchema');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token model.
|
||||||
|
* @type {mongoose.Model}
|
||||||
|
*/
|
||||||
|
const Token = mongoose.model('Token', tokenSchema);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Token instance.
|
||||||
|
* @param {Object} tokenData - The data for the new Token.
|
||||||
|
* @param {mongoose.Types.ObjectId} tokenData.userId - The user's ID. It is required.
|
||||||
|
* @param {String} tokenData.email - The user's email.
|
||||||
|
* @param {String} tokenData.token - The token. It is required.
|
||||||
|
* @param {Number} tokenData.expiresIn - The number of seconds until the token expires.
|
||||||
|
* @returns {Promise<mongoose.Document>} The new Token instance.
|
||||||
|
* @throws Will throw an error if token creation fails.
|
||||||
|
*/
|
||||||
|
async function createToken(tokenData) {
|
||||||
|
try {
|
||||||
|
const currentTime = new Date();
|
||||||
|
const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000);
|
||||||
|
|
||||||
|
const newTokenData = {
|
||||||
|
...tokenData,
|
||||||
|
createdAt: currentTime,
|
||||||
|
expiresAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newToken = new Token(newTokenData);
|
||||||
|
return await newToken.save();
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('An error occurred while creating token:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a Token document that matches the provided query.
|
||||||
|
* @param {Object} query - The query to match against.
|
||||||
|
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||||
|
* @param {String} query.token - The token value.
|
||||||
|
* @param {String} query.email - The email of the user.
|
||||||
|
* @returns {Promise<Object|null>} The matched Token document, or null if not found.
|
||||||
|
* @throws Will throw an error if the find operation fails.
|
||||||
|
*/
|
||||||
|
async function findToken(query) {
|
||||||
|
try {
|
||||||
|
const conditions = [];
|
||||||
|
|
||||||
|
if (query.userId) {
|
||||||
|
conditions.push({ userId: query.userId });
|
||||||
|
}
|
||||||
|
if (query.token) {
|
||||||
|
conditions.push({ token: query.token });
|
||||||
|
}
|
||||||
|
if (query.email) {
|
||||||
|
conditions.push({ email: query.email });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await Token.findOne({
|
||||||
|
$and: conditions,
|
||||||
|
}).lean();
|
||||||
|
|
||||||
|
return token;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('An error occurred while finding token:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a Token document that matches the provided query.
|
||||||
|
* @param {Object} query - The query to match against.
|
||||||
|
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||||
|
* @param {String} query.token - The token value.
|
||||||
|
* @param {Object} updateData - The data to update the Token with.
|
||||||
|
* @returns {Promise<mongoose.Document|null>} The updated Token document, or null if not found.
|
||||||
|
* @throws Will throw an error if the update operation fails.
|
||||||
|
*/
|
||||||
|
async function updateToken(query, updateData) {
|
||||||
|
try {
|
||||||
|
return await Token.findOneAndUpdate(query, updateData, { new: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('An error occurred while updating token:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all Token documents that match the provided token, user ID, or email.
|
||||||
|
* @param {Object} query - The query to match against.
|
||||||
|
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||||
|
* @param {String} query.token - The token value.
|
||||||
|
* @param {String} query.email - The email of the user.
|
||||||
|
* @returns {Promise<Object>} The result of the delete operation.
|
||||||
|
* @throws Will throw an error if the delete operation fails.
|
||||||
|
*/
|
||||||
|
async function deleteTokens(query) {
|
||||||
|
try {
|
||||||
|
return await Token.deleteMany({
|
||||||
|
$or: [{ userId: query.userId }, { token: query.token }, { email: query.email }],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('An error occurred while deleting tokens:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createToken,
|
||||||
|
findToken,
|
||||||
|
updateToken,
|
||||||
|
deleteTokens,
|
||||||
|
};
|
|
@ -1,11 +1,3 @@
|
||||||
const {
|
|
||||||
getMessages,
|
|
||||||
saveMessage,
|
|
||||||
recordMessage,
|
|
||||||
updateMessage,
|
|
||||||
deleteMessagesSince,
|
|
||||||
deleteMessages,
|
|
||||||
} = require('./Message');
|
|
||||||
const {
|
const {
|
||||||
comparePassword,
|
comparePassword,
|
||||||
deleteUserById,
|
deleteUserById,
|
||||||
|
@ -16,8 +8,6 @@ const {
|
||||||
countUsers,
|
countUsers,
|
||||||
findUser,
|
findUser,
|
||||||
} = require('./userMethods');
|
} = require('./userMethods');
|
||||||
const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation');
|
|
||||||
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
|
||||||
const {
|
const {
|
||||||
findFileById,
|
findFileById,
|
||||||
createFile,
|
createFile,
|
||||||
|
@ -27,26 +17,40 @@ const {
|
||||||
getFiles,
|
getFiles,
|
||||||
updateFileUsage,
|
updateFileUsage,
|
||||||
} = require('./File');
|
} = require('./File');
|
||||||
const Key = require('./Key');
|
const {
|
||||||
const User = require('./User');
|
getMessages,
|
||||||
|
saveMessage,
|
||||||
|
recordMessage,
|
||||||
|
updateMessage,
|
||||||
|
deleteMessagesSince,
|
||||||
|
deleteMessages,
|
||||||
|
} = require('./Message');
|
||||||
|
const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation');
|
||||||
|
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
||||||
|
const { createToken, findToken, updateToken, deleteTokens } = require('./Token');
|
||||||
const Session = require('./Session');
|
const Session = require('./Session');
|
||||||
const Balance = require('./Balance');
|
const Balance = require('./Balance');
|
||||||
|
const User = require('./User');
|
||||||
|
const Key = require('./Key');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
User,
|
|
||||||
Key,
|
|
||||||
Session,
|
|
||||||
Balance,
|
|
||||||
|
|
||||||
comparePassword,
|
comparePassword,
|
||||||
deleteUserById,
|
deleteUserById,
|
||||||
generateToken,
|
generateToken,
|
||||||
getUserById,
|
getUserById,
|
||||||
countUsers,
|
|
||||||
createUser,
|
|
||||||
updateUser,
|
updateUser,
|
||||||
|
createUser,
|
||||||
|
countUsers,
|
||||||
findUser,
|
findUser,
|
||||||
|
|
||||||
|
findFileById,
|
||||||
|
createFile,
|
||||||
|
updateFile,
|
||||||
|
deleteFile,
|
||||||
|
deleteFiles,
|
||||||
|
getFiles,
|
||||||
|
updateFileUsage,
|
||||||
|
|
||||||
getMessages,
|
getMessages,
|
||||||
saveMessage,
|
saveMessage,
|
||||||
recordMessage,
|
recordMessage,
|
||||||
|
@ -64,11 +68,13 @@ module.exports = {
|
||||||
savePreset,
|
savePreset,
|
||||||
deletePresets,
|
deletePresets,
|
||||||
|
|
||||||
findFileById,
|
createToken,
|
||||||
createFile,
|
findToken,
|
||||||
updateFile,
|
updateToken,
|
||||||
deleteFile,
|
deleteTokens,
|
||||||
deleteFiles,
|
|
||||||
getFiles,
|
User,
|
||||||
updateFileUsage,
|
Key,
|
||||||
|
Session,
|
||||||
|
Balance,
|
||||||
};
|
};
|
||||||
|
|
70
api/models/inviteUser.js
Normal file
70
api/models/inviteUser.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { createToken, findToken } = require('./Token');
|
||||||
|
const logger = require('~/config/winston');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module inviteUser
|
||||||
|
* @description This module provides functions to create and get user invites
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function createInvite
|
||||||
|
* @description This function creates a new user invite
|
||||||
|
* @param {string} email - The email of the user to invite
|
||||||
|
* @returns {Promise<Object>} A promise that resolves to the saved invite document
|
||||||
|
* @throws {Error} If there is an error creating the invite
|
||||||
|
*/
|
||||||
|
const createInvite = async (email) => {
|
||||||
|
try {
|
||||||
|
let token = crypto.randomBytes(32).toString('hex');
|
||||||
|
const hash = bcrypt.hashSync(token, 10);
|
||||||
|
const encodedToken = encodeURIComponent(token);
|
||||||
|
|
||||||
|
const fakeUserId = new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
|
await createToken({
|
||||||
|
userId: fakeUserId,
|
||||||
|
email,
|
||||||
|
token: hash,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresIn: 604800,
|
||||||
|
});
|
||||||
|
|
||||||
|
return encodedToken;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[createInvite] Error creating invite', error);
|
||||||
|
return { message: 'Error creating invite' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function getInvite
|
||||||
|
* @description This function retrieves a user invite
|
||||||
|
* @param {string} encodedToken - The token of the invite to retrieve
|
||||||
|
* @param {string} email - The email of the user to validate
|
||||||
|
* @returns {Promise<Object>} A promise that resolves to the retrieved invite document
|
||||||
|
* @throws {Error} If there is an error retrieving the invite, if the invite does not exist, or if the email does not match
|
||||||
|
*/
|
||||||
|
const getInvite = async (encodedToken, email) => {
|
||||||
|
try {
|
||||||
|
const token = decodeURIComponent(encodedToken);
|
||||||
|
const hash = bcrypt.hashSync(token, 10);
|
||||||
|
const invite = await findToken({ token: hash, email });
|
||||||
|
|
||||||
|
if (!invite) {
|
||||||
|
throw new Error('Invite not found or email does not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
return invite;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[getInvite] Error getting invite', error);
|
||||||
|
return { error: true, message: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createInvite,
|
||||||
|
getInvite,
|
||||||
|
};
|
|
@ -18,8 +18,13 @@ const tokenSchema = new Schema({
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true,
|
required: true,
|
||||||
default: Date.now,
|
default: Date.now,
|
||||||
expires: 900,
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = mongoose.model('Token', tokenSchema);
|
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
||||||
|
|
||||||
|
module.exports = tokenSchema;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"list-balances": "node ./list-balances.js",
|
"list-balances": "node ./list-balances.js",
|
||||||
"user-stats": "node ./user-stats.js",
|
"user-stats": "node ./user-stats.js",
|
||||||
"create-user": "node ./create-user.js",
|
"create-user": "node ./create-user.js",
|
||||||
|
"invite-user": "node ./invite-user.js",
|
||||||
"ban-user": "node ./ban-user.js",
|
"ban-user": "node ./ban-user.js",
|
||||||
"delete-user": "node ./delete-user.js"
|
"delete-user": "node ./delete-user.js"
|
||||||
},
|
},
|
||||||
|
|
27
api/server/middleware/checkInviteUser.js
Normal file
27
api/server/middleware/checkInviteUser.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const { getInvite } = require('~/models/inviteUser');
|
||||||
|
const { deleteTokens } = require('~/models/Token');
|
||||||
|
|
||||||
|
async function checkInviteUser(req, res, next) {
|
||||||
|
const token = req.body.token;
|
||||||
|
|
||||||
|
if (!token || token === 'undefined') {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const invite = await getInvite(token, req.body.email);
|
||||||
|
|
||||||
|
if (!invite || invite.error === true) {
|
||||||
|
return res.status(400).json({ message: 'Invalid invite token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteTokens({ token: invite.token });
|
||||||
|
req.invite = invite;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(429).json({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = checkInviteUser;
|
|
@ -10,6 +10,7 @@ const requireLocalAuth = require('./requireLocalAuth');
|
||||||
const canDeleteAccount = require('./canDeleteAccount');
|
const canDeleteAccount = require('./canDeleteAccount');
|
||||||
const requireLdapAuth = require('./requireLdapAuth');
|
const requireLdapAuth = require('./requireLdapAuth');
|
||||||
const abortMiddleware = require('./abortMiddleware');
|
const abortMiddleware = require('./abortMiddleware');
|
||||||
|
const checkInviteUser = require('./checkInviteUser');
|
||||||
const requireJwtAuth = require('./requireJwtAuth');
|
const requireJwtAuth = require('./requireJwtAuth');
|
||||||
const validateModel = require('./validateModel');
|
const validateModel = require('./validateModel');
|
||||||
const moderateText = require('./moderateText');
|
const moderateText = require('./moderateText');
|
||||||
|
@ -33,6 +34,7 @@ module.exports = {
|
||||||
moderateText,
|
moderateText,
|
||||||
validateModel,
|
validateModel,
|
||||||
requireJwtAuth,
|
requireJwtAuth,
|
||||||
|
checkInviteUser,
|
||||||
requireLdapAuth,
|
requireLdapAuth,
|
||||||
requireLocalAuth,
|
requireLocalAuth,
|
||||||
canDeleteAccount,
|
canDeleteAccount,
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
|
||||||
function validateRegistration(req, res, next) {
|
function validateRegistration(req, res, next) {
|
||||||
|
if (req.invite) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (isEnabled(process.env.ALLOW_REGISTRATION)) {
|
if (isEnabled(process.env.ALLOW_REGISTRATION)) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
res.status(403).send('Registration is not allowed.');
|
return res.status(403).json({
|
||||||
|
message: 'Registration is not allowed.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ const {
|
||||||
checkBan,
|
checkBan,
|
||||||
loginLimiter,
|
loginLimiter,
|
||||||
requireJwtAuth,
|
requireJwtAuth,
|
||||||
|
checkInviteUser,
|
||||||
registerLimiter,
|
registerLimiter,
|
||||||
requireLdapAuth,
|
requireLdapAuth,
|
||||||
requireLocalAuth,
|
requireLocalAuth,
|
||||||
|
@ -32,7 +33,14 @@ router.post(
|
||||||
loginController,
|
loginController,
|
||||||
);
|
);
|
||||||
router.post('/refresh', refreshController);
|
router.post('/refresh', refreshController);
|
||||||
router.post('/register', registerLimiter, checkBan, validateRegistration, registrationController);
|
router.post(
|
||||||
|
'/register',
|
||||||
|
registerLimiter,
|
||||||
|
checkBan,
|
||||||
|
checkInviteUser,
|
||||||
|
validateRegistration,
|
||||||
|
registrationController,
|
||||||
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/requestPasswordReset',
|
'/requestPasswordReset',
|
||||||
resetPasswordLimiter,
|
resetPasswordLimiter,
|
||||||
|
|
|
@ -10,12 +10,11 @@ const {
|
||||||
generateToken,
|
generateToken,
|
||||||
deleteUserById,
|
deleteUserById,
|
||||||
} = require('~/models/userMethods');
|
} = require('~/models/userMethods');
|
||||||
|
const { createToken, findToken, deleteTokens, Session } = require('~/models');
|
||||||
const { sendEmail, checkEmailConfig } = require('~/server/utils');
|
const { sendEmail, checkEmailConfig } = require('~/server/utils');
|
||||||
const { registerSchema } = require('~/strategies/validators');
|
const { registerSchema } = require('~/strategies/validators');
|
||||||
const { hashToken } = require('~/server/utils/crypto');
|
const { hashToken } = require('~/server/utils/crypto');
|
||||||
const isDomainAllowed = require('./isDomainAllowed');
|
const isDomainAllowed = require('./isDomainAllowed');
|
||||||
const Token = require('~/models/schema/tokenSchema');
|
|
||||||
const Session = require('~/models/Session');
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const domains = {
|
const domains = {
|
||||||
|
@ -87,12 +86,13 @@ const sendVerificationEmail = async (user) => {
|
||||||
template: 'verifyEmail.handlebars',
|
template: 'verifyEmail.handlebars',
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Token({
|
await createToken({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
token: hash,
|
token: hash,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
}).save();
|
expiresIn: 900,
|
||||||
|
});
|
||||||
|
|
||||||
logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
||||||
};
|
};
|
||||||
|
@ -103,7 +103,7 @@ const sendVerificationEmail = async (user) => {
|
||||||
*/
|
*/
|
||||||
const verifyEmail = async (req) => {
|
const verifyEmail = async (req) => {
|
||||||
const { email, token } = req.body;
|
const { email, token } = req.body;
|
||||||
let emailVerificationData = await Token.findOne({ email: decodeURIComponent(email) });
|
let emailVerificationData = await findToken({ email: decodeURIComponent(email) });
|
||||||
|
|
||||||
if (!emailVerificationData) {
|
if (!emailVerificationData) {
|
||||||
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${email}]`);
|
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${email}]`);
|
||||||
|
@ -123,7 +123,7 @@ const verifyEmail = async (req) => {
|
||||||
return new Error('User not found');
|
return new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await emailVerificationData.deleteOne();
|
await deleteTokens({ token: emailVerificationData.token });
|
||||||
logger.info(`[verifyEmail] Email verification successful. [Email: ${email}]`);
|
logger.info(`[verifyEmail] Email verification successful. [Email: ${email}]`);
|
||||||
return { message: 'Email verification was successful' };
|
return { message: 'Email verification was successful' };
|
||||||
};
|
};
|
||||||
|
@ -231,18 +231,16 @@ const requestPasswordReset = async (req) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = await Token.findOne({ userId: user._id });
|
await deleteTokens({ userId: user._id });
|
||||||
if (token) {
|
|
||||||
await token.deleteOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [resetToken, hash] = createTokenHash();
|
const [resetToken, hash] = createTokenHash();
|
||||||
|
|
||||||
await new Token({
|
await createToken({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
token: hash,
|
token: hash,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
}).save();
|
expiresIn: 900,
|
||||||
|
});
|
||||||
|
|
||||||
const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`;
|
const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`;
|
||||||
|
|
||||||
|
@ -282,7 +280,10 @@ const requestPasswordReset = async (req) => {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const resetPassword = async (userId, token, password) => {
|
const resetPassword = async (userId, token, password) => {
|
||||||
let passwordResetToken = await Token.findOne({ userId });
|
let passwordResetToken = await createToken({
|
||||||
|
userId,
|
||||||
|
expiresIn: 900,
|
||||||
|
});
|
||||||
|
|
||||||
if (!passwordResetToken) {
|
if (!passwordResetToken) {
|
||||||
return new Error('Invalid or expired password reset token');
|
return new Error('Invalid or expired password reset token');
|
||||||
|
@ -366,7 +367,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||||
const resendVerificationEmail = async (req) => {
|
const resendVerificationEmail = async (req) => {
|
||||||
try {
|
try {
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
await Token.deleteMany({ email });
|
await deleteTokens(email);
|
||||||
const user = await findUser({ email }, 'email _id name');
|
const user = await findUser({ email }, 'email _id name');
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -392,12 +393,13 @@ const resendVerificationEmail = async (req) => {
|
||||||
template: 'verifyEmail.handlebars',
|
template: 'verifyEmail.handlebars',
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Token({
|
await createToken({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
token: hash,
|
token: hash,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
}).save();
|
expiresIn: 900,
|
||||||
|
});
|
||||||
|
|
||||||
logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
||||||
|
|
||||||
|
|
287
api/server/utils/emails/inviteUser.handlebars
Normal file
287
api/server/utils/emails/inviteUser.handlebars
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
<html
|
||||||
|
xmlns='http://www.w3.org/1999/xhtml'
|
||||||
|
xmlns:v='urn:schemas-microsoft-com:vml'
|
||||||
|
xmlns:o='urn:schemas-microsoft-com:office:office'
|
||||||
|
>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG />
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
<![endif]-->
|
||||||
|
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||||
|
<meta name='x-apple-disable-message-reformatting' />
|
||||||
|
<meta name='color-scheme' content='light dark' />
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
|
||||||
|
<!--<![endif]-->
|
||||||
|
<title></title>
|
||||||
|
<style type='text/css'>
|
||||||
|
@media (prefers-color-scheme: dark) { .darkmode { background-color: #212121 !important; }
|
||||||
|
.darkmode p { color: #ffffff !important; } } @media only screen and (min-width: 520px) {
|
||||||
|
.u-row { width: 500px !important; } .u-row .u-col { vertical-align: top; } .u-row .u-col-100 {
|
||||||
|
width: 500px !important; } } @media (max-width: 520px) { .u-row-container { max-width: 100%
|
||||||
|
!important; padding-left: 0px !important; padding-right: 0px !important; } .u-row .u-col {
|
||||||
|
min-width: 320px !important; max-width: 100% !important; display: block !important; } .u-row {
|
||||||
|
width: 100% !important; } .u-col { width: 100% !important; } .u-col>div { margin: 0 auto; } }
|
||||||
|
body { margin: 0; padding: 0; } table, tr, td { vertical-align: top; border-collapse:
|
||||||
|
collapse; } p { margin: 0; } .ie-container table, .mso-container table { table-layout: fixed;
|
||||||
|
} * { line-height: inherit; } a[x-apple-data-detectors='true'] { color: inherit !important;
|
||||||
|
text-decoration: none !important; } table, td { color: #ffffff; } #u_body a { color: #0000ee;
|
||||||
|
text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body
|
||||||
|
class='clean-body u_body'
|
||||||
|
style='margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #212121;color: #ffffff'
|
||||||
|
>
|
||||||
|
<!--[if IE]><div class="ie-container"><![endif]-->
|
||||||
|
<!--[if mso]><div class="mso-container"><![endif]-->
|
||||||
|
<table
|
||||||
|
id='u_body'
|
||||||
|
style='border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #212121;width:100%'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr style='vertical-align: top'>
|
||||||
|
<td
|
||||||
|
style='word-break: break-word;border-collapse: collapse !important;vertical-align: top'
|
||||||
|
>
|
||||||
|
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #212121;"><![endif]-->
|
||||||
|
<div class='u-row-container' style='padding: 0px;background-color: transparent'>
|
||||||
|
<div
|
||||||
|
class='u-row'
|
||||||
|
style='margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style='border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;'
|
||||||
|
>
|
||||||
|
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
|
||||||
|
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color: #212121;width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
|
||||||
|
<div
|
||||||
|
class='u-col u-col-100'
|
||||||
|
style='max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style='background-color: #212121;height: 100%;width: 100% !important;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
|
||||||
|
>
|
||||||
|
<!--[if (!mso)&(!IE)]><!-->
|
||||||
|
<div
|
||||||
|
style='box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
|
||||||
|
>
|
||||||
|
<!--<![endif]-->
|
||||||
|
<table
|
||||||
|
style='font-family:arial,helvetica,sans-serif;'
|
||||||
|
role='presentation'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
width='100%'
|
||||||
|
border='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
|
||||||
|
align='left'
|
||||||
|
>
|
||||||
|
<!--[if mso]><table width="100%"><tr><td><![endif]-->
|
||||||
|
<h1
|
||||||
|
style='margin: 0px; line-height: 140%; text-align: left; word-wrap: break-word; font-size: 22px; font-weight: 700;'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div>You have been invited to join {{appName}}!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table
|
||||||
|
style='font-family:arial,helvetica,sans-serif;'
|
||||||
|
role='presentation'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
width='100%'
|
||||||
|
border='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
|
||||||
|
align='left'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
|
||||||
|
>
|
||||||
|
<div>Hi,</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table
|
||||||
|
style='font-family:arial,helvetica,sans-serif;'
|
||||||
|
role='presentation'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
width='100%'
|
||||||
|
border='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
|
||||||
|
align='left'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
|
||||||
|
>
|
||||||
|
<p style='line-height: 140%;'>You have been invited to join {{appName}}. Click the
|
||||||
|
button below to create your account and get started.</p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table
|
||||||
|
style='font-family:arial,helvetica,sans-serif;'
|
||||||
|
role='presentation'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
width='100%'
|
||||||
|
border='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
|
||||||
|
align='left'
|
||||||
|
>
|
||||||
|
<!--[if mso]><style>.v-button {background: transparent !important;}</style><![endif]-->
|
||||||
|
<div align='left'>
|
||||||
|
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{inviteLink}}" style="height:37px; v-text-anchor:middle; width:142px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
|
||||||
|
<a
|
||||||
|
href='{{inviteLink}}'
|
||||||
|
target='_blank'
|
||||||
|
class='v-button'
|
||||||
|
style='box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #10a37f; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;'
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style='display:block;padding:10px 20px;line-height:120%;'
|
||||||
|
><span style='line-height: 16.8px;'>Create Account</span></span>
|
||||||
|
</span></span>
|
||||||
|
</a>
|
||||||
|
<!--[if mso]></center></v:roundrect><![endif]-->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table
|
||||||
|
style='font-family:arial,helvetica,sans-serif;'
|
||||||
|
role='presentation'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
width='100%'
|
||||||
|
border='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
|
||||||
|
align='left'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Hurry up, the invite will expiry in 7 days
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table
|
||||||
|
style='font-family:arial,helvetica,sans-serif;'
|
||||||
|
role='presentation'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
width='100%'
|
||||||
|
border='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
|
||||||
|
align='left'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
|
||||||
|
>
|
||||||
|
<div>Best regards,</div>
|
||||||
|
<div>The {{appName}} Team</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table
|
||||||
|
style='font-family:arial,helvetica,sans-serif;'
|
||||||
|
role='presentation'
|
||||||
|
cellpadding='0'
|
||||||
|
cellspacing='0'
|
||||||
|
width='100%'
|
||||||
|
border='0'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style='overflow-wrap:break-word;word-break:break-word;padding:0px 10px 10px;font-family:arial,helvetica,sans-serif;'
|
||||||
|
align='left'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style='font-size: 14px; line-height: 140%; text-align: right; word-wrap: break-word;'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div><sub>©
|
||||||
|
{{year}}
|
||||||
|
{{appName}}. All rights reserved.</sub></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if (!mso)&(!IE)]><!-->
|
||||||
|
</div>
|
||||||
|
<!--<![endif]-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--[if (mso)|(IE)]></td><![endif]-->
|
||||||
|
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso]></div><![endif]-->
|
||||||
|
<!--[if IE]></div><![endif]-->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,10 +1,11 @@
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate, useOutletContext } from 'react-router-dom';
|
import { useNavigate, useOutletContext, useLocation } from 'react-router-dom';
|
||||||
import { useRegisterUserMutation } from 'librechat-data-provider/react-query';
|
import { useRegisterUserMutation } from 'librechat-data-provider/react-query';
|
||||||
import type { TRegisterUser, TError } from 'librechat-data-provider';
|
import type { TRegisterUser, TError } from 'librechat-data-provider';
|
||||||
import type { TLoginLayoutContext } from '~/common';
|
import type { TLoginLayoutContext } from '~/common';
|
||||||
import { ErrorMessage } from './ErrorMessage';
|
import { ErrorMessage } from './ErrorMessage';
|
||||||
|
import { Spinner } from '~/components/svg';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
const Registration: React.FC = () => {
|
const Registration: React.FC = () => {
|
||||||
|
@ -21,10 +22,19 @@ const Registration: React.FC = () => {
|
||||||
const password = watch('password');
|
const password = watch('password');
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [countdown, setCountdown] = useState<number>(3);
|
const [countdown, setCountdown] = useState<number>(3);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const token = queryParams.get('token');
|
||||||
|
|
||||||
const registerUser = useRegisterUserMutation({
|
const registerUser = useRegisterUserMutation({
|
||||||
|
onMutate: () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
setIsSubmitting(false);
|
||||||
setCountdown(3);
|
setCountdown(3);
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setCountdown((prevCountdown) => {
|
setCountdown((prevCountdown) => {
|
||||||
|
@ -39,18 +49,13 @@ const Registration: React.FC = () => {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
|
setIsSubmitting(false);
|
||||||
if ((error as TError).response?.data?.message) {
|
if ((error as TError).response?.data?.message) {
|
||||||
setErrorMessage((error as TError).response?.data?.message ?? '');
|
setErrorMessage((error as TError).response?.data?.message ?? '');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (startupConfig?.registrationEnabled === false) {
|
|
||||||
navigate('/login');
|
|
||||||
}
|
|
||||||
}, [startupConfig, navigate]);
|
|
||||||
|
|
||||||
const renderInput = (id: string, label: string, type: string, validation: object) => (
|
const renderInput = (id: string, label: string, type: string, validation: object) => (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
@ -110,7 +115,9 @@ const Registration: React.FC = () => {
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
aria-label="Registration form"
|
aria-label="Registration form"
|
||||||
method="POST"
|
method="POST"
|
||||||
onSubmit={handleSubmit((data: TRegisterUser) => registerUser.mutate(data))}
|
onSubmit={handleSubmit((data: TRegisterUser) =>
|
||||||
|
registerUser.mutate({ ...data, token: token ?? undefined }),
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{renderInput('name', 'com_auth_full_name', 'text', {
|
{renderInput('name', 'com_auth_full_name', 'text', {
|
||||||
required: localize('com_auth_name_required'),
|
required: localize('com_auth_name_required'),
|
||||||
|
@ -170,7 +177,7 @@ const Registration: React.FC = () => {
|
||||||
aria-label="Submit registration"
|
aria-label="Submit registration"
|
||||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500"
|
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500"
|
||||||
>
|
>
|
||||||
{localize('com_auth_continue')}
|
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -33,12 +33,15 @@ const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) =>
|
||||||
// Define Tailwind CSS classes based on state
|
// Define Tailwind CSS classes based on state
|
||||||
const baseStyles = 'border border-solid border-gray-300 dark:border-gray-600 transition-colors';
|
const baseStyles = 'border border-solid border-gray-300 dark:border-gray-600 transition-colors';
|
||||||
|
|
||||||
const pressedStyles = 'bg-blue-200 border-blue-200 dark:bg-blue-900 dark:border-blue-600';
|
let dynamicStyles = '';
|
||||||
const hoverStyles = 'bg-gray-100 dark:bg-gray-700';
|
|
||||||
|
|
||||||
return `${baseStyles} ${
|
if (isPressed && activeButton === id) {
|
||||||
isPressed && activeButton === id ? pressedStyles : isHovered ? hoverStyles : ''
|
dynamicStyles = 'bg-blue-200 border-blue-200 dark:bg-blue-900 dark:border-blue-600';
|
||||||
}`;
|
} else if (isHovered) {
|
||||||
|
dynamicStyles = 'bg-gray-100 dark:bg-gray-700';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseStyles} ${dynamicStyles}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
91
config/invite-user.js
Normal file
91
config/invite-user.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
const path = require('path');
|
||||||
|
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
||||||
|
const { sendEmail, checkEmailConfig } = require('~/server/utils');
|
||||||
|
const { askQuestion, silentExit } = require('./helpers');
|
||||||
|
const { createInvite } = require('~/models/inviteUser');
|
||||||
|
const User = require('~/models/User');
|
||||||
|
const connect = require('./connect');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await connect();
|
||||||
|
|
||||||
|
console.purple('--------------------------');
|
||||||
|
console.purple('Invite a new user account!');
|
||||||
|
console.purple('--------------------------');
|
||||||
|
|
||||||
|
if (process.argv.length < 5) {
|
||||||
|
console.orange('Usage: npm run invite-user <email>');
|
||||||
|
console.orange('Note: if you do not pass in the arguments, you will be prompted for them.');
|
||||||
|
console.purple('--------------------------');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email service is enabled
|
||||||
|
if (!checkEmailConfig()) {
|
||||||
|
console.red('Error: Email service is not enabled!');
|
||||||
|
silentExit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the email of the user to be invited
|
||||||
|
let email = '';
|
||||||
|
if (process.argv.length >= 3) {
|
||||||
|
email = process.argv[2];
|
||||||
|
}
|
||||||
|
if (!email) {
|
||||||
|
email = await askQuestion('Email:');
|
||||||
|
}
|
||||||
|
// Validate the email
|
||||||
|
if (!email.includes('@')) {
|
||||||
|
console.red('Error: Invalid email address!');
|
||||||
|
silentExit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user already exists
|
||||||
|
const userExists = await User.findOne({ email });
|
||||||
|
if (userExists) {
|
||||||
|
console.red('Error: A user with that email already exists!');
|
||||||
|
silentExit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await createInvite(email);
|
||||||
|
const inviteLink = `${process.env.DOMAIN_CLIENT}/register?token=${token}`;
|
||||||
|
|
||||||
|
const appName = process.env.APP_TITLE || 'LibreChat';
|
||||||
|
|
||||||
|
if (!checkEmailConfig()) {
|
||||||
|
console.green('Send this link to the user:', inviteLink);
|
||||||
|
silentExit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendEmail({
|
||||||
|
email: email,
|
||||||
|
subject: `Invite to join ${appName}!`,
|
||||||
|
payload: {
|
||||||
|
appName: appName,
|
||||||
|
inviteLink: inviteLink,
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
},
|
||||||
|
template: 'inviteUser.handlebars',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error: ' + error.message);
|
||||||
|
silentExit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done!
|
||||||
|
console.green('Invitation sent successfully!');
|
||||||
|
silentExit(0);
|
||||||
|
})();
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
if (!err.message.includes('fetch failed')) {
|
||||||
|
console.error('There was an uncaught error:');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.message.includes('fetch failed')) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
|
@ -26,6 +26,7 @@
|
||||||
"stop:deployed": "docker-compose -f ./deploy-compose.yml down",
|
"stop:deployed": "docker-compose -f ./deploy-compose.yml down",
|
||||||
"upgrade": "node config/upgrade.js",
|
"upgrade": "node config/upgrade.js",
|
||||||
"create-user": "node config/create-user.js",
|
"create-user": "node config/create-user.js",
|
||||||
|
"invite-user": "node config/invite-user.js",
|
||||||
"ban-user": "node config/ban-user.js",
|
"ban-user": "node config/ban-user.js",
|
||||||
"delete-user": "node config/delete-user.js",
|
"delete-user": "node config/delete-user.js",
|
||||||
"backend": "cross-env NODE_ENV=production node api/server/index.js",
|
"backend": "cross-env NODE_ENV=production node api/server/index.js",
|
||||||
|
|
|
@ -254,6 +254,7 @@ export type TRegisterUser = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirm_password?: string;
|
confirm_password?: string;
|
||||||
|
token?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLoginUser = {
|
export type TLoginUser = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue