Skip to content

Commit f02f91d

Browse files
authored
Merge pull request #14 from foyzulkarim/feature/add-password-auth
Add local authentication strategy and user registration
2 parents 3440ddd + e6b85be commit f02f91d

File tree

9 files changed

+663
-141
lines changed

9 files changed

+663
-141
lines changed

package-lock.json

Lines changed: 355 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"license": "ISC",
2525
"dependencies": {
2626
"@octokit/rest": "^20.1.1",
27+
"bcrypt": "^5.1.1",
2728
"compression": "^1.7.4",
2829
"connect-mongo": "^5.1.0",
2930
"cookie-parser": "^1.4.6",
@@ -42,6 +43,7 @@
4243
"octokit": "^3.2.1",
4344
"passport": "^0.7.0",
4445
"passport-github2": "^0.1.12",
46+
"passport-local": "^1.0.0",
4547
"pm2": "^5.3.1",
4648
"short-uuid": "^5.2.0",
4749
"ulid": "^2.3.0",

src/auth/githubStrategy.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const config = require('../configs');
2+
const GitHubStrategy = require('passport-github2').Strategy;
3+
4+
const { encryptToken } = require('./util');
5+
const {
6+
getByGitHubId,
7+
create,
8+
updateById,
9+
} = require('../domains/user/service');
10+
const { AppError } = require('../libraries/error-handling/AppError');
11+
12+
const getGitHubStrategy = () => {
13+
return new GitHubStrategy(
14+
{
15+
clientID: config.GITHUB_CLIENT_ID,
16+
clientSecret: config.GITHUB_CLIENT_SECRET,
17+
callbackURL: `${config.HOST}/api/auth/github/callback`,
18+
},
19+
async (accessToken, refreshToken, profile, cb) => {
20+
try {
21+
const trimmedPayloadForSession = await getOrCreateUserFromGitHubProfile(
22+
{
23+
profile,
24+
accessToken,
25+
}
26+
);
27+
28+
cb(null, trimmedPayloadForSession); // Pass the user object to the session
29+
} catch (error) {
30+
cb(error, null);
31+
}
32+
}
33+
);
34+
};
35+
36+
async function getOrCreateUserFromGitHubProfile({ profile, accessToken }) {
37+
const isAdmin = config.ADMIN_USERNAMES.includes(profile.username);
38+
// Create a new user from GitHub API Profile data
39+
const payload = {
40+
githubId: profile.id,
41+
nodeId: profile.nodeId,
42+
displayName: profile.displayName,
43+
username: profile.username,
44+
profileUrl: profile.profileUrl,
45+
46+
avatarUrl: profile._json.avatar_url,
47+
apiUrl: profile._json.url,
48+
company: profile._json.company,
49+
blog: profile._json.blog,
50+
location: profile._json.location,
51+
email: profile._json.email,
52+
hireable: profile._json.hireable,
53+
bio: profile._json.bio,
54+
public_repos: profile._json.public_repos,
55+
public_gists: profile._json.public_gists,
56+
followers: profile._json.followers,
57+
following: profile._json.following,
58+
created_at: profile._json.created_at,
59+
updated_at: profile._json.updated_at,
60+
61+
isDemo: false,
62+
isVerified: true,
63+
isAdmin,
64+
};
65+
66+
let user = await getByGitHubId(profile.id);
67+
68+
const tokenInfo = encryptToken(accessToken);
69+
if (user) {
70+
if (user.isDeactivated) {
71+
throw new AppError('user-is-deactivated', 'User is deactivated', 401);
72+
}
73+
74+
// Update the user with the latest data
75+
user = Object.assign(user, payload, {
76+
accessToken: tokenInfo.token,
77+
accessTokenIV: tokenInfo.iv,
78+
updatedAt: new Date(),
79+
});
80+
await updateById(user._id, user);
81+
} else {
82+
// Create a new user
83+
user = await create({
84+
...payload,
85+
accessToken: tokenInfo.token,
86+
accessTokenIV: tokenInfo.iv,
87+
});
88+
}
89+
const userObj = user.toObject();
90+
const trimmedPayloadForSession = {
91+
_id: userObj._id,
92+
githubId: userObj.githubId,
93+
nodeId: userObj.nodeId,
94+
isAdmin: userObj.isAdmin,
95+
isDeactivated: userObj.isDeactivated,
96+
isDemo: userObj.isDemo,
97+
// UI info
98+
username: userObj.username,
99+
displayName: userObj.displayName,
100+
avatarUrl: userObj.avatarUrl,
101+
email: userObj.email,
102+
};
103+
return trimmedPayloadForSession;
104+
}
105+
106+
module.exports = {
107+
getGitHubStrategy,
108+
getOrCreateUserFromGitHubProfile,
109+
};

src/auth/index.js

Lines changed: 9 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,11 @@
1-
const config = require('../configs');
2-
const crypto = require('crypto');
3-
const logger = require('../libraries/log/logger');
4-
const GitHubStrategy = require('passport-github2').Strategy;
1+
const { encryptToken, decryptToken } = require('./util');
2+
const { updateById } = require('../domains/user/service');
53

64
const {
7-
getByGitHubId,
8-
create,
9-
updateById,
10-
} = require('../domains/user/service');
11-
const { AppError } = require('../libraries/error-handling/AppError');
12-
13-
function encryptToken(token) {
14-
const encryptionKey = config.ENCRYPTION_KEY;
15-
const iv = crypto.randomBytes(16); // Generate a secure IV
16-
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
17-
let encrypted = cipher.update(token, 'utf-8', 'hex');
18-
encrypted += cipher.final('hex');
19-
20-
return {
21-
token: encrypted,
22-
iv: iv.toString('hex'),
23-
};
24-
}
25-
26-
function decryptToken(encryptedToken, iv) {
27-
const encryptionKey = config.ENCRYPTION_KEY;
28-
const decipher = crypto.createDecipheriv(
29-
'aes-256-cbc',
30-
encryptionKey,
31-
Buffer.from(iv, 'hex')
32-
);
33-
let decrypted = decipher.update(encryptedToken, 'hex', 'utf-8');
34-
decrypted += decipher.final('utf-8');
35-
return decrypted;
36-
}
37-
38-
async function getOrCreateUserFromGitHubProfile({ profile, accessToken }) {
39-
const isAdmin = config.ADMIN_USERNAMES.includes(profile.username);
40-
// Create a new user from GitHub API Profile data
41-
const payload = {
42-
githubId: profile.id,
43-
nodeId: profile.nodeId,
44-
displayName: profile.displayName,
45-
username: profile.username,
46-
profileUrl: profile.profileUrl,
47-
48-
avatarUrl: profile._json.avatar_url,
49-
apiUrl: profile._json.url,
50-
company: profile._json.company,
51-
blog: profile._json.blog,
52-
location: profile._json.location,
53-
email: profile._json.email,
54-
hireable: profile._json.hireable,
55-
bio: profile._json.bio,
56-
public_repos: profile._json.public_repos,
57-
public_gists: profile._json.public_gists,
58-
followers: profile._json.followers,
59-
following: profile._json.following,
60-
created_at: profile._json.created_at,
61-
updated_at: profile._json.updated_at,
62-
63-
isDemo: false,
64-
isVerified: true,
65-
isAdmin,
66-
};
67-
68-
let user = await getByGitHubId(profile.id);
69-
70-
const tokenInfo = encryptToken(accessToken);
71-
if (user) {
72-
if (user.isDeactivated) {
73-
throw new AppError('user-is-deactivated', 'User is deactivated', 401);
74-
}
75-
76-
// Update the user with the latest data
77-
user = Object.assign(user, payload, {
78-
accessToken: tokenInfo.token,
79-
accessTokenIV: tokenInfo.iv,
80-
updatedAt: new Date(),
81-
});
82-
await updateById(user._id, user);
83-
} else {
84-
// Create a new user
85-
user = await create({
86-
...payload,
87-
accessToken: tokenInfo.token,
88-
accessTokenIV: tokenInfo.iv,
89-
});
90-
}
91-
const userObj = user.toObject();
92-
const trimmedPayloadForSession = {
93-
_id: userObj._id,
94-
githubId: userObj.githubId,
95-
nodeId: userObj.nodeId,
96-
isAdmin: userObj.isAdmin,
97-
isDeactivated: userObj.isDeactivated,
98-
isDemo: userObj.isDemo,
99-
// UI info
100-
username: userObj.username,
101-
displayName: userObj.displayName,
102-
avatarUrl: userObj.avatarUrl,
103-
email: userObj.email,
104-
};
105-
return trimmedPayloadForSession;
106-
}
107-
108-
const getGitHubStrategy = () => {
109-
return new GitHubStrategy(
110-
{
111-
clientID: config.GITHUB_CLIENT_ID,
112-
clientSecret: config.GITHUB_CLIENT_SECRET,
113-
callbackURL: `${config.HOST}/api/auth/github/callback`,
114-
},
115-
async (accessToken, refreshToken, profile, cb) => {
116-
try {
117-
const trimmedPayloadForSession = await getOrCreateUserFromGitHubProfile(
118-
{
119-
profile,
120-
accessToken,
121-
}
122-
);
123-
124-
cb(null, trimmedPayloadForSession); // Pass the user object to the session
125-
} catch (error) {
126-
cb(error, null);
127-
}
128-
}
129-
);
130-
};
5+
getGitHubStrategy,
6+
getOrCreateUserFromGitHubProfile,
7+
} = require('./githubStrategy');
8+
const { localStrategy, registerUser } = require('./localStrategy');
1319

13210
// clear the accessToken value from database after logout
13311
const clearAuthInfo = async (userId) => {
@@ -139,9 +17,11 @@ const clearAuthInfo = async (userId) => {
13917
};
14018

14119
module.exports = {
142-
getOrCreateUserFromGitHubProfile,
14320
getGitHubStrategy,
21+
getOrCreateUserFromGitHubProfile,
14422
clearAuthInfo,
14523
encryptToken,
14624
decryptToken,
25+
localStrategy,
26+
registerUser,
14727
};

src/auth/localStrategy.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const LocalStrategy = require('passport-local').Strategy;
2+
const bcrypt = require('bcrypt');
3+
const { getByUsername, getByEmail, create } = require('../domains/user/service');
4+
const { AppError } = require('../libraries/error-handling/AppError');
5+
6+
const verifyCallback = async (username, password, done) => {
7+
try {
8+
const user = await getByUsername(username);
9+
if (!user) {
10+
return done(null, false, { message: 'Incorrect username.' });
11+
}
12+
console.log('user', user);
13+
const salt = await bcrypt.genSalt(10);
14+
const hashedPassword = await bcrypt.hash(password, salt);
15+
const isValidPassword = await bcrypt.compare(password, user.password);
16+
17+
console.log(
18+
'user',
19+
password,
20+
user.password,
21+
hashedPassword,
22+
isValidPassword
23+
);
24+
if (!isValidPassword) {
25+
return done(null, false, { message: 'Incorrect password.' });
26+
}
27+
28+
return done(null, user);
29+
} catch (err) {
30+
return done(err);
31+
}
32+
};
33+
34+
const registerUser = async ({ email, password }) => {
35+
try {
36+
console.log('registerUser', email, password);
37+
// Check if user already exists
38+
const existingUser = await getByEmail(email);
39+
if (existingUser) {
40+
throw new AppError('user-already-exists', 'Email already taken', 400);
41+
}
42+
43+
// Hash the password
44+
const salt = await bcrypt.genSalt(10);
45+
const hashedPassword = await bcrypt.hash(password, salt);
46+
47+
// Create user payload
48+
const payload = {
49+
email,
50+
username: email,
51+
password: hashedPassword,
52+
displayName: email,
53+
isDemo: false,
54+
isVerified: false,
55+
isAdmin: false,
56+
};
57+
58+
// Create the user
59+
const newUser = await create(payload);
60+
61+
// Prepare the user object for the session
62+
const userObj = newUser.toObject();
63+
const trimmedPayloadForSession = {
64+
_id: userObj._id,
65+
isAdmin: userObj.isAdmin,
66+
isDeactivated: userObj.isDeactivated,
67+
isDemo: userObj.isDemo,
68+
username: userObj.username,
69+
displayName: userObj.displayName,
70+
};
71+
72+
return trimmedPayloadForSession;
73+
} catch (error) {
74+
throw new AppError('registration-failed', error.message, 400);
75+
}
76+
};
77+
78+
const localStrategy = new LocalStrategy(verifyCallback);
79+
80+
module.exports = { localStrategy, registerUser };

src/auth/util.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const config = require('../configs');
2+
const crypto = require('crypto');
3+
4+
function encryptToken(token) {
5+
const encryptionKey = config.ENCRYPTION_KEY;
6+
const iv = crypto.randomBytes(16); // Generate a secure IV
7+
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
8+
let encrypted = cipher.update(token, 'utf-8', 'hex');
9+
encrypted += cipher.final('hex');
10+
11+
return {
12+
token: encrypted,
13+
iv: iv.toString('hex'),
14+
};
15+
}
16+
17+
function decryptToken(encryptedToken, iv) {
18+
const encryptionKey = config.ENCRYPTION_KEY;
19+
const decipher = crypto.createDecipheriv(
20+
'aes-256-cbc',
21+
encryptionKey,
22+
Buffer.from(iv, 'hex')
23+
);
24+
let decrypted = decipher.update(encryptedToken, 'hex', 'utf-8');
25+
decrypted += decipher.final('utf-8');
26+
return decrypted;
27+
}
28+
29+
module.exports = {
30+
encryptToken,
31+
decryptToken,
32+
};

0 commit comments

Comments
 (0)