create-nodejs-express-starter 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +3 -0
- package/.editorconfig +9 -0
- package/.env.example +22 -0
- package/.eslintignore +2 -0
- package/.eslintrc.json +32 -0
- package/.gitignore +14 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +4 -0
- package/Dockerfile +15 -0
- package/LICENSE +21 -0
- package/README.md +440 -0
- package/bin/createNodejsApp.js +106 -0
- package/docker-compose.dev.yml +4 -0
- package/docker-compose.prod.yml +4 -0
- package/docker-compose.test.yml +4 -0
- package/docker-compose.yml +30 -0
- package/jest.config.js +9 -0
- package/package.json +117 -0
- package/src/app.js +82 -0
- package/src/config/config.js +64 -0
- package/src/config/logger.js +26 -0
- package/src/config/morgan.js +25 -0
- package/src/config/passport.js +30 -0
- package/src/config/roles.js +12 -0
- package/src/config/tokens.js +10 -0
- package/src/controllers/auth.controller.js +59 -0
- package/src/controllers/index.js +2 -0
- package/src/controllers/user.controller.js +43 -0
- package/src/docs/components.yml +92 -0
- package/src/docs/swaggerDef.js +21 -0
- package/src/index.js +57 -0
- package/src/middlewares/auth.js +33 -0
- package/src/middlewares/error.js +47 -0
- package/src/middlewares/rateLimiter.js +11 -0
- package/src/middlewares/requestId.js +14 -0
- package/src/middlewares/validate.js +21 -0
- package/src/models/index.js +2 -0
- package/src/models/plugins/index.js +2 -0
- package/src/models/plugins/paginate.plugin.js +70 -0
- package/src/models/plugins/toJSON.plugin.js +43 -0
- package/src/models/token.model.js +44 -0
- package/src/models/user.model.js +91 -0
- package/src/routes/v1/auth.route.js +291 -0
- package/src/routes/v1/docs.route.js +21 -0
- package/src/routes/v1/health.route.js +43 -0
- package/src/routes/v1/index.js +39 -0
- package/src/routes/v1/user.route.js +252 -0
- package/src/services/auth.service.js +99 -0
- package/src/services/email.service.js +63 -0
- package/src/services/index.js +4 -0
- package/src/services/token.service.js +123 -0
- package/src/services/user.service.js +89 -0
- package/src/utils/ApiError.js +14 -0
- package/src/utils/catchAsync.js +5 -0
- package/src/utils/pick.js +17 -0
- package/src/validations/auth.validation.js +60 -0
- package/src/validations/custom.validation.js +21 -0
- package/src/validations/index.js +2 -0
- package/src/validations/user.validation.js +54 -0
- package/tests/fixtures/token.fixture.js +14 -0
- package/tests/fixtures/user.fixture.js +46 -0
- package/tests/integration/auth.test.js +587 -0
- package/tests/integration/docs.test.js +14 -0
- package/tests/integration/health.test.js +32 -0
- package/tests/integration/user.test.js +625 -0
- package/tests/unit/middlewares/error.test.js +168 -0
- package/tests/unit/models/plugins/paginate.plugin.test.js +61 -0
- package/tests/unit/models/plugins/toJSON.plugin.test.js +89 -0
- package/tests/unit/models/user.model.test.js +57 -0
- package/tests/utils/setupTestDB.js +18 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const httpStatus = require('http-status');
|
|
2
|
+
const tokenService = require('./token.service');
|
|
3
|
+
const userService = require('./user.service');
|
|
4
|
+
const Token = require('../models/token.model');
|
|
5
|
+
const ApiError = require('../utils/ApiError');
|
|
6
|
+
const { tokenTypes } = require('../config/tokens');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Login with username and password
|
|
10
|
+
* @param {string} email
|
|
11
|
+
* @param {string} password
|
|
12
|
+
* @returns {Promise<User>}
|
|
13
|
+
*/
|
|
14
|
+
const loginUserWithEmailAndPassword = async (email, password) => {
|
|
15
|
+
const user = await userService.getUserByEmail(email);
|
|
16
|
+
if (!user || !(await user.isPasswordMatch(password))) {
|
|
17
|
+
throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password');
|
|
18
|
+
}
|
|
19
|
+
return user;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Logout
|
|
24
|
+
* @param {string} refreshToken
|
|
25
|
+
* @returns {Promise}
|
|
26
|
+
*/
|
|
27
|
+
const logout = async (refreshToken) => {
|
|
28
|
+
const refreshTokenDoc = await Token.findOne({ token: refreshToken, type: tokenTypes.REFRESH, blacklisted: false });
|
|
29
|
+
if (!refreshTokenDoc) {
|
|
30
|
+
throw new ApiError(httpStatus.NOT_FOUND, 'Not found');
|
|
31
|
+
}
|
|
32
|
+
await refreshTokenDoc.deleteOne();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Refresh auth tokens
|
|
37
|
+
* @param {string} refreshToken
|
|
38
|
+
* @returns {Promise<Object>}
|
|
39
|
+
*/
|
|
40
|
+
const refreshAuth = async (refreshToken) => {
|
|
41
|
+
try {
|
|
42
|
+
const refreshTokenDoc = await tokenService.verifyToken(refreshToken, tokenTypes.REFRESH);
|
|
43
|
+
const user = await userService.getUserById(refreshTokenDoc.user);
|
|
44
|
+
if (!user) {
|
|
45
|
+
throw new Error();
|
|
46
|
+
}
|
|
47
|
+
await refreshTokenDoc.deleteOne();
|
|
48
|
+
return tokenService.generateAuthTokens(user);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reset password
|
|
56
|
+
* @param {string} resetPasswordToken
|
|
57
|
+
* @param {string} newPassword
|
|
58
|
+
* @returns {Promise}
|
|
59
|
+
*/
|
|
60
|
+
const resetPassword = async (resetPasswordToken, newPassword) => {
|
|
61
|
+
try {
|
|
62
|
+
const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD);
|
|
63
|
+
const user = await userService.getUserById(resetPasswordTokenDoc.user);
|
|
64
|
+
if (!user) {
|
|
65
|
+
throw new Error();
|
|
66
|
+
}
|
|
67
|
+
await userService.updateUserById(user.id, { password: newPassword });
|
|
68
|
+
await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed');
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Verify email
|
|
76
|
+
* @param {string} verifyEmailToken
|
|
77
|
+
* @returns {Promise}
|
|
78
|
+
*/
|
|
79
|
+
const verifyEmail = async (verifyEmailToken) => {
|
|
80
|
+
try {
|
|
81
|
+
const verifyEmailTokenDoc = await tokenService.verifyToken(verifyEmailToken, tokenTypes.VERIFY_EMAIL);
|
|
82
|
+
const user = await userService.getUserById(verifyEmailTokenDoc.user);
|
|
83
|
+
if (!user) {
|
|
84
|
+
throw new Error();
|
|
85
|
+
}
|
|
86
|
+
await Token.deleteMany({ user: user.id, type: tokenTypes.VERIFY_EMAIL });
|
|
87
|
+
await userService.updateUserById(user.id, { isEmailVerified: true });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new ApiError(httpStatus.UNAUTHORIZED, 'Email verification failed');
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
loginUserWithEmailAndPassword,
|
|
95
|
+
logout,
|
|
96
|
+
refreshAuth,
|
|
97
|
+
resetPassword,
|
|
98
|
+
verifyEmail,
|
|
99
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const nodemailer = require('nodemailer');
|
|
2
|
+
const config = require('../config/config');
|
|
3
|
+
const logger = require('../config/logger');
|
|
4
|
+
|
|
5
|
+
const transport = nodemailer.createTransport(config.email.smtp);
|
|
6
|
+
/* istanbul ignore next */
|
|
7
|
+
if (config.env !== 'test') {
|
|
8
|
+
transport
|
|
9
|
+
.verify()
|
|
10
|
+
.then(() => logger.info('Connected to email server'))
|
|
11
|
+
.catch(() => logger.warn('Unable to connect to email server. Make sure you have configured the SMTP options in .env'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Send an email
|
|
16
|
+
* @param {string} to
|
|
17
|
+
* @param {string} subject
|
|
18
|
+
* @param {string} text
|
|
19
|
+
* @returns {Promise}
|
|
20
|
+
*/
|
|
21
|
+
const sendEmail = async (to, subject, text) => {
|
|
22
|
+
const msg = { from: config.email.from, to, subject, text };
|
|
23
|
+
await transport.sendMail(msg);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Send reset password email
|
|
28
|
+
* @param {string} to
|
|
29
|
+
* @param {string} token
|
|
30
|
+
* @returns {Promise}
|
|
31
|
+
*/
|
|
32
|
+
const sendResetPasswordEmail = async (to, token) => {
|
|
33
|
+
const subject = 'Reset password';
|
|
34
|
+
// replace this url with the link to the reset password page of your front-end app
|
|
35
|
+
const resetPasswordUrl = `http://link-to-app/reset-password?token=${token}`;
|
|
36
|
+
const text = `Dear user,
|
|
37
|
+
To reset your password, click on this link: ${resetPasswordUrl}
|
|
38
|
+
If you did not request any password resets, then ignore this email.`;
|
|
39
|
+
await sendEmail(to, subject, text);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Send verification email
|
|
44
|
+
* @param {string} to
|
|
45
|
+
* @param {string} token
|
|
46
|
+
* @returns {Promise}
|
|
47
|
+
*/
|
|
48
|
+
const sendVerificationEmail = async (to, token) => {
|
|
49
|
+
const subject = 'Email Verification';
|
|
50
|
+
// replace this url with the link to the email verification page of your front-end app
|
|
51
|
+
const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;
|
|
52
|
+
const text = `Dear user,
|
|
53
|
+
To verify your email, click on this link: ${verificationEmailUrl}
|
|
54
|
+
If you did not create an account, then ignore this email.`;
|
|
55
|
+
await sendEmail(to, subject, text);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
transport,
|
|
60
|
+
sendEmail,
|
|
61
|
+
sendResetPasswordEmail,
|
|
62
|
+
sendVerificationEmail,
|
|
63
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const moment = require('moment');
|
|
3
|
+
const httpStatus = require('http-status');
|
|
4
|
+
const config = require('../config/config');
|
|
5
|
+
const userService = require('./user.service');
|
|
6
|
+
const { Token } = require('../models');
|
|
7
|
+
const ApiError = require('../utils/ApiError');
|
|
8
|
+
const { tokenTypes } = require('../config/tokens');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate token
|
|
12
|
+
* @param {ObjectId} userId
|
|
13
|
+
* @param {Moment} expires
|
|
14
|
+
* @param {string} type
|
|
15
|
+
* @param {string} [secret]
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
const generateToken = (userId, expires, type, secret = config.jwt.secret) => {
|
|
19
|
+
const payload = {
|
|
20
|
+
sub: userId,
|
|
21
|
+
iat: moment().unix(),
|
|
22
|
+
exp: expires.unix(),
|
|
23
|
+
type,
|
|
24
|
+
};
|
|
25
|
+
return jwt.sign(payload, secret);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Save a token
|
|
30
|
+
* @param {string} token
|
|
31
|
+
* @param {ObjectId} userId
|
|
32
|
+
* @param {Moment} expires
|
|
33
|
+
* @param {string} type
|
|
34
|
+
* @param {boolean} [blacklisted]
|
|
35
|
+
* @returns {Promise<Token>}
|
|
36
|
+
*/
|
|
37
|
+
const saveToken = async (token, userId, expires, type, blacklisted = false) => {
|
|
38
|
+
const tokenDoc = await Token.create({
|
|
39
|
+
token,
|
|
40
|
+
user: userId,
|
|
41
|
+
expires: expires.toDate(),
|
|
42
|
+
type,
|
|
43
|
+
blacklisted,
|
|
44
|
+
});
|
|
45
|
+
return tokenDoc;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Verify token and return token doc (or throw an error if it is not valid)
|
|
50
|
+
* @param {string} token
|
|
51
|
+
* @param {string} type
|
|
52
|
+
* @returns {Promise<Token>}
|
|
53
|
+
*/
|
|
54
|
+
const verifyToken = async (token, type) => {
|
|
55
|
+
const payload = jwt.verify(token, config.jwt.secret);
|
|
56
|
+
const tokenDoc = await Token.findOne({ token, type, user: payload.sub, blacklisted: false });
|
|
57
|
+
if (!tokenDoc) {
|
|
58
|
+
throw new Error('Token not found');
|
|
59
|
+
}
|
|
60
|
+
return tokenDoc;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate auth tokens
|
|
65
|
+
* @param {User} user
|
|
66
|
+
* @returns {Promise<Object>}
|
|
67
|
+
*/
|
|
68
|
+
const generateAuthTokens = async (user) => {
|
|
69
|
+
const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
|
|
70
|
+
const accessToken = generateToken(user.id, accessTokenExpires, tokenTypes.ACCESS);
|
|
71
|
+
|
|
72
|
+
const refreshTokenExpires = moment().add(config.jwt.refreshExpirationDays, 'days');
|
|
73
|
+
const refreshToken = generateToken(user.id, refreshTokenExpires, tokenTypes.REFRESH);
|
|
74
|
+
await saveToken(refreshToken, user.id, refreshTokenExpires, tokenTypes.REFRESH);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
access: {
|
|
78
|
+
token: accessToken,
|
|
79
|
+
expires: accessTokenExpires.toDate(),
|
|
80
|
+
},
|
|
81
|
+
refresh: {
|
|
82
|
+
token: refreshToken,
|
|
83
|
+
expires: refreshTokenExpires.toDate(),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate reset password token
|
|
90
|
+
* @param {string} email
|
|
91
|
+
* @returns {Promise<string>}
|
|
92
|
+
*/
|
|
93
|
+
const generateResetPasswordToken = async (email) => {
|
|
94
|
+
const user = await userService.getUserByEmail(email);
|
|
95
|
+
if (!user) {
|
|
96
|
+
throw new ApiError(httpStatus.NOT_FOUND, 'No users found with this email');
|
|
97
|
+
}
|
|
98
|
+
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
|
|
99
|
+
const resetPasswordToken = generateToken(user.id, expires, tokenTypes.RESET_PASSWORD);
|
|
100
|
+
await saveToken(resetPasswordToken, user.id, expires, tokenTypes.RESET_PASSWORD);
|
|
101
|
+
return resetPasswordToken;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate verify email token
|
|
106
|
+
* @param {User} user
|
|
107
|
+
* @returns {Promise<string>}
|
|
108
|
+
*/
|
|
109
|
+
const generateVerifyEmailToken = async (user) => {
|
|
110
|
+
const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
|
|
111
|
+
const verifyEmailToken = generateToken(user.id, expires, tokenTypes.VERIFY_EMAIL);
|
|
112
|
+
await saveToken(verifyEmailToken, user.id, expires, tokenTypes.VERIFY_EMAIL);
|
|
113
|
+
return verifyEmailToken;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
generateToken,
|
|
118
|
+
saveToken,
|
|
119
|
+
verifyToken,
|
|
120
|
+
generateAuthTokens,
|
|
121
|
+
generateResetPasswordToken,
|
|
122
|
+
generateVerifyEmailToken,
|
|
123
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const httpStatus = require('http-status');
|
|
2
|
+
const { User } = require('../models');
|
|
3
|
+
const ApiError = require('../utils/ApiError');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a user
|
|
7
|
+
* @param {Object} userBody
|
|
8
|
+
* @returns {Promise<User>}
|
|
9
|
+
*/
|
|
10
|
+
const createUser = async (userBody) => {
|
|
11
|
+
if (await User.isEmailTaken(userBody.email)) {
|
|
12
|
+
throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken');
|
|
13
|
+
}
|
|
14
|
+
return User.create(userBody);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Query for users
|
|
19
|
+
* @param {Object} filter - Mongo filter
|
|
20
|
+
* @param {Object} options - Query options
|
|
21
|
+
* @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc)
|
|
22
|
+
* @param {number} [options.limit] - Maximum number of results per page (default = 10)
|
|
23
|
+
* @param {number} [options.page] - Current page (default = 1)
|
|
24
|
+
* @returns {Promise<QueryResult>}
|
|
25
|
+
*/
|
|
26
|
+
const queryUsers = async (filter, options) => {
|
|
27
|
+
const users = await User.paginate(filter, options);
|
|
28
|
+
return users;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get user by id
|
|
33
|
+
* @param {ObjectId} id
|
|
34
|
+
* @returns {Promise<User>}
|
|
35
|
+
*/
|
|
36
|
+
const getUserById = async (id) => {
|
|
37
|
+
return User.findById(id);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get user by email
|
|
42
|
+
* @param {string} email
|
|
43
|
+
* @returns {Promise<User>}
|
|
44
|
+
*/
|
|
45
|
+
const getUserByEmail = async (email) => {
|
|
46
|
+
return User.findOne({ email });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update user by id
|
|
51
|
+
* @param {ObjectId} userId
|
|
52
|
+
* @param {Object} updateBody
|
|
53
|
+
* @returns {Promise<User>}
|
|
54
|
+
*/
|
|
55
|
+
const updateUserById = async (userId, updateBody) => {
|
|
56
|
+
const user = await getUserById(userId);
|
|
57
|
+
if (!user) {
|
|
58
|
+
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
|
59
|
+
}
|
|
60
|
+
if (updateBody.email && (await User.isEmailTaken(updateBody.email, userId))) {
|
|
61
|
+
throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken');
|
|
62
|
+
}
|
|
63
|
+
Object.assign(user, updateBody);
|
|
64
|
+
await user.save();
|
|
65
|
+
return user;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Delete user by id
|
|
70
|
+
* @param {ObjectId} userId
|
|
71
|
+
* @returns {Promise<User>}
|
|
72
|
+
*/
|
|
73
|
+
const deleteUserById = async (userId) => {
|
|
74
|
+
const user = await getUserById(userId);
|
|
75
|
+
if (!user) {
|
|
76
|
+
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
|
77
|
+
}
|
|
78
|
+
await user.deleteOne();
|
|
79
|
+
return user;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
createUser,
|
|
84
|
+
queryUsers,
|
|
85
|
+
getUserById,
|
|
86
|
+
getUserByEmail,
|
|
87
|
+
updateUserById,
|
|
88
|
+
deleteUserById,
|
|
89
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class ApiError extends Error {
|
|
2
|
+
constructor(statusCode, message, isOperational = true, stack = '') {
|
|
3
|
+
super(message);
|
|
4
|
+
this.statusCode = statusCode;
|
|
5
|
+
this.isOperational = isOperational;
|
|
6
|
+
if (stack) {
|
|
7
|
+
this.stack = stack;
|
|
8
|
+
} else {
|
|
9
|
+
Error.captureStackTrace(this, this.constructor);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = ApiError;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an object composed of the picked object properties
|
|
3
|
+
* @param {Object} object
|
|
4
|
+
* @param {string[]} keys
|
|
5
|
+
* @returns {Object}
|
|
6
|
+
*/
|
|
7
|
+
const pick = (object, keys) => {
|
|
8
|
+
return keys.reduce((obj, key) => {
|
|
9
|
+
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
|
|
10
|
+
// eslint-disable-next-line no-param-reassign
|
|
11
|
+
obj[key] = object[key];
|
|
12
|
+
}
|
|
13
|
+
return obj;
|
|
14
|
+
}, {});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = pick;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const Joi = require('joi');
|
|
2
|
+
const { password } = require('./custom.validation');
|
|
3
|
+
|
|
4
|
+
const register = {
|
|
5
|
+
body: Joi.object().keys({
|
|
6
|
+
email: Joi.string().required().email(),
|
|
7
|
+
password: Joi.string().required().custom(password),
|
|
8
|
+
name: Joi.string().required(),
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const login = {
|
|
13
|
+
body: Joi.object().keys({
|
|
14
|
+
email: Joi.string().required(),
|
|
15
|
+
password: Joi.string().required(),
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const logout = {
|
|
20
|
+
body: Joi.object().keys({
|
|
21
|
+
refreshToken: Joi.string().required(),
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const refreshTokens = {
|
|
26
|
+
body: Joi.object().keys({
|
|
27
|
+
refreshToken: Joi.string().required(),
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const forgotPassword = {
|
|
32
|
+
body: Joi.object().keys({
|
|
33
|
+
email: Joi.string().email().required(),
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const resetPassword = {
|
|
38
|
+
query: Joi.object().keys({
|
|
39
|
+
token: Joi.string().required(),
|
|
40
|
+
}),
|
|
41
|
+
body: Joi.object().keys({
|
|
42
|
+
password: Joi.string().required().custom(password),
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const verifyEmail = {
|
|
47
|
+
query: Joi.object().keys({
|
|
48
|
+
token: Joi.string().required(),
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
register,
|
|
54
|
+
login,
|
|
55
|
+
logout,
|
|
56
|
+
refreshTokens,
|
|
57
|
+
forgotPassword,
|
|
58
|
+
resetPassword,
|
|
59
|
+
verifyEmail,
|
|
60
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const objectId = (value, helpers) => {
|
|
2
|
+
if (!value.match(/^[0-9a-fA-F]{24}$/)) {
|
|
3
|
+
return helpers.message('"{{#label}}" must be a valid mongo id');
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const password = (value, helpers) => {
|
|
9
|
+
if (value.length < 8) {
|
|
10
|
+
return helpers.message('password must be at least 8 characters');
|
|
11
|
+
}
|
|
12
|
+
if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) {
|
|
13
|
+
return helpers.message('password must contain at least 1 letter and 1 number');
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
objectId,
|
|
20
|
+
password,
|
|
21
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const Joi = require('joi');
|
|
2
|
+
const { password, objectId } = require('./custom.validation');
|
|
3
|
+
|
|
4
|
+
const createUser = {
|
|
5
|
+
body: Joi.object().keys({
|
|
6
|
+
email: Joi.string().required().email(),
|
|
7
|
+
password: Joi.string().required().custom(password),
|
|
8
|
+
name: Joi.string().required(),
|
|
9
|
+
role: Joi.string().required().valid('user', 'admin'),
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getUsers = {
|
|
14
|
+
query: Joi.object().keys({
|
|
15
|
+
name: Joi.string(),
|
|
16
|
+
role: Joi.string(),
|
|
17
|
+
sortBy: Joi.string(),
|
|
18
|
+
limit: Joi.number().integer(),
|
|
19
|
+
page: Joi.number().integer(),
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getUser = {
|
|
24
|
+
params: Joi.object().keys({
|
|
25
|
+
userId: Joi.string().custom(objectId),
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const updateUser = {
|
|
30
|
+
params: Joi.object().keys({
|
|
31
|
+
userId: Joi.required().custom(objectId),
|
|
32
|
+
}),
|
|
33
|
+
body: Joi.object()
|
|
34
|
+
.keys({
|
|
35
|
+
email: Joi.string().email(),
|
|
36
|
+
password: Joi.string().custom(password),
|
|
37
|
+
name: Joi.string(),
|
|
38
|
+
})
|
|
39
|
+
.min(1),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const deleteUser = {
|
|
43
|
+
params: Joi.object().keys({
|
|
44
|
+
userId: Joi.string().custom(objectId),
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
createUser,
|
|
50
|
+
getUsers,
|
|
51
|
+
getUser,
|
|
52
|
+
updateUser,
|
|
53
|
+
deleteUser,
|
|
54
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const moment = require('moment');
|
|
2
|
+
const config = require('../../src/config/config');
|
|
3
|
+
const { tokenTypes } = require('../../src/config/tokens');
|
|
4
|
+
const tokenService = require('../../src/services/token.service');
|
|
5
|
+
const { userOne, admin } = require('./user.fixture');
|
|
6
|
+
|
|
7
|
+
const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
|
|
8
|
+
const userOneAccessToken = tokenService.generateToken(userOne._id, accessTokenExpires, tokenTypes.ACCESS);
|
|
9
|
+
const adminAccessToken = tokenService.generateToken(admin._id, accessTokenExpires, tokenTypes.ACCESS);
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
userOneAccessToken,
|
|
13
|
+
adminAccessToken,
|
|
14
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const { faker } = require('@faker-js/faker');
|
|
4
|
+
const User = require('../../src/models/user.model');
|
|
5
|
+
|
|
6
|
+
const password = 'password1';
|
|
7
|
+
const salt = bcrypt.genSaltSync(8);
|
|
8
|
+
const hashedPassword = bcrypt.hashSync(password, salt);
|
|
9
|
+
|
|
10
|
+
const userOne = {
|
|
11
|
+
_id: new mongoose.Types.ObjectId(),
|
|
12
|
+
name: faker.person.fullName(),
|
|
13
|
+
email: faker.internet.email().toLowerCase(),
|
|
14
|
+
password,
|
|
15
|
+
role: 'user',
|
|
16
|
+
isEmailVerified: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const userTwo = {
|
|
20
|
+
_id: new mongoose.Types.ObjectId(),
|
|
21
|
+
name: faker.person.fullName(),
|
|
22
|
+
email: faker.internet.email().toLowerCase(),
|
|
23
|
+
password,
|
|
24
|
+
role: 'user',
|
|
25
|
+
isEmailVerified: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const admin = {
|
|
29
|
+
_id: new mongoose.Types.ObjectId(),
|
|
30
|
+
name: faker.person.fullName(),
|
|
31
|
+
email: faker.internet.email().toLowerCase(),
|
|
32
|
+
password,
|
|
33
|
+
role: 'admin',
|
|
34
|
+
isEmailVerified: false,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const insertUsers = async (users) => {
|
|
38
|
+
await User.insertMany(users.map((user) => ({ ...user, password: hashedPassword })));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
userOne,
|
|
43
|
+
userTwo,
|
|
44
|
+
admin,
|
|
45
|
+
insertUsers,
|
|
46
|
+
};
|