diff --git a/rpg-docs/.meteor/packages b/rpg-docs/.meteor/packages index 0a0338e8..9534a61f 100644 --- a/rpg-docs/.meteor/packages +++ b/rpg-docs/.meteor/packages @@ -53,3 +53,4 @@ service-configuration@1.0.11 google-config-ui dynamic-import ddp-rate-limiter +rate-limit diff --git a/rpg-docs/Model/Users/Users.js b/rpg-docs/Model/Users/Users.js index f9011409..bdbbdda4 100644 --- a/rpg-docs/Model/Users/Users.js +++ b/rpg-docs/Model/Users/Users.js @@ -1,3 +1,75 @@ +Schemas.UserProfile = new SimpleSchema({ + username: { + type: String, + optional: true, + }, +}); + +Schemas.User = new SimpleSchema({ + username: { + type: String, + optional: true, + }, + profile: { + type: Schemas.UserProfile, + optional: true, + }, + emails: { + type: Array, + optional: true, + }, + "emails.$": { + type: Object, + }, + "emails.$.address": { + type: String, + regEx: SimpleSchema.RegEx.Email, + }, + "emails.$.verified": { + type: Boolean, + }, + registered_emails: { + type: Array, + optional: true, + }, + "registered_emails.$": { + type: Object, + blackbox: true, + }, + createdAt: { + type: Date + }, + services: { + type: Object, + optional: true, + blackbox: true, + }, + roles: { + type: Object, + optional: true, + blackbox: true, + }, + roles: { + type: Array, + optional: true, + }, + "roles.$": { + type: String + }, + // In order to avoid an 'Exception in setInterval callback' from Meteor + heartbeat: { + type: Date, + optional: true, + }, + apiKey: { + type: String, + index: 1, + optional: true, + }, +}); + +Meteor.users.attachSchema(Schemas.User); + Meteor.users.allow({ update: function(userId, doc, fields, modifier) { if ( @@ -21,3 +93,13 @@ Meteor.users.allow({ } } }); + +if (Meteor.isServer) Meteor.methods({ + generateMyApiKey() { + var user = Meteor.users.findOne(this.userId); + if (!user) return; + if (user && user.apiKey) return; + var apiKey = Random.id(30); + Meteor.users.update(this.userId, {$set: {apiKey}}); + }, +}); diff --git a/rpg-docs/Routes/API.js b/rpg-docs/Routes/API.js index aab73b3d..451ac2ee 100644 --- a/rpg-docs/Routes/API.js +++ b/rpg-docs/Routes/API.js @@ -4,7 +4,11 @@ Router.map(function() { where: "server", action: function() { this.response.setHeader("Content-Type", "application/json"); - this.response.end(vMixCharacter(this.params._id)); + var query = this.params.query; + var key = query && query.key; + ifKeyValid(key, this.response, () => + this.response.end(vMixCharacter(this.params._id)) + ); }, }); this.route("vmixParty", { @@ -12,7 +16,38 @@ Router.map(function() { where: "server", action: function() { this.response.setHeader("Content-Type", "application/json"); - this.response.end(vMixParty(this.params._id)); + var query = this.params.query; + var key = query && query.key; + ifKeyValid(key, this.response, () => + this.response.end(vMixParty(this.params._id)) + ); }, }); }); + +var ifKeyValid = function(apiKey, response, callback){ + if (!apiKey){ + response.writeHead(403, "You must use an api key to access this api"); + response.end(); + } else if (!isKeyValid(apiKey)){ + response.writeHead(403, "API key is invalid"); + response.end(); + } else if (isRateLimited(apiKey)){ + response.writeHead(429, "Too many requests"); + response.end(); + } else { + rateLimiter.increment({apiKey}) + callback(); + } +}; + +var isKeyValid = function(apiKey){ + return !!Meteor.users.findOne({apiKey}); +}; + +var rateLimiter = new RateLimiter(); +rateLimiter.addRule({apiKey: String}, 2, 10000); + +var isRateLimited = function(apiKey){ + return !rateLimiter.check({apiKey}).allowed; +}; diff --git a/rpg-docs/client/views/user/profile/profile.html b/rpg-docs/client/views/user/profile/profile.html index a68b5ba7..5a8e6a50 100644 --- a/rpg-docs/client/views/user/profile/profile.html +++ b/rpg-docs/client/views/user/profile/profile.html @@ -42,6 +42,26 @@ + + + API Key + + + {{#if apiKey}} + {{#unless showApiKey}} + + Show + + {{else}} + {{apiKey}} + {{/unless}} + {{else}} + + Generate + + {{/if}} + +
{{> atForm state="signIn"}} diff --git a/rpg-docs/client/views/user/profile/profile.js b/rpg-docs/client/views/user/profile/profile.js index e8ce64a9..ca64f6ea 100644 --- a/rpg-docs/client/views/user/profile/profile.js +++ b/rpg-docs/client/views/user/profile/profile.js @@ -1,10 +1,17 @@ +Template.profile.onCreated(function(){ + this.showApiKey = new ReactiveVar(false); +}); + Template.profile.helpers({ profileName: function() { var user = Meteor.user(); return user.profile && user.profile.username || user.username || "Tap to set username"; - } + }, + showApiKey: function(){ + return Template.instance().showApiKey.get(); + }, }); Template.profile.events({ @@ -25,4 +32,11 @@ Template.profile.events({ data: {}, }); }, + "click .showApiKey": function(event, instance){ + instance.showApiKey.set(!instance.showApiKey.get()); + }, + "click .generateMyApiKey": function(event, instance){ + Meteor.call("generateMyApiKey"); + instance.showApiKey.set(true); + }, }); diff --git a/rpg-docs/server/publications/user.js b/rpg-docs/server/publications/user.js index 373fd5f9..ceb648ff 100644 --- a/rpg-docs/server/publications/user.js +++ b/rpg-docs/server/publications/user.js @@ -1,3 +1,8 @@ Meteor.publish("user", function(){ - return Meteor.users.find(this.userId, {fields: {roles: 1}}); + return Meteor.users.find(this.userId, {fields: { + roles: 1, + username: 1, + profile: 1, + apiKey: 1, + }}); });