Implement both tokens and rate limiting to API

Closes #141, but still needs better UI on failure
This commit is contained in:
Stefan Zermatten
2017-10-12 16:24:12 +02:00
parent 1d2de197a4
commit b308595dac
6 changed files with 161 additions and 4 deletions

View File

@@ -53,3 +53,4 @@ service-configuration@1.0.11
google-config-ui
dynamic-import
ddp-rate-limiter
rate-limit

View File

@@ -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}});
},
});

View File

@@ -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;
};

View File

@@ -42,6 +42,26 @@
</a>
</td>
</tr>
<tr>
<td>
API Key
</td>
<td class="apiKey">
{{#if apiKey}}
{{#unless showApiKey}}
<paper-button class="showApiKey">
Show
</paper-button>
{{else}}
{{apiKey}}
{{/unless}}
{{else}}
<paper-button class="generateMyApiKey">
Generate
</paper-button>
{{/if}}
</td>
</tr>
</table>
<div style="max-width: 250px">
{{> atForm state="signIn"}}

View File

@@ -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);
},
});

View File

@@ -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,
}});
});