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

@@ -31,12 +31,14 @@
"express-jwt": "^8.4.1", "express-jwt": "^8.4.1",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"helmet": "^7.0.0", "helmet": "^7.0.0",
"jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"knex": "^2.5.1", "knex": "^2.5.1",
"mongodb-core": "^3.2.7", "mongodb-core": "^3.2.7",
"mongoose": "^7.5.4", "mongoose": "^7.5.4",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"nodemailer": "^6.9.5", "nodemailer": "^6.9.5",
"nodemailer-sendgrid": "^1.0.3",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"objection": "^3.1.2", "objection": "^3.1.2",
"uuid": "^9.0.1" "uuid": "^9.0.1"

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 /// Router instance
const router = require('express').Router(); const router = require('express').Router();
const account = require('./account/routes.js');
const cities = require('./cities/routes.js'); const cities = require('./cities/routes.js');
const countries = require('./countries/routes.js'); const countries = require('./countries/routes.js');
const metaData = require('./meta-data/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 states = require('./states/routes.js');
const test = require('./test/routes.js'); const test = require('./test/routes.js');
router.use('/account', account);
router.use('/cities', cities); router.use('/cities', cities);
router.use('/countries', countries); router.use('/countries', countries);
router.use('/meta-data', metaData); router.use('/meta-data', metaData);

View File

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

View File

@@ -1,7 +1,9 @@
{ {
"authentication": { "authentication": {
"pwdSecret":"Nx2g_IWo2Zt_LS$+",
"jwtSecret":"9o3BBz0EsrwXliwEJ/SFuywZoN8=", "jwtSecret":"9o3BBz0EsrwXliwEJ/SFuywZoN8=",
"jwtTimeout":720, "jwtTimeout":24,
"jwtRenewalTimeout":720,
"tokenSecret":"9Z'jMt|(h_f(&/S+zv.K", "tokenSecret":"9Z'jMt|(h_f(&/S+zv.K",
"jwtOptions": { "jwtOptions": {
"header": { "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"; "use strict";
const { ROOT_PATH, API_CONFIG } = process.env;
const apiConfig = require( `${ROOT_PATH}/${API_CONFIG}` );
const crypto = require('crypto');
function getPagination( query ){ const secret = apiConfig.authentication.jwtSecret;
let limit = { /**
page : 0, * Convert string to sha256 string in hex
elements : 10 * @param {*} text
}; * @returns
*/
if( query.page ){ function toSha256( text ){
limit.page = parseInt( query.page ) || 0; return crypto.createHmac( "sha256" , "" ).update( text ).digest( 'hex' );
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;
} }
async function getPage( page, elements, model, filter=null, projection=null){ /**
const skip = elements * page; * Generate string with fixed length with random content.
const total = await model.count( filter ); * Length is limited to 64 characters.
const list = await model.find( filter , projection, { skip : skip , limit : elements } ); * @param {*} text
return { * @returns
total : total, */
limit : elements, function genKey( len = 6 , key="" ){
skip : skip, if( len >= 64 ){
data : list 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){ module.exports = { genKey , toSha256 };
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 };

View File

@@ -18,10 +18,10 @@ const schema = new Schema({
last_name: { type: String }, last_name: { type: String },
middle_name: { type: String }, middle_name: { type: String },
email: { type: String, unique: true, lowercase: true }, email: { type: String, unique: true, lowercase: true },
password: { type: String }, password: { type: String , maxLength : 256 },
phone: { type: String }, phone: { type: String },
phone2: { 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 }, gender: { type: String },
address: { type: String }, address: { type: String },
dob: { type: String }, dob: { type: String },
@@ -58,6 +58,9 @@ const schema = new Schema({
resetExpires: { type: Date }, // or a long integer resetExpires: { type: Date }, // or a long integer
resetAttempts: { type: Number }, resetAttempts: { type: Number },
session_token : { type : String, maxLength : 256 },
session_token_exp : { type: Date },
is_hidden: { type: Boolean, default: false }, is_hidden: { type: Boolean, default: false },
}); });