diff --git a/gulpfile.js b/gulpfile.js index 31dd0c49..ba8c5c2a 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -4,7 +4,7 @@ var gulp = require('gulp'), concat = require('gulp-concat'); gulp.task('js', function () { - gulp.src(['server/VERSION.js', 'server/config/api_key.js', 'server/public/assets/js/*.js', '!server/public/assets/js/embed*', '!server/public/assets/js/remotecontroller.js', '!server/public/assets/js/callback.js']) + gulp.src(['server/VERSION.js', 'server/config/api_key.js', 'server/public/assets/js/*.js', '!server/public/assets/js/embed*', '!server/public/assets/js/token*', '!server/public/assets/js/remotecontroller.js', '!server/public/assets/js/callback.js']) .pipe(uglify({ mangle: true, compress: true, @@ -25,6 +25,17 @@ gulp.task('embed', function () { .pipe(gulp.dest('server/public/assets/dist')); }); +gulp.task('token', function() { + gulp.src(['server/public/assets/js/token*', 'server/public/assets/js/helpers.js']) + .pipe(uglify({ + mangle: true, + compress: true, + enclose: true + })) + .pipe(concat('token.min.js')) + .pipe(gulp.dest('server/public/assets/dist')); +}) + gulp.task('callback', function () { gulp.src(['server/VERSION.js', 'server/config/api_key.js', 'server/public/assets/js/callback.js']) .pipe(uglify({ @@ -53,6 +64,7 @@ gulp.task('remotecontroller', function () { gulp.task('default', function(){ gulp.watch(['server/VERSION.js', 'server/public/assets/js/*.js'], ['js']); + gulp.watch(['server/public/assets/js/token*.js', 'server/public/assets/js/helpers.js'], ['token']); gulp.watch(['server/VERSION.js', 'server/public/assets/js/*.js'], ['embed']); gulp.watch(['server/VERSION.js', 'server/public/assets/js/callback.js', 'server/public/assets/js/helpers.js'], ['callback']); //gulp.watch('server/public/assets/js/*.js', ['nochan']); diff --git a/server/README.md b/server/README.md index 85e61c3d..6145cb36 100644 --- a/server/README.md +++ b/server/README.md @@ -6,7 +6,7 @@ Under ``` /server/apps/ ```, there are two files, ``` admin.js ``` and ``` clien All PUT, DELETE and POST endpoints have a 2-second waitlimit for each command per client. You'll get a response with Retry-After header for how long you have to wait. Shuffling in a player has a 5-second waitlimit, but per channel instead of per client. -If you want to skip the wait-times, send a mail to the team at contact@zoff.me, and get a token. Tokens are added to all the POST, PUT, DELETE, requests as ``` token: TOKEN ```. +If you want to skip the wait-times, create a token at https://zoff.me/api/apply. Tokens are added to all the POST, PUT, DELETE, requests as ``` token: TOKEN ```. All requests return things on this form (results field is added if successful.) diff --git a/server/handlers/db.js b/server/handlers/db.js index 17154969..75639232 100644 --- a/server/handlers/db.js +++ b/server/handlers/db.js @@ -12,6 +12,7 @@ var ObjectId = mongojs.ObjectId; db.collection("chat_logs").createIndex({ "createdAt": 1 }, { expireAfterSeconds: 600 }); db.collection("timeout_api").createIndex({ "createdAt": 1 }, { expireAfterSeconds: 5 }); +db.collection("api_links").createIndex({ "createdAt": 1 }, { expireAfterSeconds: 86400 }); db.on('connected', function(err) { console.log("connected"); }) diff --git a/server/handlers/suggestions.js b/server/handlers/suggestions.js index 66ed798e..fac9e349 100644 --- a/server/handlers/suggestions.js +++ b/server/handlers/suggestions.js @@ -9,7 +9,7 @@ function thumbnail(msg, coll, guid, offline, socket) { if(msg.thumbnail.substring(0,2) != "//") msg.thumbnail = "//" + msg.thumbnail; var channel = msg.channel.toLowerCase(); var hash = Functions.hash_pass(Functions.decrypt_string(socket.zoff_id, msg.adminpass)); - db.collection(channel + "_settings").update({id: "configs"}, function(err, docs){ + db.collection(channel + "_settings").find({id: "config"}, function(err, docs){ if(docs.length > 0 && (docs[0].userpass == undefined || docs[0].userpass == "" || (msg.hasOwnProperty('pass') && docs[0].userpass == Functions.decrypt_string(socketid, msg.pass)))) { if(docs !== null && docs.length !== 0 && docs[0].adminpass !== "" && docs[0].adminpass == hash){ db.collection("suggested_thumbnails").update({channel: channel}, {$set:{thumbnail: msg.thumbnail}}, {upsert:true}, function(err, docs){ @@ -35,7 +35,7 @@ function description(msg, coll, guid, offline, socket) { } var channel = msg.channel.toLowerCase(); var hash = Functions.hash_pass(Functions.decrypt_string(socket.zoff_id, msg.adminpass)); - db.collection(channel + "_settings").update({id: "configs"}, function(err, docs){ + db.collection(channel + "_settings").find({id: "config"}, function(err, docs){ if(docs.length > 0 && (docs[0].userpass == undefined || docs[0].userpass == "" || (msg.hasOwnProperty('pass') && docs[0].userpass == Functions.decrypt_string(socketid, msg.pass)))) { if(docs !== null && docs.length !== 0 && docs[0].adminpass !== "" && docs[0].adminpass == hash){ db.collection("suggested_descriptions").update({channel: channel}, {$set:{description: msg.description}}, {upsert:true}, function(err, docs){ diff --git a/server/public/assets/admin/authenticated/js/main.js b/server/public/assets/admin/authenticated/js/main.js index 061721d1..b0f7e85c 100644 --- a/server/public/assets/admin/authenticated/js/main.js +++ b/server/public/assets/admin/authenticated/js/main.js @@ -30,17 +30,21 @@ $(document).on("click", "#refresh_all", function(e){ } $(".header-api-fields").removeClass("hide"); for(var i = 0; i < response.length; i++) { - var to_add = api_token_list; + var to_add = api_token_list.clone(); + to_add.find(".api_token_limit").val(response[i].limit); to_add.attr("id", response[i]._id); to_add.find(".api_token_name").text(response[i].name); to_add.find(".api_token_usage").text(response[i].usage); + to_add.find(".api_token_limit").attr("id", response[i]._id + "-limit"); to_add.find("#delete_api_token").attr("data-id", response[i]._id); + to_add.find("#update_api_token").attr("data-id", response[i]._id); $(".channel_things").append(to_add); } }, error: function(err) { } }); + if(!$(".channel_things").hasClass("hide")) { $(".channel_things").addClass("hide") } @@ -116,11 +120,39 @@ if(!$(".channel_things").hasClass("hide")) { } $(".preloader-wrapper").removeClass("hide"); +$(document).on("click", "#update_api_token", function(e) { + e.preventDefault(); + + var id = $(this).attr("data-id"); + var limit = $("#" + id + "-limit").val(); + var that = this; + $(that).toggleClass("disabled"); + $("#delete_api_token").toggleClass("disabled"); + $.ajax({ + type: "PUT", + url: "api/api_token", + data: { + id: id, + limit: limit, + }, + success: function(response) { + if(response == "OK") { + Materialize.toast("Updated limit!", 2000, "green lighten"); + } else { + Materialize.toast("Something went wrong...", 2000, "red lighten"); + } + $(that).toggleClass("disabled"); + $("#delete_api_token").toggleClass("disabled"); + } + }); +}); + $(document).on("click", "#delete_api_token", function(e) { e.preventDefault(); var id = $(this).attr("data-id"); var that = this; $(that).toggleClass("disabled"); + $("#update_api_token").toggleClass("disabled"); $.ajax({ type: "DELETE", url: "api/api_token", @@ -134,6 +166,7 @@ $(document).on("click", "#delete_api_token", function(e) { } else { Materialize.toast("Something went wrong...", 2000, "red lighten"); $(that).toggleClass("disabled"); + $("#update_api_token").toggleClass("disabled"); } }, }) @@ -153,10 +186,13 @@ function loaded() { $(".header-api-fields").removeClass("hide"); for(var i = 0; i < response.length; i++) { var to_add = api_token_list.clone(); + to_add.find(".api_token_limit").val(response[i].limit); to_add.attr("id", response[i]._id); to_add.find(".api_token_name").text(response[i].name); to_add.find(".api_token_usage").text(response[i].usage); + to_add.find(".api_token_limit").attr("id", response[i]._id + "-limit"); to_add.find("#delete_api_token").attr("data-id", response[i]._id); + to_add.find("#update_api_token").attr("data-id", response[i]._id); $(".channel_things").append(to_add); } }, diff --git a/server/public/assets/css/style.css b/server/public/assets/css/style.css index f90e7cdf..b9c054c7 100755 --- a/server/public/assets/css/style.css +++ b/server/public/assets/css/style.css @@ -91,6 +91,27 @@ body { height: 32px; } +.token-container { + padding-top: 64px; +} + +.token-form { + position: relative; +} + +.full-form-token { + background: rgba(0,0,0,.5); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.center-loader-token { + margin: auto; +} + #send-loader { position: absolute; top: 0px; @@ -405,6 +426,10 @@ li.disabled span { user-select: text; } +.auto-pointer { + cursor: auto !important; +} + #chatchannel li:nth-child(even), #chatall li:nth-child(even) { background: rgba(0,0,0,.1); } diff --git a/server/public/assets/dist/token.min.js b/server/public/assets/dist/token.min.js new file mode 100644 index 00000000..d919ec88 --- /dev/null +++ b/server/public/assets/dist/token.min.js @@ -0,0 +1 @@ +!function(){$(document).ready(function(){$("#about").modal(),$(".help-button-footer").hide(),$("#contact").modal(),$("#contact-container").empty(),$("#contact-container").html("Send a mail to us: contact@zoff.me"),$("#submit-contact-form").hide(),$(".token-form").on("submit",function(e){e.preventDefault();var t=$("#email_address").val();$("#email_address").attr("readonly",!0),$(".submit").toggleClass("disabled"),$(".full-form-token").removeClass("hide");var a=grecaptcha.getResponse();$.ajax({type:"POST",url:"/api/apply",data:{email:t,"g-recaptcha-response":a},success:function(e){$(".full-form-token").addClass("hide"),"OK"!=e?($("#email_address").attr("readonly",!0),$(".submit").toggleClass("disabled"),Materialize.toast("Couldn't send email.",3e3,"red lighten")):(Materialize.toast("Email sent!",3e3,"green lighten"),grecaptcha.reset())},error:function(e){$(".full-form-token").addClass("hide"),$("#email_address").attr("readonly",!1),$(".submit").toggleClass("disabled")}})}),$("#submit-contact-form").on("click",function(e){e.preventDefault(),$("#contact-form").submit()})});Element.prototype.remove=function(){this.parentElement.removeChild(this)},NodeList.prototype.remove=HTMLCollection.prototype.remove=function(){for(var e=0,t=this.length;e startTime + Player.np.start && !fix_too_far) { + if(player.getCurrentTime() > startTime + Player.np.start && !fix_too_far && autoplay) { Player.seekTo(seekTo); Player.playVideo(); fix_too_far = true; diff --git a/server/public/assets/js/token_apply.js b/server/public/assets/js/token_apply.js new file mode 100644 index 00000000..549ad1f9 --- /dev/null +++ b/server/public/assets/js/token_apply.js @@ -0,0 +1,47 @@ +$(document).ready(function() { + $("#about").modal(); + $(".help-button-footer").hide(); + $("#contact").modal(); + + $("#contact-container").empty(); + $("#contact-container").html("Send a mail to us: contact@zoff.me"); + $("#submit-contact-form").hide(); + + $(".token-form").on("submit", function(e) { + e.preventDefault(); + var email = $("#email_address").val(); + $("#email_address").attr("readonly", true); + $(".submit").toggleClass("disabled"); + $(".full-form-token").removeClass("hide"); + var captcha_response = grecaptcha.getResponse(); + $.ajax({ + type: "POST", + url: "/api/apply", + data: { + email: email, + "g-recaptcha-response": captcha_response, + }, + success: function(response) { + $(".full-form-token").addClass("hide"); + if(response != "OK") { + $("#email_address").attr("readonly", true); + $(".submit").toggleClass("disabled"); + Materialize.toast("Couldn't send email.", 3000, "red lighten"); + } else { + Materialize.toast("Email sent!", 3000, "green lighten"); + grecaptcha.reset(); + } + }, + error: function(response) { + $(".full-form-token").addClass("hide"); + $("#email_address").attr("readonly", false); + $(".submit").toggleClass("disabled"); + } + }); + }); + + $('#submit-contact-form').on('click', function(e) { + e.preventDefault(); + $("#contact-form").submit(); + }); +}); diff --git a/server/public/layouts/admin/authenticated.handlebars b/server/public/layouts/admin/authenticated.handlebars index fdd34ffc..f5660c7e 100644 --- a/server/public/layouts/admin/authenticated.handlebars +++ b/server/public/layouts/admin/authenticated.handlebars @@ -111,21 +111,24 @@
-
+
Name
-
+
Usage
+
+ Limit +
-
+
-
-
-
- REMOVE +
+ + UPDATE + X
diff --git a/server/public/layouts/client/token.handlebars b/server/public/layouts/client/token.handlebars new file mode 100644 index 00000000..8aae6f2d --- /dev/null +++ b/server/public/layouts/client/token.handlebars @@ -0,0 +1,72 @@ +
+ + {{> modal/about}} + +
+
+
+
+ {{#if activated}} +

API-token

+

Here is your api token

+

{{token}}

+

Use it wisely, and don't lose it!

+

As of now, the tokens have no limit for how many requests you can do is 100 requests a second. If you need a higher limit, just contact the team and we'll set you up for as much as you need.

+ {{else}} +

API-token

+

Apply for a API-token with your email here! You'll get an email on the specified address, with a link. Follow that link, and the token will be shown to you! Take good care of it, and don't lose it. It won't be shown to you again.

+

If you're wondering anything about how the api works, there is a guide on our GitHub. You can also click HERE to be taken to the detailed README.

+

As of now, the tokens have no limit for how many requests you can do is 100 requests a second. If you need a higher limit, just contact the team and we'll set you up for as much as you need.

+ {{/if}} +
+ {{#if activated}} + + {{else}} +
+
+
+ + +
+
+
+ {{{captcha}}} +
+
+ +
+
+
+ {{> spinner}} +
+
+
+ {{/if}} +

Any lost tokens can easily be deleted by our admins, so just send us an email if something goes awry. Just click the CONTACT button in the footer, and we will be with you as fast as we can!

+
+
+
diff --git a/server/public/partials/contact.handlebars b/server/public/partials/contact.handlebars index 7f3c3e8c..be424cb2 100644 --- a/server/public/partials/contact.handlebars +++ b/server/public/partials/contact.handlebars @@ -15,7 +15,7 @@
{{{captcha}}} -
+
{{> spinner}}
diff --git a/server/routing/admin/api.js b/server/routing/admin/api.js index 1bc736cf..7f00c473 100644 --- a/server/routing/admin/api.js +++ b/server/routing/admin/api.js @@ -171,7 +171,7 @@ router.route('/api/api_token').get(function(req, res) { if(req.isAuthenticated()) { token_db.collection("api_token").find({token: {$exists: true}}, function(err, all) { res.json(all); - }) + }); } else { res.sendStatus(403); } @@ -190,6 +190,24 @@ router.route('/api/api_token').delete(function(req, res){ } }); +router.route('/api/api_token').put(function(req, res){ + if(req.isAuthenticated()){ + var id = req.body.id; + var limit = req.body.limit; + if(limit < 0) { + res.sendStatus(500); + return; + } + token_db.collection("api_token").update({_id: ObjectId(id)}, {$set: {limit: limit}}, function(err, success) { + if(err) { + res.sendStatus(500); + return; + } + res.sendStatus(200); + }) + } +}); + router.route('/api/api_token').post(function(req, res){ if(req.isAuthenticated()){ var name = req.body.name; diff --git a/server/routing/client/api.js b/server/routing/client/api.js index 0a86a99f..c55a96fd 100644 --- a/server/routing/client/api.js +++ b/server/routing/client/api.js @@ -147,48 +147,50 @@ router.route('/api/list/:channel_name/:video_id').delete(function(req, res) { if(token_docs.length == 1 && token_docs[0].token == token) { authorized = true; } - checkTimeout(guid, res, authorized, "DELETE", function() { - if(token != "" && !authorized) { - updateTimeout(guid, res, authorized, "DELETE", function(err, docs) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - }); - } - validateLogin(adminpass, userpass, channel_name, "delete", res, function(exists) { - if(!exists) { - res.status(404).send(JSON.stringify(error.not_found.list)); - return; + checkOveruseApiToken(authorized, token_docs, res, function() { + checkTimeout(guid, res, authorized, "DELETE", function() { + if(token != "" && !authorized) { + updateTimeout(guid, res, authorized, "DELETE", function(err, docs) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; + }); } - db.collection(channel_name).find({id:video_id, now_playing: false}, function(err, docs){ - if(docs.length == 0) { - res.status(404).send(JSON.stringify(error.not_found.local)); + validateLogin(adminpass, userpass, channel_name, "delete", res, function(exists) { + if(!exists) { + res.status(404).send(JSON.stringify(error.not_found.list)); return; } - var dont_increment = false; - if(docs[0]){ - if(docs[0].type == "suggested"){ - dont_increment = true; + db.collection(channel_name).find({id:video_id, now_playing: false}, function(err, docs){ + if(docs.length == 0) { + res.status(404).send(JSON.stringify(error.not_found.local)); + return; } - db.collection(channel_name).remove({id:video_id}, function(err, docs){ - if(authorized) { - incrementToken(token); + var dont_increment = false; + if(docs[0]){ + if(docs[0].type == "suggested"){ + dont_increment = true; } - io.to(channel_name).emit("channel", {type:"deleted", value: video_id}); - if(!dont_increment) { - db.collection("frontpage_lists").update({_id: channel_name, count: {$gt: 0}}, {$inc: {count: -1}, $set:{accessed: Functions.get_time()}}, {upsert: true}, function(err, docs){ + db.collection(channel_name).remove({id:video_id}, function(err, docs){ + if(authorized) { + incrementToken(token); + } + io.to(channel_name).emit("channel", {type:"deleted", value: video_id}); + if(!dont_increment) { + db.collection("frontpage_lists").update({_id: channel_name, count: {$gt: 0}}, {$inc: {count: -1}, $set:{accessed: Functions.get_time()}}, {upsert: true}, function(err, docs){ + updateTimeout(guid, res, authorized, "DELETE", function(err, docs) { + res.status(200).send(JSON.stringify(error.no_error)); + return; + }); + }); + } else { updateTimeout(guid, res, authorized, "DELETE", function(err, docs) { res.status(200).send(JSON.stringify(error.no_error)); return; }); - }); - } else { - updateTimeout(guid, res, authorized, "DELETE", function(err, docs) { - res.status(200).send(JSON.stringify(error.no_error)); - return; - }); - } - }); - } + } + }); + } + }); }); }); }); @@ -247,64 +249,66 @@ router.route('/api/conf/:channel_name').put(function(req, res) { if(token_docs.length == 1 && token_docs[0].token == token) { authorized = true; } - checkTimeout(guid, res, authorized, "CONFIG", function() { - if(token != "" && !authorized) { - updateTimeout(guid, res, authorized, "CONFIG", function(err, docs) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - }); - } - validateLogin(adminpass, userpass, channel_name, "config", res, function(exists, conf) { - if(!exists && conf.length == 0) { - res.status(404).send(JSON.stringify(error.not_found.list)); - return; + checkOveruseApiToken(authorized, token_docs, res, function() { + checkTimeout(guid, res, authorized, "CONFIG", function() { + if(token != "" && !authorized) { + updateTimeout(guid, res, authorized, "CONFIG", function(err, docs) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; + }); } + validateLogin(adminpass, userpass, channel_name, "config", res, function(exists, conf) { + if(!exists && conf.length == 0) { + res.status(404).send(JSON.stringify(error.not_found.list)); + return; + } - if((!userpass_changed && frontpage) || (userpass_changed && userpass == "")) { - userpass = ""; - } else if(userpass_changed && userpass != "") { - frontpage = false; - } - var description = ""; + if((!userpass_changed && frontpage) || (userpass_changed && userpass == "")) { + userpass = ""; + } else if(userpass_changed && userpass != "") { + frontpage = false; + } + var description = ""; - var obj = { - addsongs:addsongs, - allvideos:allvideos, - frontpage:frontpage, - skip:skipping, - vote:voting, - removeplay:removeplay, - shuffle:shuffling, - longsongs:longsongs, - adminpass:adminpass, - desc: description, - }; - if(userpass_changed) { - obj["userpass"] = userpass; - } else if (frontpage) { - obj["userpass"] = ""; - } - db.collection(channel_name + "_settings").update({views:{$exists:true}}, { - $set:obj - }, function(err, docs){ + var obj = { + addsongs:addsongs, + allvideos:allvideos, + frontpage:frontpage, + skip:skipping, + vote:voting, + removeplay:removeplay, + shuffle:shuffling, + longsongs:longsongs, + adminpass:adminpass, + desc: description, + }; + if(userpass_changed) { + obj["userpass"] = userpass; + } else if (frontpage) { + obj["userpass"] = ""; + } + db.collection(channel_name + "_settings").update({views:{$exists:true}}, { + $set:obj + }, function(err, docs){ - if(obj.adminpass !== "") obj.adminpass = true; - if(obj.hasOwnProperty("userpass") && obj.userpass != "") obj.userpass = true; - else obj.userpass = false; - io.to(channel_name).emit("conf", [obj]); + if(obj.adminpass !== "") obj.adminpass = true; + if(obj.hasOwnProperty("userpass") && obj.userpass != "") obj.userpass = true; + else obj.userpass = false; + io.to(channel_name).emit("conf", [obj]); - db.collection("frontpage_lists").update({_id: channel_name}, {$set:{ - frontpage:frontpage, accessed: Functions.get_time()} - }, - {upsert:true}, function(err, docs){ - if(authorized) { - incrementToken(token); - } - updateTimeout(guid, res, authorized, "CONFIG", function(err, docs) { - var to_return = error.no_error; - to_return.results = [obj]; - res.status(200).send(JSON.stringify(to_return)); - return; + db.collection("frontpage_lists").update({_id: channel_name}, {$set:{ + frontpage:frontpage, accessed: Functions.get_time()} + }, + {upsert:true}, function(err, docs){ + if(authorized) { + incrementToken(token); + } + updateTimeout(guid, res, authorized, "CONFIG", function(err, docs) { + var to_return = error.no_error; + to_return.results = [obj]; + res.status(200).send(JSON.stringify(to_return)); + return; + }); }); }); }); @@ -348,44 +352,46 @@ router.route('/api/list/:channel_name/:video_id').put(function(req,res) { if(token_docs.length == 1 && token_docs[0].token == token) { authorized = true; } - checkTimeout(guid, res, authorized, "PUT", function() { - if(token != "" && !authorized) { - updateTimeout(guid, res, authorized, "PUT", function(err, docs) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - }); - } - validateLogin(adminpass, userpass, channel_name, "vote", res, function(exists) { - if(!exists) { - res.status(404).send(JSON.stringify(error.not_found.list)); - return; + checkOveruseApiToken(authorized, token_docs, res, function() { + checkTimeout(guid, res, authorized, "PUT", function() { + if(token != "" && !authorized) { + updateTimeout(guid, res, authorized, "PUT", function(err, docs) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; + }); } - db.collection(channel_name).find({id: video_id, now_playing: false, type:"video"}, function(err, song) { - if(song.length == 0) { - res.status(404).send(JSON.stringify(error.not_found.local)); + validateLogin(adminpass, userpass, channel_name, "vote", res, function(exists) { + if(!exists) { + res.status(404).send(JSON.stringify(error.not_found.list)); return; - } else if(song[0].guids.indexOf(guid) > -1) { - res.status(409).send(JSON.stringify(error.conflicting)); - return; - } else { - song[0].votes += 1; - song[0].guids.push(guid); - db.collection(channel_name).update({id: video_id}, {$inc:{votes:1}, $set:{added:Functions.get_time(), type: "video"}, $push :{guids: guid}}, function(err, success) { - if(authorized) { - incrementToken(token); - } - io.to(channel_name).emit("channel", {type: "vote", value: video_id, time: Functions.get_time()}); - List.getNextSong(channel_name, function() { - updateTimeout(guid, res, authorized, "PUT", function(err, docs) { - var to_return = error.no_error; - to_return.results = song; - res.status(200).send(JSON.stringify(to_return)); - return; + } + db.collection(channel_name).find({id: video_id, now_playing: false, type:"video"}, function(err, song) { + if(song.length == 0) { + res.status(404).send(JSON.stringify(error.not_found.local)); + return; + } else if(song[0].guids.indexOf(guid) > -1) { + res.status(409).send(JSON.stringify(error.conflicting)); + return; + } else { + song[0].votes += 1; + song[0].guids.push(guid); + db.collection(channel_name).update({id: video_id}, {$inc:{votes:1}, $set:{added:Functions.get_time(), type: "video"}, $push :{guids: guid}}, function(err, success) { + if(authorized) { + incrementToken(token); + } + io.to(channel_name).emit("channel", {type: "vote", value: video_id, time: Functions.get_time()}); + List.getNextSong(channel_name, function() { + updateTimeout(guid, res, authorized, "PUT", function(err, docs) { + var to_return = error.no_error; + to_return.results = song; + res.status(200).send(JSON.stringify(to_return)); + return; + }); }); }); - }); - } - }) + } + }) + }); }); }); }); @@ -420,35 +426,37 @@ router.route('/api/list/:channel_name/__np__').post(function(req, res) { if(token_docs.length == 1 && token_docs[0].token == token) { authorized = true; } - checkTimeout(guid, res, authorized, "POST", function() { - if(token != "" && !authorized) { - updateTimeout(guid, res, authorized, "POST", function(err, docs) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - }); - } - db.collection(channel_name).find({now_playing: true}, toShowChannel, function(err, list) { - if(list.length > 0) { - db.collection(channel_name + "_settings").find({ id: "config" }, function(err, conf) { - if(authorized) { - incrementToken(token); - } - if(conf.length == 0) { - res.status(404).send(JSON.stringify(error.not_found.list)); - return; - } else if(conf[0].userpass != userpass && conf[0].userpass != "") { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - } - updateTimeout(guid, res, authorized, "POST", function(err, docs) { - var to_return = error.no_error; - to_return.results = list; - res.status(200).send(JSON.stringify(to_return)); - }); + checkOveruseApiToken(authorized, token_docs, res, function() { + checkTimeout(guid, res, authorized, "POST", function() { + if(token != "" && !authorized) { + updateTimeout(guid, res, authorized, "POST", function(err, docs) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; }); - } else { - res.status(404).send(JSON.stringify(error.not_found.list)); } + db.collection(channel_name).find({now_playing: true}, toShowChannel, function(err, list) { + if(list.length > 0) { + db.collection(channel_name + "_settings").find({ id: "config" }, function(err, conf) { + if(authorized) { + incrementToken(token); + } + if(conf.length == 0) { + res.status(404).send(JSON.stringify(error.not_found.list)); + return; + } else if(conf[0].userpass != userpass && conf[0].userpass != "") { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; + } + updateTimeout(guid, res, authorized, "POST", function(err, docs) { + var to_return = error.no_error; + to_return.results = list; + res.status(200).send(JSON.stringify(to_return)); + }); + }); + } else { + res.status(404).send(JSON.stringify(error.not_found.list)); + } + }); }); }); }); @@ -502,86 +510,88 @@ router.route('/api/list/:channel_name/:video_id').post(function(req,res) { if(token_docs.length == 1 && token_docs[0].token == token) { authorized = true; } - checkTimeout(guid, res, authorized, "POST", function() { - if(token != "" && !authorized) { - updateTimeout(guid, res, authorized, "POST", function(err, docs) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - }); - } - var type = fetch_only ? "fetch_song" : "add"; - validateLogin(adminpass, userpass, channel_name, type, res, function(exists, conf, authenticated) { - db.collection(channel_name).find({id: video_id}, function(err, result) { - if(result.length == 0 || result[0].type == "suggested") { - var song_type = authenticated ? "video" : "suggested"; - if(fetch_only && result.length == 0) { - res.status(404).send(JSON.stringify(error.not_found.local)); + checkOveruseApiToken(authorized, token_docs, res, function() { + checkTimeout(guid, res, authorized, "POST", function() { + if(token != "" && !authorized) { + updateTimeout(guid, res, authorized, "POST", function(err, docs) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; + }); + } + var type = fetch_only ? "fetch_song" : "add"; + validateLogin(adminpass, userpass, channel_name, type, res, function(exists, conf, authenticated) { + db.collection(channel_name).find({id: video_id}, function(err, result) { + if(result.length == 0 || result[0].type == "suggested") { + var song_type = authenticated ? "video" : "suggested"; + if(fetch_only && result.length == 0) { + res.status(404).send(JSON.stringify(error.not_found.local)); + return; + } + db.collection(channel_name).find({now_playing: true}, function(err, now_playing) { + var set_np = false; + if(now_playing.length == 0 && authenticated) { + set_np = true; + } + var new_song = {"added": Functions.get_time(),"guids":[guid],"id":video_id,"now_playing":set_np,"title":title,"votes":1, "duration":duration, "start": parseInt(start_time), "end": parseInt(end_time), "type": song_type}; + Search.get_correct_info(new_song, channel_name, false, function(element, found) { + if(!found) { + res.status(404).send(JSON.stringify(error.not_found.youtube)); + return; + } + new_song = element; + db.collection("frontpage_lists").find({"_id": channel_name}, function(err, count) { + var create_frontpage_lists = false; + if(count.length == 0) { + create_frontpage_lists = true; + } + if(!exists) { + var configs = {"addsongs":false, "adminpass":"", "allvideos":true, "frontpage":true, "longsongs":false, "removeplay": false, "shuffle": true, "skip": false, "skips": [], "startTime":Functions.get_time(), "views": [], "vote": false, "desc": ""}; + db.collection(channel_name + "_settings").insert(configs, function(err, docs){ + io.to(channel_name).emit("conf", configs); + }); + } + db.collection(channel_name).update({"id": new_song.id}, new_song, {upsert: true}, function(err, success) { + if(authorized) { + incrementToken(token); + } + if(create_frontpage_lists) { + db.collection("frontpage_lists").update({"_id": channel_name, "count" : (authenticated ? 1 : 0), "frontpage": true, "accessed": Functions.get_time(), "viewers": 1}, {upsert: true}, function(err, docs) { + if(authenticated) { + io.to(channel_name).emit("channel", {type: "added", value: new_song}); + } else { + io.to(channel_name).emit("suggested", new_song); + } + postEnd(channel_name, configs, new_song, guid, res, authenticated, authorized); + }); + } else if(set_np) { + Frontpage.update_frontpage(channel_name, video_id, title, function() { + io.to(channel_name).emit("np", {np: [new_song], conf: [conf]}); + postEnd(channel_name, configs, new_song, guid, res, authenticated, authorized); + }); + } else { + db.collection("frontpage_lists").update({"_id": channel_name}, {$inc: {count: (authenticated ? 1 : 0)}}, function(err, docs) { + if(authenticated) { + io.to(channel_name).emit("channel", {type: "added", value: new_song}); + } else { + io.to(channel_name).emit("suggested", new_song); + } + postEnd(channel_name, configs, new_song, guid, res, authenticated, authorized); + }); + } + }); + }) + }); + }); + } else if(fetch_only) { + var to_return = error.no_error; + to_return.results = result; + res.status(200).send(JSON.stringify(to_return)); + return; + } else { + res.status(409).send(JSON.stringify(error.conflicting)); return; } - db.collection(channel_name).find({now_playing: true}, function(err, now_playing) { - var set_np = false; - if(now_playing.length == 0 && authenticated) { - set_np = true; - } - var new_song = {"added": Functions.get_time(),"guids":[guid],"id":video_id,"now_playing":set_np,"title":title,"votes":1, "duration":duration, "start": parseInt(start_time), "end": parseInt(end_time), "type": song_type}; - Search.get_correct_info(new_song, channel_name, false, function(element, found) { - if(!found) { - res.status(404).send(JSON.stringify(error.not_found.youtube)); - return; - } - new_song = element; - db.collection("frontpage_lists").find({"_id": channel_name}, function(err, count) { - var create_frontpage_lists = false; - if(count.length == 0) { - create_frontpage_lists = true; - } - if(!exists) { - var configs = {"addsongs":false, "adminpass":"", "allvideos":true, "frontpage":true, "longsongs":false, "removeplay": false, "shuffle": true, "skip": false, "skips": [], "startTime":Functions.get_time(), "views": [], "vote": false, "desc": ""}; - db.collection(channel_name + "_settings").insert(configs, function(err, docs){ - io.to(channel_name).emit("conf", configs); - }); - } - db.collection(channel_name).update({"id": new_song.id}, new_song, {upsert: true}, function(err, success) { - if(authorized) { - incrementToken(token); - } - if(create_frontpage_lists) { - db.collection("frontpage_lists").update({"_id": channel_name, "count" : (authenticated ? 1 : 0), "frontpage": true, "accessed": Functions.get_time(), "viewers": 1}, {upsert: true}, function(err, docs) { - if(authenticated) { - io.to(channel_name).emit("channel", {type: "added", value: new_song}); - } else { - io.to(channel_name).emit("suggested", new_song); - } - postEnd(channel_name, configs, new_song, guid, res, authenticated, authorized); - }); - } else if(set_np) { - Frontpage.update_frontpage(channel_name, video_id, title, function() { - io.to(channel_name).emit("np", {np: [new_song], conf: [conf]}); - postEnd(channel_name, configs, new_song, guid, res, authenticated, authorized); - }); - } else { - db.collection("frontpage_lists").update({"_id": channel_name}, {$inc: {count: (authenticated ? 1 : 0)}}, function(err, docs) { - if(authenticated) { - io.to(channel_name).emit("channel", {type: "added", value: new_song}); - } else { - io.to(channel_name).emit("suggested", new_song); - } - postEnd(channel_name, configs, new_song, guid, res, authenticated, authorized); - }); - } - }); - }) - }); - }); - } else if(fetch_only) { - var to_return = error.no_error; - to_return.results = result; - res.status(200).send(JSON.stringify(to_return)); - return; - } else { - res.status(409).send(JSON.stringify(error.conflicting)); - return; - } + }); }); }); }); @@ -707,46 +717,77 @@ router.route('/api/conf/:channel_name').post(function(req, res) { if(token_docs.length == 1 && token_docs[0].token == token) { authorized = true; } - checkTimeout(guid, res, authorized, "POST", function() { - if(token != "" && !authorized) { - updateTimeout(guid, res, authorized, "DELETE", function(err, docs) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - }); - } - db.collection(channel_name + "_settings").find({ id: "config" }, toShowConfig, function(err, docs) { - if(docs.length > 0 && docs[0].userpass == userpass) { - var conf = docs[0]; - if(conf.adminpass != "") { - conf.adminpass = true; - } else { - conf.adminpass = false; - } - if(conf.userpass != "") { - conf.userpass = true; - } else { - conf.userpass = false; - } - if(authorized) { - incrementToken(token); - } - updateTimeout(guid, res, authorized, "POST", function(err, docs) { - var to_return = error.no_error; - to_return.results = conf; - res.status(200).send(JSON.stringify(to_return)); + checkOveruseApiToken(authorized, token_docs, res, function() { + checkTimeout(guid, res, authorized, "POST", function() { + if(token != "" && !authorized) { + updateTimeout(guid, res, authorized, "DELETE", function(err, docs) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; }); - } else if(docs.length > 0 && docs[0].userpass != userpass) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - } else { - res.status(404).send(JSON.stringify(error.not_found.list)); - return; } + db.collection(channel_name + "_settings").find({ id: "config" }, toShowConfig, function(err, docs) { + if(docs.length > 0 && docs[0].userpass == userpass) { + var conf = docs[0]; + if(conf.adminpass != "") { + conf.adminpass = true; + } else { + conf.adminpass = false; + } + if(conf.userpass != "") { + conf.userpass = true; + } else { + conf.userpass = false; + } + if(authorized) { + incrementToken(token); + } + updateTimeout(guid, res, authorized, "POST", function(err, docs) { + var to_return = error.no_error; + to_return.results = conf; + res.status(200).send(JSON.stringify(to_return)); + }); + } else if(docs.length > 0 && docs[0].userpass != userpass) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; + } else { + res.status(404).send(JSON.stringify(error.not_found.list)); + return; + } + }); }); }); }); }); +function checkOveruseApiToken(authorized, token_docs, res, callback) { + if(authorized || (authorized && token_docs[0].limit == 0)) { + callback(); + return; + } + db.collection("timeout_api").find({guid: token_docs[0].token}, function(e, doc) { + if(doc.length == 1) { + var this_doc = doc[0]; + var date = new Date(this_doc[0].createdAt); + date.setSeconds(date.getSeconds() + 1); + var now = new Date(); + var retry_in = (date.getTime() - now.getTime()) / 1000; + if(this_doc.used > token_docs[0].limit && retry_in > 0) { + res.header({'Retry-After': retry_in}); + res.status(429).send(JSON.stringify(error.tooMany)); + return; + } else { + db.collection("timeout_api").update({guid: token}, {$inc: {used: 1}}, function(e, d) { + callback(); + }); + } + } else { + db.collection("timeout_api").insert({guid: token, used: 0, createdAt: new Date(), type: "ALL"}, function(e, d) { + callback(); + }); + } + }); +} + router.route('/api/list/:channel_name').post(function(req, res) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); @@ -777,37 +818,39 @@ router.route('/api/list/:channel_name').post(function(req, res) { if(token_docs.length == 1 && token_docs[0].token == token) { authorized = true; } - checkTimeout(guid, res, authorized, "POST", function() { - if(token != "" && !authorized) { - updateTimeout(guid, res, authorized, "POST", function(err, docs) { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - }); - } - db.collection(channel_name).find({views: {$exists: false}}, toShowChannel, function(err, list) { - if(list.length > 0) { - db.collection(channel_name + "_settings").find({ id: "config" }, function(err, conf) { - if(conf.length == 0) { - res.status(404).send(JSON.stringify(error.not_found.list)); - return; - } else if(conf[0].userpass != userpass && conf[0].userpass != "") { - res.status(403).send(JSON.stringify(error.not_authenticated)); - return; - } - if(authorized) { - incrementToken(token); - } - updateTimeout(guid, res, authorized, "POST", function(err, docs) { - var to_return = error.no_error; - to_return.results = list; - res.status(200).send(JSON.stringify(to_return)); - return; - }); + checkOveruseApiToken(authorized, token_docs, res, function() { + checkTimeout(guid, res, authorized, "POST", function() { + if(token != "" && !authorized) { + updateTimeout(guid, res, authorized, "POST", function(err, docs) { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; }); - } else { - res.status(404).send(JSON.stringify(error.not_found.list)); - return; } + db.collection(channel_name).find({views: {$exists: false}}, toShowChannel, function(err, list) { + if(list.length > 0) { + db.collection(channel_name + "_settings").find({ id: "config" }, function(err, conf) { + if(conf.length == 0) { + res.status(404).send(JSON.stringify(error.not_found.list)); + return; + } else if(conf[0].userpass != userpass && conf[0].userpass != "") { + res.status(403).send(JSON.stringify(error.not_authenticated)); + return; + } + if(authorized) { + incrementToken(token); + } + updateTimeout(guid, res, authorized, "POST", function(err, docs) { + var to_return = error.no_error; + to_return.results = list; + res.status(200).send(JSON.stringify(to_return)); + return; + }); + }); + } else { + res.status(404).send(JSON.stringify(error.not_found.list)); + return; + } + }); }); }); }); @@ -831,8 +874,8 @@ router.route('/api/imageblob').post(function(req, res) { }); }); -var nodemailer = require('nodemailer'); try { + var nodemailer = require('nodemailer'); var mailconfig = require(path.join(__dirname, '../../config/mailconfig.js')); var recaptcha_config = require(path.join(__dirname, '../../config/recaptcha.js')); var Recaptcha = require('express-recaptcha'); @@ -840,6 +883,61 @@ try { var RECAPTCHA_SECRET_KEY = recaptcha_config.key; var recaptcha = new Recaptcha(RECAPTCHA_SITE_KEY, RECAPTCHA_SECRET_KEY); + router.route('/api/apply').post(recaptcha.middleware.verify, function(req, res) { + if(req.body.email == "" || req.body.email == undefined) { + res.send("failed"); + return; + } + if(req.recaptcha.error == null) { + var name = req.body.email; + var id = crypto.createHash('sha256').update(uniqid()).digest('base64'); + var uniqid_link = crypto.createHash('sha256').update(uniqid()).digest('hex'); + token_db.collection("api_token").find({name: name}, function(err, results_find) { + if(results_find.length > 0) { + res.send("failed"); + return; + } + token_db.collection("api_token").insert({name: name, token: id, usage: 0, active: false}, function(err, docs){ + token_db.collection("api_links").insert({id: uniqid_link, token: id, createdAt: new Date()}, function(err, docs) { + let transporter = nodemailer.createTransport(mailconfig); + + transporter.verify(function(error, success) { + if (error) { + token_db.collection("api_links").remove({id: uniqid_link}, function(e,d) { + res.send("failed"); + return; + }) + } else { + var subject = 'ZOFF: API-key'; + var message = "Link to API-key: https://zoff.me/api/apply/" + uniqid_link + "\n\nThis link expires in 1 day."; + var msg = { + from: mailconfig.from, + to: name, + subject: subject, + text: message, + html: message, + } + transporter.sendMail(msg, (error, info) => { + if (error) { + res.status(400).send("failed"); + transporter.close(); + return; + } + res.status(200).send("success"); + transporter.close(); + return; + }); + } + }); + }) + }); + }) + } else { + res.send("failed"); + return; + } + }); + router.route('/api/mail').post(recaptcha.middleware.verify, function(req, res) { if(req.recaptcha.error == null) { let transporter = nodemailer.createTransport(mailconfig); @@ -916,7 +1014,7 @@ function checkTimeout(guid, res, authorized, type, callback) { }, function(err, docs) { if(docs.length > 0) { var date = new Date(docs[0].createdAt); - date.setSeconds(date.getSeconds() + 2); + date.setSeconds(date.getSeconds() + 1); var now = new Date(); var retry_in = (date.getTime() - now.getTime()) / 1000; if(retry_in > 0) { diff --git a/server/routing/client/router.js b/server/routing/client/router.js index 625ed940..651e6b17 100644 --- a/server/routing/client/router.js +++ b/server/routing/client/router.js @@ -4,6 +4,8 @@ var path = require('path'); var year = new Date().getYear()+1900; var path = require('path'); var analytics = "xx"; +var mongojs = require('mongojs'); +var token_db = mongojs("tokens"); try { analytics = require(path.join(path.join(__dirname, '../../config/'), 'analytics.js')); } catch(e) { @@ -44,6 +46,52 @@ router.route('/').post(function(req, res, next){ root(req, res, next); }); +router.route('/api/apply/:id').get(function(req,res) { + var id = req.params.id; + token_db.collection('api_links').find({id: id}, function(err, result) { + if(result.length == 1) { + token_db.collection('api_links').remove({id: id}, function(e,d) { + token_db.collection('api_token').update({id: result[0].token}, {$set: {active: true }}, function(e,d) { + var data = { + year: year, + javascript_file: "token.min.js", + captcha: res.recaptcha, + analytics: analytics, + activated: true, + token: result[0].token, + correct: true, + } + res.render('layouts/client/token', data); + }); + }) + } else { + var data = { + year: year, + javascript_file: "token.min.js", + captcha: res.recaptcha, + analytics: analytics, + activated: false, + id:"", + correct: false, + } + res.render('layouts/client/token', data); + } + }); +}); + + +router.route('/api/apply').get(function(req, res, next) { + var data = { + year: year, + javascript_file: "token.min.js", + captcha: res.recaptcha, + analytics: analytics, + activated: false, + id: "", + correct: false, + } + res.render('layouts/client/token', data); +}); function root(req, res, next) { try{