Skip to content

Commit 41c4788

Browse files
committed
Complete core auth; pending tests and session handling
1 parent a6626c3 commit 41c4788

File tree

12 files changed

+439
-3
lines changed

12 files changed

+439
-3
lines changed

packages/app/.env.docker

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ CLOUD_FRONT_KEYPAIR_ID=Q8A4T7ZMUYRW3Z
2222
CLOUD_FRONT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----RSA KEY FOR CLOUDFRONT=-----END RSA PRIVATE KEY-----
2323
BUCKET_KEY=s3_bucket_folder_name_can_customise_as_per_your_likings
2424
ADMINSEMAILS=admin_Email@gmail.com
25+
CRYPTO_KEY=your-32-byte-key-hex-encoded

packages/app/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ CLOUD_FRONT_KEYPAIR_ID=Q8A4T7ZMUYRW3Z
2222
CLOUD_FRONT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----RSA KEY FOR CLOUDFRONT=-----END RSA PRIVATE KEY-----
2323
BUCKET_KEY=s3_bucket_folder_name_can_customise_as_per_your_likings
2424
ADMINSEMAILS=admin_Email@gmail.com
25+
CRYPTO_KEY=your-32-byte-key-hex-encoded
2526

packages/app/__mocks__/otplib.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* eslint-disable no-unused-vars */
2+
// Mock for otplib to avoid ES module issues in Jest
3+
class OTP {
4+
constructor() {
5+
this.options = {};
6+
}
7+
8+
generate(secret) {
9+
return "123456";
10+
}
11+
12+
verify({ token, secret }) {
13+
return true;
14+
}
15+
}
16+
17+
const authenticator = {
18+
generate: jest.fn(() => "123456"),
19+
verify: jest.fn(() => true),
20+
generateSecret: jest.fn(() => "JBSWY3DPEHPK3PXP"),
21+
keyuri: jest.fn(
22+
(user, service, secret) =>
23+
`otpauth://totp/${service}:${user}?secret=${secret}&issuer=${service}`
24+
),
25+
options: {},
26+
};
27+
28+
module.exports = {
29+
OTP,
30+
authenticator,
31+
};

packages/app/controllers/auth.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,193 @@ async function resetPasswordController(req, res, next) {
498498
}
499499
}
500500

501+
async function siginWithMFAController(req, res, next) {
502+
try {
503+
const userService = new UserService();
504+
const { token } = req.body;
505+
const mfaSessionToken = req.cookies.mfaToken;
506+
if (!mfaSessionToken) {
507+
return res.status(401).json({
508+
error: STATUS_CODES[401],
509+
message: "MFA token not found",
510+
statusCode: 401,
511+
});
512+
}
513+
514+
// check if mfaSessionToken is valid
515+
516+
const user = await userService.getUserById(mfaSessionToken.userId);
517+
if (!user) {
518+
return res.status(404).json({
519+
error: STATUS_CODES[404],
520+
message: Messages.USER_NOT_FOUND,
521+
statusCode: 404,
522+
});
523+
}
524+
525+
const isVerified = await userService.mfaLogin(user, token);
526+
if (!isVerified) {
527+
return res.status(400).json({
528+
error: STATUS_CODES[400],
529+
message: "Invalid token",
530+
statusCode: 400,
531+
});
532+
}
533+
534+
// set session
535+
536+
return res.status(200).json({ statusCode: 200 });
537+
} catch (error) {
538+
next(error);
539+
}
540+
}
541+
542+
async function enableMFAController(req, res, next) {
543+
try {
544+
const userService = new UserService();
545+
const { userId } = req.userData;
546+
const user = await userService.getUserById(userId);
547+
if (!user) {
548+
return res.status(404).json({
549+
error: STATUS_CODES[404],
550+
message: Messages.USER_NOT_FOUND,
551+
statusCode: 404,
552+
});
553+
}
554+
555+
const { qrCode } = await userService.enableMfa(user);
556+
if (!qrCode) {
557+
return res.status(500).json({
558+
error: STATUS_CODES[500],
559+
message: "Failed to enable MFA",
560+
statusCode: 500,
561+
});
562+
}
563+
564+
return res.status(200).json({
565+
statusCode: 200,
566+
data: { qrCode },
567+
});
568+
} catch (error) {
569+
res.status(error.statusCode || 500).json({
570+
error: STATUS_CODES[error.statusCode] || STATUS_CODES[500],
571+
message: error.message,
572+
statusCode: error.statusCode || 500,
573+
});
574+
next(error);
575+
}
576+
}
577+
578+
async function verifyMFAController(req, res, next) {
579+
try {
580+
const userService = new UserService();
581+
const { token } = req.body;
582+
const { userId } = req.userData;
583+
const user = await userService.getUserById(userId);
584+
if (!user) {
585+
return res.status(404).json({
586+
error: STATUS_CODES[404],
587+
message: Messages.USER_NOT_FOUND,
588+
statusCode: 404,
589+
});
590+
}
591+
592+
const isVerified = await userService.verifyMfa(user, token);
593+
if (!isVerified) {
594+
return res.status(400).json({
595+
error: STATUS_CODES[400],
596+
message: "Invalid token",
597+
statusCode: 400,
598+
});
599+
}
600+
601+
return res.status(200).json({ statusCode: 200 });
602+
} catch (error) {
603+
res.status(error.statusCode || 500).json({
604+
error: STATUS_CODES[error.statusCode] || STATUS_CODES[500],
605+
message: error.message,
606+
statusCode: error.statusCode || 500,
607+
});
608+
next(error);
609+
}
610+
}
611+
612+
async function cancelMFAController(req, res, next) {
613+
try {
614+
const userService = new UserService();
615+
const { userId } = req.userData;
616+
const user = await userService.getUserById(userId);
617+
if (!user) {
618+
return res.status(404).json({
619+
error: STATUS_CODES[404],
620+
message: Messages.USER_NOT_FOUND,
621+
statusCode: 404,
622+
});
623+
}
624+
625+
const isDisabled = await userService.disableMfa(user);
626+
if (!isDisabled) {
627+
return res.status(500).json({
628+
error: STATUS_CODES[500],
629+
message: "Failed to disable MFA",
630+
statusCode: 500,
631+
});
632+
}
633+
634+
return res.status(200).json({ statusCode: 200 });
635+
} catch (error) {
636+
res.status(error.statusCode || 500).json({
637+
error: STATUS_CODES[error.statusCode] || STATUS_CODES[500],
638+
message: error.message,
639+
statusCode: error.statusCode || 500,
640+
});
641+
next(error);
642+
}
643+
}
644+
645+
async function disableMFAController(req, res, next) {
646+
try {
647+
const userService = new UserService();
648+
const { password } = req.body;
649+
const { userId } = req.userData;
650+
const user = await userService.getUserById(userId);
651+
if (!user) {
652+
return res.status(404).json({
653+
error: STATUS_CODES[404],
654+
message: Messages.USER_NOT_FOUND,
655+
statusCode: 404,
656+
});
657+
}
658+
659+
const matchPassword = await user.matchPassword(password);
660+
if (!matchPassword) {
661+
return res.status(404).json({
662+
error: STATUS_CODES[404],
663+
message: Messages.INCORRECT_EMAIL_PASS,
664+
statusCode: 404,
665+
});
666+
}
667+
668+
const isDisabled = await userService.disableMfa(user);
669+
if (!isDisabled) {
670+
return res.status(500).json({
671+
error: STATUS_CODES[500],
672+
message: "Failed to disable MFA",
673+
statusCode: 500,
674+
});
675+
}
676+
677+
return res.status(200).json({ statusCode: 200 });
678+
} catch (error) {
679+
res.status(error.statusCode || 500).json({
680+
error: STATUS_CODES[error.statusCode] || STATUS_CODES[500],
681+
message: error.message,
682+
statusCode: error.statusCode || 500,
683+
});
684+
next(error);
685+
}
686+
}
687+
501688
function validateSessionController(req, res) {
502689
return res.status(200).json({ statusCode: 200, userData: req.userData });
503690
}
@@ -511,4 +698,9 @@ module.exports = {
511698
resetPasswordSessionController,
512699
resetPasswordController,
513700
validateSessionController,
701+
siginWithMFAController,
702+
enableMFAController,
703+
verifyMFAController,
704+
cancelMFAController,
705+
disableMFAController,
514706
};

packages/app/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
module.exports = {
22
preset: "@shelf/jest-mongodb",
33
watchPathIgnorePatterns: ["globalConfig"],
4+
moduleNameMapper: {
5+
// Mock otplib to avoid ES module issues
6+
"^otplib$": "<rootDir>/__mocks__/otplib.js",
7+
},
48
};

packages/app/models/keys.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ keySchema.pre("save", async function (next) {
4444

4545
keySchema.methods.data = function () {
4646
return {
47-
_id: this._id,
47+
_id: this._id,
4848
key_description: this.key_description,
4949
subscription_id: this.subscription_id,
5050
expires_at: this.expires_at,
5151
created_at: this._id.getTimestamp(),
52-
updated_at: this.updated_at
52+
updated_at: this.updated_at,
5353
};
5454
};
5555

packages/app/models/users.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ const userSchema = new mongoose.Schema({
6363
type: Date,
6464
default: null,
6565
},
66+
mfaEnabled: {
67+
type: Boolean,
68+
default: false,
69+
},
70+
mfaSecret: {
71+
encrypted: { type: String, default: null },
72+
iv: { type: String, default: null },
73+
tag: { type: String, default: null },
74+
},
75+
mfaTempSecret: {
76+
type: String,
77+
default: null,
78+
},
79+
mfaTempSecretExpiresAt: {
80+
type: Date,
81+
default: null,
82+
},
6683
});
6784

6885
userSchema.methods.matchPassword = async function (password) {

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
"jsonwebtoken": "^9.0.2",
3434
"mongoose": "^8.5.1",
3535
"multer": "^1.4.5-lts.1",
36+
"otplib": "^13.3.0",
3637
"puppeteer-core": "^24.34.0",
38+
"qrcode": "^1.5.4",
3739
"swagger-jsdoc": "^6.2.8",
3840
"swagger-ui-express": "^5.0.1",
3941
"uuid": "^9.0.1",

packages/app/routes/auth.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const {
1010
resetPasswordSessionController,
1111
resetPasswordController,
1212
validateSessionController,
13+
siginWithMFAController,
14+
enableMFAController,
15+
verifyMFAController,
16+
cancelMFAController,
17+
disableMFAController,
1318
} = require("../controllers/auth");
1419

1520
router.post("/signup", signupController);
@@ -20,5 +25,10 @@ router.post("/password/forgot", forgotPasswordController);
2025
router.get("/password/forgot/:token?", resetPasswordSessionController);
2126
router.patch("/password/reset", resetPasswordController);
2227
router.get("/validate-session", authMiddleware(), validateSessionController);
28+
router.post("/mfa/signin", siginWithMFAController);
29+
router.post("/mfa/enable", authMiddleware(), enableMFAController);
30+
router.post("/mfa/verify", authMiddleware(), verifyMFAController);
31+
router.post("/mfa/cancel", authMiddleware(), cancelMFAController);
32+
router.post("/mfa/disable", authMiddleware(), disableMFAController);
2333

2434
module.exports = router;

0 commit comments

Comments
 (0)