diff --git a/rpg-docs/Routes/API.js b/rpg-docs/Routes/API.js index df167870..6ad6d71b 100644 --- a/rpg-docs/Routes/API.js +++ b/rpg-docs/Routes/API.js @@ -33,7 +33,6 @@ var ifKeyValid = function(apiKey, response, callback){ response.writeHead(403, "API key is invalid"); response.end(); } else if (isRateLimited(apiKey)){ - console.log(`Rate limit hit by API key ${apiKey}`); response.writeHead(429, "Too many requests"); response.end(); } else { @@ -53,5 +52,11 @@ var rateLimiter = new RateLimiter(); rateLimiter.addRule({apiKey: String}, 2, 10000); var isRateLimited = function(apiKey){ - return !rateLimiter.check({apiKey}).allowed; + const limited = !rateLimiter.check({apiKey}).allowed + if (limited) { + console.log(`Rate limit hit by API key ${apiKey}`); + return true; + } else { + return false; + } }; diff --git a/rpg-docs/Routes/Routes.js b/rpg-docs/Routes/Routes.js index 65b4cbb0..5d67332a 100644 --- a/rpg-docs/Routes/Routes.js +++ b/rpg-docs/Routes/Routes.js @@ -13,6 +13,11 @@ Router.plugin("ensureSignedIn", { Router.plugin("dataNotFound", {notFoundTemplate: "notFound"}); +var handleSubError = function(e){ + Session.set("error", {reason: e.reason, href: location.href}); + Router.go("/error"); +}; + Router.map(function() { this.route("/", { name: "home", @@ -36,7 +41,9 @@ Router.map(function() { path: "/character/:_id/", waitOn: function(){ return [ - subsManager.subscribe("singleCharacter", this.params._id), + subsManager.subscribe( + "singleCharacter", this.params._id, {onError: handleSubError} + ), ]; }, action: function(){ @@ -52,7 +59,9 @@ Router.map(function() { path: "/character/:_id/:urlName", waitOn: function(){ return [ - subsManager.subscribe("singleCharacter", this.params._id), + subsManager.subscribe( + "singleCharacter", this.params._id, {onError: handleSubError} + ), ]; }, data: function() { @@ -82,7 +91,9 @@ Router.map(function() { path: "/character/:_id/:urlName/print", waitOn: function(){ return [ - subsManager.subscribe("singleCharacter", this.params._id), + subsManager.subscribe( + "singleCharacter", this.params._id, {onError: handleSubError} + ), ]; }, data: function() { @@ -153,4 +164,11 @@ Router.map(function() { document.title = appName; }, }); + + this.route("/error", { + name: "error", + onAfterAction: function() { + document.title = `${appName} - Error`; + }, + }); }); diff --git a/rpg-docs/client/views/meta/error/error.html b/rpg-docs/client/views/meta/error/error.html new file mode 100644 index 00000000..945893f9 --- /dev/null +++ b/rpg-docs/client/views/meta/error/error.html @@ -0,0 +1,20 @@ + diff --git a/rpg-docs/client/views/meta/error/error.js b/rpg-docs/client/views/meta/error/error.js new file mode 100644 index 00000000..3870d686 --- /dev/null +++ b/rpg-docs/client/views/meta/error/error.js @@ -0,0 +1,17 @@ +Template.error.onRendered(function(){ + const error = Session.get("error") || {}; + if (error.href) window.history.replaceState("", "", error.href); +}); + +Template.error.helpers({ + errorMessage: function(){ + const error = Session.get("error") || {}; + return error.reason; + }, +}); + +Template.error.events({ + "click .try-again": function(event, instance){ + window.location.reload(); + }, +}); diff --git a/rpg-docs/server/lib/logRateError.js b/rpg-docs/server/lib/logRateError.js new file mode 100644 index 00000000..b261c3eb --- /dev/null +++ b/rpg-docs/server/lib/logRateError.js @@ -0,0 +1,8 @@ +logRateError = function(reply, ruleInput){ + // reply = {allowed, timeToReset, numInvocationsLeft} + // ruleInput = {userId, clientAddress, type, name, connectionId} + console.log( + `Limit hit for ${ruleInput.type} "${ruleInput.name}" ` + + `by user ${ruleInput.userId} from ${ruleInput.clientAddress}` + ); +} diff --git a/rpg-docs/server/publications/singleCharacter.js b/rpg-docs/server/publications/singleCharacter.js index 32e6f6a6..777a2ecb 100644 --- a/rpg-docs/server/publications/singleCharacter.js +++ b/rpg-docs/server/publications/singleCharacter.js @@ -38,9 +38,13 @@ Meteor.publish("singleCharacter", function(characterId){ DDPRateLimiter.addRule({ name: "singleCharacter", type: "subscription", - userId(){ return true; }, + userId: null, connectionId(){ return true; }, -}, 8, 5000); +}, 8, 10000, function(reply, ruleInput){ + if(!reply.allowed){ + logRateError(reply, ruleInput); + } +}); Meteor.publish("singleCharacterName", function(characterId){ userId = this.userId;