mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +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
|
|
@ -1,42 +1,26 @@
|
|||
const crypto = require('crypto');
|
||||
const cookies = require('cookie');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { Session, User } = require('~/models');
|
||||
const {
|
||||
registerUser,
|
||||
resetPassword,
|
||||
setAuthTokens,
|
||||
requestPasswordReset,
|
||||
} = require('~/server/services/AuthService');
|
||||
const { Session, getUserById } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const registrationController = async (req, res) => {
|
||||
try {
|
||||
const response = await registerUser(req.body);
|
||||
if (response.status === 200) {
|
||||
const { status, user } = response;
|
||||
let newUser = await User.findOne({ _id: user._id });
|
||||
if (!newUser) {
|
||||
newUser = new User(user);
|
||||
await newUser.save();
|
||||
}
|
||||
const token = await setAuthTokens(user._id, res);
|
||||
res.setHeader('Authorization', `Bearer ${token}`);
|
||||
res.status(status).send({ user });
|
||||
} else {
|
||||
const { status, message } = response;
|
||||
res.status(status).send({ message });
|
||||
}
|
||||
const { status, message } = response;
|
||||
res.status(status).send({ message });
|
||||
} catch (err) {
|
||||
logger.error('[registrationController]', err);
|
||||
return res.status(500).json({ message: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
const getUserController = async (req, res) => {
|
||||
return res.status(200).send(req.user);
|
||||
};
|
||||
|
||||
const resetPasswordRequestController = async (req, res) => {
|
||||
try {
|
||||
const resetService = await requestPasswordReset(req);
|
||||
|
|
@ -77,7 +61,7 @@ const refreshController = async (req, res) => {
|
|||
|
||||
try {
|
||||
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||
const user = await User.findOne({ _id: payload.id });
|
||||
const user = await getUserById(payload.id, '-password -__v');
|
||||
if (!user) {
|
||||
return res.status(401).redirect('/login');
|
||||
}
|
||||
|
|
@ -86,8 +70,7 @@ const refreshController = async (req, res) => {
|
|||
|
||||
if (process.env.NODE_ENV === 'CI') {
|
||||
const token = await setAuthTokens(userId, res);
|
||||
const userObj = user.toJSON();
|
||||
return res.status(200).send({ token, user: userObj });
|
||||
return res.status(200).send({ token, user });
|
||||
}
|
||||
|
||||
// Hash the refresh token
|
||||
|
|
@ -98,8 +81,7 @@ const refreshController = async (req, res) => {
|
|||
const session = await Session.findOne({ user: userId, refreshTokenHash: hashedToken });
|
||||
if (session && session.expiration > new Date()) {
|
||||
const token = await setAuthTokens(userId, res, session._id);
|
||||
const userObj = user.toJSON();
|
||||
res.status(200).send({ token, user: userObj });
|
||||
res.status(200).send({ token, user });
|
||||
} else if (req?.query?.retry) {
|
||||
// Retrying from a refresh token request that failed (401)
|
||||
res.status(403).send('No session found');
|
||||
|
|
@ -115,7 +97,6 @@ const refreshController = async (req, res) => {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
getUserController,
|
||||
refreshController,
|
||||
registrationController,
|
||||
resetPasswordController,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
const {
|
||||
User,
|
||||
Session,
|
||||
Balance,
|
||||
deleteFiles,
|
||||
deleteConvos,
|
||||
deletePresets,
|
||||
deleteMessages,
|
||||
deleteUserById,
|
||||
} = require('~/models');
|
||||
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
||||
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
|
||||
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
|
||||
const { deleteAllSharedLinks } = require('~/models/Share');
|
||||
const { Transaction } = require('~/models/Transaction');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
@ -59,7 +61,7 @@ const updateUserPluginsController = async (req, res) => {
|
|||
res.status(200).send();
|
||||
} catch (err) {
|
||||
logger.error('[updateUserPluginsController]', err);
|
||||
res.status(500).json({ message: err.message });
|
||||
return res.status(500).json({ message: 'Something went wrong.' });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -75,18 +77,50 @@ const deleteUserController = async (req, res) => {
|
|||
await deletePresets(user.id); // delete user presets
|
||||
await deleteConvos(user.id); // delete user convos
|
||||
await deleteUserPluginAuth(user.id, null, true); // delete user plugin auth
|
||||
await User.deleteOne({ _id: user.id }); // delete user
|
||||
await deleteUserById(user.id); // delete user
|
||||
await deleteFiles(null, user.id); // delete user files
|
||||
await deleteAllSharedLinks(user.id); // delete user shared links
|
||||
/* TODO: queue job for cleaning actions and assistants of non-existant users */
|
||||
logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`);
|
||||
res.status(200).send({ message: 'User deleted' });
|
||||
} catch (err) {
|
||||
logger.error('[deleteUserController]', err);
|
||||
res.status(500).send({ message: err.message });
|
||||
return res.status(500).json({ message: 'Something went wrong.' });
|
||||
}
|
||||
};
|
||||
|
||||
const verifyEmailController = async (req, res) => {
|
||||
try {
|
||||
const verifyEmailService = await verifyEmail(req);
|
||||
if (verifyEmailService instanceof Error) {
|
||||
return res.status(400).json(verifyEmailService);
|
||||
} else {
|
||||
return res.status(200).json(verifyEmailService);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[verifyEmailController]', e);
|
||||
return res.status(500).json({ message: 'Something went wrong.' });
|
||||
}
|
||||
};
|
||||
|
||||
const resendVerificationController = async (req, res) => {
|
||||
try {
|
||||
const result = await resendVerificationEmail(req);
|
||||
if (result instanceof Error) {
|
||||
return res.status(400).json(result);
|
||||
} else {
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[verifyEmailController]', e);
|
||||
return res.status(500).json({ message: 'Something went wrong.' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getUserController,
|
||||
updateUserPluginsController,
|
||||
deleteUserController,
|
||||
verifyEmailController,
|
||||
updateUserPluginsController,
|
||||
resendVerificationController,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
const User = require('~/models/User');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const { getUserById } = require('~/models/userMethods');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const loginController = async (req, res) => {
|
||||
try {
|
||||
const user = await User.findById(req.user._id);
|
||||
const user = await getUserById(req.user._id, '-password -__v');
|
||||
|
||||
// If user doesn't exist, return error
|
||||
if (!user) {
|
||||
// typeof user !== User) { // this doesn't seem to resolve the User type ??
|
||||
return res.status(400).json({ message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
if (!user.emailVerified && !isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN)) {
|
||||
return res.status(422).json({ message: 'Email not verified' });
|
||||
}
|
||||
|
||||
const token = await setAuthTokens(user._id, res);
|
||||
|
||||
return res.status(200).send({ token, user });
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
const Keyv = require('keyv');
|
||||
const uap = require('ua-parser-js');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { isEnabled, removePorts } = require('../utils');
|
||||
const { isEnabled, removePorts } = require('~/server/utils');
|
||||
const keyvMongo = require('~/cache/keyvMongo');
|
||||
const denyRequest = require('./denyRequest');
|
||||
const { getLogStores } = require('~/cache');
|
||||
const User = require('~/models/User');
|
||||
const { findUser } = require('~/models');
|
||||
|
||||
const banCache = new Keyv({ store: keyvMongo, namespace: ViolationTypes.BAN, ttl: 0 });
|
||||
const message = 'Your account has been temporarily banned due to violations of our service.';
|
||||
|
|
@ -55,7 +55,7 @@ const checkBan = async (req, res, next = () => {}) => {
|
|||
let userId = req.user?.id ?? req.user?._id ?? null;
|
||||
|
||||
if (!userId && req?.body?.email) {
|
||||
const user = await User.findOne({ email: req.body.email }, '_id').lean();
|
||||
const user = await findUser({ email: req.body.email }, '_id');
|
||||
userId = user?._id ? user._id.toString() : userId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +1,43 @@
|
|||
const abortMiddleware = require('./abortMiddleware');
|
||||
const checkBan = require('./checkBan');
|
||||
const checkDomainAllowed = require('./checkDomainAllowed');
|
||||
const uaParser = require('./uaParser');
|
||||
const setHeaders = require('./setHeaders');
|
||||
const loginLimiter = require('./loginLimiter');
|
||||
const validateModel = require('./validateModel');
|
||||
const requireJwtAuth = require('./requireJwtAuth');
|
||||
const requireLdapAuth = require('./requireLdapAuth');
|
||||
const uploadLimiters = require('./uploadLimiters');
|
||||
const registerLimiter = require('./registerLimiter');
|
||||
const messageLimiters = require('./messageLimiters');
|
||||
const requireLocalAuth = require('./requireLocalAuth');
|
||||
const validateEndpoint = require('./validateEndpoint');
|
||||
const concurrentLimiter = require('./concurrentLimiter');
|
||||
const validateMessageReq = require('./validateMessageReq');
|
||||
const buildEndpointOption = require('./buildEndpointOption');
|
||||
const validatePasswordReset = require('./validatePasswordReset');
|
||||
const validateRegistration = require('./validateRegistration');
|
||||
const validateImageRequest = require('./validateImageRequest');
|
||||
const validatePasswordReset = require('./validatePasswordReset');
|
||||
const moderateText = require('./moderateText');
|
||||
const noIndex = require('./noIndex');
|
||||
const importLimiters = require('./importLimiters');
|
||||
const buildEndpointOption = require('./buildEndpointOption');
|
||||
const validateMessageReq = require('./validateMessageReq');
|
||||
const checkDomainAllowed = require('./checkDomainAllowed');
|
||||
const concurrentLimiter = require('./concurrentLimiter');
|
||||
const validateEndpoint = require('./validateEndpoint');
|
||||
const requireLocalAuth = require('./requireLocalAuth');
|
||||
const canDeleteAccount = require('./canDeleteAccount');
|
||||
const requireLdapAuth = require('./requireLdapAuth');
|
||||
const abortMiddleware = require('./abortMiddleware');
|
||||
const requireJwtAuth = require('./requireJwtAuth');
|
||||
const validateModel = require('./validateModel');
|
||||
const moderateText = require('./moderateText');
|
||||
const setHeaders = require('./setHeaders');
|
||||
const limiters = require('./limiters');
|
||||
const uaParser = require('./uaParser');
|
||||
const checkBan = require('./checkBan');
|
||||
const noIndex = require('./noIndex');
|
||||
|
||||
module.exports = {
|
||||
...uploadLimiters,
|
||||
...abortMiddleware,
|
||||
...messageLimiters,
|
||||
...limiters,
|
||||
noIndex,
|
||||
checkBan,
|
||||
uaParser,
|
||||
setHeaders,
|
||||
loginLimiter,
|
||||
moderateText,
|
||||
validateModel,
|
||||
requireJwtAuth,
|
||||
requireLdapAuth,
|
||||
registerLimiter,
|
||||
requireLocalAuth,
|
||||
canDeleteAccount,
|
||||
validateEndpoint,
|
||||
concurrentLimiter,
|
||||
checkDomainAllowed,
|
||||
validateMessageReq,
|
||||
buildEndpointOption,
|
||||
validateRegistration,
|
||||
validateImageRequest,
|
||||
validatePasswordReset,
|
||||
validateModel,
|
||||
moderateText,
|
||||
noIndex,
|
||||
...importLimiters,
|
||||
checkDomainAllowed,
|
||||
canDeleteAccount,
|
||||
};
|
||||
|
|
|
|||
22
api/server/middleware/limiters/index.js
Normal file
22
api/server/middleware/limiters/index.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const createTTSLimiters = require('./ttsLimiters');
|
||||
const createSTTLimiters = require('./sttLimiters');
|
||||
|
||||
const loginLimiter = require('./loginLimiter');
|
||||
const importLimiters = require('./importLimiters');
|
||||
const uploadLimiters = require('./uploadLimiters');
|
||||
const registerLimiter = require('./registerLimiter');
|
||||
const messageLimiters = require('./messageLimiters');
|
||||
const verifyEmailLimiter = require('./verifyEmailLimiter');
|
||||
const resetPasswordLimiter = require('./resetPasswordLimiter');
|
||||
|
||||
module.exports = {
|
||||
...uploadLimiters,
|
||||
...importLimiters,
|
||||
...messageLimiters,
|
||||
loginLimiter,
|
||||
registerLimiter,
|
||||
createTTSLimiters,
|
||||
createSTTLimiters,
|
||||
verifyEmailLimiter,
|
||||
resetPasswordLimiter,
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { logViolation } = require('../../cache');
|
||||
const { removePorts } = require('../utils');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { logViolation } = require('~/cache');
|
||||
|
||||
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;
|
||||
const windowMs = LOGIN_WINDOW * 60 * 1000;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { logViolation } = require('../../cache');
|
||||
const denyRequest = require('./denyRequest');
|
||||
const denyRequest = require('~/server/middleware/denyRequest');
|
||||
const { logViolation } = require('~/cache');
|
||||
|
||||
const {
|
||||
MESSAGE_IP_MAX = 40,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { logViolation } = require('../../cache');
|
||||
const { removePorts } = require('../utils');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { logViolation } = require('~/cache');
|
||||
|
||||
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;
|
||||
const windowMs = REGISTER_WINDOW * 60 * 1000;
|
||||
35
api/server/middleware/limiters/resetPasswordLimiter.js
Normal file
35
api/server/middleware/limiters/resetPasswordLimiter.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { logViolation } = require('~/cache');
|
||||
|
||||
const {
|
||||
RESET_PASSWORD_WINDOW = 2,
|
||||
RESET_PASSWORD_MAX = 2,
|
||||
RESET_PASSWORD_VIOLATION_SCORE: score,
|
||||
} = process.env;
|
||||
const windowMs = RESET_PASSWORD_WINDOW * 60 * 1000;
|
||||
const max = RESET_PASSWORD_MAX;
|
||||
const windowInMinutes = windowMs / 60000;
|
||||
const message = `Too many attempts, please try again after ${windowInMinutes} minute(s)`;
|
||||
|
||||
const handler = async (req, res) => {
|
||||
const type = ViolationTypes.RESET_PASSWORD_LIMIT;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max,
|
||||
windowInMinutes,
|
||||
};
|
||||
|
||||
await logViolation(req, res, type, errorMessage, score);
|
||||
return res.status(429).json({ message });
|
||||
};
|
||||
|
||||
const resetPasswordLimiter = rateLimit({
|
||||
windowMs,
|
||||
max,
|
||||
handler,
|
||||
keyGenerator: removePorts,
|
||||
});
|
||||
|
||||
module.exports = resetPasswordLimiter;
|
||||
35
api/server/middleware/limiters/verifyEmailLimiter.js
Normal file
35
api/server/middleware/limiters/verifyEmailLimiter.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { logViolation } = require('~/cache');
|
||||
|
||||
const {
|
||||
VERIFY_EMAIL_WINDOW = 2,
|
||||
VERIFY_EMAIL_MAX = 2,
|
||||
VERIFY_EMAIL_VIOLATION_SCORE: score,
|
||||
} = process.env;
|
||||
const windowMs = VERIFY_EMAIL_WINDOW * 60 * 1000;
|
||||
const max = VERIFY_EMAIL_MAX;
|
||||
const windowInMinutes = windowMs / 60000;
|
||||
const message = `Too many attempts, please try again after ${windowInMinutes} minute(s)`;
|
||||
|
||||
const handler = async (req, res) => {
|
||||
const type = ViolationTypes.VERIFY_EMAIL_LIMIT;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max,
|
||||
windowInMinutes,
|
||||
};
|
||||
|
||||
await logViolation(req, res, type, errorMessage, score);
|
||||
return res.status(429).json({ message });
|
||||
};
|
||||
|
||||
const verifyEmailLimiter = rateLimit({
|
||||
windowMs,
|
||||
max,
|
||||
handler,
|
||||
keyGenerator: removePorts,
|
||||
});
|
||||
|
||||
module.exports = verifyEmailLimiter;
|
||||
|
|
@ -21,7 +21,13 @@ const requireLocalAuth = (req, res, next) => {
|
|||
log({
|
||||
title: '(requireLocalAuth) Error: No user',
|
||||
});
|
||||
return res.status(422).send(info);
|
||||
return res.status(404).send(info);
|
||||
}
|
||||
if (info && info.message) {
|
||||
log({
|
||||
title: '(requireLocalAuth) Error: ' + info.message,
|
||||
});
|
||||
return res.status(422).send({ message: info.message });
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
const createTTSLimiters = require('./ttsLimiters');
|
||||
const createSTTLimiters = require('./sttLimiters');
|
||||
|
||||
module.exports = {
|
||||
createTTSLimiters,
|
||||
createSTTLimiters,
|
||||
};
|
||||
|
|
@ -1,22 +1,23 @@
|
|||
const express = require('express');
|
||||
const {
|
||||
resetPasswordRequestController,
|
||||
resetPasswordController,
|
||||
refreshController,
|
||||
registrationController,
|
||||
} = require('../controllers/AuthController');
|
||||
const { loginController } = require('../controllers/auth/LoginController');
|
||||
const { logoutController } = require('../controllers/auth/LogoutController');
|
||||
resetPasswordController,
|
||||
resetPasswordRequestController,
|
||||
} = require('~/server/controllers/AuthController');
|
||||
const { loginController } = require('~/server/controllers/auth/LoginController');
|
||||
const { logoutController } = require('~/server/controllers/auth/LogoutController');
|
||||
const {
|
||||
checkBan,
|
||||
loginLimiter,
|
||||
registerLimiter,
|
||||
requireJwtAuth,
|
||||
registerLimiter,
|
||||
requireLdapAuth,
|
||||
requireLocalAuth,
|
||||
resetPasswordLimiter,
|
||||
validateRegistration,
|
||||
validatePasswordReset,
|
||||
} = require('../middleware');
|
||||
} = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ router.post('/refresh', refreshController);
|
|||
router.post('/register', registerLimiter, checkBan, validateRegistration, registrationController);
|
||||
router.post(
|
||||
'/requestPasswordReset',
|
||||
resetPasswordLimiter,
|
||||
checkBan,
|
||||
validatePasswordReset,
|
||||
resetPasswordRequestController,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
const express = require('express');
|
||||
const { uaParser, checkBan, requireJwtAuth, createFileLimiters } = require('~/server/middleware');
|
||||
const { createTTSLimiters, createSTTLimiters } = require('~/server/middleware/speech');
|
||||
const {
|
||||
uaParser,
|
||||
checkBan,
|
||||
requireJwtAuth,
|
||||
createFileLimiters,
|
||||
createTTSLimiters,
|
||||
createSTTLimiters,
|
||||
} = require('~/server/middleware');
|
||||
const { createMulterInstance } = require('./multer');
|
||||
|
||||
const files = require('./files');
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
||||
|
||||
const passport = require('passport');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const passport = require('passport');
|
||||
const { loginLimiter, checkBan, checkDomainAllowed } = require('~/server/middleware');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const domains = {
|
||||
client: process.env.DOMAIN_CLIENT,
|
||||
server: process.env.DOMAIN_SERVER,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
const express = require('express');
|
||||
const requireJwtAuth = require('../middleware/requireJwtAuth');
|
||||
const canDeleteAccount = require('../middleware/canDeleteAccount');
|
||||
const { requireJwtAuth, canDeleteAccount, verifyEmailLimiter } = require('~/server/middleware');
|
||||
const {
|
||||
getUserController,
|
||||
updateUserPluginsController,
|
||||
deleteUserController,
|
||||
} = require('../controllers/UserController');
|
||||
verifyEmailController,
|
||||
updateUserPluginsController,
|
||||
resendVerificationController,
|
||||
} = require('~/server/controllers/UserController');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', requireJwtAuth, getUserController);
|
||||
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
|
||||
router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController);
|
||||
router.post('/verify', verifyEmailController);
|
||||
router.post('/verify/resend', verifyEmailLimiter, resendVerificationController);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { errorsToString } = require('librechat-data-provider');
|
||||
const {
|
||||
countUsers,
|
||||
createUser,
|
||||
findUser,
|
||||
updateUser,
|
||||
generateToken,
|
||||
getUserById,
|
||||
} = require('~/models/userMethods');
|
||||
const { sendEmail, checkEmailConfig } = require('~/server/utils');
|
||||
const { registerSchema } = require('~/strategies/validators');
|
||||
const isDomainAllowed = require('./isDomainAllowed');
|
||||
const Token = require('~/models/schema/tokenSchema');
|
||||
const { sendEmail } = require('~/server/utils');
|
||||
const Session = require('~/models/Session');
|
||||
const { logger } = require('~/config');
|
||||
const User = require('~/models/User');
|
||||
|
||||
const domains = {
|
||||
client: process.env.DOMAIN_CLIENT,
|
||||
|
|
@ -15,19 +22,7 @@ const domains = {
|
|||
};
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
/**
|
||||
* Check if email configuration is set
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function checkEmailConfig() {
|
||||
return (
|
||||
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
|
||||
!!process.env.EMAIL_USERNAME &&
|
||||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM
|
||||
);
|
||||
}
|
||||
const genericVerificationMessage = 'Please check your email to verify your email address.';
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
|
|
@ -58,10 +53,73 @@ const logoutUser = async (userId, refreshToken) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*
|
||||
* @param {Object} user <email, password, name, username>
|
||||
* @returns
|
||||
* Send Verification Email
|
||||
* @param {Partial<MongoUser> & { _id: ObjectId, email: string, name: string}} user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const sendVerificationEmail = async (user) => {
|
||||
let verifyToken = crypto.randomBytes(32).toString('hex');
|
||||
const hash = bcrypt.hashSync(verifyToken, 10);
|
||||
|
||||
await new Token({
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
token: hash,
|
||||
createdAt: Date.now(),
|
||||
}).save();
|
||||
|
||||
const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${user.email}`;
|
||||
logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
||||
|
||||
sendEmail(
|
||||
user.email,
|
||||
'Verify your email',
|
||||
{
|
||||
appName: process.env.APP_TITLE || 'LibreChat',
|
||||
name: user.name,
|
||||
verificationLink: verificationLink,
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
'verifyEmail.handlebars',
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify Email
|
||||
* @param {Express.Request} req
|
||||
*/
|
||||
const verifyEmail = async (req) => {
|
||||
const { email, token } = req.body;
|
||||
let emailVerificationData = await Token.findOne({ email });
|
||||
|
||||
if (!emailVerificationData) {
|
||||
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${email}]`);
|
||||
return new Error('Invalid or expired password reset token');
|
||||
}
|
||||
|
||||
const isValid = bcrypt.compareSync(token, emailVerificationData.token);
|
||||
|
||||
if (!isValid) {
|
||||
logger.warn(`[verifyEmail] [Invalid or expired email verification token] [Email: ${email}]`);
|
||||
return new Error('Invalid or expired email verification token');
|
||||
}
|
||||
|
||||
const updatedUser = await updateUser(emailVerificationData.userId, { emailVerified: true });
|
||||
if (!updatedUser) {
|
||||
logger.warn(`[verifyEmail] [User not found] [Email: ${email}]`);
|
||||
return new Error('User not found');
|
||||
}
|
||||
|
||||
await emailVerificationData.deleteOne();
|
||||
logger.info(`[verifyEmail] Email verification successful. [Email: ${email}]`);
|
||||
return { message: 'Email verification was successful' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
* @param {MongoUser} user <email, password, name, username>
|
||||
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
||||
*/
|
||||
const registerUser = async (user) => {
|
||||
const { error } = registerSchema.safeParse(user);
|
||||
|
|
@ -73,13 +131,13 @@ const registerUser = async (user) => {
|
|||
{ name: 'Validation error:', value: errorMessage },
|
||||
);
|
||||
|
||||
return { status: 422, message: errorMessage };
|
||||
return { status: 404, message: errorMessage };
|
||||
}
|
||||
|
||||
const { email, password, name, username } = user;
|
||||
|
||||
try {
|
||||
const existingUser = await User.findOne({ email }).lean();
|
||||
const existingUser = await findUser({ email }, 'email _id');
|
||||
|
||||
if (existingUser) {
|
||||
logger.info(
|
||||
|
|
@ -90,38 +148,46 @@ const registerUser = async (user) => {
|
|||
|
||||
// Sleep for 1 second
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// TODO: We should change the process to always email and be generic is signup works or fails (user enum)
|
||||
return { status: 500, message: 'Something went wrong' };
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
}
|
||||
|
||||
if (!(await isDomainAllowed(email))) {
|
||||
const errorMessage = 'Registration from this domain is not allowed.';
|
||||
const errorMessage =
|
||||
'The email address provided cannot be used. Please use a different email address.';
|
||||
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
||||
return { status: 403, message: errorMessage };
|
||||
}
|
||||
|
||||
//determine if this is the first registered user (not counting anonymous_user)
|
||||
const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
|
||||
const isFirstRegisteredUser = (await countUsers()) === 0;
|
||||
|
||||
const newUser = await new User({
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const newUserData = {
|
||||
provider: 'local',
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
name,
|
||||
avatar: null,
|
||||
role: isFirstRegisteredUser ? 'ADMIN' : 'USER',
|
||||
});
|
||||
password: bcrypt.hashSync(password, salt),
|
||||
};
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const hash = bcrypt.hashSync(newUser.password, salt);
|
||||
newUser.password = hash;
|
||||
await newUser.save();
|
||||
const emailEnabled = checkEmailConfig();
|
||||
const newUserId = await createUser(newUserData, false);
|
||||
if (emailEnabled) {
|
||||
await sendVerificationEmail({
|
||||
_id: newUserId,
|
||||
email,
|
||||
name,
|
||||
});
|
||||
} else {
|
||||
await updateUser(newUserId, { emailVerified: true });
|
||||
}
|
||||
|
||||
return { status: 200, user: newUser };
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
} catch (err) {
|
||||
return { status: 500, message: err?.message || 'Something went wrong' };
|
||||
logger.error('[registerUser] Error in registering user:', err);
|
||||
return { status: 500, message: 'Something went wrong' };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -131,7 +197,7 @@ const registerUser = async (user) => {
|
|||
*/
|
||||
const requestPasswordReset = async (req) => {
|
||||
const { email } = req.body;
|
||||
const user = await User.findOne({ email }).lean();
|
||||
const user = await findUser({ email }, 'email _id');
|
||||
const emailEnabled = checkEmailConfig();
|
||||
|
||||
logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`);
|
||||
|
|
@ -208,13 +274,9 @@ const resetPassword = async (userId, token, password) => {
|
|||
}
|
||||
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const user = await updateUser(userId, { password: hash });
|
||||
|
||||
await User.updateOne({ _id: userId }, { $set: { password: hash } }, { new: true });
|
||||
|
||||
const user = await User.findById({ _id: userId });
|
||||
const emailEnabled = checkEmailConfig();
|
||||
|
||||
if (emailEnabled) {
|
||||
if (checkEmailConfig()) {
|
||||
sendEmail(
|
||||
user.email,
|
||||
'Password Reset Successfully',
|
||||
|
|
@ -242,8 +304,8 @@ const resetPassword = async (userId, token, password) => {
|
|||
*/
|
||||
const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||
try {
|
||||
const user = await User.findOne({ _id: userId });
|
||||
const token = await user.generateToken();
|
||||
const user = await getUserById(userId);
|
||||
const token = await generateToken(user);
|
||||
|
||||
let session;
|
||||
let refreshTokenExpires;
|
||||
|
|
@ -273,11 +335,69 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resend Verification Email
|
||||
* @param {Object} req
|
||||
* @param {Object} req.body
|
||||
* @param {String} req.body.email
|
||||
* @returns {Promise<{status: number, message: string}>}
|
||||
*/
|
||||
const resendVerificationEmail = async (req) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
await Token.deleteMany({ email });
|
||||
const user = await findUser({ email }, 'email _id name');
|
||||
|
||||
if (!user) {
|
||||
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
}
|
||||
|
||||
let verifyToken = crypto.randomBytes(32).toString('hex');
|
||||
const hash = bcrypt.hashSync(verifyToken, 10);
|
||||
|
||||
await new Token({
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
token: hash,
|
||||
createdAt: Date.now(),
|
||||
}).save();
|
||||
|
||||
const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${user.email}`;
|
||||
logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
||||
|
||||
sendEmail(
|
||||
user.email,
|
||||
'Verify your email',
|
||||
{
|
||||
appName: process.env.APP_TITLE || 'LibreChat',
|
||||
name: user.name,
|
||||
verificationLink: verificationLink,
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
'verifyEmail.handlebars',
|
||||
);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
message: genericVerificationMessage,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`[resendVerificationEmail] Error resending verification email: ${error.message}`);
|
||||
return {
|
||||
status: 500,
|
||||
message: 'Something went wrong.',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
registerUser,
|
||||
logoutUser,
|
||||
verifyEmail,
|
||||
registerUser,
|
||||
setAuthTokens,
|
||||
resetPassword,
|
||||
isDomainAllowed,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
setAuthTokens,
|
||||
resendVerificationEmail,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { ErrorTypes } = require('librechat-data-provider');
|
||||
const { encrypt, decrypt } = require('~/server/utils');
|
||||
const { User, Key } = require('~/models');
|
||||
const { updateUser, Key } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
|
|
@ -16,16 +16,13 @@ const { logger } = require('~/config');
|
|||
*/
|
||||
const updateUserPluginsService = async (user, pluginKey, action) => {
|
||||
try {
|
||||
const userPlugins = user.plugins || [];
|
||||
if (action === 'install') {
|
||||
return await User.updateOne(
|
||||
{ _id: user._id },
|
||||
{ $set: { plugins: [...user.plugins, pluginKey] } },
|
||||
);
|
||||
return await updateUser(user._id, { plugins: [...userPlugins, pluginKey] });
|
||||
} else if (action === 'uninstall') {
|
||||
return await User.updateOne(
|
||||
{ _id: user._id },
|
||||
{ $set: { plugins: user.plugins.filter((plugin) => plugin !== pluginKey) } },
|
||||
);
|
||||
return await updateUser(user._id, {
|
||||
plugins: userPlugins.filter((plugin) => plugin !== pluginKey),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('[updateUserPluginsService]', err);
|
||||
|
|
@ -166,30 +163,12 @@ const checkUserKeyExpiry = (expiresAt, endpoint) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a user document from the database based on the provided email.
|
||||
* @async
|
||||
* @param {string} email - The email of the user to find.
|
||||
* @returns {Promise<Object|null>} The user document if found, otherwise null.
|
||||
* @throws {Error} Throws an error if there is a problem during user retrieval.
|
||||
*/
|
||||
const findUserByEmail = async (email) => {
|
||||
try {
|
||||
const user = await User.findOne({ email });
|
||||
return user;
|
||||
} catch (error) {
|
||||
logger.error(`[findUserByEmail] Error occurred while finding user by email: ${email}`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
updateUserPluginsService,
|
||||
getUserKey,
|
||||
getUserKeyValues,
|
||||
getUserKeyExpiry,
|
||||
updateUserKey,
|
||||
deleteUserKey,
|
||||
getUserKeyValues,
|
||||
getUserKeyExpiry,
|
||||
checkUserKeyExpiry,
|
||||
findUserByEmail,
|
||||
updateUserPluginsService,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const {
|
|||
deprecatedAzureVariables,
|
||||
conflictingAzureVariables,
|
||||
} = require('librechat-data-provider');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { isEnabled, checkEmailConfig } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const secretDefaults = {
|
||||
|
|
@ -111,12 +111,7 @@ Latest version: ${Constants.CONFIG_VERSION}
|
|||
}
|
||||
|
||||
function checkPasswordReset() {
|
||||
const emailEnabled =
|
||||
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
|
||||
!!process.env.EMAIL_USERNAME &&
|
||||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM;
|
||||
|
||||
const emailEnabled = checkEmailConfig();
|
||||
const passwordResetAllowed = isEnabled(process.env.ALLOW_PASSWORD_RESET);
|
||||
|
||||
if (!emailEnabled && passwordResetAllowed) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<!DOCTYPE HTML
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<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]>
|
||||
<head>
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
|
|
@ -11,176 +13,184 @@
|
|||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<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; } .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; }
|
||||
</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 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">
|
||||
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
|
||||
<div>Hi {{name}},</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>
|
||||
<div>Your password has been updated successfully! </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>
|
||||
<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
|
||||
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'
|
||||
>
|
||||
<div
|
||||
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
|
||||
>
|
||||
<div>Hi {{name}},</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>
|
||||
<div>Your password has been updated successfully! </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,9 +1,11 @@
|
|||
<!DOCTYPE HTML
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<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]>
|
||||
<head>
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
|
|
@ -11,229 +13,272 @@
|
|||
</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>
|
||||
<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 requested to reset your password.
|
||||
</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 {{name}},</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%;">Please click the button below to reset your password.</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="{{link}}" style="height:37px; v-text-anchor:middle; width:142px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
|
||||
<a href="{{link}}" 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;">Reset
|
||||
Password</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>If you did not request a password reset, please ignore this email.</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>
|
||||
<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 requested to reset your password.
|
||||
</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 {{name}},</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%;'>Please click the button below to
|
||||
reset your password.</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="{{link}}" style="height:37px; v-text-anchor:middle; width:142px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
|
||||
<a
|
||||
href='{{link}}'
|
||||
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;'>Reset Password</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>If you did not request a password reset, please ignore this
|
||||
email.</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,9 +1,11 @@
|
|||
<!DOCTYPE HTML
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<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]>
|
||||
<head>
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
|
|
@ -11,229 +13,278 @@
|
|||
</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;
|
||||
}
|
||||
|
||||
.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>
|
||||
<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; } .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>Welcome to {{appName}}!</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>
|
||||
<div>Dear {{name}},</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>
|
||||
<div>Thank you for registering with {{appName}}. To complete your registration and verify your email address, please click the button below:</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">
|
||||
<!--[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="href="{{verificationLink}}"" style="height:37px; v-text-anchor:middle; width:114px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
|
||||
<a href="href="{{verificationLink}}"" 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%;">
|
||||
<div>
|
||||
<div>Verify Email</div>
|
||||
</div>
|
||||
</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>If you did not create an account with {{appName}}, please ignore this email.</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>
|
||||
<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>Welcome to {{appName}}!</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>
|
||||
<div>Dear {{name}},</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>
|
||||
<div>Thank you for registering with
|
||||
{{appName}}. To complete your registration and verify your
|
||||
email address, please click the button below:</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'
|
||||
>
|
||||
<!--[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="href="{{verificationLink}}"" style="height:37px; v-text-anchor:middle; width:114px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
|
||||
<a
|
||||
href='{{verificationLink}}'
|
||||
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%;'>
|
||||
<div>
|
||||
<div>Verify Email</div>
|
||||
</div>
|
||||
</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>If you did not create an account with
|
||||
{{appName}}, please ignore this email.</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>
|
||||
|
|
@ -135,7 +135,7 @@ async function importLibreChatConvo(
|
|||
});
|
||||
}
|
||||
|
||||
if (!firstMessageDate) {
|
||||
if (!firstMessageDate && message.createdAt) {
|
||||
firstMessageDate = new Date(message.createdAt);
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ async function importLibreChatConvo(
|
|||
const idMapping = new Map();
|
||||
|
||||
for (const message of messagesToImport) {
|
||||
if (!firstMessageDate) {
|
||||
if (!firstMessageDate && message.createdAt) {
|
||||
firstMessageDate = new Date(message.createdAt);
|
||||
}
|
||||
const newMessageId = uuidv4();
|
||||
|
|
@ -171,6 +171,10 @@ async function importLibreChatConvo(
|
|||
throw new Error('Invalid LibreChat file format');
|
||||
}
|
||||
|
||||
if (firstMessageDate === 'Invalid Date') {
|
||||
firstMessageDate = null;
|
||||
}
|
||||
|
||||
importBatchBuilder.finishConversation(jsonData.title, firstMessageDate ?? new Date(), options);
|
||||
await importBatchBuilder.saveBatch();
|
||||
logger.debug(`user: ${requestUserId} | Conversation "${jsonData.title}" imported`);
|
||||
|
|
|
|||
|
|
@ -2,15 +2,29 @@ const streamResponse = require('./streamResponse');
|
|||
const removePorts = require('./removePorts');
|
||||
const countTokens = require('./countTokens');
|
||||
const handleText = require('./handleText');
|
||||
const cryptoUtils = require('./crypto');
|
||||
const citations = require('./citations');
|
||||
const sendEmail = require('./sendEmail');
|
||||
const cryptoUtils = require('./crypto');
|
||||
const queue = require('./queue');
|
||||
const files = require('./files');
|
||||
const math = require('./math');
|
||||
|
||||
/**
|
||||
* Check if email configuration is set
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function checkEmailConfig() {
|
||||
return (
|
||||
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
|
||||
!!process.env.EMAIL_USERNAME &&
|
||||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
...streamResponse,
|
||||
checkEmailConfig,
|
||||
...cryptoUtils,
|
||||
...handleText,
|
||||
...citations,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue