Implement password reset (except trigger it). Add good password validation. Fix responses / flow for request token endpoints
Seva
4 years ago
29 | 29 | "update": { |
30 | 30 | "display-name": function(req, res) { |
31 | 31 | new Promise(function(resolve, reject) { |
32 | if (req.body.display_name) { | |
32 | if (req.body.display_name && req.body.display_name.trim()) { | |
33 | 33 | resolve(); |
34 | 34 | } else { |
35 | reject("Could not update username"); | |
35 | reject("Could not update username: Username cannot be empty"); | |
36 | 36 | } |
37 | 37 | }).then(function() { |
38 | 38 | return utils.query("UPDATE users SET display_name=? WHERE user_id=?", [ |
52 | 52 | }, |
53 | 53 | "password": function(req, res) { |
54 | 54 | new Promise(function(resolve, reject) { |
55 | if (req.body.new_password && req.body.current_password) { | |
55 | if (utils.validate.password(req.body.new_password)) { | |
56 | 56 | resolve(); |
57 | 57 | } else { |
58 | reject("Could not update username"); | |
58 | reject("New password invalid"); | |
59 | 59 | } |
60 | 60 | }).then(function() { |
61 | return bcrypt.compare(req.body.current_password, req.user.password); | |
61 | if (req.body.current_password) { | |
62 | return bcrypt.compare(req.body.current_password, req.user.password); | |
63 | } | |
64 | return false; | |
62 | 65 | }).then(function(result) { |
63 | 66 | return new Promise(function(resolve, reject) { |
64 | 67 | if (result) { |
89 | 92 | "logout": function(req, res) { |
90 | 93 | req.logout(); |
91 | 94 | res.send(utils.ok()); |
95 | }, | |
96 | "password-reset": { | |
97 | "": [{ | |
98 | auth: false | |
99 | }, function(req, res) { | |
100 | let email; | |
101 | ||
102 | new Promise(function(resolve, reject) { | |
103 | if (utils.validate.password(req.body.new_password)) { | |
104 | resolve(); | |
105 | } else { | |
106 | reject("New password invalid"); | |
107 | } | |
108 | }).then(function() { | |
109 | if (req.body.token) { | |
110 | return utils.query("SELECT * FROM password_reset WHERE reset_id=? AND expires>?", [ | |
111 | req.body.token, Date.now() | |
112 | ]); | |
113 | } | |
114 | return []; | |
115 | }).then(function(rows) { | |
116 | return new Promise(function(resolve, reject) { | |
117 | if (rows.length) { | |
118 | email = rows[0].email; | |
119 | resolve(); | |
120 | } else { | |
121 | reject("Token Invalid"); | |
122 | } | |
123 | }); | |
124 | }).then(function() { | |
125 | return bcrypt.hash(req.body.new_password, 10); | |
126 | }).then(function(hashword) { | |
127 | return utils.query("UPDATE users SET password=? WHERE email=?", [ | |
128 | hashword, email | |
129 | ]); | |
130 | }).then(function() { | |
131 | return utils.query("DELETE FROM password_reset WHERE ?", { | |
132 | reset_id: req.body.token | |
133 | }); | |
134 | }).then(function() { | |
135 | res.send(utils.ok({ | |
136 | email: email | |
137 | })); | |
138 | }).catch(function(error) { | |
139 | console.error(error); | |
140 | res.send(utils.error({ | |
141 | message: error || "Could not update password", | |
142 | type: "error" | |
143 | })); | |
144 | }); | |
145 | }], | |
146 | "request": [{ | |
147 | auth: false | |
148 | }, function(req, res) { | |
149 | let email; | |
150 | ||
151 | utils.query('SELECT email FROM password_reset WHERE reset_id=? AND expires<?', [ | |
152 | req.body.token, Date.now() | |
153 | ]).then(function(rows) { | |
154 | return new Promise(function(resolve, reject) { | |
155 | if (rows.length) { | |
156 | reject("There is already a valid password reset link, please check your inbox"); | |
157 | } else { | |
158 | email = rows[0].email; | |
159 | resolve(); | |
160 | } | |
161 | }); | |
162 | }).then(function() { | |
163 | return utils.generate_token("password_reset", "reset_id", email); | |
164 | }).then(function(token) { | |
165 | return utils.email.send_password_reset_email(email, { | |
166 | token: token | |
167 | }).catch(function(error) { | |
168 | // If the confirmation email fails to send, we need to delete the row | |
169 | // from the email_confirmations table so that it can be sent again. | |
170 | console.error(error); | |
171 | return utils.query("DELETE FROM password_reset WHERE ?", { | |
172 | reset_id: token | |
173 | }).then(function() { | |
174 | return new Promise(function(resolve, reject) { | |
175 | reject(error); | |
176 | }); | |
177 | }); | |
178 | }); | |
179 | }).then(function() { | |
180 | return res.send(utils.ok({ | |
181 | message: "Password reset email sent, please check your inbox" | |
182 | })); | |
183 | }).catch(function(error) { | |
184 | console.error(error); | |
185 | res.send(utils.error({ | |
186 | message: error, | |
187 | type: "error" | |
188 | })); | |
189 | }); | |
190 | }] | |
92 | 191 | } |
93 | 192 | } |
94 | 193 | };⏎ |
24 | 24 | if (!req.body.password || |
25 | 25 | !req.body.email || |
26 | 26 | !req.body.display_name || |
27 | !req.body.reg_id | |
27 | !req.body.token | |
28 | 28 | ) { |
29 | 29 | return res.send(utils.error({ |
30 | 30 | message: "Invalid request", |
51 | 51 | |
52 | 52 | // Look for a registration session matching the reg_id |
53 | 53 | utils.query('SELECT * FROM registrations WHERE ?', { |
54 | reg_id: req.body.reg_id | |
54 | reg_id: req.body.token | |
55 | 55 | }).then(function(rows) { |
56 | 56 | return new Promise(function(resolve, reject) { |
57 | 57 | if (!rows.length) { |
95 | 95 | }).then(function() { |
96 | 96 | // Delete the registration session, the new user is created |
97 | 97 | return utils.query('DELETE FROM registrations WHERE ?', { |
98 | reg_id: req.body.reg_id | |
98 | reg_id: req.body.token | |
99 | 99 | }); |
100 | 100 | }).then(function() { |
101 | 101 | // Create a confirmation session and send the confirmation email |
107 | 107 | }); |
108 | 108 | }).then(function() { |
109 | 109 | return utils.email.send_confirmation_email(req.body.email, { |
110 | confirmation_id: confirmation_id | |
110 | token: confirmation_id | |
111 | 111 | }); |
112 | 112 | }).then(function() { |
113 | 113 | // Finally we are done |
127 | 127 | auth: false |
128 | 128 | }, function(req, res) { |
129 | 129 | utils.query('SELECT * FROM email_confirmations WHERE ?', { |
130 | confirmation_id: req.body.confirmation_id | |
130 | confirmation_id: req.body.token | |
131 | 131 | }).then(function(rows) { |
132 | 132 | return new Promise(function(resolve, reject) { |
133 | 133 | if (!rows.length) { |
147 | 147 | ]); |
148 | 148 | }).then(function() { |
149 | 149 | return utils.query("DELETE FROM email_confirmations WHERE ?", { |
150 | confirmation_id: req.body.confirmation_id | |
150 | confirmation_id: req.body.token | |
151 | 151 | }); |
152 | 152 | }).then(function() { |
153 | 153 | res.send(utils.ok({ |
167 | 167 | let email; |
168 | 168 | |
169 | 169 | utils.query('SELECT email FROM email_confirmations WHERE confirmation_id=? AND expires<?', [ |
170 | req.body.confirmation_id, Date.now() | |
170 | req.body.token, Date.now() | |
171 | 171 | ]).then(function(rows) { |
172 | 172 | return new Promise(function(resolve, reject) { |
173 | 173 | if (rows.length) { |
174 | reject("There is already a valid confirmation token, please check your inbox"); | |
174 | reject("There is already a valid confirmation link, please check your inbox"); | |
175 | 175 | } else { |
176 | 176 | email = rows[0].email; |
177 | 177 | resolve(); |
178 | 178 | } |
179 | 179 | }); |
180 | 180 | }).then(function() { |
181 | return utils.email.generate_new_confirmation_token(email); | |
182 | }).then(function(confirmation_id) { | |
181 | return utils.generate_token("email_confirmations", "confirmation_id", email); | |
182 | }).then(function(token) { | |
183 | 183 | return utils.email.send_confirmation_email(email, { |
184 | confirmation_id: confirmation_id | |
185 | }); | |
186 | }).then(function() { | |
187 | res.send(utils.ok({ | |
184 | token: token | |
185 | }).catch(function(error) { | |
186 | // If the confirmation email fails to send, we need to delete the row | |
187 | // from the email_confirmations table so that it can be sent again. | |
188 | console.error(error); | |
189 | return utils.query("DELETE FROM email_confirmations WHERE ?", { | |
190 | confirmation_id: token | |
191 | }).then(function() { | |
192 | return new Promise(function(resolve, reject) { | |
193 | reject(error); | |
194 | }); | |
195 | }); | |
196 | }); | |
197 | }).then(function() { | |
198 | return res.send(utils.ok({ | |
188 | 199 | message: "New confirmation email sent, please check your inbox" |
189 | 200 | })); |
190 | 201 | }).catch(function(error) { |
194 | 205 | type: "error" |
195 | 206 | })); |
196 | 207 | }); |
197 | }], | |
208 | }] | |
198 | 209 | } |
199 | 210 | } |
200 | 211 | };⏎ |
18 | 18 | auth: false |
19 | 19 | }, function(req, res) { |
20 | 20 | res.sendFile(path.join(HTML_ROOT, "confirm-email.html")); |
21 | }], | |
22 | "password-reset": [{ | |
23 | auth: false | |
24 | }, function(req, res) { | |
25 | res.sendFile(path.join(HTML_ROOT, "password-reset.html")); | |
21 | 26 | }] |
22 | 27 | } |
23 | 28 | };⏎ |
12 | 12 | module.exports = function(utils) { |
13 | 13 | return { |
14 | 14 | send_confirmation_email: function(recipient, query) { |
15 | return generateEmail("confirmation.html", { | |
15 | return generate_email("confirmation.html", { | |
16 | 16 | query: qs.encode(query) |
17 | 17 | }).then(function(html) { |
18 | 18 | return mg.messages().send({ |
24 | 24 | }); |
25 | 25 | }, |
26 | 26 | send_password_reset_email: function(recipient, query) { |
27 | return generateEmail("password_reset.html", { | |
27 | return generate_email("password_reset.html", { | |
28 | 28 | query: qs.encode(query) |
29 | 29 | }).then(function(html) { |
30 | 30 | return mg.messages().send({ |
34 | 34 | html: html |
35 | 35 | }); |
36 | 36 | }); |
37 | }, | |
38 | generate_new_confirmation_token: function(email) { | |
39 | let confirmation_id = utils.uuid(); | |
40 | // So there either is no token at all, or there is one but it has expired | |
41 | return utils.query( | |
42 | "INSERT INTO email_confirmations (confirmation_id, email, expires)" + | |
43 | "VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE confirmation_id=VALUES(confirmation_id), expires=VALUES(expires)", | |
44 | [confirmation_id, email, Date.now() + date.one_day] | |
45 | ).then(function() { | |
46 | return confirmation_id; | |
47 | }); | |
48 | 37 | } |
49 | 38 | }; |
50 | 39 | }; |
51 | 40 | |
52 | function generateEmail(name, params) { | |
41 | function generate_email(name, params) { | |
53 | 42 | return new Promise(function(resolve, reject) { |
54 | 43 | fs.readFile(path.resolve(__dirname, name), function(error, data) { |
55 | 44 | if (error) { |
2 | 2 | } = require('uuid'); |
3 | 3 | const fs = require('fs'); |
4 | 4 | const getSize = require('get-folder-size'); |
5 | const date = require('./date'); | |
5 | 6 | const status_codes = require('../../status.json'); |
6 | 7 | const max_video_dir_size = 25000000000; // 20 GB |
7 | 8 | |
55 | 56 | // 1 Number |
56 | 57 | return str && str.match(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/gm) !== null; |
57 | 58 | } |
59 | }, | |
60 | generate_token: function(table, key, email) { | |
61 | let token = uuid(); | |
62 | // So there either is no token at all, or there is one but it has expired | |
63 | return query( | |
64 | `INSERT INTO ${table} (${key}, email, expires) ` + | |
65 | `VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE ${key}=VALUES(${key}), expires=VALUES(expires)`, | |
66 | [token, email, Date.now() + date.one_day] | |
67 | ).then(function() { | |
68 | return token; | |
69 | }); | |
58 | 70 | }, |
59 | 71 | get_storage_remaining: function() { |
60 | 72 | return new Promise(function(resolve, reject) { |
80 | 80 | if (!rows.length || rows[0].expires < Date.now()) { |
81 | 81 | // User was registered but they never got a |
82 | 82 | // token generated (registered through CLI) |
83 | // or they just haven't confirmed their email yet | |
84 | utils.email.generate_new_confirmation_token(email).then(function(confirmation_id) { | |
85 | utils.email.send_confirmation_email(email, { | |
86 | confirmation_id: confirmation_id | |
87 | }); | |
83 | // or they haven't confirmed their email yet | |
84 | // and their token has expired. | |
85 | return utils.email.generate_new_confirmation_token(email); | |
86 | } | |
87 | }).then(function(token) { | |
88 | if (token) { | |
89 | return utils.email.send_confirmation_email(email, { | |
90 | token: token | |
88 | 91 | }); |
89 | } // else there is already a valid token, they just need to confirm | |
90 | ||
92 | } | |
93 | // else no token was generated which means there is | |
94 | // already a valid token, they just need to confirm | |
95 | }).then(function() { | |
91 | 96 | done(null, false, { |
92 | 97 | message: 'You need to confirm your email, please check your inbox' |
93 | 98 | }); |
99 | }).catch(function(error) { | |
100 | console.error(error); | |
101 | done(error); | |
94 | 102 | }); |
95 | 103 | } |
96 | 104 | } |
1 | 1 | <html> |
2 | 2 | |
3 | 3 | <head> |
4 | <title>Homepage</title> | |
4 | <title>Watch Together</title> | |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
6 | 6 | <link href="https://fonts.googleapis.com/css2?family=Asap&family=VT323&display=swap" rel="stylesheet"> |
7 | 7 | <link href="../css/style.css" rel="stylesheet"> |
1 | 1 | <html> |
2 | 2 | |
3 | 3 | <head> |
4 | <title>Login to Watch Together Service</title> | |
4 | <title>Watch Together - Login</title> | |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
6 | 6 | <link href="https://fonts.googleapis.com/css2?family=Asap&family=VT323&display=swap" rel="stylesheet"> |
7 | 7 | <link href="../css/style.css" rel="stylesheet"> |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | <title>Watch Together - Password Reset</title> | |
5 | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
6 | <link href="https://fonts.googleapis.com/css2?family=Asap&family=VT323&display=swap" rel="stylesheet"> | |
7 | <link href="../css/style.css" rel="stylesheet"> | |
8 | </head> | |
9 | ||
10 | <body> | |
11 | <div class="banner"></div> | |
12 | <div class="centered-form"> | |
13 | <div class="field-wrapper"> | |
14 | <label>New Password:</label> | |
15 | <input type="password" id="new_password" /> | |
16 | </div> | |
17 | <div class="field-wrapper"> | |
18 | <label>Confirm Password:</label> | |
19 | <input type="password" id="confirm_password" /> | |
20 | </div> | |
21 | <button style="float: right; margin: 10px;" id="update_password_button">Update Password</button> | |
22 | </div> | |
23 | <script src="../js/password-reset.js"></script> | |
24 | </body> | |
25 | ||
26 | </html>⏎ |
1 | 1 | <html> |
2 | 2 | |
3 | 3 | <head> |
4 | <title>Register For Watch Together Service</title> | |
4 | <title>Watch Together - Register</title> | |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
6 | 6 | <link href="https://fonts.googleapis.com/css2?family=Asap&family=VT323&display=swap" rel="stylesheet"> |
7 | 7 | <link href="../css/style.css" rel="stylesheet"> |
11 | 11 | |
12 | 12 | $("#request_new_confirmation").click(function() { |
13 | 13 | api.post("/registration/confirm-email/request", { |
14 | confirmation_id: qs_data.confirmation_id | |
14 | token: qs_data.token | |
15 | 15 | }).then(function(data) { |
16 | 16 | utils.show_banner(data.message); |
17 | 17 | }).catch(function(data) { |
20 | 20 | }); |
21 | 21 | }); |
22 | 22 | |
23 | if (qs_data.confirmation_id) { | |
23 | if (qs_data.token) { | |
24 | 24 | api.post("/registration/confirm-email", { |
25 | confirmation_id: qs_data.confirmation_id | |
25 | token: qs_data.token | |
26 | 26 | }).then(function(data) { |
27 | 27 | $("#success_message").text(data.message); |
28 | 28 | $("#loading").hide(); |
0 | const $ = require('jquery'); | |
1 | const api = require('./utils/api'); | |
2 | const utils = require('./utils'); | |
3 | const qs = require('querystring'); | |
4 | ||
5 | let qs_data = qs.parse(location.search.substr(1)); | |
6 | console.log(qs_data); | |
7 | ||
8 | $("#update_password_button").click(function() { | |
9 | let new_password = $("#new_password").val(); | |
10 | let confirm_password = $("#confirm_password").val(); | |
11 | ||
12 | if (new_password !== confirm_password) { | |
13 | utils.show_banner("Passwords don't match", "error"); | |
14 | } else if (!utils.validate.password.length(new_password)) { | |
15 | utils.show_banner("Password should be at least 8 charactors", "error"); | |
16 | } else if (!utils.validate.password.letters(new_password)) { | |
17 | utils.show_banner("Password should have at least 1 letter", "error"); | |
18 | } else if (!utils.validate.password.numbers(new_password)) { | |
19 | utils.show_banner("Password should have at least 1 number", "error"); | |
20 | } else { | |
21 | api.post("/account/password-reset", { | |
22 | token: qs_data.token, | |
23 | password: new_password | |
24 | }).then(function(data) { | |
25 | return api.post("/account/login", { | |
26 | email: data.email, | |
27 | password: new_password | |
28 | }); | |
29 | }).then(function() { | |
30 | window.location = "/"; | |
31 | }).catch(function(data) { | |
32 | console.error(data); | |
33 | utils.show_banner("Error: " + data.message || "Server Error", data.type || "error"); | |
34 | }); | |
35 | } | |
36 | });⏎ |
27 | 27 | utils.show_banner("Password should have at least 1 number", "error"); |
28 | 28 | } else { |
29 | 29 | api.post("/registration", { |
30 | reg_id: qs_data.reg_id, | |
30 | token: qs_data.token, | |
31 | 31 | email: email, |
32 | 32 | display_name: display_name, |
33 | 33 | password: new_password |
0 | new Promise(function(resolve) { | |
1 | resolve(); | |
2 | }).then(function() { | |
3 | return new Promise(function(resolve, reject) { | |
4 | resolve("Nested reject"); | |
5 | }).then(function(e) { | |
6 | console.log("then inside then", e); | |
7 | }).catch(function(e) { | |
8 | console.log("catch inside then", e); | |
9 | return new Promise(function(resolve, reject) { | |
10 | resolve("resolve x2 inside then"); | |
11 | }).then(function(e) { | |
12 | console.log("then after catch inside then", e); | |
13 | return new Promise(function(resolve, reject) { | |
14 | reject(e); | |
15 | }); | |
16 | }); | |
17 | }); | |
18 | }).then(function(a) { | |
19 | console.log("final then", a); | |
20 | }).catch(function(e) { | |
21 | console.log("final catch", e); | |
22 | });⏎ |
8 | 8 | "index": './src/js/index.js', |
9 | 9 | "login": './src/js/login.js', |
10 | 10 | "register": './src/js/register.js', |
11 | "confirm-email": './src/js/confirm-email.js' | |
11 | "confirm-email": './src/js/confirm-email.js', | |
12 | "password-reset": './src/js/password-reset.js' | |
12 | 13 | }, |
13 | 14 | output: { |
14 | 15 | filename: './js/[name].js', |