/**
## /user/register
Exports the [user] register method for the /api/user/register route.
@requires /view
@requires /user/acl
@requires /user/login
@requires /utils/reqHost
@requires /utils/mailer
@requires /utils/languageTemplates
@requires /utils/bcrypt
@requires crypto
@requires module:/utils/processEnv
@module /user/register
*/
import crypto from 'crypto';
import bcrypt from '../utils/bcrypt.cjs';
import languageTemplates from '../utils/languageTemplates.js';
import mailer from '../utils/mailer.js';
import reqHost from '../utils/reqHost.js';
import view from '../view.js';
import acl from './acl.js';
/**
@function register
@description
Returns the user regestration or password reset form depending on the reset request parameter flag.
Returns the `registerUserBody` method with a request [user] body present.
@param {req} req HTTP request.
@param {res} res HTTP response.
@property {Object} req.params HTTP request parameter.
@property {Object} [req.body]
Post body object with user data.
*/
export default async function register(req, res) {
// acl module will export an empty require object without the ACL being configured.
if (acl === null) {
return res.status(500).send('ACL unavailable.');
}
req.params.host = reqHost(req);
// Register request [post] body.
if (req.body) return registerUserBody(req, res);
// The login view will set the cookie to null.
res.setHeader(
'Set-Cookie',
`${xyzEnv.TITLE}=null;HttpOnly;Max-Age=0;Path=${xyzEnv.DIR || '/'}`,
);
req.params.template = req.params.reset
? 'password_reset_view'
: 'register_view';
// Get request for registration form view.
view(req, res);
}
/**
@function registerUserBody
@description
Will attempt to register the user object provided as request body as a new user. The request body JSON object must contain a user email, and password as string entries.
An email with a verification token will be sent to verify a newly registered account.
The `host=string` request parameter is required for the link passed to the user verification mail template.
@param {req} req HTTP request.
@param {res} res HTTP response.
@property {Object} req.params HTTP request parameter.
@property {Object} req.body
Post body object with user data.
*/
async function registerUserBody(req, res) {
checkUserBody(req, res);
if (res.finished) return;
// The password will be reset for exisiting user accounts.
await passwordReset(req, res);
if (res.finished) return;
// Get the date for logs.
const date = new Date().toISOString().replace(/\..*/, '');
const expiry_date = parseInt(
(new Date().getTime() + xyzEnv.APPROVAL_EXPIRY * 1000 * 60 * 60 * 24) /
1000,
);
const USER = {
access_log: [`${date}@${req.ips?.pop() || req.ip}`],
email: req.body.email,
language: req.body.language,
password: req.body.password,
password_reset: req.body.password,
verificationtoken: req.body.verificationtoken,
};
if (xyzEnv.APPROVAL_EXPIRY) {
USER['expires_on'] = expiry_date;
}
// Create new user account
const rows = await acl(
`
INSERT INTO acl_schema.acl_table (${Object.keys(USER).join(',')})
VALUES (${Object.keys(USER).map((NULL, i) => `\$${i + 1}`)})`,
Object.values(USER),
);
if (rows instanceof Error) {
return res.status(500).send('Failed to access ACL.');
}
await mailer({
host: req.params.host,
language: req.body.language,
link: `${req.params.host}/api/user/verify/${req.body.verificationtoken}`,
remote_address: req.params.remote_address,
template: 'verify_account',
to: req.body.email,
});
// Return msg. No redirect for password reset.
res.send(
await languageTemplates({
language: req.body.language,
template: 'new_account_registered',
}),
);
}
/**
@function checkUserBody
@description
The request body JSON object must contain a user email, and password as string entries.
The email address is tested against following regex: `/^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$/`.
The ACL can be restricted for email addresses provided as `xyzEnv.USER_DOMAINS`.
A valid password must be provided. Password rules can be defined as `xyzEnv.PASSWORD_REGEXP`. The default rule for password being `'(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])^.{12,}$'`.
The `req.body.password` will be hashed with bcrypt.
A `req.body.verificationtoken` will be created with crypto.
The `req.body.language` will be checked against Intl.Collator.supportedLocalesOf.
@param {req} req HTTP request.
@param {res} res HTTP response.
@property {Object} req.params HTTP request parameter.
@property {Object} req.body
Post body object with user data.
*/
function checkUserBody(req, res) {
if (!req.body.email) return res.status(400).send('No email provided');
// Test email address
if (!/^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$/.test(req.body.email)) {
return res.status(400).send('Provided email address is invalid');
}
// Test whether email domain is allowed to register
if (xyzEnv.USER_DOMAINS) {
// Get array of allowed user email domains from split xyzEnvironment variable.
const allowed_domains = xyzEnv.USER_DOMAINS.split(',');
// Get the user_domain from user email.
const user_domain = req.body.email.split('@')[1];
// Check whether not some of the allowed_domain includes the user_domain.
if (!allowed_domains.some((domain) => user_domain.includes(domain))) {
return res.status(400).send('Provided email address is invalid');
}
}
// Test whether a password has been provided.
if (!req.body.password) return res.status(400).send('No password provided');
// Create regex to text password complexity from xyzEnv or set default.
const passwordRgx = new RegExp(
xyzEnv.PASSWORD_REGEXP || '(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])^.{12,}$',
);
// Test whether the provided password is valid.
if (!passwordRgx.test(req.body.password)) {
res.status(403).send('Invalid password provided');
return;
}
// Hash the password.
req.body.password = bcrypt.hashSync(req.body.password, 8);
// Create random verification token.
req.body.verificationtoken = crypto.randomBytes(20).toString('hex');
// Lookup the provided language key.
req.body.language =
Intl.Collator.supportedLocalesOf([req.body.language], {
localeMatcher: 'lookup',
})[0] || 'en';
}
/**
@function passwordReset
@description
The passwordReset method checks whether a user record exists for the email provided in the request body.
An email with a verification token will be sent to verify the password reset.
@param {req} req HTTP request.
@param {res} res HTTP response.
@property {Object} req.params HTTP request parameter.
@property {Object} req.body
Post body object with user data.
*/
async function passwordReset(req, res) {
// Attempt to retrieve ACL record with matching email field.
let rows = await acl(
`
SELECT email, password, password_reset, language, blocked
FROM acl_schema.acl_table
WHERE lower(email) = lower($1);`,
[req.body.email],
);
if (rows instanceof Error) {
return res.status(500).send('Failed to access ACL.');
}
const user = rows[0];
// Register new user.
if (!user) return;
// Setting the password to NULL will disable access to the account and prevent resetting the password.
if (user?.password === null) {
res.status(401).send('User account has restricted access');
return;
}
// Blocked user may not reset their password.
if (user.blocked) {
res.status(403).send(
await languageTemplates({
language: req.body.language,
template: 'user_blocked',
}),
);
return;
}
// Get the date for logs.
const date = new Date().toISOString().replace(/\..*/, '');
const expiry_date = parseInt(
(new Date().getTime() + xyzEnv.APPROVAL_EXPIRY * 1000 * 60 * 60 * 24) /
1000,
);
const VALUES = [
req.body.email,
req.body.password,
req.body.verificationtoken,
`${date}@${req.params.remote_address}`,
];
if (xyzEnv.APPROVAL_EXPIRY && user.expires_on) {
VALUES.push(expiry_date);
}
// Set new password and verification token.
// New passwords will only apply after account verification.
rows = await acl(
`
UPDATE acl_schema.acl_table
SET
password_reset = $2,
verificationtoken = $3,
access_log = array_append(access_log, $4)
${xyzEnv.APPROVAL_EXPIRY && user.expires_on ? ',expires_on = $5' : ''}
WHERE lower(email) = lower($1);`,
VALUES,
);
if (rows instanceof Error) {
return res.status(500).send('Failed to access ACL.');
}
// Sent mail with verification token to the account email address.
await mailer({
host: req.params.host,
language: req.body.language,
link: `${req.params.host}/api/user/verify/${req.body.verificationtoken}/?language=${req.body.language}`,
remote_address: req.params.remote_address,
template: 'verify_password_reset',
to: user.email,
});
const password_reset_verification = await languageTemplates({
language: req.body.language,
template: 'password_reset_verification',
});
res.send(password_reset_verification);
}