Implement both tokens and rate limiting to API
Closes #141, but still needs better UI on failure
This commit is contained in:
@@ -53,3 +53,4 @@ service-configuration@1.0.11
|
|||||||
google-config-ui
|
google-config-ui
|
||||||
dynamic-import
|
dynamic-import
|
||||||
ddp-rate-limiter
|
ddp-rate-limiter
|
||||||
|
rate-limit
|
||||||
|
|||||||
@@ -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({
|
Meteor.users.allow({
|
||||||
update: function(userId, doc, fields, modifier) {
|
update: function(userId, doc, fields, modifier) {
|
||||||
if (
|
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}});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ Router.map(function() {
|
|||||||
where: "server",
|
where: "server",
|
||||||
action: function() {
|
action: function() {
|
||||||
this.response.setHeader("Content-Type", "application/json");
|
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", {
|
this.route("vmixParty", {
|
||||||
@@ -12,7 +16,38 @@ Router.map(function() {
|
|||||||
where: "server",
|
where: "server",
|
||||||
action: function() {
|
action: function() {
|
||||||
this.response.setHeader("Content-Type", "application/json");
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -42,6 +42,26 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
<div style="max-width: 250px">
|
<div style="max-width: 250px">
|
||||||
{{> atForm state="signIn"}}
|
{{> atForm state="signIn"}}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
|
Template.profile.onCreated(function(){
|
||||||
|
this.showApiKey = new ReactiveVar(false);
|
||||||
|
});
|
||||||
|
|
||||||
Template.profile.helpers({
|
Template.profile.helpers({
|
||||||
profileName: function() {
|
profileName: function() {
|
||||||
var user = Meteor.user();
|
var user = Meteor.user();
|
||||||
return user.profile && user.profile.username ||
|
return user.profile && user.profile.username ||
|
||||||
user.username ||
|
user.username ||
|
||||||
"Tap to set username";
|
"Tap to set username";
|
||||||
}
|
},
|
||||||
|
showApiKey: function(){
|
||||||
|
return Template.instance().showApiKey.get();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.profile.events({
|
Template.profile.events({
|
||||||
@@ -25,4 +32,11 @@ Template.profile.events({
|
|||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
"click .showApiKey": function(event, instance){
|
||||||
|
instance.showApiKey.set(!instance.showApiKey.get());
|
||||||
|
},
|
||||||
|
"click .generateMyApiKey": function(event, instance){
|
||||||
|
Meteor.call("generateMyApiKey");
|
||||||
|
instance.showApiKey.set(true);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
Meteor.publish("user", function(){
|
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,
|
||||||
|
}});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user