MASSIVE update to improve API with stuff I picked up from work
Seva Luchianov
3 years ago
19 | 19 | "console": true, |
20 | 20 | "HTML_ROOT": true, |
21 | 21 | "VIDEO_ROOT": true, |
22 | "NO_CAPTCHA": true, | |
23 | 22 | "NO_SCAN": true |
24 | 23 | }, |
25 | 24 | "-W030": true, |
28 | 27 | "scripts": { |
29 | 28 | "start": "npm-run-all --parallel watch-server watch-js", |
30 | 29 | "build": "parcel build src/html/*.html --no-source-maps", |
31 | "watch-server": "node-dev --no-deps --dedupe server/watch-together-server.js --debug", | |
30 | "watch-server": "node-dev --no-deps --dedupe server/watch-together-server.js --no-captcha --no-scan", | |
32 | 31 | "watch-js": "parcel watch src/html/*.html" |
33 | 32 | }, |
34 | 33 | "dependencies": { |
0 | 0 | const utils = require('../../utils'); |
1 | 1 | const time = require('../../utils/time'); |
2 | const mailgun = require('../../utils/mailgun'); | |
3 | const websocket = require('../../websocket'); | |
2 | 4 | const passport = require('passport'); |
3 | 5 | const bcrypt = require('bcrypt'); |
6 | const qs = require('querystring'); | |
4 | 7 | const path = require('path'); |
5 | const rimraf = require("rimraf"); | |
6 | const websocket = require('../../websocket'); | |
7 | const qs = require('querystring'); | |
8 | 8 | |
9 | 9 | module.exports = { |
10 | 10 | post: { |
17 | 17 | return next(err); |
18 | 18 | } |
19 | 19 | if (!user) { |
20 | return res.send(utils.unauthed({ | |
20 | return res.send(utils.not_ok(utils.status.unauthorized, { | |
21 | 21 | message: info.message |
22 | 22 | })); |
23 | 23 | } |
24 | ||
24 | 25 | req.logIn(user, function(err) { |
25 | 26 | if (err) { |
26 | 27 | return next(err); |
34 | 35 | }], |
35 | 36 | "update": { |
36 | 37 | "display-name": function(req, res) { |
37 | utils.validate.condition( | |
38 | req.body.display_name && req.body.display_name.trim(), | |
39 | "Could not update username: Username cannot be empty" | |
40 | ).then(function() { | |
38 | utils.validate.keys(req.body, [ | |
39 | 'display_name' | |
40 | ]).then(function() { | |
41 | req.body.display_name = req.body.display_name.trim(); | |
42 | if (!req.body.display_name) { | |
43 | return utils.reject("Could not update username: Username cannot be empty"); | |
44 | } | |
41 | 45 | return utils.query("UPDATE users" + utils.set_where({ |
42 | 46 | display_name: req.body.display_name |
43 | 47 | }, { |
47 | 51 | res.send(utils.ok({ |
48 | 52 | message: result.changedRows ? "Username Updated" : "Username Unchanged" |
49 | 53 | })); |
50 | }).catch(function(error) { | |
51 | console.error(error); | |
52 | res.send(utils.error({ | |
53 | message: error || "Could not update username", | |
54 | type: "error" | |
55 | })); | |
56 | }); | |
54 | }).catch(utils.handle_err_res(res, "Could not update username")); | |
57 | 55 | }, |
58 | 56 | "password": function(req, res) { |
59 | utils.validate.condition( | |
60 | utils.validate.password(req.body.new_password), | |
61 | "New password not good enough" | |
62 | ).then(function() { | |
63 | if (req.body.current_password) { | |
57 | utils.validate.keys(req.body, [ | |
58 | 'current_password', | |
59 | ['new_password', utils.validate.password, "New password not strong enough"] | |
60 | ]).then(function() { | |
61 | if (utils.validate.password(req.body.new_password)) { | |
64 | 62 | return bcrypt.compare(req.body.current_password, req.user.password); |
65 | 63 | } |
66 | return false; | |
67 | }).then(function(result) { | |
68 | return utils.validate.condition(result, "Current password incorrect"); | |
69 | }).then(function() { | |
70 | return bcrypt.hash(req.body.new_password, 10); | |
64 | return utils.reject("New password not good enough"); | |
65 | }).then(function(passwords_match) { | |
66 | if (passwords_match) { | |
67 | return bcrypt.hash(req.body.new_password, 10); | |
68 | } | |
69 | return utils.reject("Current password incorrect"); | |
71 | 70 | }).then(function(hashword) { |
72 | 71 | return utils.query("UPDATE users" + utils.set_where({ |
73 | 72 | password: hashword |
82 | 81 | if (result.changedRows) { |
83 | 82 | return generate_token_and_send_email( |
84 | 83 | "password_reset", "reset_id", |
85 | req.body.email, "send_password_reset_email" | |
84 | req.body.email, "send_password_changed_email" | |
86 | 85 | ); |
87 | 86 | } |
88 | }).catch(function(error) { | |
89 | console.error(error); | |
90 | res.send(utils.error({ | |
91 | message: error || "Could not update password", | |
92 | type: "error" | |
93 | })); | |
94 | }); | |
87 | }).catch(utils.handle_err_res(res, "Could not update password")); | |
95 | 88 | } |
96 | 89 | }, |
97 | 90 | "logout": function(req, res) { |
106 | 99 | let captcha_bypass_token; |
107 | 100 | let email; |
108 | 101 | |
109 | utils.validate.condition( | |
110 | utils.validate.password(req.body.new_password), | |
111 | "New password not good enough" | |
112 | ).then(function() { | |
113 | if (req.body.token) { | |
114 | return utils.query("SELECT * FROM password_reset WHERE reset_id=? AND expires>?", [ | |
115 | req.body.token, Date.now() | |
116 | ]); | |
117 | } | |
118 | return []; | |
119 | }).then(function(rows) { | |
120 | return utils.validate.query.any(rows, "Token Invalid"); | |
121 | }).then(function([row]) { | |
122 | email = row.email; | |
102 | utils.validate.keys(req.body, [ | |
103 | ['new_password', utils.validate.password, "New password not strong enough"], | |
104 | ['token', utils.validate.uuid] | |
105 | ]).then(function() { | |
106 | return utils.query("SELECT * FROM password_reset WHERE reset_id=? AND expires>?", [ | |
107 | req.body.token, Date.now() | |
108 | ]); | |
109 | }).then(function([password_reset_data]) { | |
110 | if (!password_reset_data) { | |
111 | return utils.reject("Invalid password reset token"); | |
112 | } | |
113 | email = password_reset_data.email; | |
123 | 114 | return bcrypt.hash(req.body.new_password, 10); |
124 | 115 | }).then(function(hashword) { |
116 | // I don't check if you are resetting your password | |
117 | // to the same thing it was because what's the point? | |
125 | 118 | return utils.query("UPDATE users" + utils.set_where({ |
126 | 119 | password: hashword |
127 | 120 | }, { |
142 | 135 | captcha_bypass_token: captcha_bypass_token, |
143 | 136 | email: email |
144 | 137 | })); |
145 | }).then(function() { | |
138 | ||
146 | 139 | return generate_token_and_send_email( |
147 | 140 | "password_reset", "reset_id", |
148 | 141 | email, "send_password_changed_email" |
149 | 142 | ); |
150 | }).catch(function(error) { | |
151 | console.error(error); | |
152 | res.send(utils.error({ | |
153 | message: error || "Could not update password", | |
154 | type: "error" | |
155 | })); | |
156 | }); | |
143 | }).catch(utils.handle_err_res(res, "Could not update password")); | |
157 | 144 | }], |
158 | 145 | "request": [{ |
159 | 146 | auth: false, |
160 | 147 | captcha: true |
161 | 148 | }, function(req, res) { |
162 | utils.query('SELECT email_confirmed FROM users WHERE ?', { | |
163 | email: req.body.email | |
164 | }).then(function(rows) { | |
165 | return utils.validate.query.any(rows, "Unknown Email"); | |
166 | }).then(function([row]) { | |
167 | return utils.validate.condition(row.email_confirmed, "You must confirm your email first, please check your inbox"); | |
168 | }).then(function() { | |
169 | return utils.query('SELECT * FROM password_reset WHERE email=? AND expires<?', [ | |
149 | utils.validate.keys(req.body, [ | |
150 | ['email', utils.validate.email, "Invalid Email"] | |
151 | ]).then(function() { | |
152 | return utils.query('SELECT email_confirmed FROM users WHERE ?', { | |
153 | email: req.body.email | |
154 | }); | |
155 | }).then(function([user]) { | |
156 | if (!user) { | |
157 | return utils.reject("Unknown Email"); | |
158 | } | |
159 | if (!user.email_confirmed) { | |
160 | return utils.reject("You must confirm your email first, please check your inbox"); | |
161 | } | |
162 | return utils.query('SELECT reset_id FROM password_reset WHERE email=? AND expires<?', [ | |
170 | 163 | req.body.email, Date.now() |
171 | 164 | ]); |
172 | }).then(function(rows) { | |
173 | return utils.validate.query.empty(rows, "There is already a valid password reset link, please check your inbox"); | |
174 | }).then(function() { | |
165 | }).then(function([password_reset_data]) { | |
166 | if (password_reset_data) { | |
167 | return utils.reject([utils.status.ok, { | |
168 | message: "You should already have a valid password reset link, please check your inbox" | |
169 | }]); | |
170 | } | |
175 | 171 | return generate_token_and_send_email( |
176 | 172 | "password_reset", "reset_id", |
177 | 173 | req.body.email, "send_password_reset_email" |
180 | 176 | return res.send(utils.ok({ |
181 | 177 | message: "Password reset email sent, please check your inbox" |
182 | 178 | })); |
183 | }).catch(function(error) { | |
184 | console.error(error); | |
185 | res.send(utils.error({ | |
186 | message: error, | |
187 | type: "error" | |
188 | })); | |
189 | }); | |
179 | }).catch(utils.handle_err_res(res, "Error sending your password reset link, try again later")); | |
190 | 180 | }] |
191 | 181 | }, |
192 | 182 | "delete": { |
193 | 183 | "": function(req, res) { |
194 | return utils.query("SELECT * FROM delete_account WHERE delete_id=? AND expires>?", [ | |
195 | req.body.code, Date.now() | |
196 | ]).then(function(rows) { | |
197 | return utils.validate.query.any(rows, "Code Invalid, request a new code"); | |
198 | }).then(function() { | |
199 | if (req.body.current_password) { | |
200 | return bcrypt.compare(req.body.current_password, req.user.password); | |
201 | } | |
202 | return false; | |
203 | }).then(function(result) { | |
204 | return utils.validate.condition(result, "Current password incorrect"); | |
205 | }).then(function() { | |
184 | utils.validate.keys(req.body, [ | |
185 | 'code', 'current_password', | |
186 | ]).then(function() { | |
187 | return utils.query( | |
188 | "SELECT * FROM delete_account WHERE delete_id=? AND expires>?", | |
189 | [req.body.code, Date.now()] | |
190 | ); | |
191 | }).then(function([delete_account_data]) { | |
192 | if (!delete_account_data) { | |
193 | return utils.reject("Code is invalid, request a new code"); | |
194 | } | |
195 | return bcrypt.compare(req.body.current_password, req.user.password); | |
196 | }).then(function(passwords_match) { | |
197 | if (!passwords_match) { | |
198 | return utils.reject("Current password incorrect"); | |
199 | } | |
200 | ||
201 | // Otherwise nuke everything related to this account :o | |
206 | 202 | return utils.query("SELECT video_id FROM videos WHERE ?", { |
207 | 203 | created_by: req.user.user_id |
208 | 204 | }); |
209 | }).then(function(rows) { | |
210 | let async_funcs = []; | |
211 | for (const row of rows) { | |
212 | async_funcs.push(function(callback) { | |
213 | rimraf(path.join(VIDEO_ROOT, row.video_id), callback); | |
214 | }); | |
215 | } | |
216 | ||
217 | return utils.asyncCallback(async_funcs); | |
205 | }).then(function(videos) { | |
206 | return Promise.all(videos.map(function(video) { | |
207 | return utils.rimraf(path.join(VIDEO_ROOT, video.video_id)); | |
208 | })); | |
218 | 209 | }).then(function() { |
219 | 210 | utils.get_storage_remaining().then(function(bytes) { |
220 | 211 | websocket.broadcast({ |
235 | 226 | registered_by: req.user.registered_by |
236 | 227 | }); |
237 | 228 | }).then(function() { |
238 | return utils.email.send_account_deleted_email(req.user.email); | |
229 | return mailgun.send_account_deleted_email(req.user.email); | |
239 | 230 | }).then(function() { |
240 | 231 | req.logout(); |
241 | 232 | res.send(utils.ok()); |
242 | }).catch(function(error) { | |
243 | console.error(error); | |
244 | res.send(utils.error({ | |
245 | message: error, | |
246 | type: "error" | |
247 | })); | |
248 | }); | |
233 | }).catch(utils.handle_err_res(res, "Error deleting your account, try again later")); | |
249 | 234 | }, |
250 | 235 | "request": function(req, res) { |
251 | return utils.query('SELECT * FROM delete_account WHERE email=? AND expires<?', [ | |
252 | req.body.email, Date.now() | |
253 | ]).then(function(rows) { | |
254 | return utils.validate.query.any(rows, "There is already a valid account deletion code, please check your inbox"); | |
255 | }).then(function() { | |
236 | utils.validate.keys(req.body, [ | |
237 | ['email', utils.validate.email, "Invalid Email"] | |
238 | ]).then(function() { | |
239 | return utils.query( | |
240 | 'SELECT * FROM delete_account WHERE email=? AND expires<?', | |
241 | [req.body.email, Date.now()] | |
242 | ); | |
243 | }).then(function([delete_account_data]) { | |
244 | if (delete_account_data) { | |
245 | return utils.reject([utils.status.ok, { | |
246 | message: "There is already a valid account deletion code, please check your inbox" | |
247 | }]); | |
248 | } | |
256 | 249 | return utils.tiny_id(); |
257 | 250 | }).then(function(delete_id) { |
258 | 251 | return generate_token_and_send_email( |
266 | 259 | } |
267 | 260 | ); |
268 | 261 | }).then(function() { |
269 | return res.send(utils.ok({ | |
262 | res.send(utils.ok({ | |
270 | 263 | message: "Account deletion email sent, please check your inbox for the code" |
271 | 264 | })); |
272 | }).catch(function(error) { | |
273 | console.error(error); | |
274 | res.send(utils.error({ | |
275 | message: error, | |
276 | type: "error" | |
277 | })); | |
278 | }); | |
265 | }).catch(utils.handle_err_res(res, "Error sending your account deletion code, try again later")); | |
279 | 266 | } |
280 | 267 | } |
281 | 268 | } |
284 | 271 | function generate_token_and_send_email(table, token_id, email, send_command, overrides) { |
285 | 272 | overrides = overrides || {}; |
286 | 273 | return utils.generate_token(table, token_id, email, overrides).then(function(token) { |
287 | return utils.email[send_command](email, overrides.email_params || { | |
274 | return mailgun[send_command](email, overrides.email_params || { | |
288 | 275 | query: qs.stringify({ |
289 | 276 | token: token |
290 | 277 | }) |
0 | 0 | const utils = require('../../utils'); |
1 | const time = require('../../utils/time'); | |
2 | const mailgun = require('../../utils/mailgun'); | |
1 | 3 | const bcrypt = require('bcrypt'); |
2 | const time = require('../../utils/time'); | |
3 | 4 | const qs = require('querystring'); |
4 | 5 | |
5 | 6 | module.exports = { |
14 | 15 | res.send(utils.ok({ |
15 | 16 | token: token |
16 | 17 | })); |
17 | }).catch(function(error) { | |
18 | console.error(error); | |
19 | res.send(utils.error()); | |
20 | }); | |
18 | }).catch(utils.handle_err_res(res, "Error creating registration token, try again later")); | |
21 | 19 | }, |
22 | 20 | "": [{ |
23 | auth: false, | |
24 | captcha: true | |
25 | }, function(req, res) { | |
26 | if (!req.body.token || | |
27 | !req.body.email || | |
28 | !req.body.display_name || | |
29 | !req.body.password | |
30 | ) { | |
31 | return res.send(utils.error({ | |
32 | message: "Invalid request", | |
33 | type: "error" | |
34 | })); | |
21 | auth: false, | |
22 | captcha: true | |
23 | }, | |
24 | function(req, res) { | |
25 | let registered_by; | |
26 | let confirmation_id; | |
27 | ||
28 | utils.validate.keys(req.body, [ | |
29 | ['token', utils.validate.uuid], | |
30 | 'display_name', | |
31 | ['email', utils.validate.email, "Invalid Email"], | |
32 | ['password', utils.validate.password, "Password not strong enough"] | |
33 | ]).then(function() { | |
34 | // Look for a registration session matching the reg_id | |
35 | return utils.query('SELECT * FROM registrations WHERE ?', { | |
36 | reg_id: req.body.token | |
37 | }); | |
38 | }).then(function([registration_data]) { | |
39 | if (!registration_data) { | |
40 | return utils.reject("Registration token invalid"); | |
41 | } | |
42 | if (registration_data.expires < Date.now()) { | |
43 | return utils.reject("Registration token expired"); | |
44 | } | |
45 | registered_by = registration_data.user_id; | |
46 | // Make sure that the email is not taken | |
47 | return utils.query("SELECT user_id FROM users WHERE ?", { | |
48 | email: req.body.email | |
49 | }); | |
50 | }).then(function([user]) { | |
51 | if (user) { | |
52 | return utils.reject("Email already taken"); | |
53 | } | |
54 | // Hash the password and insert a new user with an unconfirmed email | |
55 | return bcrypt.hash(req.body.password, 10); | |
56 | }).then(function(hashword) { | |
57 | return utils.query("INSERT INTO users SET ?", { | |
58 | user_id: utils.uuid(), | |
59 | email: req.body.email, | |
60 | display_name: req.body.display_name, | |
61 | password: hashword, | |
62 | registered_by: registered_by, | |
63 | email_confirmed: 0 | |
64 | }); | |
65 | }).then(function() { | |
66 | // Delete the registration session, the new user is created | |
67 | return utils.query('DELETE FROM registrations WHERE ?', { | |
68 | reg_id: req.body.token | |
69 | }); | |
70 | }).then(function() { | |
71 | // Create a confirmation session and send the confirmation email | |
72 | confirmation_id = utils.uuid(); | |
73 | return utils.query("INSERT INTO email_confirmations SET ?", { | |
74 | email: req.body.email, | |
75 | confirmation_id: confirmation_id, | |
76 | expires: Date.now() + time.one_day | |
77 | }); | |
78 | }).then(function() { | |
79 | return mailgun.send_confirmation_email(req.body.email, { | |
80 | query: qs.encode({ | |
81 | token: confirmation_id | |
82 | }) | |
83 | }); | |
84 | }).then(function() { | |
85 | // Finally we are done | |
86 | res.send(utils.ok({ | |
87 | message: "You must confirm your email to complete registration, please check your inbox" | |
88 | })); | |
89 | }).catch(utils.handle_err_res(res, "Error registering your account. Try again or reach out whoever gave you a registration link")); | |
35 | 90 | } |
36 | ||
37 | if (!utils.validate.email(req.body.email)) { | |
38 | return res.send(utils.error({ | |
39 | message: "Email invalid", | |
40 | type: "error" | |
41 | })); | |
42 | } | |
43 | ||
44 | if (!utils.validate.password(req.body.password)) { | |
45 | return res.send(utils.error({ | |
46 | message: "Password invalid", | |
47 | type: "error" | |
48 | })); | |
49 | } | |
50 | ||
51 | let registered_by; | |
52 | let confirmation_id; | |
53 | ||
54 | // Look for a registration session matching the reg_id | |
55 | utils.query('SELECT * FROM registrations WHERE ?', { | |
56 | reg_id: req.body.token | |
57 | }).then(function(rows) { | |
58 | return utils.validate.query.any(rows, "Registration token invalid"); | |
59 | }).then(function([row]) { | |
60 | registered_by = row.user_id; | |
61 | return utils.validate.condition(row.expires >= Date.now(), "Registration token expired"); | |
62 | }).then(function() { | |
63 | // Make sure that the email is not taken | |
64 | return utils.query("SELECT user_id FROM users WHERE ?", { | |
65 | email: req.body.email | |
66 | }); | |
67 | }).then(function(rows) { | |
68 | return utils.validate.query.empty(rows, "Email already taken"); | |
69 | }).then(function() { | |
70 | // Hash the password and insert a new user with an unconfirmed email | |
71 | return bcrypt.hash(req.body.password, 10); | |
72 | }).then(function(hashword) { | |
73 | return utils.query("INSERT INTO users SET ?", { | |
74 | user_id: utils.uuid(), | |
75 | email: req.body.email, | |
76 | display_name: req.body.display_name, | |
77 | password: hashword, | |
78 | registered_by: registered_by, | |
79 | email_confirmed: 0 | |
80 | }); | |
81 | }).then(function() { | |
82 | // Delete the registration session, the new user is created | |
83 | return utils.query('DELETE FROM registrations WHERE ?', { | |
84 | reg_id: req.body.token | |
85 | }); | |
86 | }).then(function() { | |
87 | // Create a confirmation session and send the confirmation email | |
88 | confirmation_id = utils.uuid(); | |
89 | return utils.query("INSERT INTO email_confirmations SET ?", { | |
90 | email: req.body.email, | |
91 | confirmation_id: confirmation_id, | |
92 | expires: Date.now() + time.one_day | |
93 | }); | |
94 | }).then(function() { | |
95 | return utils.email.send_confirmation_email(req.body.email, { | |
96 | query: qs.encode({ | |
97 | token: confirmation_id | |
98 | }) | |
99 | }); | |
100 | }).then(function() { | |
101 | // Finally we are done | |
102 | res.send(utils.ok({ | |
103 | message: "Confirm your email to complete registration, please check your inbox" | |
104 | })); | |
105 | }).catch(function(error) { | |
106 | console.error(error); | |
107 | res.send(utils.error({ | |
108 | message: error, | |
109 | type: "error" | |
110 | })); | |
111 | }); | |
112 | }], | |
91 | ], | |
113 | 92 | "confirm-email": { |
114 | 93 | "": [{ |
115 | 94 | auth: false |
116 | 95 | }, function(req, res) { |
117 | 96 | let email; |
118 | 97 | |
119 | utils.query('SELECT * FROM email_confirmations WHERE ?', { | |
120 | confirmation_id: req.body.token | |
121 | }).then(function(rows) { | |
122 | return utils.validate.query.any(rows, "Confirmation token invalid"); | |
123 | }).then(function([row]) { | |
124 | email = row.email; | |
125 | return utils.validate.condition(row.expires >= Date.now(), "Confirmation token expired. Please request a new token"); | |
98 | utils.validate.keys(req.body, [ | |
99 | ['token', utils.validate.uuid] | |
100 | ]).then(function() { | |
101 | return utils.query('SELECT * FROM email_confirmations WHERE ?', { | |
102 | confirmation_id: req.body.token | |
103 | }); | |
104 | }).then(function([confirmation]) { | |
105 | if (!confirmation) { | |
106 | return utils.reject("Confirmation token invalid"); | |
107 | } | |
108 | if (confirmation.expires >= Date.now()) { | |
109 | return utils.reject("Confirmation token expired. Please request a new token"); | |
110 | } | |
111 | email = confirmation.email; | |
126 | 112 | }).then(function() { |
127 | 113 | return utils.query("UPDATE users" + utils.set_where({ |
128 | 114 | email_confirmed: 1 |
140 | 126 | })); |
141 | 127 | }).catch(function(error) { |
142 | 128 | console.error(error); |
143 | res.send(utils.error({ | |
129 | res.send(utils.not_ok(utils.status.error, { | |
144 | 130 | message: error, |
145 | 131 | type: "error" |
146 | 132 | })); |
150 | 136 | auth: false, |
151 | 137 | captcha: true |
152 | 138 | }, function(req, res) { |
153 | let email; | |
154 | ||
155 | utils.query('SELECT * FROM email_confirmations WHERE ?', { | |
156 | confirmation_id: req.body.token | |
157 | }).then(function(rows) { | |
158 | if (!rows.length) { | |
159 | return res.send(utils.ok({ | |
139 | utils.validate.keys(req.body, [ | |
140 | ['token', utils.validate.uuid] | |
141 | ]).then(function() { | |
142 | return utils.query('SELECT * FROM email_confirmations WHERE ?', { | |
143 | confirmation_id: req.body.token | |
144 | }); | |
145 | }).then(function([confirmation]) { | |
146 | if (!confirmation) { | |
147 | return utils.reject([utils.status.ok, { | |
160 | 148 | message: "You email might already be confirmed, please log in", |
161 | 149 | show_login: true |
162 | })); | |
150 | }]); | |
163 | 151 | } |
164 | ||
165 | const row = rows[0]; | |
166 | return utils.validate.condition( | |
167 | row.expires < Date.now(), | |
168 | "There is already a valid confirmation link, please check your inbox" | |
169 | ).then(function() { | |
170 | email = row.email; | |
171 | return utils.generate_token("email_confirmations", "confirmation_id", email); | |
172 | }).then(function(token) { | |
173 | return utils.email.send_confirmation_email(email, { | |
152 | if (confirmation.expires > Date.now()) { | |
153 | return utils.reject("There is already a valid confirmation link, please check your inbox"); | |
154 | } | |
155 | return utils.generate_token( | |
156 | "email_confirmations", | |
157 | "confirmation_id", | |
158 | confirmation.email | |
159 | ).then(function(token) { | |
160 | return mailgun.send_confirmation_email(confirmation.email, { | |
174 | 161 | query: qs.encode({ |
175 | 162 | token: token |
176 | 163 | }) |
180 | 167 | console.error(error); |
181 | 168 | return utils.query("DELETE FROM email_confirmations WHERE ?", { |
182 | 169 | confirmation_id: token |
183 | }).then(function() { | |
184 | return new Promise(function(resolve, reject) { | |
185 | reject(error); | |
186 | }); | |
187 | }); | |
170 | }).then(utils.reject); | |
188 | 171 | }); |
189 | 172 | }).then(function() { |
190 | 173 | res.send(utils.ok({ |
192 | 175 | show_login: false |
193 | 176 | })); |
194 | 177 | }); |
195 | }).catch(function(error) { | |
196 | console.error(error); | |
197 | res.send(utils.error({ | |
198 | message: error, | |
199 | type: "error" | |
200 | })); | |
201 | }); | |
178 | }).catch(utils.handle_err_res(res, "Error sending email confirmation link, try again later")); | |
202 | 179 | }] |
203 | 180 | } |
204 | 181 | } |
0 | const utils = require('../../utils'); | |
1 | const time = require('../../utils/time'); | |
2 | const clamscan = require('../../utils/clamscan'); | |
3 | const ffmpeg = require('../../utils/ffmpeg'); | |
4 | const websocket = require('../../websocket'); | |
5 | const fs = require('fs'); | |
0 | 6 | const path = require('path'); |
1 | const utils = require('../../utils'); | |
2 | const websocket = require('../../websocket'); | |
3 | const clamscan = require('../../utils/clamscan'); | |
4 | const time = require('../../utils/time'); | |
5 | const fs = require('fs'); | |
6 | const ffmpeg = require('fluent-ffmpeg'); | |
7 | // Optional path overrides - if ffmpeg is used elsewhere it | |
8 | // will need to be extracted into a util with these overrides | |
9 | process.env.FFMPEG_PATH && ffmpeg.setFfmpegPath(process.env.FFMPEG_PATH); | |
10 | process.env.FFPROBE_PATH && ffmpeg.setFfprobePath(process.env.FFPROBE_PATH); | |
11 | ||
12 | 7 | const subtitle = require('subtitle'); |
13 | 8 | const uploader = require('huge-uploader-nodejs'); |
14 | const rimraf = require("rimraf"); | |
15 | const FileType = require('file-type'); | |
16 | 9 | const upload_session = require('../../utils/upload_session'); |
17 | 10 | |
18 | const conversion_format = 'mp4'; | |
19 | 11 | const TEMP_UPLOAD_DIR = path.join(VIDEO_ROOT, "/temp"); |
20 | ||
21 | 12 | // Make sure we have directories or uploader will just fucking crash |
22 | 13 | utils.fs.mkdir_if_not_exists(VIDEO_ROOT).catch(console.error); |
23 | 14 | utils.fs.mkdir_if_not_exists(TEMP_UPLOAD_DIR).catch(console.error); |
24 | 15 | |
25 | let conversion_queue = []; | |
26 | ||
27 | function queue_conversion(execute_conversion) { | |
28 | if (conversion_queue.length) { | |
29 | conversion_queue.push(execute_conversion); | |
30 | } else { | |
31 | execute_conversion(); | |
32 | } | |
33 | } | |
34 | ||
35 | function convertFile(data, callback) { | |
36 | const video_dir = path.join(VIDEO_ROOT, data.video_id); | |
37 | utils.fs.mkdir_if_not_exists(video_dir).then(function() { | |
38 | return FileType.fromFile(data.temp_file_path); | |
39 | }).then(function(file_info) { | |
40 | const final_path = path.join(video_dir, `video.${conversion_format}`); | |
41 | ||
42 | if (file_info.ext === conversion_format) { | |
43 | console.log("We don't need to convert the file"); | |
44 | fs.rename(data.temp_file_path, final_path, function(error) { | |
45 | if (error) { | |
46 | console.log(error); | |
47 | websocket.broadcast({ | |
48 | command: "upload-error", | |
49 | data: { | |
50 | video_id: data.video_id, | |
51 | status: utils.status.video.converting, | |
52 | message: "Error converting " + data.name | |
53 | } | |
54 | }, { | |
55 | [data.user_id]: true | |
56 | }); | |
57 | } else { | |
58 | callback(); | |
59 | } | |
60 | }); | |
61 | } else { | |
62 | queue_conversion(function() { | |
63 | let percent_finished_x10 = 0; | |
64 | console.log(data); | |
65 | ||
66 | ffmpeg(data.temp_file_path).format(conversion_format).save(final_path).on('progress', function(progress) { | |
67 | console.log(`Processing: ${data.name} - ${progress.percent}% done`); | |
68 | ||
69 | // We update the user every tenth of a percent | |
70 | let rounded_percent = Math.floor(progress.percent * 10); | |
71 | if (rounded_percent > percent_finished_x10) { | |
72 | percent_finished_x10 = rounded_percent; | |
73 | websocket.broadcast({ | |
74 | command: "video-status", | |
75 | data: { | |
76 | video_id: data.video_id, | |
77 | status: utils.status.video.converting, | |
78 | progress: percent_finished_x10 / 10 | |
79 | } | |
80 | }, { | |
81 | [data.user_id]: true | |
82 | }); | |
83 | ||
84 | utils.get_storage_remaining().then(function(bytes) { | |
85 | websocket.broadcast({ | |
86 | command: "server-storage-info", | |
87 | data: bytes | |
88 | }); | |
89 | }).catch(console.error); | |
90 | } | |
91 | }).on('error', function(error) { | |
92 | console.log(error); | |
93 | websocket.broadcast({ | |
94 | command: "upload-error", | |
95 | data: { | |
96 | video_id: data.video_id, | |
97 | status: utils.status.video.converting, | |
98 | message: "Error converting " + data.name | |
99 | } | |
100 | }, { | |
101 | [data.user_id]: true | |
102 | }); | |
103 | }).on('end', function() { | |
104 | console.log("File converted"); | |
105 | ||
106 | if (conversion_queue.length) { | |
107 | conversion_queue.shift()(); | |
108 | } | |
109 | ||
110 | callback(); | |
111 | }); | |
112 | }); | |
113 | } | |
114 | }); | |
115 | } | |
116 | ||
117 | 16 | module.exports = { |
118 | 17 | post: { |
119 | 18 | "subtitle": function(req, res) { |
120 | let file_path = path.resolve(VIDEO_ROOT, req.body.video_id, "subtitle.json"); | |
121 | ||
122 | return utils.fs.stat(file_path).then(function() { | |
19 | let file_path; | |
20 | ||
21 | utils.validate.keys(req.body, [ | |
22 | ['video_id', utils.validate.uuid] | |
23 | ]).then(function() { | |
24 | file_path = path.resolve(VIDEO_ROOT, req.body.video_id, "subtitle.json"); | |
25 | return utils.fs.stat(file_path).catch(function() { | |
26 | // Errors here just mean that the subtitle file does not exist | |
27 | return utils.reject([utils.status.ok, {}]); | |
28 | }); | |
29 | }).then(function() { | |
123 | 30 | return utils.fs.readFile(file_path); |
124 | 31 | }).then(function(subtitle) { |
125 | 32 | res.send(utils.ok({ |
126 | 33 | subtitles: JSON.parse(subtitle.toString()) |
127 | 34 | })); |
128 | }).catch(function(error) { | |
129 | console.log(error); | |
130 | // Send ok response. Errors here just mean that the subtitle file does not exist | |
131 | res.send(utils.ok()); | |
132 | }); | |
35 | }).catch(utils.handle_err_res(res, "Error getting subtitles")); | |
133 | 36 | }, |
134 | 37 | "upload": { |
135 | 38 | "subtitle": function(req, res) { |
136 | const video_dir = path.resolve(VIDEO_ROOT, req.body.video_id); | |
137 | const subtitle_path = path.resolve(video_dir, "subtitle.json"); | |
138 | ||
139 | return utils.fs.stat(video_dir).then(function() { | |
140 | return utils.fs.writeFile(subtitle_path, | |
39 | let video_dir; | |
40 | let subtitle_path; | |
41 | ||
42 | utils.validate.keys(req.body, [ | |
43 | ['video_id', utils.validate.uuid], 'subtitle_string' | |
44 | ]).then(function() { | |
45 | video_dir = path.resolve(VIDEO_ROOT, req.body.video_id); | |
46 | // Make sure the video we are uploading subtitles for actually exists | |
47 | return utils.fs.stat(video_dir); | |
48 | }).then(function() { | |
49 | subtitle_path = path.resolve(video_dir, "subtitle.json"); | |
50 | return utils.fs.writeFile( | |
51 | subtitle_path, | |
141 | 52 | JSON.stringify(subtitle.parse(req.body.subtitle_string)) |
142 | 53 | ); |
143 | 54 | }).then(function() { |
144 | res.send(utils.ok()); | |
145 | // I don't want the request to time out in case this takes a long time. | |
146 | clamscan.assert_safe(subtitle_path, { | |
55 | return clamscan.assert_safe(subtitle_path, { | |
147 | 56 | ip: res.locals.ip, |
148 | 57 | user_id: req.user.user_id |
149 | }).catch(console.error); | |
150 | }).catch(function(error) { | |
151 | console.log(error); | |
152 | res.send(utils.error({ | |
153 | message: "Error uploading subtitles", | |
154 | type: "error" | |
155 | })); | |
156 | }); | |
58 | }); | |
59 | }).then(function() { | |
60 | res.send(utils.ok()); | |
61 | }).catch(utils.handle_err_res(res, "Error uploading subtitles")); | |
157 | 62 | }, |
158 | 63 | "request": function(req, res) { |
159 | if (upload_session.get(req.user)) { | |
160 | return res.send(utils.error({ | |
161 | message: "You already have an upload running", | |
162 | type: "error" | |
163 | })); | |
164 | } | |
165 | ||
166 | 64 | const TEMP_UPLOAD_DIR_USER = path.join(TEMP_UPLOAD_DIR, req.user.user_id); |
167 | 65 | |
168 | utils.fs.mkdir_if_not_exists(TEMP_UPLOAD_DIR_USER).then(function() { | |
66 | utils.validate.keys(req.body, [ | |
67 | ['bytes', bytes => typeof bytes === "number"], 'name' | |
68 | ]).then(function() { | |
69 | if (upload_session.get(req.user)) { | |
70 | return utils.reject("You already have an upload session elsewhere"); | |
71 | } | |
72 | return utils.fs.mkdir_if_not_exists(TEMP_UPLOAD_DIR_USER); | |
73 | }).then(function() { | |
169 | 74 | return utils.get_storage_remaining(); |
170 | 75 | }).then(function(bytes) { |
171 | let reserved_bytes = 0; | |
172 | for (const upload of Object.values(upload_session.get())) { | |
173 | reserved_bytes += upload.size; | |
174 | } | |
76 | let reserved_bytes = Object.values(upload_session.get()).reduce(function(reserved_bytes, upload) { | |
77 | return reserved_bytes + upload.size; | |
78 | }, 0); | |
175 | 79 | |
176 | 80 | // Multiply by 2 because there must be |
177 | 81 | // room for the converted file as well |
178 | if (req.body.bytes * 2 + reserved_bytes <= bytes) { | |
179 | upload_session.create( | |
180 | req.user, req.body.name, req.body.bytes, | |
181 | TEMP_UPLOAD_DIR_USER | |
182 | ); | |
183 | res.send(utils.ok()); | |
184 | } else { | |
185 | res.send(utils.error({ | |
186 | message: "Not enough storage available on server. Max upload size is [BYTES]", | |
187 | templates: { | |
188 | // Divide by 2 because there must be | |
189 | // room for the converted file as well | |
190 | "[BYTES]": (bytes - reserved_bytes) / 2 | |
191 | }, | |
192 | type: "error" | |
193 | })); | |
194 | } | |
195 | }).catch(function(error) { | |
196 | console.error(error); | |
197 | return res.send(utils.error({ | |
198 | message: "Error requesting upload", | |
199 | type: "error" | |
200 | })); | |
201 | }); | |
82 | if (req.body.bytes * 2 + reserved_bytes > bytes) { | |
83 | return utils.reject(`Not enough storage available on server. Max upload size is ${(bytes - reserved_bytes) / 2}`); | |
84 | } | |
85 | ||
86 | upload_session.create( | |
87 | req.user, req.body.name, req.body.bytes, | |
88 | TEMP_UPLOAD_DIR_USER | |
89 | ); | |
90 | ||
91 | res.send(utils.ok()); | |
92 | }).catch(utils.handle_err_res(res, "Error requesting upload")); | |
202 | 93 | }, |
203 | 94 | "": [{ |
204 | 95 | rateLimit: false |
205 | 96 | }, function(req, res) { |
206 | if (!upload_session.get(req.user)) { | |
207 | return res.send(utils.error({ | |
97 | let ongoing_upload = upload_session.get(req.user); | |
98 | ||
99 | if (!ongoing_upload) { | |
100 | return res.send(utils.not_ok(utils.status.error, { | |
208 | 101 | message: "Error uploading file, no upload session", |
209 | 102 | type: "error" |
210 | 103 | })); |
211 | 104 | } |
212 | 105 | |
213 | let TEMP_UPLOAD_DIR_USER = upload_session.get(req.user).temp_dir; | |
214 | 106 | uploader( |
215 | req, TEMP_UPLOAD_DIR_USER, | |
107 | req, | |
108 | ongoing_upload.temp_dir, | |
216 | 109 | // bytes / 1000000 |
217 | upload_session.get(req.user).size, // Max total file size in MB | |
110 | ongoing_upload.size, // Max total file size in MB | |
218 | 111 | 10 // Chunk size in MB |
219 | ).then(function(assembleChunks) { | |
112 | ).then(function(assemble_chunks) { | |
220 | 113 | // chunk written to disk |
221 | 114 | res.writeHead(204, 'No Content'); |
222 | 115 | res.end(); |
223 | 116 | |
117 | if (!assemble_chunks) { | |
118 | return utils.resolve(); | |
119 | } | |
120 | ||
224 | 121 | // on last chunk, assembleChunks function is returned |
225 | 122 | // the response is already sent to the browser because it can take some time if the file is huge |
226 | if (assembleChunks) { | |
227 | console.log("Final Chunk Uploaded"); | |
228 | let data = Object.assign({ | |
123 | console.log("Final Chunk Uploaded"); | |
124 | let data = Object.assign({ | |
125 | user_id: req.user.user_id | |
126 | }, ongoing_upload.video); | |
127 | ||
128 | upload_session.delete(req.user); | |
129 | ||
130 | const video_create_data = { | |
131 | video_id: data.video_id, | |
132 | name: data.name, | |
133 | status: utils.status.video.reassembling, | |
134 | expires: Date.now() + (time.one_day * 7), | |
135 | created_by: req.user.user_id | |
136 | }; | |
137 | ||
138 | return utils.query( | |
139 | "INSERT INTO videos SET ?", | |
140 | video_create_data | |
141 | ).then(function() { | |
142 | websocket.broadcast({ | |
143 | command: "new-video", | |
144 | data: video_create_data | |
145 | }, { | |
146 | [req.user.user_id]: true | |
147 | }); | |
148 | ||
149 | return assemble_chunks(); | |
150 | }).then(function(upload_data) { | |
151 | Object.assign(data, { | |
152 | temp_file_path: upload_data.filePath | |
153 | }); | |
154 | ||
155 | return clamscan.assert_safe(data.temp_file_path, { | |
156 | ip: res.locals.ip, | |
229 | 157 | user_id: req.user.user_id |
230 | }, upload_session.get(req.user).video); | |
231 | ||
158 | }); | |
159 | }).then(function() { | |
160 | return utils.query("UPDATE videos" + utils.set_where({ | |
161 | name: data.name, | |
162 | status: utils.status.video.converting | |
163 | }, { | |
164 | video_id: data.video_id | |
165 | })); | |
166 | }).then(function() { | |
167 | // Big Boi | |
168 | return ffmpeg.convert(data); | |
169 | }).then(function() { | |
170 | console.log(data.name, "converted!"); | |
171 | websocket.broadcast({ | |
172 | command: "video-status", | |
173 | data: { | |
174 | video_id: data.video_id, | |
175 | status: utils.status.video.converting, | |
176 | progress: 100 | |
177 | } | |
178 | }, { | |
179 | [req.user.user_id]: true | |
180 | }); | |
181 | ||
182 | return utils.fs.unlink(data.temp_file_path).then(function() { | |
183 | console.log("temp file deleted"); | |
184 | return utils.query("UPDATE videos" + utils.set_where({ | |
185 | status: utils.status.video.ready | |
186 | }, { | |
187 | video_id: data.video_id | |
188 | })); | |
189 | }); | |
190 | }).then(function() { | |
191 | websocket.broadcast({ | |
192 | command: "video-status", | |
193 | data: { | |
194 | video_id: data.video_id, | |
195 | status: utils.status.video.ready | |
196 | } | |
197 | }, { | |
198 | [req.user.user_id]: true | |
199 | }); | |
200 | }).catch(function(error) { | |
201 | console.error(error); | |
232 | 202 | upload_session.delete(req.user); |
233 | ||
234 | const video_create_data = { | |
235 | video_id: data.video_id, | |
236 | name: data.name, | |
237 | status: utils.status.video.reassembling, | |
238 | expires: Date.now() + (time.one_day * 7), | |
239 | created_by: req.user.user_id | |
240 | }; | |
241 | ||
242 | utils.query( | |
243 | "INSERT INTO videos SET ?", | |
244 | video_create_data | |
245 | ).then(function() { | |
203 | return utils.rimraf(ongoing_upload.temp_dir).then(function() { | |
204 | return utils.query( | |
205 | "DELETE FROM videos WHERE video_id=?", | |
206 | [data.video_id] | |
207 | ); | |
208 | }).then(function() { | |
246 | 209 | websocket.broadcast({ |
247 | command: "new-video", | |
248 | data: video_create_data | |
210 | command: "delete-video-response", | |
211 | data: utils.ok({ | |
212 | video_id: data.video_id, | |
213 | name: data.name, | |
214 | reason: "upload failed" | |
215 | }) | |
249 | 216 | }, { |
250 | 217 | [req.user.user_id]: true |
251 | 218 | }); |
252 | ||
253 | return assembleChunks(); | |
254 | }).then(function(upload_data) { | |
255 | Object.assign(data, { | |
256 | temp_file_path: upload_data.filePath | |
257 | }); | |
258 | ||
259 | return clamscan.assert_safe(data.temp_file_path, { | |
260 | ip: res.locals.ip, | |
261 | user_id: req.user.user_id | |
262 | }); | |
263 | }).then(function() { | |
264 | return utils.query("UPDATE videos" + utils.set_where({ | |
265 | name: data.name, | |
266 | status: utils.status.video.converting | |
267 | }, { | |
268 | video_id: data.video_id | |
269 | })); | |
270 | }).then(function() { | |
271 | convertFile(data, function() { | |
272 | websocket.broadcast({ | |
273 | command: "video-status", | |
274 | data: { | |
275 | video_id: data.video_id, | |
276 | status: utils.status.video.converting, | |
277 | progress: 100 | |
278 | } | |
279 | }, { | |
280 | [req.user.user_id]: true | |
281 | }); | |
282 | fs.unlink(data.temp_file_path, function() { | |
283 | console.log("temp file deleted"); | |
284 | utils.query("UPDATE videos" + utils.set_where({ | |
285 | status: utils.status.video.ready | |
286 | }, { | |
287 | video_id: data.video_id | |
288 | })).then(function() { | |
289 | websocket.broadcast({ | |
290 | command: "video-status", | |
291 | data: { | |
292 | video_id: data.video_id, | |
293 | status: utils.status.video.ready | |
294 | } | |
295 | }, { | |
296 | [req.user.user_id]: true | |
297 | }); | |
298 | ||
299 | return utils.get_storage_remaining(); | |
300 | }).then(function(bytes) { | |
301 | websocket.broadcast({ | |
302 | command: "server-storage-info", | |
303 | data: bytes | |
304 | }); | |
305 | }).catch(function(error) { | |
306 | console.error(error); | |
307 | }); | |
308 | }); | |
309 | }); | |
310 | }).catch(function(error) { | |
311 | console.error(error); | |
312 | rimraf(TEMP_UPLOAD_DIR_USER, utils.noop); | |
313 | }); | |
314 | } else { | |
315 | utils.get_storage_remaining().then(function(bytes) { | |
316 | websocket.broadcast({ | |
317 | command: "server-storage-info", | |
318 | data: bytes | |
319 | }); | |
320 | }).catch(console.error); | |
321 | } | |
322 | }).catch(function(error) { | |
323 | console.error(error); | |
324 | rimraf(TEMP_UPLOAD_DIR_USER, utils.noop); | |
325 | upload_session.delete(req.user); | |
326 | res.send(utils.error({ | |
327 | message: "Error uploading file: " + error, | |
328 | type: "error" | |
329 | })); | |
219 | }); | |
220 | }); | |
221 | }).catch(utils.handle_err_status_res(res)).finally(function() { | |
222 | return utils.get_storage_remaining().then(function(bytes) { | |
223 | websocket.broadcast({ | |
224 | command: "server-storage-info", | |
225 | data: bytes | |
226 | }); | |
227 | }).catch(console.error); | |
330 | 228 | }); |
331 | 229 | }] |
332 | 230 | } |
336 | 234 | "": [{ |
337 | 235 | rateLimit: false |
338 | 236 | }, function(req, res) { |
339 | let file_path = path.resolve(VIDEO_ROOT, req.params.video_id, "video.mp4"); | |
340 | ||
341 | return utils.fs.stat(file_path).then(function(stat) { | |
237 | let file_path; | |
238 | utils.validate.keys(req.params, [ | |
239 | ['video_id', utils.validate.uuid] | |
240 | ]).then(function() { | |
241 | file_path = path.resolve(VIDEO_ROOT, req.params.video_id, "video.mp4"); | |
242 | return utils.fs.stat(file_path); | |
243 | }).then(function(stat) { | |
342 | 244 | const range = req.headers.range; |
343 | 245 | |
344 | 246 | if (range) { |
0 | const utils = require('../utils'); | |
1 | const upload_session = require('../utils/upload_session'); | |
0 | 2 | const path = require('path'); |
1 | const upload_session = require('../utils/upload_session'); | |
2 | const rimraf = require("rimraf"); | |
3 | 3 | |
4 | 4 | module.exports = { |
5 | 5 | get: { |
9 | 9 | // Hence they have no upload session. |
10 | 10 | let current_upload_session = upload_session.get(req.user); |
11 | 11 | if (current_upload_session) { |
12 | rimraf(current_upload_session.temp_dir, function() { | |
13 | upload_session.delete(req.user); | |
14 | }); | |
12 | upload_session.delete(req.user); | |
13 | utils.rimraf(current_upload_session.temp_dir).catch(console.error); | |
15 | 14 | } |
16 | ||
17 | 15 | res.sendFile(path.join(HTML_ROOT, "index.html")); |
18 | 16 | }, |
19 | 17 | "login": [{ |
0 | const email = require('./email'); | |
0 | const utils = require('.'); | |
1 | const email = require('./mailgun'); | |
1 | 2 | let clamscan; |
2 | 3 | |
3 | if (NO_SCAN) { | |
4 | console.log("clamscan not loaded"); | |
5 | } else { | |
6 | const NodeClam = require('clamscan'); | |
7 | const ClamScan = new NodeClam().init({ | |
8 | // https://github.com/kylefarris/clamscan#getting-started | |
9 | remove_infected: true, | |
10 | quarantine_infected: false, | |
11 | debug_mode: true, | |
12 | scan_recursively: true, | |
13 | clamscan: { | |
14 | path: '/usr/bin/clamscan', | |
15 | db: null, | |
16 | scan_archives: true, | |
17 | active: true | |
18 | }, | |
19 | clamdscan: { | |
20 | active: false | |
4 | module.exports = { | |
5 | init: function(no_scan) { | |
6 | if (no_scan) { | |
7 | console.log("clamscan not loaded"); | |
8 | return utils.resolve(); | |
21 | 9 | } |
22 | }); | |
23 | 10 | |
24 | ClamScan.then(async function(new_clamscan) { | |
25 | try { | |
11 | const NodeClam = require('clamscan'); | |
12 | const ClamScan = new NodeClam().init({ | |
13 | // https://github.com/kylefarris/clamscan#getting-started | |
14 | remove_infected: true, | |
15 | quarantine_infected: false, | |
16 | debug_mode: true, | |
17 | scan_recursively: true, | |
18 | clamscan: { | |
19 | path: '/usr/bin/clamscan', | |
20 | db: null, | |
21 | scan_archives: true, | |
22 | active: true | |
23 | }, | |
24 | clamdscan: { | |
25 | active: false | |
26 | } | |
27 | }); | |
28 | ||
29 | return ClamScan.then(function(new_clamscan) { | |
26 | 30 | clamscan = new_clamscan; |
27 | const version = await clamscan.get_version(); | |
31 | return clamscan.get_version(); | |
32 | }).then(function(version) { | |
28 | 33 | console.log(`ClamAV Version: ${version}`); |
29 | } catch (error) { | |
34 | }).catch(function(error) { | |
30 | 35 | console.error(error); |
36 | }); | |
37 | }, | |
38 | assert_safe: function(file_path, infos) { | |
39 | if (!clamscan) { | |
40 | return utils.resolve(); | |
31 | 41 | } |
32 | }).catch(function(error) { | |
33 | console.error(error); | |
34 | }); | |
35 | } | |
36 | 42 | |
37 | module.exports = { | |
38 | assert_safe: NO_SCAN ? () => new Promise(r => r()) : function(file_path, infos) { | |
39 | 43 | return clamscan.is_infected(file_path).then(function(scan_result) { |
40 | 44 | return new Promise(function(resolve, reject) { |
41 | 45 | if (scan_result.is_infected) { |
47 | 51 | user_id: infos.user_id, |
48 | 52 | file: file_path, |
49 | 53 | viruses: scan_result.viruses.join(', ') |
50 | }); | |
54 | }).catch(console.error); | |
51 | 55 | } else { |
52 | 56 | resolve(); |
53 | 57 | } |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Here is your account deletion code<br> | |
9 | <span style="font-size: 20px; font-weight: bold;">[CODE]</span><br> | |
10 | This code will expire in 5 minutes | |
11 | </div> | |
12 | </body> | |
13 | ||
14 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Your Watch Together account has been deleted sucessfully | |
9 | </div> | |
10 | </body> | |
11 | ||
12 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | <span style="font-size: 22px;">Welcome to Watch Together!</span><br><br> | |
9 | Confirm your email by clicking the link below.<br><br> | |
10 | <a href="https://watch-together.luchianov.dev/confirm-email?[QUERY]" style="color: #226fff"> | |
11 | https://watch-together.luchianov.dev/confirm-email?[QUERY] | |
12 | </a><br><br> | |
13 | Your email will only be used for password reset functionality.<br> | |
14 | You will never recieve unsolicited emails from this domain.<br><br> | |
15 | If you did not sign up for a Watch Together account please ignore this email. | |
16 | </div> | |
17 | </body> | |
18 | ||
19 | </html>⏎ |
0 | const path = require('path'); | |
1 | const fs = require('fs'); | |
2 | ||
3 | const DOMAIN = "watch-together.luchianov.dev"; | |
4 | const mailgun = require("mailgun-js"); | |
5 | const mg = process.env.MG_API_KEY ? mailgun({ | |
6 | apiKey: process.env.MG_API_KEY, | |
7 | domain: DOMAIN | |
8 | }) : { | |
9 | messages: { | |
10 | send: options => new Promise(function(resolve) { | |
11 | console.log(`Email to ${options.to} not sent - no API key`, options); | |
12 | resolve(); | |
13 | }) | |
14 | } | |
15 | }; | |
16 | ||
17 | module.exports = Object.entries({ | |
18 | confirmation: { | |
19 | subject: 'Confirm Your Watch Together Email' | |
20 | }, | |
21 | password_reset: { | |
22 | subject: 'Reset Your Watch Together Password' | |
23 | }, | |
24 | password_changed: { | |
25 | subject: 'Watch Together Password Changed' | |
26 | }, | |
27 | virus_detected: { | |
28 | subject: 'Watch Together Virus Detected' | |
29 | }, | |
30 | account_delete_code: { | |
31 | subject: 'Watch Together Account Deletion Requested' | |
32 | }, | |
33 | account_deleted: { | |
34 | subject: 'Watch Together Account Deleted' | |
35 | } | |
36 | }).reduce(function(acc, [file, data]) { | |
37 | return Object.assign(acc, { | |
38 | [`send_${file}_email`]: function(recipient, params) { | |
39 | return generate_email(`${file}.html`, params || {}).then(function(html) { | |
40 | console.log(`Send ${file} email to ${recipient}`); | |
41 | return mg.messages().send({ | |
42 | from: `Watch Together <noreply@${DOMAIN}>`, | |
43 | to: recipient, | |
44 | subject: data.subject, | |
45 | html: html | |
46 | }); | |
47 | }).then(console.log); | |
48 | } | |
49 | }); | |
50 | }, {}); | |
51 | ||
52 | function generate_email(name, params) { | |
53 | return new Promise(function(resolve, reject) { | |
54 | fs.readFile(path.resolve(__dirname, name), function(error, data) { | |
55 | if (error) { | |
56 | reject(error); | |
57 | } else { | |
58 | let html = data.toString(); | |
59 | for (let [key, value] of Object.entries(params)) { | |
60 | key = `[${key.toUpperCase()}]`; | |
61 | html = html.split(key).join(value); | |
62 | } | |
63 | resolve(html); | |
64 | } | |
65 | }); | |
66 | }); | |
67 | }⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Your Watch Together password has been changed.<br> | |
9 | If you did not change your password,<br> | |
10 | click the link below and reset your password immediately<br><br> | |
11 | <a href="https://watch-together.luchianov.dev/password-reset?[QUERY]" style="color: #226fff"> | |
12 | https://watch-together.luchianov.dev/password-reset?[QUERY] | |
13 | </a><br><br> | |
14 | Otherwise, please ignore this email. | |
15 | </div> | |
16 | </body> | |
17 | ||
18 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Click the link below to reset your password.<br><br> | |
9 | <a href="https://watch-together.luchianov.dev/password-reset?[QUERY]" style="color: #226fff"> | |
10 | https://watch-together.luchianov.dev/password-reset?[QUERY] | |
11 | </a><br><br> | |
12 | If you did not request to reset your password please ignore this email. | |
13 | </div> | |
14 | </body> | |
15 | ||
16 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Malicious file uploaded to Watch Together<br><br> | |
9 | Uploaded from [IP] by user [USER_ID]<br><br> | |
10 | File path: [file]<br><br> | |
11 | Viruses detected: [VIRUSES] | |
12 | </div> | |
13 | </body> | |
14 | ||
15 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Here is your account deletion code<br> | |
9 | <span style="font-size: 20px; font-weight: bold;">[CODE]</span><br> | |
10 | This code will expire in 5 minutes | |
11 | </div> | |
12 | </body> | |
13 | ||
14 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Your Watch Together account has been deleted successfully | |
9 | </div> | |
10 | </body> | |
11 | ||
12 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | <span style="font-size: 22px;">Welcome to Watch Together!</span><br><br> | |
9 | Confirm your email by clicking the link below.<br><br> | |
10 | <a href="https://watch-together.luchianov.dev/confirm-email?[QUERY]" style="color: #226fff"> | |
11 | https://watch-together.luchianov.dev/confirm-email?[QUERY] | |
12 | </a><br><br> | |
13 | Your email will only be used for password reset functionality.<br> | |
14 | You will never recieve unsolicited emails from this domain.<br><br> | |
15 | If you did not sign up for a Watch Together account please ignore this email. | |
16 | </div> | |
17 | </body> | |
18 | ||
19 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Your Watch Together password has been changed.<br> | |
9 | If you did not change your password,<br> | |
10 | click the link below and reset your password immediately<br><br> | |
11 | <a href="https://watch-together.luchianov.dev/password-reset?[QUERY]" style="color: #226fff"> | |
12 | https://watch-together.luchianov.dev/password-reset?[QUERY] | |
13 | </a><br><br> | |
14 | Otherwise, please ignore this email. | |
15 | </div> | |
16 | </body> | |
17 | ||
18 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Click the link below to reset your password.<br><br> | |
9 | <a href="https://watch-together.luchianov.dev/password-reset?[QUERY]" style="color: #226fff"> | |
10 | https://watch-together.luchianov.dev/password-reset?[QUERY] | |
11 | </a><br><br> | |
12 | If you did not request to reset your password please ignore this email. | |
13 | </div> | |
14 | </body> | |
15 | ||
16 | </html>⏎ |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | ||
3 | <head> | |
4 | </head> | |
5 | ||
6 | <body style="margin: 20px; font-family: Helvetica;"> | |
7 | <div style="font-size: 15px;"> | |
8 | Malicious file uploaded to Watch Together<br><br> | |
9 | Uploaded from [IP] by user [USER_ID]<br><br> | |
10 | File path: [file]<br><br> | |
11 | Viruses detected: [VIRUSES] | |
12 | </div> | |
13 | </body> | |
14 | ||
15 | </html>⏎ |
0 | const utils = require('.'); | |
1 | const ffmpeg = require('fluent-ffmpeg'); | |
2 | // Optional path overrides - if ffmpeg is used elsewhere it | |
3 | // will need to be extracted into a util with these overrides | |
4 | process.env.FFMPEG_PATH && ffmpeg.setFfmpegPath(process.env.FFMPEG_PATH); | |
5 | process.env.FFPROBE_PATH && ffmpeg.setFfprobePath(process.env.FFPROBE_PATH); | |
6 | const FileType = require('file-type'); | |
7 | const path = require('path'); | |
8 | const websocket = require('../websocket'); | |
9 | ||
10 | const valid_video_formats = ['mp4']; | |
11 | ||
12 | module.exports = { | |
13 | /** | |
14 | * Converts video located at temp_file_path to a valid format if necessary. | |
15 | * Returns a Promise that resolves after conversion is complete. | |
16 | * | |
17 | * Note: ffmpeg.format is a very long running process, so nothing with any sort | |
18 | * of timeout should depend on this promise (such as the response to a request) | |
19 | * @param {temp_file_path, name, video_id, user_id} data | |
20 | */ | |
21 | convert: function(data) { | |
22 | const video_dir = path.join(VIDEO_ROOT, data.video_id); | |
23 | return utils.fs.mkdir_if_not_exists(video_dir).then(function() { | |
24 | return FileType.fromFile(data.temp_file_path); | |
25 | }).then(function(file_info) { | |
26 | if (valid_video_formats.includes(file_info.ext)) { | |
27 | console.log("We don't need to convert the file"); | |
28 | return utils.fs.rename( | |
29 | data.temp_file_path, | |
30 | path.join(video_dir, `video.${file_info.ext}`) | |
31 | ); | |
32 | } | |
33 | ||
34 | console.log("convert", data); | |
35 | ||
36 | return new Promise(function(resolve, reject) { | |
37 | let percent_finished_x10 = 0; | |
38 | let conversion_format = valid_video_formats[0]; | |
39 | ffmpeg(data.temp_file_path) | |
40 | .format(conversion_format) | |
41 | .save(path.join(video_dir, `video.${conversion_format}`)) | |
42 | .on('end', resolve) | |
43 | .on('error', reject) | |
44 | .on('progress', function(progress) { | |
45 | console.log(`Processing: ${data.name} - ${progress.percent}% done`); | |
46 | ||
47 | // We update the user every tenth of a percent | |
48 | let rounded_percent = Math.floor(progress.percent * 10); | |
49 | if (rounded_percent > percent_finished_x10) { | |
50 | percent_finished_x10 = rounded_percent; | |
51 | websocket.broadcast({ | |
52 | command: "video-status", | |
53 | data: { | |
54 | video_id: data.video_id, | |
55 | status: utils.status.video.converting, | |
56 | progress: percent_finished_x10 / 10 | |
57 | } | |
58 | }, { | |
59 | [data.user_id]: true | |
60 | }); | |
61 | ||
62 | utils.get_storage_remaining().then(function(bytes) { | |
63 | websocket.broadcast({ | |
64 | command: "server-storage-info", | |
65 | data: bytes | |
66 | }); | |
67 | }).catch(console.error); | |
68 | } | |
69 | }); | |
70 | }); | |
71 | }).catch(function(error) { | |
72 | websocket.broadcast({ | |
73 | command: "upload-error", | |
74 | data: { | |
75 | video_id: data.video_id, | |
76 | status: utils.status.video.converting, | |
77 | message: "Error converting " + data.name | |
78 | } | |
79 | }, { | |
80 | [data.user_id]: true | |
81 | }); | |
82 | ||
83 | return utils.reject(error); | |
84 | }); | |
85 | } | |
86 | };⏎ |
0 | 0 | const { |
1 | 1 | v4: uuid |
2 | 2 | } = require('uuid'); |
3 | const { | |
4 | promisify | |
5 | } = require('util'); | |
3 | 6 | const fs = require('fs'); |
4 | const getSize = require('get-folder-size'); | |
5 | 7 | const zxcvbn = require('zxcvbn'); |
6 | 8 | const crypto = require('crypto'); |
7 | 9 | const mysql = require('mysql'); |
8 | 10 | const time = require('./time'); |
9 | const status_codes = require('../../status.json'); | |
11 | const STATUS_CODES = require('../../status.json'); | |
12 | const STATUS_CODES_ENUM = Object.assign( | |
13 | require('../../status.json'), | |
14 | enumify(Object.keys(STATUS_CODES)) | |
15 | ); | |
16 | ||
10 | 17 | const max_video_dir_size = 20000000000; // 20 GB |
11 | 18 | |
12 | 19 | const mysql_options = { |
33 | 40 | |
34 | 41 | function query(query, wildcards) { |
35 | 42 | return new Promise(function(resolve, reject) { |
36 | let args = [query, function(err, rows) { | |
37 | if (err) { | |
38 | reject(err); | |
43 | let args = [query, function(error, rows) { | |
44 | if (error) { | |
45 | console.error(error); | |
46 | reject("Internal Server Error, try again later"); | |
39 | 47 | } else { |
40 | 48 | resolve(rows); |
41 | 49 | } |
50 | 58 | } |
51 | 59 | |
52 | 60 | function condition_promise(statement, error, success) { |
53 | return new Promise(function(resolve, reject) { | |
54 | if (statement) { | |
55 | resolve(success); | |
61 | return new Promise((resolve, reject) => statement ? resolve(success) : reject(error)); | |
62 | } | |
63 | ||
64 | function not_ok_response(status_code, data) { | |
65 | console.log("Respond with status", status_code, "Data", data); | |
66 | return JSON.stringify(Object.assign({ | |
67 | status: status_code | |
68 | }, data || {})); | |
69 | } | |
70 | ||
71 | function handle_err_status_res(res) { | |
72 | return function(error) { | |
73 | if (!res.headersSent) { | |
74 | console.error("Error response wrapper invoked with unknown error", error); | |
75 | if (typeof error !== "number" || error < 300) { | |
76 | error = 500; | |
77 | } | |
78 | res.sendStatus(error); | |
56 | 79 | } else { |
57 | reject(error); | |
80 | console.error("Response already sent in response error handler"); | |
81 | console.error(error); | |
58 | 82 | } |
59 | }); | |
83 | }; | |
60 | 84 | } |
61 | 85 | |
62 | 86 | module.exports = { |
63 | noop: x => x, | |
87 | promisify: promisify, | |
88 | rimraf: promisify(require('rimraf')), | |
89 | get_storage_remaining: promisify(require('get-folder-size'))(VIDEO_ROOT).then(bytes => max_video_dir_size - bytes), | |
90 | fs: { | |
91 | stat: promisify(fs.stat), | |
92 | writeFile: promisify(fs.writeFile), | |
93 | readFile: promisify(fs.readFile), | |
94 | unlink: promisify(fs.unlink), | |
95 | rename: promisify(fs.rename), | |
96 | mkdir_if_not_exists: function(path) { | |
97 | return promisify(fs.stat).catch(function(error) { | |
98 | if (error.code === "ENOENT") { | |
99 | return promisify(fs.mkdir)(path); | |
100 | } | |
101 | return new Promise((_, r) => r(error)); | |
102 | }); | |
103 | } | |
104 | }, | |
105 | enumify: enumify, | |
106 | status: STATUS_CODES_ENUM, | |
107 | ok: function(data) { | |
108 | // ¯\_(ツ)_/¯ | |
109 | return not_ok_response(STATUS_CODES.ok, data); | |
110 | }, | |
111 | not_ok: function(status, data) { | |
112 | return not_ok_response(STATUS_CODES[status], data); | |
113 | }, | |
114 | handle_err_res: function(res, default_error_message) { | |
115 | // Invoke with (error) or ([error, data]) | |
116 | // Since this is a catch handler and can only have a single argument | |
117 | return function(arg) { | |
118 | let error = arg; | |
119 | let data; | |
120 | if (Array.isArray(arg) && arg.length === 2) { | |
121 | error = arg[0]; | |
122 | data = arg[1]; | |
123 | } | |
124 | ||
125 | if (STATUS_CODES.hasOwnProperty(error)) { | |
126 | console.error("Error response wrapper invoked with:", error); | |
127 | res.send(not_ok_response(STATUS_CODES[error], data || { | |
128 | message: default_error_message || "Unknown Error", | |
129 | type: "error" | |
130 | })); | |
131 | } else if (typeof arg === "string") { | |
132 | res.send(not_ok_response(STATUS_CODES.error, { | |
133 | message: arg, | |
134 | type: "error" | |
135 | })); | |
136 | } else { | |
137 | handle_err_status_res(res)(arg); | |
138 | } | |
139 | }; | |
140 | }, | |
141 | handle_err_status_res: handle_err_status_res, | |
64 | 142 | uuid: uuid, |
65 | tiny_id: function() { | |
66 | return new Promise(function(resolve, reject) { | |
67 | crypto.randomBytes(3, function(error, buf) { | |
68 | if (error) { | |
69 | reject(error); | |
70 | } else { | |
71 | resolve(buf.toString('hex').toUpperCase()); | |
72 | } | |
73 | }); | |
74 | }); | |
75 | }, | |
143 | tiny_id: promisify(crypto.randomBytes)(3).then(buf => buf.toString('hex').toUpperCase()), | |
76 | 144 | mysql_options: mysql_options, |
77 | 145 | query: query, |
78 | 146 | /* |
97 | 165 | }, " " + SQL_OPERATOR); |
98 | 166 | }, ""); |
99 | 167 | }, |
100 | status: status_codes, | |
101 | email: require('./email'), | |
102 | 168 | validate: { |
169 | keys: function(obj, keys) { | |
170 | for (const key_data of keys) { | |
171 | let [key, validator, error_message] = Array.isArray(key_data) ? key_data : [key_data]; | |
172 | validator = validator || (x => x); | |
173 | error_message = error_message || "Invalid Request"; | |
174 | if (!obj.hasOwnProperty(key) || !validator(obj[key])) { | |
175 | console.log("key", key, "missing or invalid in obj", obj); | |
176 | return new Promise((_, r) => r(error_message)); | |
177 | } | |
178 | } | |
179 | return new Promise(r => r()); | |
180 | }, | |
103 | 181 | email: function(str) { |
104 | 182 | return str && str.match(/.+@.+\..+/) !== null; |
105 | 183 | }, |
106 | 184 | password: function(str) { |
107 | 185 | return zxcvbn(str).score > 0; |
186 | }, | |
187 | uuid: function(str) { | |
188 | return str && str.match(/^([0-9]|[a-f]){8}\-([0-9]|[a-f]){4}\-4([0-9]|[a-f]){3}\-[8,9,a,b]([0-9]|[a-f]){3}\-([0-9]|[a-f]){12}$/i) !== null; | |
108 | 189 | }, |
109 | 190 | query: { |
110 | 191 | empty: function(rows, error) { |
116 | 197 | }, |
117 | 198 | condition: condition_promise |
118 | 199 | }, |
119 | reject: function(data) { | |
120 | return new Promise((resolve, reject) => reject(data)); | |
121 | }, | |
200 | resolve: data => new Promise(r => r(data)), | |
201 | reject: error => new Promise((_, r) => r(error)), | |
122 | 202 | generate_token: function(table, key, email, overrides) { |
123 | 203 | overrides = overrides || {}; |
124 | ||
125 | 204 | let token = overrides.token || uuid(); |
126 | 205 | // So there either is no token at all, or there is one but it has expired |
127 | 206 | return query( |
128 | 207 | `INSERT INTO ${table} (${key}, email, expires) ` + |
129 | 208 | `VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE ${key}=VALUES(${key}), expires=VALUES(expires)`, |
130 | 209 | [token, email, Date.now() + (overrides.ttl || time.one_day)] |
131 | ).then(function() { | |
132 | return token; | |
133 | }); | |
134 | }, | |
135 | get_storage_remaining: function() { | |
136 | return new Promise(function(resolve, reject) { | |
137 | getSize(VIDEO_ROOT, function(error, bytes) { | |
138 | if (error) { | |
139 | reject(error); | |
140 | } else { | |
141 | resolve(max_video_dir_size - bytes); | |
142 | } | |
143 | }); | |
144 | }); | |
210 | ).then(() => token); | |
145 | 211 | }, |
146 | 212 | get_display_name: function(user_id) { |
147 | 213 | return query("SELECT display_name FROM users WHERE ?", { |
157 | 223 | return rows[0].name; |
158 | 224 | }).catch(console.error); |
159 | 225 | }, |
160 | enumify: enumify, | |
161 | ok: function(data) { | |
162 | return Object.assign({ | |
163 | status: status_codes.ok | |
164 | }, data || {}); | |
165 | }, | |
166 | error: function(data) { | |
167 | return Object.assign({ | |
168 | status: status_codes.error | |
169 | }, data || {}); | |
170 | }, | |
171 | unauthed: function(data) { | |
172 | return Object.assign({ | |
173 | status: status_codes.unauthorized | |
174 | }, data || {}); | |
175 | }, | |
176 | fs: { | |
177 | stat: function(file_path) { | |
178 | return new Promise(function(resolve, reject) { | |
179 | fs.stat(file_path, function(error, stat) { | |
180 | if (error) { | |
181 | reject(error); | |
182 | } else { | |
183 | resolve(stat); | |
184 | } | |
185 | }); | |
186 | }); | |
187 | }, | |
188 | writeFile: function(file_path, data) { | |
189 | return new Promise(function(resolve) { | |
190 | fs.writeFile(file_path, data, resolve); | |
191 | }); | |
192 | }, | |
193 | readFile: function(file_path) { | |
194 | return new Promise(function(resolve, reject) { | |
195 | fs.readFile(file_path, function(error, data) { | |
196 | if (error) { | |
197 | reject(error); | |
198 | } else { | |
199 | resolve(data); | |
200 | } | |
201 | }); | |
202 | }); | |
203 | }, | |
204 | mkdir_if_not_exists: function(path) { | |
205 | return new Promise(function(resolve, reject) { | |
206 | fs.stat(path, function(error) { | |
207 | if (error) { | |
208 | // Check if error defined and the error code is "not exists" | |
209 | if (error.code === "ENOENT") { | |
210 | // Create the directory | |
211 | fs.mkdir(path, resolve); | |
212 | } else { | |
213 | reject(error); | |
214 | } | |
215 | } else { | |
216 | // Directory exists, move along | |
217 | resolve(); | |
218 | } | |
219 | }); | |
220 | }); | |
221 | } | |
222 | }, | |
223 | 226 | setIntervalAndTriggerImmediate: function(callback, interval) { |
224 | 227 | setTimeout(callback); |
225 | 228 | return setInterval(callback, interval); |
226 | }, | |
227 | asyncCallback: function(asyncFunctions) { | |
228 | return new Promise(function(resolve) { | |
229 | let finishedOperations = 0; | |
230 | let args = []; | |
231 | asyncFunctions.forEach(function(asyncFunction) { | |
232 | asyncFunction(function() { | |
233 | finishedOperations++; | |
234 | if (!args.length && arguments.length) { | |
235 | args = [...arguments]; | |
236 | } | |
237 | if (finishedOperations >= asyncFunctions.length) { | |
238 | resolve(args); | |
239 | } | |
240 | }); | |
241 | }); | |
242 | }); | |
243 | 229 | } |
244 | 230 | };⏎ |
0 | const path = require('path'); | |
1 | const fs = require('fs'); | |
2 | ||
3 | const DOMAIN = "watch-together.luchianov.dev"; | |
4 | const mailgun = require("mailgun-js"); | |
5 | const mg = process.env.MG_API_KEY ? mailgun({ | |
6 | apiKey: process.env.MG_API_KEY, | |
7 | domain: DOMAIN | |
8 | }) : { | |
9 | messages: { | |
10 | send: options => new Promise(function(resolve) { | |
11 | console.log(`Email to ${options.to} not sent - no API key`, options); | |
12 | resolve(); | |
13 | }) | |
14 | } | |
15 | }; | |
16 | ||
17 | const exports = Object.entries({ | |
18 | confirmation: { | |
19 | subject: 'Confirm Your Watch Together Email' | |
20 | }, | |
21 | password_reset: { | |
22 | subject: 'Reset Your Watch Together Password' | |
23 | }, | |
24 | password_changed: { | |
25 | subject: 'Watch Together Password Changed' | |
26 | }, | |
27 | virus_detected: { | |
28 | subject: 'Watch Together Virus Detected' | |
29 | }, | |
30 | account_delete_code: { | |
31 | subject: 'Watch Together Account Deletion Requested' | |
32 | }, | |
33 | account_deleted: { | |
34 | subject: 'Watch Together Account Deleted' | |
35 | } | |
36 | }).reduce(function(acc, [template_name, data]) { | |
37 | console.log("Loading email template: ", template_name); | |
38 | ||
39 | // Load Raw HTML string as value | |
40 | const template = fs.readFileSync( | |
41 | path.resolve(__dirname, 'email_templates', template_name + ".html") | |
42 | ).toString(); | |
43 | ||
44 | return Object.assign(acc, { | |
45 | [`send_${template_name}_email`]: function(recipient, params) { | |
46 | let html = template; | |
47 | for (let [key, value] of Object.entries(params)) { | |
48 | key = `[${key.toUpperCase()}]`; | |
49 | html = html.split(key).join(value); | |
50 | } | |
51 | console.log(`Send ${template_name} email to ${recipient}`); | |
52 | return mg.messages().send({ | |
53 | from: `Watch Together <noreply@${DOMAIN}>`, | |
54 | to: recipient, | |
55 | subject: data.subject, | |
56 | html: html | |
57 | }).then(console.log); | |
58 | } | |
59 | }); | |
60 | }, {}); | |
61 | ||
62 | module.exports = exports;⏎ |
11 | 11 | }]); |
12 | 12 | |
13 | 13 | console.log("Launch Config:", options); |
14 | global.NO_CAPTCHA = options["no-captcha"]; | |
15 | global.NO_SCAN = options["no-scan"]; | |
14 | require('./utils/clamscan').init(options["no-scan"]); | |
16 | 15 | |
17 | 16 | const express = require("express"); |
18 | 17 | const app = express(); |
22 | 21 | const MySQLStore = require('express-mysql-session')(session); |
23 | 22 | const passport = require('passport'); |
24 | 23 | const LocalStrategy = require('passport-local').Strategy; |
24 | const bcrypt = require('bcrypt'); | |
25 | const axios = require('axios'); | |
26 | const qs = require('querystring'); | |
25 | 27 | const path = require('path'); |
26 | 28 | const utils = require('./utils'); |
27 | 29 | const time = require('./utils/time'); |
28 | const bcrypt = require('bcrypt'); | |
29 | const axios = require('axios'); | |
30 | const qs = require('querystring'); | |
30 | const mailgun = require('./utils/mailgun'); | |
31 | 31 | const buildRoutes = require('./buildRoutes')(app); |
32 | const rimraf = require("rimraf"); | |
33 | 32 | |
34 | 33 | const PORT = process.env.PORT || 3000; |
35 | 34 | global.HTML_ROOT = path.join(__dirname, "../dist"); |
59 | 58 | }, function(email, password, done) { |
60 | 59 | utils.query("SELECT * FROM users WHERE ?", { |
61 | 60 | email: email |
62 | }).then(function(rows) { | |
63 | console.log(rows); | |
64 | if (!rows.length) { | |
65 | done(null, false, { | |
61 | }).then(function([user]) { | |
62 | console.log(user); | |
63 | if (!user) { | |
64 | return utils.resolve([null, false, { | |
66 | 65 | message: 'Unknown Email' |
66 | }]); | |
67 | } | |
68 | if (user.email_confirmed) { | |
69 | return bcrypt.compare(password, user.password).then(function(result) { | |
70 | return result ? utils.resolve([null, user]) : utils.resolve([null, false, { | |
71 | message: 'Incorrect Password' | |
72 | }]); | |
67 | 73 | }); |
68 | } else { | |
69 | const user = rows[0]; | |
70 | if (user.email_confirmed) { | |
71 | bcrypt.compare(password, user.password, function(error, result) { | |
72 | if (error) { | |
73 | console.error(error); | |
74 | done(error); | |
75 | } else if (result) { | |
76 | done(null, user); | |
77 | } else { | |
78 | done(null, false, { | |
79 | message: 'Incorrect Password' | |
80 | }); | |
81 | } | |
74 | } | |
75 | return utils.query('SELECT * FROM email_confirmations WHERE ?', { | |
76 | email: email | |
77 | }).then(function([confirmation]) { | |
78 | if (!confirmation || confirmation.expires < Date.now()) { | |
79 | // User was registered but they never got a | |
80 | // token generated (registered through CLI) | |
81 | // OR they haven't confirmed their email yet | |
82 | // and their token has expired. | |
83 | return utils.generate_token( | |
84 | "email_confirmations", | |
85 | "confirmation_id", | |
86 | ||
87 | ).then(function(token) { | |
88 | return mailgun.send_confirmation_email(email, { | |
89 | query: qs.encode({ | |
90 | token: token | |
91 | }) | |
92 | }); | |
82 | 93 | }); |
83 | } else { | |
84 | utils.query('SELECT * FROM email_confirmations WHERE ?', { | |
85 | email: email | |
86 | }).then(function(rows) { | |
87 | if (!rows.length || rows[0].expires < Date.now()) { | |
88 | // User was registered but they never got a | |
89 | // token generated (registered through CLI) | |
90 | // or they haven't confirmed their email yet | |
91 | // and their token has expired. | |
92 | return utils.generate_token("email_confirmations", "confirmation_id", email); | |
93 | } | |
94 | }).then(function(token) { | |
95 | if (token) { | |
96 | return utils.email.send_confirmation_email(email, { | |
97 | query: qs.encode({ | |
98 | token: token | |
99 | }) | |
100 | }); | |
101 | } | |
102 | // else no token was generated which means there is | |
103 | // already a valid token, they just need to confirm | |
104 | }).then(function() { | |
105 | done(null, false, { | |
106 | message: 'You need to confirm your email, please check your inbox' | |
107 | }); | |
108 | }).catch(function(error) { | |
109 | console.error(error); | |
110 | done(error); | |
111 | }); | |
112 | } | |
113 | } | |
94 | } | |
95 | }).then(function() { | |
96 | return utils.resolve([null, false, { | |
97 | message: 'You need to confirm your email, please check your inbox' | |
98 | }]); | |
99 | }); | |
100 | }).then(function(data) { | |
101 | done(...data); | |
114 | 102 | }).catch(function(error) { |
115 | 103 | console.error(error); |
116 | 104 | done(error); |
207 | 195 | next(); |
208 | 196 | } else { |
209 | 197 | console.log("Captcha bypass failed"); |
210 | res.send(utils.error()); | |
198 | res.send(utils.not_ok(utils.status.error)); | |
211 | 199 | } |
212 | 200 | }); |
213 | 201 | } else { |
241 | 229 | } else { |
242 | 230 | console.log("Captcha failed with score", response.data.score); |
243 | 231 | console.log(response.data); |
244 | res.send(utils.error({ | |
232 | res.send(utils.not_ok(utils.status.error, { | |
245 | 233 | message: "Captcha says you are a bot", |
246 | 234 | type: "error" |
247 | 235 | })); |
252 | 240 | |
253 | 241 | defaultMiddleware.set("captcha", { |
254 | 242 | default: false, |
255 | func: NO_CAPTCHA ? function(req, res, next) { | |
243 | func: options["no-captcha"] ? function(req, res, next) { | |
256 | 244 | console.log("Do not check captcha"); |
257 | 245 | console.log("captcha_token:", req.body.captcha_token); |
258 | 246 | console.log("captcha_bypass_token:", req.body.captcha_bypass_token); |
261 | 249 | delete req.body.captcha_token; |
262 | 250 | next(); |
263 | 251 | } else { |
264 | res.send(utils.error({ | |
252 | res.send(utils.not_ok(utils.status.error, { | |
265 | 253 | message: "Debug mode captcha failed - no tokens", |
266 | 254 | type: "error" |
267 | 255 | })); |
269 | 257 | } : function(req, res, next) { |
270 | 258 | check_captcha_token(req, res, next).catch(function(error) { |
271 | 259 | console.error("CAPTCHA ERROR:", error); |
272 | res.send(utils.error({ | |
260 | res.send(utils.not_ok(utils.status.error, { | |
273 | 261 | message: "Internal Server Error", |
274 | 262 | type: "error" |
275 | 263 | })); |
290 | 278 | app.use(express.static("./dist")); |
291 | 279 | |
292 | 280 | // Add fallbacks for unknown requests |
293 | app.use(function(req, res) { | |
281 | app.use(function(error, req, res, next) { | |
294 | 282 | res.sendFile(path.join(HTML_ROOT, "error.html")); |
295 | 283 | }); |
296 | 284 | |
315 | 303 | let now = Date.now(); |
316 | 304 | console.log("Clean up data on", time.toString()); |
317 | 305 | |
318 | utils.query( | |
319 | "SELECT video_id, name, created_by FROM videos WHERE expires<=?", | |
320 | [now] | |
321 | ).then(function(rows) { | |
322 | for (const row of rows) { | |
323 | rimraf(path.join(VIDEO_ROOT, row.video_id), function() { | |
306 | utils.query("SELECT video_id, name, created_by FROM videos WHERE expires<=?", [now]).then(function(videos) { | |
307 | return Promise.all(videos.map(function(video) { | |
308 | return utils.rimraf(path.join(VIDEO_ROOT, video.video_id)).then(() => video); | |
309 | })).then(function(videos) { | |
310 | videos.forEach(function(video) { | |
324 | 311 | websocket.broadcast({ |
325 | 312 | command: "delete-video-response", |
326 | 313 | data: utils.ok({ |
327 | video_id: row.video_id, | |
328 | name: row.name, | |
314 | video_id: video.video_id, | |
315 | name: video.name, | |
329 | 316 | reason: "expired" |
330 | 317 | }) |
331 | 318 | }, { |
332 | [row.created_by]: true | |
319 | [video.created_by]: true | |
333 | 320 | }); |
334 | 321 | }); |
335 | } | |
322 | }); | |
336 | 323 | }).then(function() { |
337 | utils.query( | |
338 | "DELETE FROM videos WHERE expires<=?", | |
339 | [now] | |
340 | ); | |
324 | return Promise.all([ | |
325 | "videos", | |
326 | "registrations", | |
327 | "password_reset", | |
328 | "captcha_bypass", | |
329 | "delete_account" | |
330 | ].map(function(table) { | |
331 | return utils.query(`DELETE FROM ${table} WHERE expires<=?`, [now]); | |
332 | })); | |
341 | 333 | }).catch(console.error); |
342 | ||
343 | ["registrations", "password_reset", "captcha_bypass", "delete_account"].forEach(function(table) { | |
344 | utils.query( | |
345 | `DELETE FROM ${table} WHERE expires<=?`, | |
346 | [now] | |
347 | ).catch(console.error); | |
348 | }); | |
349 | 334 | }, time.one_day * 0.25); // 4 times per day⏎ |
52 | 52 | })); |
53 | 53 | }).catch(function(error) { |
54 | 54 | console.error(error); |
55 | respond(utils.error({ | |
55 | respond(utils.not_ok(utils.status.error, { | |
56 | 56 | videos: [] |
57 | 57 | })); |
58 | 58 | }); |
233 | 233 | expires: one_week_from_now |
234 | 234 | })); |
235 | 235 | } else { |
236 | respond(utils.error({ | |
236 | respond(utils.not_ok(utils.status.error, { | |
237 | 237 | message: "Cannot renew video", |
238 | 238 | type: "error" |
239 | 239 | })); |
240 | 240 | } |
241 | 241 | }).catch(function(error) { |
242 | 242 | console.error(error); |
243 | respond(utils.error({ | |
243 | respond(utils.not_ok(utils.status.error, { | |
244 | 244 | message: "Error renewing video", |
245 | 245 | type: "error" |
246 | 246 | })); |
265 | 265 | }).catch(console.error); |
266 | 266 | }); |
267 | 267 | } else { |
268 | respond(utils.error({ | |
268 | respond(utils.not_ok(utils.status.error, { | |
269 | 269 | message: "Cannot delete video", |
270 | 270 | type: "error" |
271 | 271 | })); |
272 | 272 | } |
273 | 273 | }).catch(function(error) { |
274 | 274 | console.error(error); |
275 | respond(utils.error({ | |
275 | respond(utils.not_ok(utils.status.error, { | |
276 | 276 | message: "Error deleting video", |
277 | 277 | type: "error" |
278 | 278 | })); |
169 | 169 | }); |
170 | 170 | }).catch(function(error) { |
171 | 171 | console.error(error); |
172 | let message = error.message; | |
173 | if (error.templates) { | |
174 | for (const [target, replace] of Object.entries(error.templates)) { | |
175 | message.replace(target, replace); | |
176 | } | |
177 | } | |
178 | utils.show_banner(message, error.type); | |
172 | utils.show_banner(error.message, error.type); | |
179 | 173 | }); |
180 | 174 | } |
181 | 175 |
0 | 0 | const axios = require('axios'); |
1 | const status_codes = require('../../../status.json'); | |
1 | const STATUS_CODES = Object.assign( | |
2 | require('../../../status.json'), | |
3 | require('../../../video_status.json') | |
4 | ); | |
2 | 5 | const qs = require('querystring'); |
3 | 6 | |
4 | 7 | function resolveResponse(response) { |
5 | 8 | return new Promise(function(resolve, reject) { |
6 | if (response.data.status === status_codes.ok) { | |
9 | if (response.data.status === STATUS_CODES.ok) { | |
7 | 10 | resolve(response.data); |
8 | 11 | } else { |
9 | 12 | reject(response.data); |
12 | 15 | } |
13 | 16 | |
14 | 17 | module.exports = { |
15 | status: status_codes, | |
18 | status: STATUS_CODES, | |
16 | 19 | post: function(path, data) { |
17 | 20 | return axios.post(`/api${path}`, data).then(resolveResponse); |
18 | 21 | }, |