127.0.0.1:8000 watch-together / 585b216
MASSIVE update to improve API with stuff I picked up from work Seva Luchianov 3 years ago
28 changed file(s) with 872 addition(s) and 955 deletion(s). Raw diff Collapse all Expand all
1919 "console": true,
2020 "HTML_ROOT": true,
2121 "VIDEO_ROOT": true,
22 "NO_CAPTCHA": true,
2322 "NO_SCAN": true
2423 },
2524 "-W030": true,
2827 "scripts": {
2928 "start": "npm-run-all --parallel watch-server watch-js",
3029 "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",
3231 "watch-js": "parcel watch src/html/*.html"
3332 },
3433 "dependencies": {
00 const utils = require('../../utils');
11 const time = require('../../utils/time');
2 const mailgun = require('../../utils/mailgun');
3 const websocket = require('../../websocket');
24 const passport = require('passport');
35 const bcrypt = require('bcrypt');
6 const qs = require('querystring');
47 const path = require('path');
5 const rimraf = require("rimraf");
6 const websocket = require('../../websocket');
7 const qs = require('querystring');
88
99 module.exports = {
1010 post: {
1717 return next(err);
1818 }
1919 if (!user) {
20 return res.send(utils.unauthed({
20 return res.send(utils.not_ok(utils.status.unauthorized, {
2121 message: info.message
2222 }));
2323 }
24
2425 req.logIn(user, function(err) {
2526 if (err) {
2627 return next(err);
3435 }],
3536 "update": {
3637 "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 }
4145 return utils.query("UPDATE users" + utils.set_where({
4246 display_name: req.body.display_name
4347 }, {
4751 res.send(utils.ok({
4852 message: result.changedRows ? "Username Updated" : "Username Unchanged"
4953 }));
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"));
5755 },
5856 "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)) {
6462 return bcrypt.compare(req.body.current_password, req.user.password);
6563 }
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");
7170 }).then(function(hashword) {
7271 return utils.query("UPDATE users" + utils.set_where({
7372 password: hashword
8281 if (result.changedRows) {
8382 return generate_token_and_send_email(
8483 "password_reset", "reset_id",
85 req.body.email, "send_password_reset_email"
84 req.body.email, "send_password_changed_email"
8685 );
8786 }
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"));
9588 }
9689 },
9790 "logout": function(req, res) {
10699 let captcha_bypass_token;
107100 let email;
108101
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;
123114 return bcrypt.hash(req.body.new_password, 10);
124115 }).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?
125118 return utils.query("UPDATE users" + utils.set_where({
126119 password: hashword
127120 }, {
142135 captcha_bypass_token: captcha_bypass_token,
143136 email: email
144137 }));
145 }).then(function() {
138
146139 return generate_token_and_send_email(
147140 "password_reset", "reset_id",
148141 email, "send_password_changed_email"
149142 );
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"));
157144 }],
158145 "request": [{
159146 auth: false,
160147 captcha: true
161148 }, 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<?', [
170163 req.body.email, Date.now()
171164 ]);
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 }
175171 return generate_token_and_send_email(
176172 "password_reset", "reset_id",
177173 req.body.email, "send_password_reset_email"
180176 return res.send(utils.ok({
181177 message: "Password reset email sent, please check your inbox"
182178 }));
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"));
190180 }]
191181 },
192182 "delete": {
193183 "": 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
206202 return utils.query("SELECT video_id FROM videos WHERE ?", {
207203 created_by: req.user.user_id
208204 });
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 }));
218209 }).then(function() {
219210 utils.get_storage_remaining().then(function(bytes) {
220211 websocket.broadcast({
235226 registered_by: req.user.registered_by
236227 });
237228 }).then(function() {
238 return utils.email.send_account_deleted_email(req.user.email);
229 return mailgun.send_account_deleted_email(req.user.email);
239230 }).then(function() {
240231 req.logout();
241232 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"));
249234 },
250235 "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 }
256249 return utils.tiny_id();
257250 }).then(function(delete_id) {
258251 return generate_token_and_send_email(
266259 }
267260 );
268261 }).then(function() {
269 return res.send(utils.ok({
262 res.send(utils.ok({
270263 message: "Account deletion email sent, please check your inbox for the code"
271264 }));
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"));
279266 }
280267 }
281268 }
284271 function generate_token_and_send_email(table, token_id, email, send_command, overrides) {
285272 overrides = overrides || {};
286273 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 || {
288275 query: qs.stringify({
289276 token: token
290277 })
00 const utils = require('../../utils');
1 const time = require('../../utils/time');
2 const mailgun = require('../../utils/mailgun');
13 const bcrypt = require('bcrypt');
2 const time = require('../../utils/time');
34 const qs = require('querystring');
45
56 module.exports = {
1415 res.send(utils.ok({
1516 token: token
1617 }));
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"));
2119 },
2220 "": [{
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"));
3590 }
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 ],
11392 "confirm-email": {
11493 "": [{
11594 auth: false
11695 }, function(req, res) {
11796 let email;
11897
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;
126112 }).then(function() {
127113 return utils.query("UPDATE users" + utils.set_where({
128114 email_confirmed: 1
140126 }));
141127 }).catch(function(error) {
142128 console.error(error);
143 res.send(utils.error({
129 res.send(utils.not_ok(utils.status.error, {
144130 message: error,
145131 type: "error"
146132 }));
150136 auth: false,
151137 captcha: true
152138 }, 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, {
160148 message: "You email might already be confirmed, please log in",
161149 show_login: true
162 }));
150 }]);
163151 }
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, {
174161 query: qs.encode({
175162 token: token
176163 })
180167 console.error(error);
181168 return utils.query("DELETE FROM email_confirmations WHERE ?", {
182169 confirmation_id: token
183 }).then(function() {
184 return new Promise(function(resolve, reject) {
185 reject(error);
186 });
187 });
170 }).then(utils.reject);
188171 });
189172 }).then(function() {
190173 res.send(utils.ok({
192175 show_login: false
193176 }));
194177 });
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"));
202179 }]
203180 }
204181 }
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');
06 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
127 const subtitle = require('subtitle');
138 const uploader = require('huge-uploader-nodejs');
14 const rimraf = require("rimraf");
15 const FileType = require('file-type');
169 const upload_session = require('../../utils/upload_session');
1710
18 const conversion_format = 'mp4';
1911 const TEMP_UPLOAD_DIR = path.join(VIDEO_ROOT, "/temp");
20
2112 // Make sure we have directories or uploader will just fucking crash
2213 utils.fs.mkdir_if_not_exists(VIDEO_ROOT).catch(console.error);
2314 utils.fs.mkdir_if_not_exists(TEMP_UPLOAD_DIR).catch(console.error);
2415
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
11716 module.exports = {
11817 post: {
11918 "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() {
12330 return utils.fs.readFile(file_path);
12431 }).then(function(subtitle) {
12532 res.send(utils.ok({
12633 subtitles: JSON.parse(subtitle.toString())
12734 }));
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"));
13336 },
13437 "upload": {
13538 "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,
14152 JSON.stringify(subtitle.parse(req.body.subtitle_string))
14253 );
14354 }).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, {
14756 ip: res.locals.ip,
14857 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"));
15762 },
15863 "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
16664 const TEMP_UPLOAD_DIR_USER = path.join(TEMP_UPLOAD_DIR, req.user.user_id);
16765
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() {
16974 return utils.get_storage_remaining();
17075 }).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);
17579
17680 // Multiply by 2 because there must be
17781 // 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"));
20293 },
20394 "": [{
20495 rateLimit: false
20596 }, 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, {
208101 message: "Error uploading file, no upload session",
209102 type: "error"
210103 }));
211104 }
212105
213 let TEMP_UPLOAD_DIR_USER = upload_session.get(req.user).temp_dir;
214106 uploader(
215 req, TEMP_UPLOAD_DIR_USER,
107 req,
108 ongoing_upload.temp_dir,
216109 // 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
218111 10 // Chunk size in MB
219 ).then(function(assembleChunks) {
112 ).then(function(assemble_chunks) {
220113 // chunk written to disk
221114 res.writeHead(204, 'No Content');
222115 res.end();
223116
117 if (!assemble_chunks) {
118 return utils.resolve();
119 }
120
224121 // on last chunk, assembleChunks function is returned
225122 // 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,
229157 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);
232202 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() {
246209 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 })
249216 }, {
250217 [req.user.user_id]: true
251218 });
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);
330228 });
331229 }]
332230 }
336234 "": [{
337235 rateLimit: false
338236 }, 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) {
342244 const range = req.headers.range;
343245
344246 if (range) {
0 const utils = require('../utils');
1 const upload_session = require('../utils/upload_session');
02 const path = require('path');
1 const upload_session = require('../utils/upload_session');
2 const rimraf = require("rimraf");
33
44 module.exports = {
55 get: {
99 // Hence they have no upload session.
1010 let current_upload_session = upload_session.get(req.user);
1111 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);
1514 }
16
1715 res.sendFile(path.join(HTML_ROOT, "index.html"));
1816 },
1917 "login": [{
0 const email = require('./email');
0 const utils = require('.');
1 const email = require('./mailgun');
12 let clamscan;
23
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();
219 }
22 });
2310
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) {
2630 clamscan = new_clamscan;
27 const version = await clamscan.get_version();
31 return clamscan.get_version();
32 }).then(function(version) {
2833 console.log(`ClamAV Version: ${version}`);
29 } catch (error) {
34 }).catch(function(error) {
3035 console.error(error);
36 });
37 },
38 assert_safe: function(file_path, infos) {
39 if (!clamscan) {
40 return utils.resolve();
3141 }
32 }).catch(function(error) {
33 console.error(error);
34 });
35 }
3642
37 module.exports = {
38 assert_safe: NO_SCAN ? () => new Promise(r => r()) : function(file_path, infos) {
3943 return clamscan.is_infected(file_path).then(function(scan_result) {
4044 return new Promise(function(resolve, reject) {
4145 if (scan_result.is_infected) {
4751 user_id: infos.user_id,
4852 file: file_path,
4953 viruses: scan_result.viruses.join(', ')
50 });
54 }).catch(console.error);
5155 } else {
5256 resolve();
5357 }
+0
-15
server/utils/email/account_delete_code.html less more
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
-13
server/utils/email/account_deleted.html less more
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
-20
server/utils/email/confirmation.html less more
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
-68
server/utils/email/index.js less more
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
-19
server/utils/email/password_changed.html less more
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
-17
server/utils/email/password_reset.html less more
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
-16
server/utils/email/virus_detected.html less more
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 };
00 const {
11 v4: uuid
22 } = require('uuid');
3 const {
4 promisify
5 } = require('util');
36 const fs = require('fs');
4 const getSize = require('get-folder-size');
57 const zxcvbn = require('zxcvbn');
68 const crypto = require('crypto');
79 const mysql = require('mysql');
810 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
1017 const max_video_dir_size = 20000000000; // 20 GB
1118
1219 const mysql_options = {
3340
3441 function query(query, wildcards) {
3542 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");
3947 } else {
4048 resolve(rows);
4149 }
5058 }
5159
5260 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);
5679 } else {
57 reject(error);
80 console.error("Response already sent in response error handler");
81 console.error(error);
5882 }
59 });
83 };
6084 }
6185
6286 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,
64142 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()),
76144 mysql_options: mysql_options,
77145 query: query,
78146 /*
97165 }, " " + SQL_OPERATOR);
98166 }, "");
99167 },
100 status: status_codes,
101 email: require('./email'),
102168 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 },
103181 email: function(str) {
104182 return str && str.match(/.+@.+\..+/) !== null;
105183 },
106184 password: function(str) {
107185 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;
108189 },
109190 query: {
110191 empty: function(rows, error) {
116197 },
117198 condition: condition_promise
118199 },
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)),
122202 generate_token: function(table, key, email, overrides) {
123203 overrides = overrides || {};
124
125204 let token = overrides.token || uuid();
126205 // So there either is no token at all, or there is one but it has expired
127206 return query(
128207 `INSERT INTO ${table} (${key}, email, expires) ` +
129208 `VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE ${key}=VALUES(${key}), expires=VALUES(expires)`,
130209 [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);
145211 },
146212 get_display_name: function(user_id) {
147213 return query("SELECT display_name FROM users WHERE ?", {
157223 return rows[0].name;
158224 }).catch(console.error);
159225 },
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 },
223226 setIntervalAndTriggerImmediate: function(callback, interval) {
224227 setTimeout(callback);
225228 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 });
243229 }
244230 };
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;
1111 }]);
1212
1313 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"]);
1615
1716 const express = require("express");
1817 const app = express();
2221 const MySQLStore = require('express-mysql-session')(session);
2322 const passport = require('passport');
2423 const LocalStrategy = require('passport-local').Strategy;
24 const bcrypt = require('bcrypt');
25 const axios = require('axios');
26 const qs = require('querystring');
2527 const path = require('path');
2628 const utils = require('./utils');
2729 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');
3131 const buildRoutes = require('./buildRoutes')(app);
32 const rimraf = require("rimraf");
3332
3433 const PORT = process.env.PORT || 3000;
3534 global.HTML_ROOT = path.join(__dirname, "../dist");
5958 }, function(email, password, done) {
6059 utils.query("SELECT * FROM users WHERE ?", {
6160 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, {
6665 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 }]);
6773 });
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 email
87 ).then(function(token) {
88 return mailgun.send_confirmation_email(email, {
89 query: qs.encode({
90 token: token
91 })
92 });
8293 });
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);
114102 }).catch(function(error) {
115103 console.error(error);
116104 done(error);
207195 next();
208196 } else {
209197 console.log("Captcha bypass failed");
210 res.send(utils.error());
198 res.send(utils.not_ok(utils.status.error));
211199 }
212200 });
213201 } else {
241229 } else {
242230 console.log("Captcha failed with score", response.data.score);
243231 console.log(response.data);
244 res.send(utils.error({
232 res.send(utils.not_ok(utils.status.error, {
245233 message: "Captcha says you are a bot",
246234 type: "error"
247235 }));
252240
253241 defaultMiddleware.set("captcha", {
254242 default: false,
255 func: NO_CAPTCHA ? function(req, res, next) {
243 func: options["no-captcha"] ? function(req, res, next) {
256244 console.log("Do not check captcha");
257245 console.log("captcha_token:", req.body.captcha_token);
258246 console.log("captcha_bypass_token:", req.body.captcha_bypass_token);
261249 delete req.body.captcha_token;
262250 next();
263251 } else {
264 res.send(utils.error({
252 res.send(utils.not_ok(utils.status.error, {
265253 message: "Debug mode captcha failed - no tokens",
266254 type: "error"
267255 }));
269257 } : function(req, res, next) {
270258 check_captcha_token(req, res, next).catch(function(error) {
271259 console.error("CAPTCHA ERROR:", error);
272 res.send(utils.error({
260 res.send(utils.not_ok(utils.status.error, {
273261 message: "Internal Server Error",
274262 type: "error"
275263 }));
290278 app.use(express.static("./dist"));
291279
292280 // Add fallbacks for unknown requests
293 app.use(function(req, res) {
281 app.use(function(error, req, res, next) {
294282 res.sendFile(path.join(HTML_ROOT, "error.html"));
295283 });
296284
315303 let now = Date.now();
316304 console.log("Clean up data on", time.toString());
317305
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) {
324311 websocket.broadcast({
325312 command: "delete-video-response",
326313 data: utils.ok({
327 video_id: row.video_id,
328 name: row.name,
314 video_id: video.video_id,
315 name: video.name,
329316 reason: "expired"
330317 })
331318 }, {
332 [row.created_by]: true
319 [video.created_by]: true
333320 });
334321 });
335 }
322 });
336323 }).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 }));
341333 }).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 });
349334 }, time.one_day * 0.25); // 4 times per day
5252 }));
5353 }).catch(function(error) {
5454 console.error(error);
55 respond(utils.error({
55 respond(utils.not_ok(utils.status.error, {
5656 videos: []
5757 }));
5858 });
233233 expires: one_week_from_now
234234 }));
235235 } else {
236 respond(utils.error({
236 respond(utils.not_ok(utils.status.error, {
237237 message: "Cannot renew video",
238238 type: "error"
239239 }));
240240 }
241241 }).catch(function(error) {
242242 console.error(error);
243 respond(utils.error({
243 respond(utils.not_ok(utils.status.error, {
244244 message: "Error renewing video",
245245 type: "error"
246246 }));
265265 }).catch(console.error);
266266 });
267267 } else {
268 respond(utils.error({
268 respond(utils.not_ok(utils.status.error, {
269269 message: "Cannot delete video",
270270 type: "error"
271271 }));
272272 }
273273 }).catch(function(error) {
274274 console.error(error);
275 respond(utils.error({
275 respond(utils.not_ok(utils.status.error, {
276276 message: "Error deleting video",
277277 type: "error"
278278 }));
169169 });
170170 }).catch(function(error) {
171171 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);
179173 });
180174 }
181175
00 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 );
25 const qs = require('querystring');
36
47 function resolveResponse(response) {
58 return new Promise(function(resolve, reject) {
6 if (response.data.status === status_codes.ok) {
9 if (response.data.status === STATUS_CODES.ok) {
710 resolve(response.data);
811 } else {
912 reject(response.data);
1215 }
1316
1417 module.exports = {
15 status: status_codes,
18 status: STATUS_CODES,
1619 post: function(path, data) {
1720 return axios.post(`/api${path}`, data).then(resolveResponse);
1821 },
11 "ok": 0,
22 "error": 500,
33 "unauthorized": 403,
4 "ws_noauth": 4030,
5 "video": {
6 "ready": 0,
7 "uploading": 1,
8 "reassembling": 2,
9 "converting": 3
10 }
4 "ws_noauth": 4030
115 }
0 {
1 "ready": 0,
2 "uploading": 1,
3 "reassembling": 2,
4 "converting": 3
5 }