Skip to content

Commit 1e4f6b2

Browse files
authored
Merge pull request #18 from foyzulkarim/feature/apply-role-based-access-control
Email Verification System & Role-Based Access Control
2 parents 95730d5 + aa54fce commit 1e4f6b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1393
-743
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,7 @@ dist
130130
config.development.json
131131
config.production.json
132132
config.test.json
133+
134+
# Debug emails
135+
**/debug/**
136+
.DS_Store

package-lock.json

Lines changed: 46 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
"logs:prod": "pm2 logs src/start.js",
1818
"restart:prod": "NODE_ENV=production pm2 restart src/start.js",
1919
"create-domain": "zx scripts/dev/domain-generator.js",
20-
"bump-version": "npm version patch -m 'Bump version to %s'"
20+
"bump-version": "npm version patch -m 'Bump version to %s'",
21+
"migrate": "node src/migration-runner.js"
2122
},
2223
"keywords": [],
2324
"author": "",
2425
"license": "ISC",
2526
"dependencies": {
2627
"@octokit/rest": "^20.1.1",
28+
"@sendgrid/mail": "^8.1.4",
2729
"bcrypt": "^5.1.1",
2830
"compression": "^1.7.4",
2931
"connect-mongo": "^5.1.0",

src/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
GITHUB_CLIENT_ID=123
22
GITHUB_CLIENT_SECRET=123
33
ENCRYPTION_KEY=123
4+
SUPERADMIN_PASSWORD=123

src/auth/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const {
55
getGitHubStrategy,
66
getOrCreateUserFromGitHubProfile,
77
} = require('./githubStrategy');
8-
const { localStrategy, registerUser } = require('./localStrategy');
8+
const { localStrategy, registerUser, verifyEmail, resendVerificationEmail } = require('./localStrategy');
99
const {
1010
getGoogleStrategy,
1111
getOrCreateUserFromGoogleProfile,
@@ -29,4 +29,6 @@ module.exports = {
2929
registerUser,
3030
getGoogleStrategy,
3131
getOrCreateUserFromGoogleProfile,
32+
verifyEmail,
33+
resendVerificationEmail,
3234
};

src/auth/localStrategy.js

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
const LocalStrategy = require('passport-local').Strategy;
22
const bcrypt = require('bcrypt');
3+
const crypto = require('crypto');
34
const {
45
getByUsername,
56
getByEmail,
67
create,
8+
updateById,
9+
findByVerificationToken,
10+
refreshVerificationToken,
11+
completeEmailVerification,
712
} = require('../domains/user/service');
813
const { AppError } = require('../libraries/error-handling/AppError');
14+
const { sendVerificationEmail } = require('../libraries/email/emailService');
915

1016
const verifyCallback = async (username, password, done) => {
1117
try {
1218
// Find user by email (since we're using email as username)
1319
const user = await getByEmail(username);
14-
20+
1521
if (!user) {
1622
return done(null, false, { message: 'Incorrect email.' });
1723
}
1824

1925
// Verify this is a local auth user
2026
if (user.authType !== 'local') {
21-
return done(null, false, {
22-
message: `Please use ${user.authType} authentication for this account.`
27+
return done(null, false, {
28+
message: `Please use ${user.authType} authentication for this account.`
2329
});
2430
}
2531

32+
// Check if email is verified
33+
if (!user.isVerified) {
34+
return done(null, false, { message: 'Please verify your email address before signing in.', reason: 'email-not-verified' });
35+
}
36+
2637
// Verify password
2738
const isValidPassword = await bcrypt.compare(password, user.local.password);
2839
if (!isValidPassword) {
@@ -40,6 +51,10 @@ const verifyCallback = async (username, password, done) => {
4051
}
4152
};
4253

54+
const generateVerificationToken = () => {
55+
return crypto.randomBytes(32).toString('hex');
56+
};
57+
4358
const registerUser = async ({ email, password }) => {
4459
try {
4560
// Check if user already exists
@@ -52,23 +67,35 @@ const registerUser = async ({ email, password }) => {
5267
const salt = await bcrypt.genSalt(10);
5368
const hashedPassword = await bcrypt.hash(password, salt);
5469

70+
// Generate verification token
71+
const verificationToken = generateVerificationToken();
72+
const verificationTokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours from now
73+
5574
// Create user payload matching new schema structure
5675
const payload = {
5776
email,
5877
displayName: email,
59-
authType: 'local', // Specify auth type
78+
authType: 'local',
6079
local: {
6180
username: email,
6281
password: hashedPassword,
6382
},
6483
isDemo: false,
6584
isVerified: false,
6685
isAdmin: false,
86+
verificationToken,
87+
verificationTokenExpiry,
88+
isDeactivated: false, // we activate the user after email verification
89+
role: 'Visitor',
90+
roleId: null,
6791
};
6892

6993
// Create the user
7094
const newUser = await create(payload);
7195

96+
// Send verification email
97+
await sendVerificationEmail(email, verificationToken);
98+
7299
// Prepare the user object for the session
73100
const userObj = newUser.toObject();
74101
const trimmedPayloadForSession = {
@@ -87,6 +114,50 @@ const registerUser = async ({ email, password }) => {
87114
}
88115
};
89116

117+
const verifyEmail = async (token) => {
118+
try {
119+
const user = await findByVerificationToken(token);
120+
121+
if (!user) {
122+
throw new AppError('invalid-token', 'Invalid or expired verification token', 400);
123+
}
124+
125+
// Update user as verified
126+
await completeEmailVerification(user._id);
127+
128+
return { message: 'Email verified successfully' };
129+
} catch (error) {
130+
if (error instanceof AppError) {
131+
throw error;
132+
}
133+
throw new AppError('verification-failed', error.message, 400);
134+
}
135+
};
136+
137+
const resendVerificationEmail = async (email) => {
138+
try {
139+
const { user, verificationToken } = await refreshVerificationToken(email);
140+
141+
// Send new verification email
142+
await sendVerificationEmail(email, verificationToken);
143+
144+
return {
145+
message: 'Verification email sent successfully',
146+
email: user.email
147+
};
148+
} catch (error) {
149+
if (error instanceof AppError) {
150+
throw error;
151+
}
152+
throw new AppError('resend-verification-failed', error.message, 400);
153+
}
154+
};
155+
90156
const localStrategy = new LocalStrategy(verifyCallback);
91157

92-
module.exports = { localStrategy, registerUser };
158+
module.exports = {
159+
localStrategy,
160+
registerUser,
161+
verifyEmail,
162+
resendVerificationEmail,
163+
};

src/configs/config.example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"NODE_ENV": "development",
33
"MONGODB_URI": "mongodb://localhost:27017/mydatabase",
4+
"DB_NAME": "mydatabase",
45
"RATE": 40,
56
"PORT": 4000
67
}

src/configs/config.schema.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const schema = Joi.object({
55
.valid('development', 'production', 'test')
66
.default('development'),
77
MONGODB_URI: Joi.string().required(),
8+
DB_NAME: Joi.string().required(),
89
RATE: Joi.number().min(0).required(),
910
PORT: Joi.number().min(1000).default(4000),
1011
// LOGGLY is required when NODE_ENV is production
@@ -30,6 +31,11 @@ const schema = Joi.object({
3031
SESSION_SECRET: Joi.string().required(),
3132
ENCRYPTION_KEY: Joi.string().required(),
3233
ADMIN_USERNAMES: Joi.array().items(Joi.string()).required(),
34+
SUPERADMIN_EMAIL: Joi.string().email().required(),
35+
SUPERADMIN_PASSWORD: Joi.string().min(8).required(),
36+
// SendGrid Configuration
37+
SENDGRID_API_KEY: Joi.string().required(),
38+
SENDGRID_FROM_EMAIL: Joi.string().email().required(),
3339
});
3440

3541
module.exports = schema;

src/domains/customer/api.js

Lines changed: 0 additions & 100 deletions
This file was deleted.

src/domains/customer/event.js

Whitespace-only changes.

0 commit comments

Comments
 (0)