Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📩 feat: invite user #3012

Merged
merged 22 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2d09218
feat: basic invite-user script
berry-13 Jun 8, 2024
7774efb
feat: add invite user functionality and registration validation middl…
berry-13 Jun 8, 2024
1809551
Merge branch 'main' into invite-user
berry-13 Jun 8, 2024
f7ad1d8
fix: invite user fixes
berry-13 Jun 8, 2024
731fd0a
refactor: consolidate direct model access to a central place of funct…
berry-13 Jun 9, 2024
8d1be0c
style(Registration): add spinner to continue button
berry-13 Jun 9, 2024
a1560fd
refactor: import ordrer
berry-13 Jun 9, 2024
6c8494c
feat: improve invite user script and error handling
berry-13 Jun 9, 2024
7651c23
Merge branch 'main' into invite-user
berry-13 Jun 16, 2024
61a630d
Merge branch 'main' into invite-user
berry-13 Jun 18, 2024
70ee923
fix: merge conflict
berry-13 Jun 21, 2024
0ffedf0
Merge branch 'main' into invite-user
berry-13 Jun 21, 2024
78a83ad
refactor: remove `console.log` and use `logger`
berry-13 Jul 3, 2024
89606f7
Merge branch 'main' into invite-user
berry-13 Aug 17, 2024
4e62aaf
fix: token operation and checkinvite issues
berry-13 Aug 17, 2024
2f69a20
bring back comment and remove console log
berry-13 Aug 17, 2024
3507fde
fix: return invalid token when token is not found
berry-13 Aug 17, 2024
b0428f4
fix: getInvite fix
berry-13 Aug 17, 2024
6fe37d9
refactor: Update Token.js to use async/await syntax for update and de…
danny-avila Aug 18, 2024
569812e
feat: Refactor Token.js to use async/await syntax for createToken and…
danny-avila Aug 18, 2024
2177734
refactor(inviteUser): define functions outside of module.exports
danny-avila Aug 18, 2024
07b5390
Update AuthService.js
danny-avila Aug 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions api/models/Token.js
Original file line number Diff line number Diff line change
@@ -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,
};
58 changes: 32 additions & 26 deletions api/models/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
const {
getMessages,
saveMessage,
recordMessage,
updateMessage,
deleteMessagesSince,
deleteMessages,
} = require('./Message');
const {
comparePassword,
deleteUserById,
Expand All @@ -16,8 +8,6 @@ const {
countUsers,
findUser,
} = require('./userMethods');
const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation');
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
const {
findFileById,
createFile,
Expand All @@ -27,26 +17,40 @@ const {
getFiles,
updateFileUsage,
} = require('./File');
const Key = require('./Key');
const User = require('./User');
const {
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 Balance = require('./Balance');
const User = require('./User');
const Key = require('./Key');

module.exports = {
User,
Key,
Session,
Balance,

comparePassword,
deleteUserById,
generateToken,
getUserById,
countUsers,
createUser,
updateUser,
createUser,
countUsers,
findUser,

findFileById,
createFile,
updateFile,
deleteFile,
deleteFiles,
getFiles,
updateFileUsage,

getMessages,
saveMessage,
recordMessage,
Expand All @@ -64,11 +68,13 @@ module.exports = {
savePreset,
deletePresets,

findFileById,
createFile,
updateFile,
deleteFile,
deleteFiles,
getFiles,
updateFileUsage,
createToken,
findToken,
updateToken,
deleteTokens,

User,
Key,
Session,
Balance,
};
67 changes: 67 additions & 0 deletions api/models/inviteUser.js
berry-13 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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
*/

module.exports = {
/**
* @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
*/
createInvite: async (email) => {
danny-avila marked this conversation as resolved.
Show resolved Hide resolved
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
*/
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 };
}
},
};
9 changes: 7 additions & 2 deletions api/models/schema/tokenSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ const tokenSchema = new Schema({
type: Date,
required: true,
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;
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"list-balances": "node ./list-balances.js",
"user-stats": "node ./user-stats.js",
"create-user": "node ./create-user.js",
"invite-user": "node ./invite-user.js",
"ban-user": "node ./ban-user.js",
"delete-user": "node ./delete-user.js"
},
Expand Down
27 changes: 27 additions & 0 deletions api/server/middleware/checkInviteUser.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions api/server/middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const requireLocalAuth = require('./requireLocalAuth');
const canDeleteAccount = require('./canDeleteAccount');
const requireLdapAuth = require('./requireLdapAuth');
const abortMiddleware = require('./abortMiddleware');
const checkInviteUser = require('./checkInviteUser');
const requireJwtAuth = require('./requireJwtAuth');
const validateModel = require('./validateModel');
const moderateText = require('./moderateText');
Expand All @@ -33,6 +34,7 @@ module.exports = {
moderateText,
validateModel,
requireJwtAuth,
checkInviteUser,
requireLdapAuth,
requireLocalAuth,
canDeleteAccount,
Expand Down
8 changes: 7 additions & 1 deletion api/server/middleware/validateRegistration.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
const { isEnabled } = require('~/server/utils');

function validateRegistration(req, res, next) {
if (req.invite) {
return next();
}

if (isEnabled(process.env.ALLOW_REGISTRATION)) {
next();
} else {
res.status(403).send('Registration is not allowed.');
return res.status(403).json({
message: 'Registration is not allowed.',
});
}
}

Expand Down
10 changes: 9 additions & 1 deletion api/server/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
checkBan,
loginLimiter,
requireJwtAuth,
checkInviteUser,
registerLimiter,
requireLdapAuth,
requireLocalAuth,
Expand All @@ -32,7 +33,14 @@ router.post(
loginController,
);
router.post('/refresh', refreshController);
router.post('/register', registerLimiter, checkBan, validateRegistration, registrationController);
router.post(
'/register',
registerLimiter,
checkBan,
checkInviteUser,
validateRegistration,
registrationController,
);
router.post(
'/requestPasswordReset',
resetPasswordLimiter,
Expand Down
Loading
Loading