From bbb93244471ce819741d59d1e39c1ad06b179f7a Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 18 Aug 2024 06:23:38 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A9=20feat:=20invite=20user=20(#3012)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- api/models/Token.js | 117 +++++++ api/models/index.js | 58 ++-- api/models/inviteUser.js | 70 +++++ api/models/schema/tokenSchema.js | 9 +- api/package.json | 1 + api/server/middleware/checkInviteUser.js | 27 ++ api/server/middleware/index.js | 2 + api/server/middleware/validateRegistration.js | 8 +- api/server/routes/auth.js | 10 +- api/server/services/AuthService.js | 34 ++- api/server/utils/emails/inviteUser.handlebars | 287 ++++++++++++++++++ client/src/components/Auth/Registration.tsx | 27 +- client/src/components/Auth/SocialButton.tsx | 13 +- config/invite-user.js | 91 ++++++ package.json | 1 + packages/data-provider/src/types.ts | 1 + 16 files changed, 695 insertions(+), 61 deletions(-) create mode 100644 api/models/Token.js create mode 100644 api/models/inviteUser.js create mode 100644 api/server/middleware/checkInviteUser.js create mode 100644 api/server/utils/emails/inviteUser.handlebars create mode 100644 config/invite-user.js diff --git a/api/models/Token.js b/api/models/Token.js new file mode 100644 index 000000000..cdd156b6b --- /dev/null +++ b/api/models/Token.js @@ -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} 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} 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} 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} 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, +}; diff --git a/api/models/index.js b/api/models/index.js index 1f10d251b..380c93cc4 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,11 +1,3 @@ -const { - getMessages, - saveMessage, - recordMessage, - updateMessage, - deleteMessagesSince, - deleteMessages, -} = require('./Message'); const { comparePassword, deleteUserById, @@ -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, @@ -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, @@ -64,11 +68,13 @@ module.exports = { savePreset, deletePresets, - findFileById, - createFile, - updateFile, - deleteFile, - deleteFiles, - getFiles, - updateFileUsage, + createToken, + findToken, + updateToken, + deleteTokens, + + User, + Key, + Session, + Balance, }; diff --git a/api/models/inviteUser.js b/api/models/inviteUser.js new file mode 100644 index 000000000..c04bd9467 --- /dev/null +++ b/api/models/inviteUser.js @@ -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} 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} 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, +}; diff --git a/api/models/schema/tokenSchema.js b/api/models/schema/tokenSchema.js index d74637b64..bb223ff45 100644 --- a/api/models/schema/tokenSchema.js +++ b/api/models/schema/tokenSchema.js @@ -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; diff --git a/api/package.json b/api/package.json index 1b42197cd..c3a07c78f 100644 --- a/api/package.json +++ b/api/package.json @@ -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" }, diff --git a/api/server/middleware/checkInviteUser.js b/api/server/middleware/checkInviteUser.js new file mode 100644 index 000000000..e1ad271b5 --- /dev/null +++ b/api/server/middleware/checkInviteUser.js @@ -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; diff --git a/api/server/middleware/index.js b/api/server/middleware/index.js index 8d3fff58f..3da9e06bd 100644 --- a/api/server/middleware/index.js +++ b/api/server/middleware/index.js @@ -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'); @@ -33,6 +34,7 @@ module.exports = { moderateText, validateModel, requireJwtAuth, + checkInviteUser, requireLdapAuth, requireLocalAuth, canDeleteAccount, diff --git a/api/server/middleware/validateRegistration.js b/api/server/middleware/validateRegistration.js index 4f9641ad0..07911bd9c 100644 --- a/api/server/middleware/validateRegistration.js +++ b/api/server/middleware/validateRegistration.js @@ -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.', + }); } } diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index 96f0a1e3f..3e86ffd86 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -11,6 +11,7 @@ const { checkBan, loginLimiter, requireJwtAuth, + checkInviteUser, registerLimiter, requireLdapAuth, requireLocalAuth, @@ -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, diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index eb2dd63e6..0fe7b84fa 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -10,12 +10,11 @@ const { generateToken, deleteUserById, } = require('~/models/userMethods'); +const { createToken, findToken, deleteTokens, Session } = require('~/models'); const { sendEmail, checkEmailConfig } = require('~/server/utils'); const { registerSchema } = require('~/strategies/validators'); const { hashToken } = require('~/server/utils/crypto'); const isDomainAllowed = require('./isDomainAllowed'); -const Token = require('~/models/schema/tokenSchema'); -const Session = require('~/models/Session'); const { logger } = require('~/config'); const domains = { @@ -87,12 +86,13 @@ const sendVerificationEmail = async (user) => { template: 'verifyEmail.handlebars', }); - await new Token({ + await createToken({ userId: user._id, email: user.email, token: hash, createdAt: Date.now(), - }).save(); + expiresIn: 900, + }); logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`); }; @@ -103,7 +103,7 @@ const sendVerificationEmail = async (user) => { */ const verifyEmail = async (req) => { const { email, token } = req.body; - let emailVerificationData = await Token.findOne({ email: decodeURIComponent(email) }); + let emailVerificationData = await findToken({ email: decodeURIComponent(email) }); if (!emailVerificationData) { logger.warn(`[verifyEmail] [No email verification data found] [Email: ${email}]`); @@ -123,7 +123,7 @@ const verifyEmail = async (req) => { return new Error('User not found'); } - await emailVerificationData.deleteOne(); + await deleteTokens({ token: emailVerificationData.token }); logger.info(`[verifyEmail] Email verification successful. [Email: ${email}]`); return { message: 'Email verification was successful' }; }; @@ -231,18 +231,16 @@ const requestPasswordReset = async (req) => { }; } - let token = await Token.findOne({ userId: user._id }); - if (token) { - await token.deleteOne(); - } + await deleteTokens({ userId: user._id }); const [resetToken, hash] = createTokenHash(); - await new Token({ + await createToken({ userId: user._id, token: hash, createdAt: Date.now(), - }).save(); + expiresIn: 900, + }); const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`; @@ -282,7 +280,10 @@ const requestPasswordReset = async (req) => { * @returns */ const resetPassword = async (userId, token, password) => { - let passwordResetToken = await Token.findOne({ userId }); + let passwordResetToken = await createToken({ + userId, + expiresIn: 900, + }); if (!passwordResetToken) { return new Error('Invalid or expired password reset token'); @@ -366,7 +367,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => { const resendVerificationEmail = async (req) => { try { const { email } = req.body; - await Token.deleteMany({ email }); + await deleteTokens(email); const user = await findUser({ email }, 'email _id name'); if (!user) { @@ -392,12 +393,13 @@ const resendVerificationEmail = async (req) => { template: 'verifyEmail.handlebars', }); - await new Token({ + await createToken({ userId: user._id, email: user.email, token: hash, createdAt: Date.now(), - }).save(); + expiresIn: 900, + }); logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`); diff --git a/api/server/utils/emails/inviteUser.handlebars b/api/server/utils/emails/inviteUser.handlebars new file mode 100644 index 000000000..b68fa9903 --- /dev/null +++ b/api/server/utils/emails/inviteUser.handlebars @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + +
+
+ +
+ + + + + + + +
+ +

+
+
You have been invited to join {{appName}}!
+
+ +

+ +
+ + + + + + +
+
+
Hi,
+
+
+ + + + + + +
+
+

You have been invited to join {{appName}}. Click the + button below to create your account and get started.

+

+
+
+ + + + + + +
+ + +
+ + + + + + +
+
+
+
+ Hurry up, the invite will expiry in 7 days +
+
+
+
+ + + + + + +
+
+
Best regards,
+
The {{appName}} Team
+
+
+ + + + + + +
+
+
+
© + {{year}} + {{appName}}. All rights reserved.
+
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx index 086368508..139791553 100644 --- a/client/src/components/Auth/Registration.tsx +++ b/client/src/components/Auth/Registration.tsx @@ -1,10 +1,11 @@ import { useForm } from 'react-hook-form'; -import React, { useState, useEffect } from 'react'; -import { useNavigate, useOutletContext } from 'react-router-dom'; +import React, { useState } from 'react'; +import { useNavigate, useOutletContext, useLocation } from 'react-router-dom'; import { useRegisterUserMutation } from 'librechat-data-provider/react-query'; import type { TRegisterUser, TError } from 'librechat-data-provider'; import type { TLoginLayoutContext } from '~/common'; import { ErrorMessage } from './ErrorMessage'; +import { Spinner } from '~/components/svg'; import { useLocalize } from '~/hooks'; const Registration: React.FC = () => { @@ -21,10 +22,19 @@ const Registration: React.FC = () => { const password = watch('password'); const [errorMessage, setErrorMessage] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); const [countdown, setCountdown] = useState(3); + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + const token = queryParams.get('token'); + const registerUser = useRegisterUserMutation({ + onMutate: () => { + setIsSubmitting(true); + }, onSuccess: () => { + setIsSubmitting(false); setCountdown(3); const timer = setInterval(() => { setCountdown((prevCountdown) => { @@ -39,18 +49,13 @@ const Registration: React.FC = () => { }, 1000); }, onError: (error: unknown) => { + setIsSubmitting(false); if ((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) => (
@@ -110,7 +115,9 @@ const Registration: React.FC = () => { className="mt-6" aria-label="Registration form" 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', { required: localize('com_auth_name_required'), @@ -170,7 +177,7 @@ const Registration: React.FC = () => { 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" > - {localize('com_auth_continue')} + {isSubmitting ? : localize('com_auth_continue')}
diff --git a/client/src/components/Auth/SocialButton.tsx b/client/src/components/Auth/SocialButton.tsx index c87ccf55e..c9b4bca34 100644 --- a/client/src/components/Auth/SocialButton.tsx +++ b/client/src/components/Auth/SocialButton.tsx @@ -33,12 +33,15 @@ const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) => // Define Tailwind CSS classes based on state 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'; - const hoverStyles = 'bg-gray-100 dark:bg-gray-700'; + let dynamicStyles = ''; - return `${baseStyles} ${ - isPressed && activeButton === id ? pressedStyles : isHovered ? hoverStyles : '' - }`; + if (isPressed && activeButton === id) { + 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 ( diff --git a/config/invite-user.js b/config/invite-user.js new file mode 100644 index 000000000..79925ec05 --- /dev/null +++ b/config/invite-user.js @@ -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 '); + 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); + } +}); diff --git a/package.json b/package.json index 56b56eb8b..93471416c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "stop:deployed": "docker-compose -f ./deploy-compose.yml down", "upgrade": "node config/upgrade.js", "create-user": "node config/create-user.js", + "invite-user": "node config/invite-user.js", "ban-user": "node config/ban-user.js", "delete-user": "node config/delete-user.js", "backend": "cross-env NODE_ENV=production node api/server/index.js", diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 003f2e3d0..19104fa9d 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -254,6 +254,7 @@ export type TRegisterUser = { username: string; password: string; confirm_password?: string; + token?: string; }; export type TLoginUser = {