579 lines
20 KiB
JavaScript
579 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
const User = require('../model/user'),
|
|
UserModelFactory = require('../model/user_model_factory'),
|
|
App = require('../model/application'),
|
|
cache = require('../helpers/mem_cache'),
|
|
assert = require('assert'),
|
|
{ verifyAsync, sign } = require('../helpers/jwt_async.js'),
|
|
moment = require('moment'),
|
|
ObjectId = require('mongodb').ObjectId,
|
|
{ Errors, UserTypes, DEFAULT_LANG, TrialTypes, emailRegex } = require('../helpers/constants'),
|
|
{ AppError, AppParamError, AppAuthError, AppInputError } = require('../helpers/app_error'),
|
|
{ ensureSingleBillingAddress } = require('../helpers/user_helper'),
|
|
utils = require('../helpers/utils.js'),
|
|
mailer = require('../helpers/mailer'),
|
|
env = require('../helpers/env'),
|
|
subscriptionCtl = require("./subscription"),
|
|
mongoUtil = require('../helpers/mongo'),
|
|
debug = require('debug')('agm:user'),
|
|
Country = require('../model/country');
|
|
|
|
async function ensureParentExists(user) {
|
|
if (!user || user.kind <= 1 || !user.parent || !ObjectId.isValid(user.parent)) return;
|
|
|
|
const pUser = await User.findById(ObjectId(user.parent), null, { lean: true });
|
|
if (!pUser) AppError.throw(Errors.PARENT_NOT_EXIST);
|
|
|
|
return pUser;
|
|
}
|
|
|
|
async function clearTempData(uid) {
|
|
if (ObjectId.isValid(uid))
|
|
await App.deleteMany({ byUser: ObjectId(uid), $or: [{ status: -1 }, { status: 0 }] });
|
|
}
|
|
|
|
async function createUser_post(req, res) {
|
|
const _user = req.body;
|
|
assert(_user, AppParamError.create());
|
|
|
|
delete _user._id;
|
|
|
|
await ensureParentExists(_user);
|
|
|
|
const newUser = new User(_user);
|
|
const user = await newUser.save();
|
|
res.json(user);
|
|
}
|
|
|
|
async function getUser_get(req, res) {
|
|
const withAddresses = utils.stringToBoolean(req.query.withAddresses) || false;
|
|
if (!ObjectId.isValid(req.params.userId)) AppParamError.throw(Errors.INVALID_PARAM);
|
|
|
|
let query = User.findById(req.params.userId, null, { lean: true })
|
|
.populate({ path: 'Country', select: 'code name -_id' });
|
|
|
|
// Only populate addresses if includeAddresses is true
|
|
if (!withAddresses) {
|
|
// If not populating addresses, exclude them from the result
|
|
query = query.select('-addresses');
|
|
}
|
|
|
|
const _user = await query;
|
|
res.json(_user);
|
|
}
|
|
|
|
async function updateUser_put(req, res) {
|
|
const _user = req.body, userId = req.params.userId;
|
|
|
|
if (!_user || !ObjectId.isValid(userId)) AppParamError.throw();
|
|
delete req.body._id;
|
|
|
|
// Handle cases where addresses contain country objects and billing address logic
|
|
if (_user.addresses && Array.isArray(_user.addresses)) {
|
|
_user.addresses = _user.addresses.map(address => {
|
|
if (address.country && typeof address.country === 'object' && address.country.code) {
|
|
return { ...address, country: address.country.code };
|
|
}
|
|
return address;
|
|
});
|
|
|
|
// Ensure only one billing address exists using reusable utility
|
|
_user.addresses = ensureSingleBillingAddress(_user.addresses);
|
|
}
|
|
|
|
let kind = _user.kind;
|
|
if (!kind) {
|
|
const theUser = await User.findById(userId, 'kind', { lean: true });
|
|
if (!theUser) AppError.throw(Errors.USER_NOT_FOUND);
|
|
|
|
kind = theUser.kind;
|
|
}
|
|
|
|
const UserModel = UserModelFactory.create(kind);
|
|
const user = await UserModel.findOneAndUpdate({ _id: ObjectId(userId) }, _user, { new: true, lean: true });
|
|
res.json(user);
|
|
}
|
|
|
|
async function deleteUser(req, res) {
|
|
const _id = req?.params?.userId;
|
|
if (!_id || !ObjectId.isValid(_id)) AppParamError.throw();
|
|
|
|
const user = await User.findByIdAndRemove({ _id: ObjectId(_id) });
|
|
if (user) await user.remove();
|
|
|
|
cache.delete(_id);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
async function login_post(req, res) {
|
|
const username = req.body?.username?.trim();
|
|
const password = req.body?.password;
|
|
if (!username || !password) AppAuthError.throw(Errors.WRONG_CREDENTIAL);
|
|
|
|
const _options = { password, ...mongoUtil.getTextFieldFilter('username', username) };
|
|
const _user = await User.findOne(_options, null, { lean: true });
|
|
|
|
if (!_user) AppAuthError.throw(Errors.WRONG_CREDENTIAL);
|
|
|
|
if (_user.kind == UserTypes.DEVICE && (req.body.dev && req.body.dev == 'web')) AppAuthError.throw(Errors.INVALID_ACCOUNT);
|
|
|
|
const hasParent = Boolean(_user && _user.parent && _user.kind >= 2);
|
|
// The applicator user
|
|
let _parentUser;
|
|
if (hasParent) {
|
|
_parentUser = await User.findOne({ _id: _user.parent }, null, { lean: true });
|
|
} else if (_user.kind === UserTypes.ADMIN || _user.kind === UserTypes.APP) {
|
|
_parentUser = _user;
|
|
}
|
|
|
|
if (hasParent && !_parentUser) AppAuthError.throw(Errors.WRONG_CREDENTIAL);
|
|
|
|
await clearTempData(_user._id);
|
|
|
|
const isActive = hasParent ? (_parentUser.active && _user.active) : _user.active;
|
|
let authUser;
|
|
if (isActive) {
|
|
const premium = _parentUser.premium || 0; // To be removed soon
|
|
const token = sign({ uid: _user._id, ut: _user.kind }, env.TOKEN_SECRET, {});
|
|
// const memberInfo = {};
|
|
const memberInfo = _user.kind !== UserTypes.ADMIN ? await subscriptionCtl.resolvePaymentUser(_parentUser) : {};
|
|
|
|
let userLang = _user.lang; // #1518, Make sure only appy the language not it has not ever selected.
|
|
if (!userLang) {
|
|
const inUserLang = req.body.lang && req.body.lang.toLowerCase();
|
|
userLang = inUserLang && req.body.lang && ['en', 'pt', 'es'].includes(inUserLang) ? inUserLang : DEFAULT_LANG;
|
|
}
|
|
|
|
if (_user.kind === UserTypes.DEVICE) {
|
|
authUser = { token: token };
|
|
} else {
|
|
authUser = {
|
|
_id: _user._id, token: token, roles: [_user.kind], pui: _parentUser ? _parentUser._id : _user._id, lang: userLang, billable: _parentUser.billable, pre: premium, membership: memberInfo, contact: _user.contact
|
|
};
|
|
}
|
|
|
|
// Update user last login info
|
|
await User.updateOne({ _id: _user._id }, { loggedInAt: Date.now(), lang: userLang }, { timestamps: false });
|
|
|
|
// Set the authenticated user info into the session cache
|
|
cache.set(_user._id.toHexString(), {
|
|
puid: _parentUser._id.toHexString(), billable: _parentUser.billable, premium: premium, membership: memberInfo, lang: userLang || DEFAULT_LANG, ts: moment.utc().unix(),
|
|
kind: _user.kind
|
|
});
|
|
} else {
|
|
AppAuthError.throw(Errors.ACC_INACTIVE);
|
|
}
|
|
|
|
res.json(authUser);
|
|
}
|
|
|
|
async function isUserNameExists_post(req, res) {
|
|
if (!req.body || !req.body.username) AppParamError.throw();
|
|
|
|
const user = await User.findOne(mongoUtil.getTextFieldFilter('username', req.body.username), '_id', { lean: true });
|
|
res.json(user ? 1 : 0);
|
|
}
|
|
|
|
async function search_post(req, res) {
|
|
const _options = { markedDelete: { $ne: true } };
|
|
const { byPuid, kind } = req.body;
|
|
if (byPuid && ObjectId.isValid(byPuid)) {
|
|
_options.parent = ObjectId(byPuid);
|
|
} else {
|
|
AppParamError.throw();
|
|
}
|
|
|
|
if (kind)
|
|
_options.kind = kind;
|
|
else
|
|
_options.kind = { $in: [UserTypes.APP_ADM, UserTypes.OFFICER, UserTypes.INSPECTOR] };
|
|
|
|
const users = await User.find(_options, '-password').lean();
|
|
res.json(users || []);
|
|
}
|
|
|
|
async function clearTempData_post(req, res) {
|
|
await clearTempData(req.uid);
|
|
|
|
if (cache.get(req.uid))
|
|
cache.delete(req.uid);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
async function setUserLanguage_post(req, res) {
|
|
const lang = req.body.lang || DEFAULT_LANG;
|
|
|
|
const user = await User.findByIdAndUpdate(ObjectId(req.uid), { $set: { lang: String(lang).trim() } }, { new: true, lean: true });
|
|
|
|
// Update user language in the cache
|
|
const cUser = cache.get(req.uid);
|
|
if (cUser && cUser.lang && cUser.lang != user.lang) {
|
|
cUser.lang = user.lang;
|
|
cache.set(req.uid, cUser);
|
|
}
|
|
res.json(user);
|
|
}
|
|
|
|
async function getUserDetail_post(req, res) {
|
|
const username = req.body?.username;
|
|
if (!req.body || !username) return res.json(null);
|
|
|
|
const user = await User.findOne(mongoUtil.getTextFieldFilter('username', username), { password: 0 })
|
|
.populate('parent', 'username')
|
|
.populate({ path: 'Country', select: 'code name -_id' })
|
|
.lean();
|
|
|
|
res.json(user);
|
|
}
|
|
|
|
function getHostUrlFromReq(req) {
|
|
let host = `https://${req.hostname}`;
|
|
if (!req.app.isProd) host += ':4200';
|
|
return host;
|
|
}
|
|
|
|
async function mailPwdReset_post(req, res) {
|
|
const userEmail = req.body?.email?.trim();
|
|
if (!userEmail || emailRegex.test(userEmail) === false) AppParamError.throw(Errors.INVALID_EMAIL);
|
|
|
|
const user = await User.findOne({ ...mongoUtil.getTextFieldFilter('username', userEmail) }).lean();
|
|
|
|
try {
|
|
if (!user) AppError.throw(Errors.USER_NOT_FOUND);
|
|
if (user.active === false || user.markedDelete === true) AppError.throw(Errors.ACC_INACTIVE);
|
|
} catch (error) {
|
|
// Only throw these errors in development mode for debugging but not in production to avoid exposing sensitive information
|
|
if (!env.PRODUCTION) throw error;
|
|
else
|
|
return res.json(null);
|
|
|
|
// return res.status(500).json(null); // Testing only
|
|
}
|
|
|
|
let expireHrs = env.PWD_RESET_VALID_HRS;
|
|
if (!expireHrs.endsWith('h')) expireHrs += 'h';
|
|
|
|
const resetEmailResult = await mailer.sendResetPasswordEmail(
|
|
{
|
|
locale: user?.lang || DEFAULT_LANG,
|
|
baseUrl: getHostUrlFromReq(req),
|
|
expireHrs: expireHrs.replace('h', ''),
|
|
id: user?._id, name: user?.contact || user?.username,
|
|
token: createUserToken(user, expireHrs)
|
|
},
|
|
userEmail
|
|
);
|
|
|
|
// debug(`Password reset email sending result: ${JSON.stringify(resetEmailResult)}`);
|
|
if (!resetEmailResult || !resetEmailResult.success) {
|
|
// In production, we might want to be less specific about the error
|
|
if (!env.PRODUCTION) {
|
|
AppParamError.throw(Errors.INVALID_EMAIL);
|
|
}
|
|
}
|
|
|
|
// Even on failure in production, return success to avoid user enumeration
|
|
res.json({ result: 1 });
|
|
}
|
|
|
|
function createUserToken(user, expiresIn, secret) {
|
|
// Only require username, make id and password optional
|
|
assert(user && user.username, AppInputError.create(Errors.INVALID_ACCOUNT));
|
|
|
|
const payload = { email: user.username };
|
|
// Add id to payload if it exists
|
|
user._id && (payload.id = user._id);
|
|
user.createdAt && (payload.createdAt = user.createdAt);
|
|
user.verified && (payload.verified = user.verified);
|
|
|
|
// If a specific secret was provided, use it
|
|
// Otherwise, try to create a user-specific secret if password and createdAt exist
|
|
// If not, fall back to the application's TOKEN_SECRET
|
|
let _secret = secret;
|
|
if (!_secret && user.password && user.createdAt) {
|
|
_secret = `${user.password}-${user.createdAt.getTime()}`;
|
|
}
|
|
_secret = _secret || env.TOKEN_SECRET;
|
|
|
|
const token = sign(payload, _secret, { expiresIn: expiresIn || '3h' });
|
|
return token;
|
|
}
|
|
|
|
async function validateToken(token, secret) {
|
|
try {
|
|
if (!token) AppParamError.throw(Errors.INVALID_TOKEN);
|
|
|
|
// Define the secret key (ensure it matches the one used to sign the token)
|
|
const _secret = secret || env.TOKEN_SECRET;
|
|
// Verify the token
|
|
const payload = await verifyAsync(token, _secret);
|
|
// Ensure the token has at least an email
|
|
if (!payload || !payload.email) AppParamError.throw(Errors.INVALID_TOKEN);
|
|
|
|
return payload;
|
|
} catch (error) {
|
|
// debug('Token validation error:', token, error.message);
|
|
// Handle token validation errors
|
|
if (error.name === 'TokenExpiredError') {
|
|
AppParamError.throw(Errors.TOKEN_EXPIRED);
|
|
} else if (error.name === 'JsonWebTokenError') {
|
|
AppParamError.throw(Errors.INVALID_TOKEN);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function validateResetPwdToken_post(req, res) {
|
|
const { id, token } = req.body;
|
|
if (!req || !id || !token) AppParamError.throw(Errors.INVALID_TOKEN);
|
|
|
|
const user = await User.findOne({ _id: ObjectId(id) }).lean();
|
|
if (!user) AppError.throw(Errors.INVALID_TOKEN);
|
|
|
|
const secret = `${user.password}-${user.createdAt.getTime()}`;
|
|
const payload = await validateToken(token, secret);
|
|
if (!payload || !payload.id || payload.id.toString() !== id.toString()) {
|
|
AppParamError.throw(Errors.INVALID_TOKEN);
|
|
}
|
|
|
|
res.json({ id: payload.id, token });
|
|
}
|
|
|
|
async function resetPassword_post(req, res) {
|
|
const { id, password, token } = req.body;
|
|
if (!id || !password || !token) AppParamError.throw();
|
|
|
|
const user = await User.findOne({ _id: ObjectId(id) });
|
|
if (!user) AppError.throw(Errors.INVALID_TOKEN);
|
|
|
|
const _user = user;
|
|
const secret = `${user.get('password')}-${user.get('createdAt').getTime()}`;
|
|
const payload = await validateToken(token, secret);
|
|
if (!payload || !payload.id || payload.id.toString() !== id.toString()) {
|
|
AppParamError.throw(Errors.INVALID_TOKEN);
|
|
}
|
|
|
|
_user.password = password;
|
|
|
|
await mongoUtil.runInTransaction(async (session) => {
|
|
await _user.save(session);
|
|
await mailer.sendPasswordResetEmail(
|
|
{
|
|
locale: _user.lang || DEFAULT_LANG,
|
|
baseUrl: getHostUrlFromReq(req), name: _user?.contact || _user?.username, username: _user.username,
|
|
},
|
|
_user?.username
|
|
);
|
|
});
|
|
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
async function signup_post(req, res) {
|
|
const input = req.body;
|
|
if (!input || !input.contactEmail || !emailRegex.test(input.contactEmail) || !input.password || !input.contactName || !input.companyName || !input.country) AppParamError.throw();
|
|
|
|
// Check for the email verification token
|
|
const verifyToken = input.emailToken || input.token;
|
|
if (!verifyToken) AppParamError.throw(Errors.EMAIL_VERIFICATION_REQUIRED);
|
|
|
|
try {
|
|
// Verify the token is valid and contains the verified email
|
|
const payload = await verifyAsync(verifyToken, env.TOKEN_SECRET);
|
|
|
|
// Make sure the token contains the verified flag and matches the email in the form
|
|
if (!payload || !payload.email || !payload.verified || payload.email !== input.contactEmail) {
|
|
AppParamError.throw(Errors.EMAIL_VERIFICATION_REQUIRED);
|
|
}
|
|
} catch (error) {
|
|
env.LOG_ALL_ERRORS && debug('Email verification token error:', error.message);
|
|
AppParamError.throw(Errors.EMAIL_VERIFICATION_REQUIRED);
|
|
}
|
|
|
|
const existingUser = await User.findOne({ ...mongoUtil.getTextFieldFilter('username', input.contactEmail) }).lean();
|
|
if (existingUser) AppParamError.throw(Errors.USER_EXIST);
|
|
|
|
// Verify country code is valid
|
|
const country = await Country.findOne({ code: input.country.toUpperCase() });
|
|
if (!country) AppParamError.throw('Invalid country code');
|
|
|
|
// Create a default address from contact and address fields if no addresses provided
|
|
let addresses = [];
|
|
if (input.addresses && Array.isArray(input.addresses) && input.addresses.length > 0) {
|
|
// Process each address and ensure country codes are uppercase and valid
|
|
addresses = input.addresses.map(addr => {
|
|
if (addr.country) {
|
|
// With our new schema, just ensure the country code is uppercase
|
|
return { ...addr, country: addr.country.toUpperCase() };
|
|
}
|
|
return addr;
|
|
});
|
|
} else {
|
|
// Create a default address entry using available information
|
|
addresses = [{
|
|
name: input?.companyName || '',
|
|
line1: input?.address || '',
|
|
line2: '',
|
|
city: input?.city || '',
|
|
state: input?.state || '',
|
|
postalCode: input?.postalCode || '',
|
|
country: input?.country.toUpperCase(), // Store the country code directly
|
|
phone: input?.contactPhone || '',
|
|
email: input.contactEmail || '',
|
|
isBilling: true
|
|
}];
|
|
}
|
|
|
|
const CustomerModel = UserModelFactory.create(UserTypes.APP);
|
|
const customerData = {
|
|
name: input.companyName,
|
|
address: input.address || '',
|
|
email: input.contactEmail,
|
|
phone: input.contactPhone,
|
|
username: input.contactEmail,
|
|
contact: input.contactName,
|
|
country: input.country.toUpperCase(), // Ensure country code is uppercase
|
|
password: input.password,
|
|
membership: {
|
|
trials: {
|
|
type: TrialTypes.DAYS,
|
|
trialDays: env.NEW_ACC_TRIAL_DAYS,
|
|
startDate: moment.utc().toDate(),
|
|
},
|
|
},
|
|
addresses: addresses,
|
|
appInfo: input.appInfo || {},
|
|
taxId: input.taxId || '',
|
|
active: true,
|
|
lang: input.lang || DEFAULT_LANG,
|
|
selfSignup: true,
|
|
createdAt: moment.utc().toDate(),
|
|
};
|
|
if (typeof input.partner === 'string' && input.partner?.trim().length !== 0) {
|
|
customerData.partner = input.partner.trim();
|
|
}
|
|
const newCustomer = new CustomerModel(customerData);
|
|
|
|
const emailData = {
|
|
lang: input.lang || DEFAULT_LANG,
|
|
name: input.contactName,
|
|
username: input.contactEmail,
|
|
orgName: input.companyName,
|
|
baseUrl: getHostUrlFromReq(req),
|
|
};
|
|
|
|
try {
|
|
// Send the validation email and capture the detailed result
|
|
const mailRes = await mailer.sendWelcomeNewAccEmail(emailData, input.contactEmail);
|
|
|
|
if (!mailRes || !mailRes.success) {
|
|
AppParamError.throw(Errors.INVALID_EMAIL);
|
|
}
|
|
} catch (error) {
|
|
AppParamError.throw(Errors.EMAIL_ERROR);
|
|
}
|
|
|
|
await mongoUtil.runInTransaction(async (session) => {
|
|
await newCustomer.save(session);
|
|
});
|
|
|
|
// Send AGM Admin email about this new user
|
|
try {
|
|
await mailer.sendAdminNotification(
|
|
'[AgMission] New Applicator User Signed up',
|
|
`A new applicator user has signed up:\n\nName: ${input.contactName}\nEmail: ${input.contactEmail}\nCompany: ${input.companyName}\nCountry: ${input.country}`
|
|
);
|
|
} catch (error) {
|
|
// Log but don't fail the transaction if admin notification fails
|
|
debug('Error sending admin notification:', error);
|
|
}
|
|
|
|
res.json({ ok: true, username: input.contactEmail });
|
|
}
|
|
|
|
/**
|
|
* Request email verification. For example: before signup form submission, changing login email, etc.
|
|
* This function sends a verification link (with JWT token) to the provided email address.
|
|
* @param {Object} req - Request with email in body
|
|
* @param {Object} res - Response object
|
|
*/
|
|
async function requestEmailVerification_post(req, res) {
|
|
const email = req.body?.email?.trim();
|
|
const name = req.body?.name?.trim();
|
|
|
|
if (!email || emailRegex.test(email) === false) AppParamError.throw(Errors.INVALID_EMAIL);
|
|
|
|
// Check if the email already exists
|
|
const existingUser = await User.findOne({ ...mongoUtil.getTextFieldFilter('username', email) }).lean();
|
|
if (existingUser) AppParamError.throw(Errors.USER_EXIST);
|
|
|
|
try {
|
|
// Create a token for email verification
|
|
const token = createUserToken({ username: email }, env.EMAIL_VER_VALID_TIME, env.TOKEN_SECRET);
|
|
|
|
// Send the verification email with the token
|
|
const emailResult = await mailer.sendEmailVerificationCode(
|
|
{
|
|
username: email, name: name || email,
|
|
baseUrl: getHostUrlFromReq(req),
|
|
lang: req.body.lang || DEFAULT_LANG,
|
|
token: token
|
|
},
|
|
email
|
|
);
|
|
if (!emailResult || !emailResult.success) {
|
|
AppParamError.throw(Errors.EMAIL_ERROR);
|
|
}
|
|
|
|
res.json({ ok: true, message: 'Verification email sent' });
|
|
} catch (error) {
|
|
env.LOG_ALL_ERRORS && debug('Error sending verification email:', error);
|
|
AppParamError.throw(Errors.EMAIL_ERROR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify email token before allowing signup form submission
|
|
* @param {Object} req - Request with token in body
|
|
* @param {Object} res - Response object
|
|
*/
|
|
async function verifyEmailCode_post(req, res) {
|
|
const token = req.body?.token?.trim();
|
|
if (!token) AppParamError.throw(Errors.INVALID_TOKEN);
|
|
|
|
try {
|
|
// Use the existing validateToken function
|
|
const payload = await validateToken(token, env.TOKEN_SECRET);
|
|
|
|
// Create a temporary user object with verified flag for signup
|
|
const tempUser = {
|
|
username: payload.email,
|
|
createdAt: new Date(),
|
|
verified: true // Additional property to indicate verification
|
|
};
|
|
|
|
// Use createUserToken with a longer expiry time for signup flow
|
|
const signupToken = createUserToken(tempUser, env.NEW_ACC_VALID_TIME, env.TOKEN_SECRET);
|
|
|
|
res.json({ ok: true, email: payload.email, token: signupToken });
|
|
} catch (error) {
|
|
env.LOG_ALL_ERRORS && debug('Email verification token error:', error.message);
|
|
|
|
// If validateToken didn't throw an error but we caught another one
|
|
if (error.name === 'TokenExpiredError') {
|
|
AppParamError.throw(Errors.VERIFICATION_CODE_EXPIRED);
|
|
} else {
|
|
AppParamError.throw(Errors.INVALID_VERIFICATION_CODE);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
isUserNameExists_post, createUser_post, getUser_get, updateUser_put, deleteUser, login_post, search_post, clearTempData_post, setUserLanguage_post,
|
|
getUserDetail_post, mailPwdReset_post, validateResetPwdToken_post, resetPassword_post,
|
|
ensureParentExists, clearTempData, getHostUrlFromReq,
|
|
requestEmailVerification_post, verifyEmailCode_post, signup_post
|
|
}
|