Skip to content

Commit

Permalink
feat(users): allow sending sigin verification code to email
Browse files Browse the repository at this point in the history
  • Loading branch information
rhahao committed Mar 8, 2023
1 parent 7547000 commit fa6c055
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 6 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 38 additions & 1 deletion src/classes/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FieldValue, getFirestore } from 'firebase-admin/firestore';
import * as OTPAuth from 'otpauth';
import randomstring from 'randomstring';
import { decryptData, encryptData } from '../utils/encryption-utils.js';
import { sendUserResetPassword, sendVerificationEmail } from '../utils/sendEmail.js';
import { sendEmailOTPCode, sendUserResetPassword, sendVerificationEmail } from '../utils/sendEmail.js';
import { congregations } from './Congregations.js';

const db = getFirestore(); //get default database
Expand Down Expand Up @@ -32,6 +32,7 @@ export class User {
this.secret = '';
this.auth_provider = '';
this.isTest = false;
this.emailOTP = {};
}
}

Expand All @@ -46,6 +47,7 @@ User.prototype.loadDetails = async function () {
this.sessions = userSnap.data().about?.sessions || [];
this.global_role = userSnap.data().about.role;
this.mfaEnabled = userSnap.data().about?.mfaEnabled || false;
this.emailOTP = userSnap.data().about?.emailOTP || {};
this.cong_id = userSnap.data().congregation?.id || '';
this.cong_role = userSnap.data().congregation?.role || [];
this.pocket_local_id = userSnap.data().congregation?.local_id || null;
Expand Down Expand Up @@ -469,3 +471,38 @@ User.prototype.updateSessionsInfo = async function (visitorid) {
cong.reloadMembers();
}
};

User.prototype.createTempOTPCode = async function (language) {
const codeValue = randomstring.generate({
length: 6,
charset: 'numeric',
});
const codeEncrypted = encryptData(codeValue);

const data = {
code: codeEncrypted,
expired: new Date().getTime() + 5 * 60000, // expired after 5 min
};

await db.collection('users').doc(this.id).update({ 'about.emailOTP': data });
this.emailOTP = data;

sendEmailOTPCode(this.user_uid, codeValue, language);
};

User.prototype.verifyTempOTPCode = async function (code) {
if (this.emailOTP.code) {
const verify = decryptData(this.emailOTP.code);
const timeExpired = this.emailOTP.expired;

const currentTime = new Date().getTime();

if (code === verify && currentTime <= timeExpired) {
await db.collection('users').doc(this.id).update({ 'about.emailOTP': FieldValue.delete() });
this.emailOTP = {};
return true;
}
}

return false;
};
120 changes: 120 additions & 0 deletions src/controllers/auth-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,123 @@ export const verifyPasswordlessInfo = async (req, res, next) => {
next(err);
}
};

export const createUserTempOTPCode = async (req, res, next) => {
try {
const errors = validationResult(req);

if (!errors.isEmpty()) {
let msg = '';
errors.array().forEach((error) => {
msg += `${msg === '' ? '' : ', '}${error.param}: ${error.msg}`;
});

res.locals.type = 'warn';
res.locals.message = `invalid input: ${msg}`;

res.status(400).json({
message: 'Bad request: provided inputs are invalid.',
});

return;
}

const { uid } = req.headers;
const language = req.headers.applanguage || 'e';
const user = await users.findUserByAuthUid(uid);

if (user) {
await user.createTempOTPCode(language);

res.locals.type = 'info';
res.locals.message = `temporary code for signin has been queued for sending`;

res.status(200).json({ message: 'CHECK_EMAIL' });
return;
}

res.locals.type = 'warn';
res.locals.message = `user record could not be found`;
res.status(404).json({ message: 'ACCOUNT_NOT_FOUND' });
} catch (err) {
next(err);
}
};

export const verifyUserTempOTPCode = async (req, res, next) => {
try {
const errors = validationResult(req);

if (!errors.isEmpty()) {
let msg = '';
errors.array().forEach((error) => {
msg += `${msg === '' ? '' : ', '}${error.param}: ${error.msg}`;
});

res.locals.type = 'warn';
res.locals.message = `invalid input: ${msg}`;

res.status(400).json({
message: 'Bad request: provided inputs are invalid.',
});

return;
}

const { uid, visitorid } = req.headers;
const { code } = req.body;
const user = await users.findUserByAuthUid(uid);

if (!user) {
res.locals.type = 'warn';
res.locals.message = `user record could not be found`;
res.status(404).json({ message: 'ACCOUNT_NOT_FOUND' });
return;
}

const result = await user.verifyTempOTPCode(code);

if (!result) {
res.locals.type = 'warn';
res.locals.message = 'Email OTP token invalid';
res.status(403).json({ message: 'EMAIL_OTP_INVALID' });
return;
}

const { id, sessions, username, cong_name, cong_number, cong_role, cong_id, pocket_local_id, pocket_members } = user;

let newSessions = sessions.map((session) => {
if (session.visitorid === visitorid) {
return {
...session,
mfaVerified: true,
sws_last_seen: new Date().getTime(),
};
} else {
return session;
}
});

await user.updateSessions(newSessions);

// init response object
const obj = {
message: 'TOKEN_VALID',
id,
username,
cong_name,
cong_number,
cong_role,
cong_id,
global_role: user.global_role,
pocket_local_id,
pocket_members,
};

res.locals.type = 'info';
res.locals.message = 'OTP token verification success';
res.status(200).json(obj);
} catch (err) {
next(err);
}
};
5 changes: 4 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"passwordLessIgnore": "If you did not request this link, you can safely ignore this email.",
"passwordLessLinkFull": "If you are having trouble opening the link above, please use the following link. Make sure the link is not cropped when you are opening it.",
"thankYou": "Thank you",
"sws2appsTeam": "The Scheduling Workbox System Team"
"sws2appsTeam": "The Scheduling Workbox System Team",
"emailOTPCodeSubject": "Verification Code for for sign in to CPE app",
"emailOTPCodeTemplate": "<p>Hello</p><p>Here’s your verification code to sign in to Congregation Program for Everyone:</p><p class='otp-code'>{{ emailOTPCode }}</p><p>Please make sure you never share this code with anyone.<br><strong>Note:</strong> The code will expire in 5 minutes.</p>",
"emailFooter": "<p>Thank you<br>The Scheduling Workbox System Team</p>"
}
27 changes: 25 additions & 2 deletions src/routes/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import express from 'express';
import { body, check } from 'express-validator';
import { createSignInLink, loginUser, verifyPasswordlessInfo } from '../controllers/auth-controller.js';
import { body, check, header } from 'express-validator';
import {
createSignInLink,
createUserTempOTPCode,
loginUser,
verifyPasswordlessInfo,
verifyUserTempOTPCode,
} from '../controllers/auth-controller.js';

const router = express.Router();

Expand All @@ -16,4 +22,21 @@ router.post(
verifyPasswordlessInfo
);

// request email otp code
router.get(
'/request-otp-code',
header('uid').isString().notEmpty(),
header('visitorid').isString().notEmpty(),
createUserTempOTPCode
);

// verify email otp code
router.post(
'/verify-otp-code',
header('uid').isString().notEmpty(),
header('visitorid').isString().notEmpty(),
body('code').isNumeric().isLength({ min: 6 }),
verifyUserTempOTPCode
);

export default router;
17 changes: 17 additions & 0 deletions src/utils/sendEmail.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,20 @@ export const sendUserResetPassword = async (recipient, fullname, resetPasswordLi

return !retry;
};

export const sendEmailOTPCode = async (recipient, code, templateLang) => {
const t = i18n(templateLang.toLowerCase());

const options = {
from: gmailConfig.sender,
to: recipient,
subject: t('emailOTPCodeSubject'),
template: 'emailOTPCodeSignIn',
context: {
emailOTPCodeTemplate: t('emailOTPCodeTemplate', { emailOTPCode: code }),
emailFooter: t('emailFooter'),
},
};

sendEmail(options, 'Email OTP code sent to user');
};
19 changes: 19 additions & 0 deletions src/views/emailOTPCodeSignIn.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verification code for sign in to CPE app</title>
<style>
.otp-code {
font-weight: bold;
font-size: 20px
}
</style>
</head>
<body>
{{{ emailOTPCodeTemplate }}}
{{{ emailFooter }}}
</body>
</html>

0 comments on commit fa6c055

Please sign in to comment.