1const bcrypt = require("bcrypt");
2
3const { promisify } = require("util");
4const { Op } = require("sequelize");
5const bcryptHashP = promisify(bcrypt.hash);
6const bcryptCompareP = promisify(bcrypt.compare);
7
8const { User, Post, Reply } = require("../models");
9const { siteName, makeRandomId, displayDate } = require("../helpers");
10
11const { sendEmail } = require("../email");
12
13const loginForm = (req, res) => {
14 if (req.session && req.session.user) {
15 res.redirect("/");
16 return;
17 }
18 if (req.query.next) {
19 req.session.nextUrl = req.query.next;
20 }
21 res.render("login", { title: "Log In" });
22};
23
24const signUpForm = (req, res) => {
25 if (req.session && req.session.user) {
26 res.redirect("/");
27 return;
28 }
29 res.render("signup", { title: "Sign Up" });
30};
31
32const forgotPasswordForm = (req, res) => {
33 if (req.session && req.session.user) {
34 res.redirect("/");
35 return;
36 }
37 res.render("forgot_password", { title: "Forgot Password" });
38};
39
40const userProfilePage = async (req, res, next) => {
41 try {
42 const userInfo = await User.findOne({
43 where: {
44 username: req.params.username,
45 },
46 attributes: [
47 "id",
48 "username",
49 "email",
50 "avatar",
51 "role",
52 "createdAt",
53 "website",
54 ],
55 });
56
57 if (!userInfo) {
58 const err = {
59 message: "User not found!",
60 status: 404,
61 };
62 next(err);
63 return;
64 }
65
66 let sameUser = false;
67 if (req.session.user.id == userInfo.id) {
68 sameUser = true;
69 }
70
71 const promises = [];
72
73 // post count
74 promises.push(
75 Post.count({
76 where: { createdByUser: userInfo.id },
77 }),
78 );
79 // reply count
80 promises.push(
81 Reply.count({
82 where: { createdByUser: userInfo.id },
83 }),
84 );
85
86 const [postCount, replyCount] = await Promise.all(promises);
87
88 userInfo.createdAtFormatted = displayDate(userInfo.createdAt);
89 userInfo.roleFormatted = userInfo.role;
90 if (userInfo.role === "regular") {
91 userInfo.roleFormatted = "user";
92 }
93
94 res.render("user_profile", {
95 title: `${userInfo.username}'s Profile`,
96 userInfo,
97 postCount,
98 replyCount,
99 sameUser,
100 });
101 return;
102 } catch (e) {
103 next(e);
104 console.log(e);
105 return;
106 }
107};
108
109const signupNewUser = async (req, res, next) => {
110 try {
111 const users = await User.findAll({
112 where: {
113 username: req.xop.username,
114 },
115 });
116
117 if (users.length) {
118 req.flash("error", ["Username already in use"]);
119 res.render("signup", {
120 title: "Sign Up",
121 body: req.body,
122 flashes: req.flash(),
123 });
124 return;
125 }
126
127 // we need to create the account and log the user in
128 const passwordHash = await bcryptHashP(req.body.password, 10);
129
130 const user = await User.create({
131 username: req.xop.username,
132 passwordHash,
133 email: req.xop.email,
134 role: process.env.FIRST_RUN ? "admin" : "regular",
135 });
136
137 // set session
138 req.session.user = {
139 id: user.id,
140 username: user.username,
141 };
142
143 let redirectUrl = "/";
144
145 if (req.session.nextUrl) {
146 redirectUrl = req.session.nextUrl;
147 delete req.session.nextUrl;
148 }
149
150 res.redirect(redirectUrl);
151
152 return;
153 } catch (e) {
154 next(e);
155 console.log(e);
156 return;
157 }
158};
159
160const loginUser = async (req, res, next) => {
161 try {
162 const user = await User.findOne({
163 where: {
164 username: req.xop.username,
165 },
166 });
167
168 if (!user) {
169 req.flash("error", ["User not found"]);
170 res.render("login", {
171 title: "Log In",
172 body: req.body,
173 flashes: req.flash(),
174 });
175 return;
176 }
177
178 const passwordsMatch = await bcryptCompareP(
179 req.xop.password,
180 user.passwordHash,
181 );
182
183 if (!passwordsMatch) {
184 req.flash("error", ["Invalid Password"]);
185 res.render("login", {
186 title: "Log In",
187 body: req.body,
188 flashes: req.flash(),
189 });
190 return;
191 }
192
193 // set session and redirect to /
194 req.session.user = {
195 id: user.id,
196 username: user.username,
197 };
198
199 let redirectUrl = "/";
200
201 if (req.session.nextUrl) {
202 redirectUrl = req.session.nextUrl;
203 delete req.session.nextUrl;
204 }
205
206 res.redirect(redirectUrl);
207 return;
208 } catch (e) {
209 next(e);
210 console.log(e);
211 return;
212 }
213};
214
215const logoutUser = (req, res, next) => {
216 req.session.destroy((err) => {
217 if (err) {
218 next(err);
219 return;
220 }
221 res.redirect("/");
222 });
223};
224
225const sendForgotPasswordMail = async (req, res, next) => {
226 try {
227 const user = await User.findOne({
228 where: {
229 email: req.xop.email,
230 },
231 attributes: ["id", "email", "username", "resetPasswordToken"],
232 });
233
234 if (!user) {
235 req.flash("error", ["Email not registered"]);
236 res.render("forgot_password", {
237 title: "Forgot Password",
238 body: req.body,
239 flashes: req.flash(),
240 });
241 return;
242 }
243
244 const resetToken = makeRandomId(8);
245 user.resetPasswordToken = resetToken;
246 await user.save();
247
248 const resetURL = `http://${req.headers.host}/reset-password?token=${resetToken}&email=${user.email}`;
249
250 let html = `<p>To reset your password, click <a href="${resetURL}">here</a></p>`;
251 html += `<p>Best,<br/>The ${siteName} admin</p>`;
252
253 let text = `Visit ${resetURL} to reset your password.`;
254 text += `Best, \r\nThe ${siteName} admin`;
255
256 const msg = {
257 to: user.email,
258 subject: `Reset password instructions: ${siteName}`,
259 text,
260 html,
261 };
262
263 await sendEmail(msg);
264 req.flash("success", ["Check email for reset instructions"]);
265 res.render("forgot_password", {
266 title: "Forgot Password",
267 body: req.body,
268 flashes: req.flash(),
269 });
270 return;
271 } catch (e) {
272 next(e);
273 console.log(e);
274 return;
275 }
276};
277
278const getResetPasswordForm = async (req, res, next) => {
279 try {
280 const user = await User.findOne({
281 where: {
282 email: req.xop.email,
283 resetPasswordToken: req.xop.token,
284 },
285 attributes: ["id", "resetPasswordToken", "email", "username"],
286 });
287
288 if (!user) {
289 req.flash("error", ["Email or reset token not valid"]);
290 res.render("forgot_password", {
291 title: "Forgot Password",
292 body: { email: req.xop.email },
293 flashes: req.flash(),
294 });
295 return;
296 }
297
298 const pageData = {
299 username: user.username,
300 email: req.xop.email,
301 token: req.xop.token,
302 };
303
304 req.flash("success", [`Hi, ${user.username}!`]);
305 res.render("reset_password", {
306 title: "Reset Password",
307 body: pageData,
308 flashes: req.flash(),
309 });
310 return;
311 } catch (e) {
312 next(e);
313 console.log(e);
314 return;
315 }
316};
317
318const resetPassword = async (req, res, next) => {
319 try {
320 const user = await User.findOne({
321 where: {
322 email: req.xop.email,
323 resetPasswordToken: req.xop.token,
324 },
325 attributes: ["id", "passwordHash", "resetPasswordToken"],
326 });
327
328 if (!user) {
329 req.flash("error", [`Email or reset token not found`]);
330 res.render("reset_password", {
331 title: "Reset Password",
332 body: req.body,
333 flashes: req.flash(),
334 });
335 return;
336 }
337
338 const passwordHash = await bcryptHashP(req.body.password, 10);
339 user.passwordHash = passwordHash;
340 user.resetPasswordToken = "";
341 await user.save();
342
343 req.flash("success", ["Password updated"]);
344 res.render("login", { title: "Log In", flashes: req.flash() });
345 return;
346 } catch (e) {
347 next(e);
348 console.log(e);
349 return;
350 }
351};
352
353const changeUserPassword = async (req, res, next) => {
354 try {
355 const user = await User.findOne({
356 where: {
357 id: req.session.user.id,
358 },
359 attributes: ["id", "passwordHash", "username"],
360 });
361
362 const passwordsMatch = await bcryptCompareP(
363 req.xop.password_old,
364 user.passwordHash,
365 );
366
367 if (!passwordsMatch) {
368 req.flash("error", ["Current password is incorrect"]);
369 const url = req.header("Referer") || `/u/${user.username}`;
370 res.redirect(url);
371 return;
372 }
373
374 const passwordHash = await bcryptHashP(req.xop.password, 10);
375 user.passwordHash = passwordHash;
376 user.resetPasswordToken = "";
377 await user.save();
378
379 req.flash("success", ["Password updated"]);
380 res.redirect("/logout");
381 return;
382 } catch (e) {
383 next(e);
384 console.log(e);
385 return;
386 }
387};
388
389const updateUserProfile = async (req, res, next) => {
390 try {
391 const user = await User.findOne({
392 where: {
393 id: req.session.user.id,
394 },
395 attributes: ["id", "avatar", "website", "username"],
396 });
397
398 user.website = req.xop.website;
399 user.avatar = req.xop.avatar;
400 await user.save();
401
402 req.flash("success", ["Profile updated"]);
403 const url = req.header("Referer") || `/u/${user.username}`;
404 res.redirect(url);
405 return;
406 } catch (e) {
407 next(e);
408 console.log(e);
409 return;
410 }
411};
412
413module.exports = {
414 loginForm,
415 signUpForm,
416 forgotPasswordForm,
417 userProfilePage,
418
419 signupNewUser,
420 loginUser,
421 logoutUser,
422 sendForgotPasswordMail,
423 getResetPasswordForm,
424 resetPassword,
425 changeUserPassword,
426 updateUserProfile,
427};