EN-60: Adding simplified Sign Up/Login/Renew/Recover process.

This commit is contained in:
2023-11-09 02:40:16 -06:00
parent 1521ea4b77
commit 26877171ba
11 changed files with 482 additions and 47 deletions

View File

@@ -0,0 +1,14 @@
'use strict';
const router = require('express').Router();
const services= require('./services.js');
router.post('/authorize', services.AuthorizeJWT);
router.get('/authorize/:session_token', services.RenewJWT);
router.post('/signup', services.TryCreateAccount);
router.patch('/signup', services.ConfirmAccount);
router.post('/recover', services.RecoverPwd);
router.patch('/recover', services.ConfirmRecoverPwd);
module.exports = router;

View File

@@ -0,0 +1,263 @@
"use strict";
const jsonwebtoken = require('jsonwebtoken');
const { API_CONFIG, ROOT_PATH, LIB_PATH, HANDLERS_PATH } = process.env;
const apiConfig = require( `${ROOT_PATH}/${API_CONFIG}` );
const { genKey, toSha256 } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` );
const { emailEvent , EMAIL_EVENTS } = require( `${ROOT_PATH}/${HANDLERS_PATH}/MailClient` );
const { create_account, already_exists, login, login_with_session_token, reset_password } = require( `${ROOT_PATH}/${HANDLERS_PATH}/Account` );
const { Validator } = require( "jsonschema" );
const jwtSecret = apiConfig.authentication.jwtSecret;
const jwtTimeout = apiConfig.authentication.jwtTimeout;//Timeout in hours
const jwtRenewalTimeout = apiConfig.authentication.jwtRenewalTimeout;//Timeout in hours
const jwtOptions = apiConfig.authentication.jwtOptions;
const validator = new Validator();
const create_account_schema = {
type : 'object',
properties : {
email : { type : 'string' , maxLength : 256 },
password : { type : 'string', maxLength : 256},
otp : { type : 'string', maxLength : 6 },
company_type : { type : 'string', enum : ["Shipper" , "Carrier"] },
checksum : { type : 'string', maxLength : 32 }
},
required : [ 'email', 'password' ]
};
const confirm_account_schema = {
type : 'object',
properties : create_account_schema.properties,//Same properties
required : [ 'email', 'password', 'otp', 'company_type', 'checksum' ]//Different requirements
};
const login_account_schema = {
type : 'object',
properties : create_account_schema.properties,//Same properties
required : [ 'email', 'password' ]//Different requirements
};
const password_recover_schema = create_account_schema;
const confirm_password_recover_schema = {
type : 'object',
properties : create_account_schema.properties,//Same properties
required : [ 'email', 'password', 'otp', 'checksum' ]//Different requirements
};
const AuthorizeJWT = async(req, res) => {
try{
if( validator.validate( req.body , login_account_schema ).valid ){
const { email, password } = req.body;
const user = await login( email, password );
if( !user ){
return res.status(401).send( { error : "Invalid credentials" } );
}
const current_date = new Date();
const iat = Math.floor( (current_date.getTime())/1000 );
const renewal_exp = ( iat + 3600*jwtRenewalTimeout ) * 1000;
/**
* Renew session token on every login event.
* Previous session token is lost
*/
const session_token = toSha256( `${new Date()}` );
const session_token_exp = new Date( renewal_exp );
user.session_token = session_token;
user.session_token_exp = session_token_exp;
await user.save();
const payload = {
iat: iat,
exp: iat + jwtTimeout * 3600,
aud: jwtOptions.audience,
iss: jwtOptions.audience,
sub: user.id,
};
const jwt = jsonwebtoken.sign( payload , jwtSecret );
return res.status(200).send( {
accessToken : jwt,
payload : payload,
session_token,
session_token_exp,
user : user
} );
}else{
return res.status(400).send( { error : "Invalid request" } );
}
}catch( err ){
console.error( err );
res.status(500).send({ error : "Login: Internal error" });
}
};
const RenewJWT = async(req, res) => {
try{
const login_session_token = req.params.session_token;
const user = await login_with_session_token( login_session_token );
if( !user ){
return res.status(401).send( { error : "Invalid or Expired Session Token" } );
}
const current_date = new Date();
const iat = Math.floor( (current_date.getTime())/1000 );
const renewal_exp = ( iat + 3600*jwtRenewalTimeout ) * 1000;
/**
* Renew session token on every login event.
* Previous session token is lost
*/
const session_token = toSha256( `${new Date()}` );
const session_token_exp = new Date( renewal_exp );
user.session_token = session_token;
user.session_token_exp = session_token_exp;
await user.save();
const payload = {
iat: iat,
exp: iat + jwtTimeout * 3600,
aud: jwtOptions.audience,
iss: jwtOptions.audience,
sub: user.id,
};
const jwt = jsonwebtoken.sign( payload , jwtSecret );
return res.status(200).send( {
accessToken : jwt,
payload : payload,
session_token,
session_token_exp,
user : user
} );
}catch( err ){
console.error( err );
res.status(500).send({ error : "Renew: Internal error" });
}
};
const TryCreateAccount = async(req, res) => {
try{
if( validator.validate( req.body , create_account_schema ).valid ){
const otp = genKey();
const { email : receiver , password } = req.body;
const email = receiver;
const it_exists = await already_exists( email );
if( it_exists ){
return res.status(400).send({ error : "Email already exists" });
}
const content = { OTP : otp, user_name : email };
const checksum_entry = {
email : receiver,
password,
otp
};
const checksum = toSha256( JSON.stringify(checksum_entry)).substr(0, 32);
await emailEvent( EMAIL_EVENTS.ACCOUNT_VERIFY , receiver , content );
console.log(
content
);
res.status(200).send( { checksum } );
}else{
res.status(400).send( { error : "Invalid request" } );
}
}catch( err ){
console.error( err );
res.status(500).send({ error : "Account creation: Internal error" });
}
};
const ConfirmAccount = async(req, res) => {
try{
if( validator.validate( req.body , confirm_account_schema ).valid ){
const { email, password, otp, company_type, checksum } = req.body;
const it_exists = await already_exists( email );
if( it_exists ){
return res.status(400).send({ error : "User already registered!" });
}
const checksum_entry = {email, password, otp};
const recomputed_checksum = toSha256( JSON.stringify(checksum_entry)).substr(0, 32);
if( recomputed_checksum != checksum ){
return res.status(400).send({ error : "Wrong OTP" });
}
await create_account( email, password, company_type );
const content = { user_name : email };
const receiver = email;
// await emailEvent( EMAIL_EVENTS.ACCOUNT_CONFIRMED , receiver , content );
console.log(
content
);
return res.status(200).send( { msg : "User created successfully!" } );
}else{
return res.status(400).send( { error : "Invalid request" } );
}
}catch( err ){
console.error( err );
return res.status(500).send({ error : "Account creation: Internal error" });
}
};
const RecoverPwd = async(req, res) => {
try{
if( validator.validate( req.body , password_recover_schema ).valid ){
const otp = genKey();
const { email : receiver , password } = req.body;
const email = receiver;
const it_exists = await already_exists( email );
if( !it_exists ){
return res.status(400).send({ error : "Email is not registered!" });
}
const content = { OTP : otp, user_name : email };
const checksum_entry = {
email : receiver,
password,
otp
};
const checksum = toSha256( JSON.stringify(checksum_entry)).substr(0, 32);
await emailEvent( EMAIL_EVENTS.ACCOUNT_VERIFY , receiver , content );
console.log(
content
);
res.status(200).send( { checksum } );
}else{
res.status(400).send( { error : "Invalid request" } );
}
}catch( err ){
console.error( err );
res.status(500).send({ error : "Password Recover: Internal error" });
}
};
const ConfirmRecoverPwd = async(req, res) => {
try{
if( validator.validate( req.body , confirm_password_recover_schema ).valid ){
const { email, password, otp, checksum } = req.body;
const it_exists = await already_exists( email );
if( !it_exists ){
return res.status(400).send({ error : "Email is not registered!" });
}
const checksum_entry = {email, password, otp};
const recomputed_checksum = toSha256( JSON.stringify(checksum_entry)).substr(0, 32);
if( recomputed_checksum != checksum ){
return res.status(400).send({ error : "Wrong OTP" });
}
await reset_password( email, password );
return res.status(200).send( { msg : "Password is reset!" } );
}else{
return res.status(400).send( { error : "Invalid request" } );
}
}catch( err ){
console.error( err );
return res.status(500).send({ error : "Password Recover Confirmation: Internal error" });
}
};
module.exports = { AuthorizeJWT, RenewJWT, TryCreateAccount, ConfirmAccount, RecoverPwd, ConfirmRecoverPwd};

View File

@@ -4,6 +4,7 @@ const { ROOT_PATH , LIB_PATH } = process.env;
/// Router instance
const router = require('express').Router();
const account = require('./account/routes.js');
const cities = require('./cities/routes.js');
const countries = require('./countries/routes.js');
const metaData = require('./meta-data/routes.js');
@@ -17,6 +18,7 @@ const publicLoadAttachments = require('./public-load-attachments/routes.js');
const states = require('./states/routes.js');
const test = require('./test/routes.js');
router.use('/account', account);
router.use('/cities', cities);
router.use('/countries', countries);
router.use('/meta-data', metaData);

View File

@@ -1,7 +1,9 @@
{
"authentication": {
"pwdSecret":"Nx2g_IWo2Zt_LS$+",
"jwtSecret":"9o3BBz0EsrwXliwEJ/SFuywZoN8=",
"jwtTimeout":720,
"jwtTimeout":24,
"jwtRenewalTimeout":720,
"tokenSecret":"9Z'jMt|(h_f(&/S+zv.K",
"jwtOptions": {
"header": {

View File

@@ -1,7 +1,9 @@
{
"authentication": {
"pwdSecret":"Nx2g_IWo2Zt_LS$+",
"jwtSecret":"9o3BBz0EsrwXliwEJ/SFuywZoN8=",
"jwtTimeout":720,
"jwtTimeout":24,
"jwtRenewalTimeout":720,
"tokenSecret":"9Z'jMt|(h_f(&/S+zv.K",
"jwtOptions": {
"header": {

View File

@@ -0,0 +1,58 @@
'user strict';
const { ROOT_PATH, API_CONFIG, MODELS_PATH, LIB_PATH } = process.env;
const apiConfig = require( `${ROOT_PATH}/${API_CONFIG}` );
const { toSha256 } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` );
const UserModel = require( `${ROOT_PATH}/${MODELS_PATH}/users.model.js` );
const pwd_secret = apiConfig.authentication.pwdSecret;
async function create_account( email, password, company_type ){
let permissions;
if( company_type === "Shipper"){
permissions = "role_shipper";
}else{
permissions = "role_carrier";
}
let safe_password = toSha256( password + pwd_secret );
const user = new UserModel({
email,
password : safe_password,
permissions
});
await user.save();
}
async function reset_password( email, password ){
let safe_password = toSha256( password + pwd_secret );
const user = await UserModel.findOne({ email });
user.password = safe_password,
await user.save();
return user;
}
async function already_exists( email ){
const user = await UserModel.findOne( { email } );
if( !user ){
return false;
}else{
return true;
}
}
async function login( email , password ){
let safe_password = toSha256( password + pwd_secret );
const user = await UserModel.findOne({
email , password : safe_password
},{ password : 0 });
return user;
}
async function login_with_session_token( session_token ){
const user = await UserModel.findOne({
session_token,
session_token_exp : { $gte: new Date() }
},{ password : 0 });
return user;
}
module.exports = { create_account, already_exists, login, login_with_session_token, reset_password };

View File

@@ -0,0 +1,66 @@
'user strict';
const { ROOT_PATH, API_CONFIG } = process.env;
const apiConfig = require( `${ROOT_PATH}/${API_CONFIG}` );
const nodemailer = require("nodemailer");
const SendGrid = require("nodemailer-sendgrid");
const SiteName = "ETA Viaporte";
const sendgridConfig = apiConfig.sendgrid;
const transporter = nodemailer.createTransport(
SendGrid({
host: sendgridConfig.HOST,
apiKey: sendgridConfig.API_KEY
})
);
async function sendMailTemplate( templateId, receiver, subject, content ){
/**TODO: Remove in production */
const default_mail = "testing@etaviaporte.com"
if( receiver.indexOf( default_mail ) >= 0 ){
receiver = default_mail;
}
return await transporter.sendMail({
from: sendgridConfig.FROM,
to: receiver,
subject: subject,
templateId: templateId,
dynamic_template_data: content
});
}
async function AccountVerifyEmail( receiver , content ){
const templateId = "d-e9b7966303694964a64b6e4954e9715d";
const subject = "[ETA] Account Verification";
const content_to_send = {
project_name: SiteName,
user_name: content.user_name,
user_email: receiver,
OTP : content.OTP
};
return await sendMailTemplate( templateId, receiver, subject, content_to_send );
}
async function AccountConfirmed( receiver, content ){
const templateId = "d-4daaab1b85d443ceba38826f606e9931";
const subject = "[ETA] Welcome to ETA";
const content_to_send = {
user_name: content.user_name,
};
return await sendMailTemplate( templateId, receiver, subject, content_to_send );
}
async function AccountPwdResetEmail( receiver, content ){
const templateId = "d-e9b7966303694964a64b6e4954e9715d";
const subject = "[ETA] Password Reset";
const content_to_send = {
project_name: SiteName,
user_name: content.user_name,
user_email: receiver,
OTP : content.OTP
};
return await sendMailTemplate( templateId, receiver, subject, content_to_send );
}
module.exports = { AccountVerifyEmail, AccountConfirmed, AccountPwdResetEmail };

View File

@@ -0,0 +1,43 @@
'user strict';
const { ROOT_PATH, HANDLERS_PATH, MODELS_PATH, API_CONFIG } = process.env;
const { AccountVerifyEmail, AccountConfirmed, AccountPwdResetEmail } = require('./SendGrid.handler');
const EMAIL_EVENTS={
ACCOUNT_VERIFY:1,
ACCOUNT_CONFIRMED:2,
ACCOUNT_PWD_RESET:3,
}
/**
* Send an email according to the event.
* @param eventId : string
* @param email_content : { string receiver, {*} content }
* @returns
*/
async function emailEvent( eventId, receiver , content ){
switch( eventId ){
case EMAIL_EVENTS.ACCOUNT_VERIFY:
{
return await AccountVerifyEmail( receiver, content );
}
break;
case EMAIL_EVENTS.ACCOUNT_CONFIRMED:
{
return await AccountConfirmed( receiver, content );
}
break;
case EMAIL_EVENTS.ACCOUNT_PWD_RESET:
{
return await AccountPwdResetEmail( receiver, content );
}
break;
default:
{
throw new Error(`Email event not defined ${eventId}`);
}
break;
}
return await usersModel.findById( id , { password : 0 } );
}
module.exports = { emailEvent , EMAIL_EVENTS };

View File

@@ -1,50 +1,30 @@
"use strict";
const { ROOT_PATH, API_CONFIG } = process.env;
const apiConfig = require( `${ROOT_PATH}/${API_CONFIG}` );
const crypto = require('crypto');
function getPagination( query ){
let limit = {
page : 0,
elements : 10
};
if( query.page ){
limit.page = parseInt( query.page ) || 0;
if( limit.page < 0 ){
limit.page = 0;
}
}
if( query.elements ){
limit.elements = parseInt( query.elements ) || 10;
/** Safe pagination limit */
if( limit.elements > 1000 ){
limit.elements = 1000;
}
else if( limit.elements < 0 ){
limit.elements = 10;
}
}
return limit;
const secret = apiConfig.authentication.jwtSecret;
/**
* Convert string to sha256 string in hex
* @param {*} text
* @returns
*/
function toSha256( text ){
return crypto.createHmac( "sha256" , "" ).update( text ).digest( 'hex' );
}
async function getPage( page, elements, model, filter=null, projection=null){
const skip = elements * page;
const total = await model.count( filter );
const list = await model.find( filter , projection, { skip : skip , limit : elements } );
return {
total : total,
limit : elements,
skip : skip,
data : list
/**
* Generate string with fixed length with random content.
* Length is limited to 64 characters.
* @param {*} text
* @returns
*/
function genKey( len = 6 , key="" ){
if( len >= 64 ){
throw "invalid key len";
}
const complete_string = toSha256( key + new Date() + secret );
return complete_string.substr(0 , len );
}
async function queryPage(page, elements, model, filter=null, projection=null){
const skip = elements * page;
const total = await model.count( filter );
return {
query : model.find( filter , projection, { skip : skip , limit : elements } ),
total : total,
skip : skip
};
}
module.exports = { getPagination , getPage, queryPage };
module.exports = { genKey , toSha256 };

View File

@@ -18,10 +18,10 @@ const schema = new Schema({
last_name: { type: String },
middle_name: { type: String },
email: { type: String, unique: true, lowercase: true },
password: { type: String },
password: { type: String , maxLength : 256 },
phone: { type: String },
phone2: { type: String },
permissions: [{ type: String, default: 'role_admin' }],
permissions: [{ type: String, default: 'role_admin', enum : ['role_admin', 'role_shipper', 'role_carrier', 'role_driver' ] }],
gender: { type: String },
address: { type: String },
dob: { type: String },
@@ -58,6 +58,9 @@ const schema = new Schema({
resetExpires: { type: Date }, // or a long integer
resetAttempts: { type: Number },
session_token : { type : String, maxLength : 256 },
session_token_exp : { type: Date },
is_hidden: { type: Boolean, default: false },
});