-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathauth.password.controller.js
More file actions
187 lines (175 loc) · 7.01 KB
/
Copy pathauth.password.controller.js
File metadata and controls
187 lines (175 loc) · 7.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* Module dependencies
*/
import jwt from 'jsonwebtoken';
import AuthService from '../services/auth.service.js';
import UserService from '../../users/services/users.service.js';
import mails from '../../../lib/helpers/mailer/index.js';
import getBaseUrl from '../../../lib/helpers/getBaseUrl.js';
import errors from '../../../lib/helpers/errors.js';
import responses from '../../../lib/helpers/responses.js';
import config from '../../../config/index.js';
import logger from '../../../lib/services/logger.js';
const tokenCookieOptions = {
httpOnly: true,
secure: config.cookie.secure,
sameSite: config.cookie.sameSite,
};
/**
* @desc Endpoint to init password reset mail
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<void>} Sends a JSON response with reset status.
*/
const forgot = async (req, res) => {
/**
* @returns {void} Sends a uniform 200 response regardless of account existence or mail outcome.
*/
const uniformSuccess = () => responses.success(res, 'If that email exists, a reset link has been sent.')({ status: true });
// check input
if (!req.body.email) return responses.error(res, 422, 'Unprocessable Entity', 'Mail field must not be blank')();
const email = String(req.body.email);
let user;
try {
user = await UserService.getBrut({ email });
} catch (err) {
return responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
}
// Unknown email or OAuth user — perform a dummy bcrypt compare to equalise
// response timing (anti-timing-oracle). The sentinel is a valid precomputed
// bcrypt hash (cost=10, 60 chars) so bcrypt runs the full KDF instead of
// short-circuiting on format validation.
if (!user || user.provider !== 'local') {
await AuthService.comparePassword(
'dummy-timing-equaliser',
'$2b$10$wotSb2xPb8VF8lpBkulk0.e2xvYvDedhjMyuG/7w3GdPl1vdvogY2',
);
return uniformSuccess();
}
// Known local user — generate token, persist, and send the reset email.
try {
const edit = {
resetPasswordToken: jwt.sign({ exp: Date.now() + 3600000 }, config.jwt.secret, { algorithm: 'HS256' }),
resetPasswordExpires: Date.now() + 3600000,
};
user = await UserService.update(user, edit, 'recover');
} catch (err) {
return responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
}
try {
const mail = await mails.sendMail({
template: 'reset-password-email',
to: user.email,
subject: 'Password Reset',
params: {
displayName: [user.firstName, user.lastName].filter(Boolean).join(' '),
url: `${getBaseUrl()}/reset?token=${user.resetPasswordToken}`,
appName: config.app.title,
appContact: config.app.contact,
},
});
if (!mail || !mail.accepted) {
logger.error('auth.password.forgot: mail not accepted', { email: user.email });
}
} catch (mailErr) {
logger.error('auth.password.forgot: sendMail threw', { message: mailErr?.message, stack: mailErr?.stack });
}
return uniformSuccess();
};
/**
* @desc Endpoint to validate Reset Token from link
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const validateResetToken = async (req, res) => {
try {
const user = await UserService.getBrut({ resetPasswordToken: String(req.params.token) });
if (!user || !user.email) return res.redirect('/api/password/reset/invalid');
res.redirect(`/api/password/reset/${req.params.token}`);
} catch (_err) {
return res.redirect('/api/password/reset/invalid');
}
};
/**
* @desc Endpoint to reset password from url with token
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<void>} Sends a JSON response with the updated user and token.
*/
const reset = async (req, res) => {
let user;
// check input
if (!req.body.token || !req.body.newPassword) return responses.error(res, 400, 'Bad Request', 'Password or Token fields must not be blank')();
const token = String(req.body.token);
const newPassword = String(req.body.newPassword);
// get user by token, update with new password, login again
try {
user = await UserService.getBrut({ resetPasswordToken: token });
if (!user || !user.email) return responses.error(res, 400, 'Bad Request', 'Password reset token is invalid or has expired.')();
const checkedPassword = AuthService.checkPassword(newPassword);
const edit = {
password: await AuthService.hashPassword(checkedPassword),
resetPasswordToken: null,
resetPasswordExpires: null,
};
user = await UserService.update(user, edit, 'recover');
// send confirmation mail (fire-and-forget)
mails.sendMail({
template: 'reset-password-confirm-email',
to: user.email,
subject: 'Your password has been changed',
params: {
displayName: [user.firstName, user.lastName].filter(Boolean).join(' '),
appName: config.app.title,
appContact: config.app.contact,
},
}).catch((err) => logger.warn('auth.password.reset: confirmation email failed', { message: err?.message, stack: err?.stack }));
return res
.status(200)
.cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), tokenCookieOptions)
.json({
user,
tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000,
type: 'sucess',
message: 'Password changed successfully',
});
} catch (err) {
return responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
}
};
/**
* Change Password
*/
const updatePassword = async (req, res) => {
let user;
let password;
// check input
if (!req.body.newPassword) return responses.error(res, 400, 'Bad Request', 'Please provide a new password')();
// get user, check password, update user, login again
try {
user = await UserService.getBrut({ id: req.user.id });
if (!user || !user.email) return responses.error(res, 400, 'Bad Request', 'User is not found')();
if (!(await AuthService.comparePassword(req.body.currentPassword, user.password)))
return responses.error(res, 422, 'Unprocessable Entity', 'Current password is incorrect')();
if (req.body.newPassword !== req.body.verifyPassword) return responses.error(res, 422, 'Unprocessable Entity', 'Passwords do not match')();
password = AuthService.checkPassword(req.body.newPassword);
user = await UserService.update(user, { password }, 'recover');
return res
.status(200)
.cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), tokenCookieOptions)
.json({
user,
tokenExpiresIn: Date.now() + config.jwt.expiresIn * 1000,
type: 'sucess',
message: 'Password changed successfully',
});
} catch (err) {
responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
}
};
export default {
forgot,
validateResetToken,
reset,
updatePassword,
};