From b94f5ebb4bf4cf9a5d319d078a5bda1ed783d4f2 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 5 Feb 2019 13:08:28 -0800 Subject: [PATCH 01/10] add getUserId API endpoint --- app/.gitignore | 1 + app/Routes/API.js | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/.gitignore b/app/.gitignore index 086ca397..9e3b6267 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -8,3 +8,4 @@ private/oldClient nohup.out node_modules dump +.idea/ diff --git a/app/Routes/API.js b/app/Routes/API.js index 7df32907..e375c4f0 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -42,6 +42,29 @@ Router.map(function() { ); }, }); + + this.route("getUserId", { // GET /api/user?username=:un + path: "/api/user", + where: "server", + action: function () { + this.response.setHeader("Content-Type", "application/json"); + var query = this.params.query; + var key = query && query.key; + var username = query && query.username; + ifKeyValid(key, this.response, "getUserId", () => { + Meteor.call("getUserId", username, (err, result) => { + if (err) { + console.error(err); + this.response.writeHead(404, "User not found"); + this.response.end(); + } else { + console.log(result); + this.response.end(JSON.stringify({id: result})); + } + }); + }); + } + }); }); var ifKeyValid = function(apiKey, response, method, callback){ From d1d22c0d89b2c0d341db5a6c9a8caad6fda5d3ed Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 5 Feb 2019 13:09:56 -0800 Subject: [PATCH 02/10] formatting, add helper func for POST endpoints --- app/Routes/API.js | 167 +++++++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 76 deletions(-) diff --git a/app/Routes/API.js b/app/Routes/API.js index e375c4f0..b9cb005a 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -1,47 +1,47 @@ -Router.map(function() { - this.route("vmixCharacter", { - path: "/vmix-character/:_id/", - where: "server", - action: function() { - this.response.setHeader("Content-Type", "application/json"); - var query = this.params.query; - var key = query && query.key; - ifKeyValid(key, this.response, "vmixCharacter", () => - this.response.end(vMixCharacter(this.params._id)) - ); - }, - }); - this.route("vmixParty", { - path: "/vmix-party/:_id/", - where: "server", - action: function() { - this.response.setHeader("Content-Type", "application/json"); - var query = this.params.query; - var key = query && query.key; - ifKeyValid(key, this.response, "vmixParty", () => - this.response.end(vMixParty(this.params._id)) - ); - }, - }); +Router.map(function () { + this.route("vmixCharacter", { + path: "/vmix-character/:_id/", + where: "server", + action: function () { + this.response.setHeader("Content-Type", "application/json"); + var query = this.params.query; + var key = query && query.key; + ifKeyValid(key, this.response, "vmixCharacter", () => + this.response.end(vMixCharacter(this.params._id)) + ); + }, + }); + this.route("vmixParty", { + path: "/vmix-party/:_id/", + where: "server", + action: function () { + this.response.setHeader("Content-Type", "application/json"); + var query = this.params.query; + var key = query && query.key; + ifKeyValid(key, this.response, "vmixParty", () => + this.response.end(vMixParty(this.params._id)) + ); + }, + }); - this.route("jsonCharacterSheet", { - path: "/character/:_id/json", - where: "server", - action: function() { - this.response.setHeader("Content-Type", "application/json"); - var query = this.params.query; - var key = query && query.key; - ifKeyValid(key, this.response, "jsonCharacterSheet", () => { - if (canViewCharacter(this.params._id, userIdFromKey(key))){ - this.response.end(JSONExport(this.params._id)) - } else { - this.response.writeHead(403, "You do not have permission to view this character"); - this.response.end(); - } - } - ); - }, - }); + this.route("jsonCharacterSheet", { + path: "/character/:_id/json", + where: "server", + action: function () { + this.response.setHeader("Content-Type", "application/json"); + var query = this.params.query; + var key = query && query.key; + ifKeyValid(key, this.response, "jsonCharacterSheet", () => { + if (canViewCharacter(this.params._id, userIdFromKey(key))) { + this.response.end(JSONExport(this.params._id)) + } else { + this.response.writeHead(403, "You do not have permission to view this character"); + this.response.end(); + } + } + ); + }, + }); this.route("getUserId", { // GET /api/user?username=:un path: "/api/user", @@ -67,35 +67,50 @@ Router.map(function() { }); }); -var ifKeyValid = function(apiKey, response, method, 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, method)){ - response.writeHead(429, "Too many requests"); - response.end(JSON.stringify({ - "timeToReset": rateLimiter.check({apiKey: apiKey, method: method}).timeToReset - })); - } else { - rateLimiter.increment({apiKey: apiKey, method: method}) - callback(); - } +var ifPostOK = function (router, endpoint, callback) { + router.response.setHeader("Content-Type", "application/json"); + var header = router.request.headers; + var key = header && header['Authorization']; + ifKeyValid(key, router.response, endpoint, () => { + if (canEditCharacter(router.params._id, userIdFromKey(key))) { + callback(); + } else { + router.response.writeHead(403, "You do not have permission to edit this character"); + router.response.end(); + } + } + ); }; -var isKeyValid = function(apiKey){ - var user = Meteor.users.findOne({apiKey}); - if (!user) return false; - var blackListed = Blacklist.findOne({userId: user._id}); - return !blackListed; +var ifKeyValid = function (apiKey, response, method, 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, method)) { + response.writeHead(429, "Too many requests"); + response.end(JSON.stringify({ + "timeToReset": rateLimiter.check({apiKey: apiKey, method: method}).timeToReset + })); + } else { + rateLimiter.increment({apiKey: apiKey, method: method}); + callback(); + } }; -var userIdFromKey = function(apiKey){ - var user = Meteor.users.findOne({apiKey}); // we know user exists from isKeyValid - return user._id; -} +var isKeyValid = function (apiKey) { + var user = Meteor.users.findOne({apiKey}); + if (!user) return false; + var blackListed = Blacklist.findOne({userId: user._id}); + return !blackListed; +}; + +var userIdFromKey = function (apiKey) { + var user = Meteor.users.findOne({apiKey}); // we know user exists from isKeyValid + return user._id; +}; var rateLimiter = new RateLimiter(); rateLimiter.addRule({apiKey: String}, 5, 5000); @@ -103,12 +118,12 @@ rateLimiter.addRule({apiKey: String, method: "vmixCharacter"}, 2, 10000); rateLimiter.addRule({apiKey: String, method: "vmixParty"}, 2, 10000); rateLimiter.addRule({apiKey: String, method: "jsonCharacterSheet"}, 5, 5000); -var isRateLimited = function(apiKey, method){ - const limited = !rateLimiter.check({apiKey: apiKey, method: method}).allowed - if (limited) { - console.log(`Rate limit hit by API key ${apiKey}`); - return true; - } else { - return false; - } +var isRateLimited = function (apiKey, method) { + const limited = !rateLimiter.check({apiKey: apiKey, method: method}).allowed; + if (limited) { + console.log(`Rate limit hit by API key ${apiKey}`); + return true; + } else { + return false; + } }; From 612575d0e655056809cae389e95f4b0eb4ba28b2 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 5 Feb 2019 13:14:09 -0800 Subject: [PATCH 03/10] add skeletons, ratelimits for endpoints --- app/Routes/API.js | 96 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/app/Routes/API.js b/app/Routes/API.js index b9cb005a..36d6438c 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -24,7 +24,7 @@ Router.map(function () { }, }); - this.route("jsonCharacterSheet", { + this.route("jsonCharacterSheet", { // GET /character/:_id/json?key=:key path: "/character/:_id/json", where: "server", action: function () { @@ -43,7 +43,7 @@ Router.map(function () { }, }); - this.route("getUserId", { // GET /api/user?username=:un + this.route("getUserId", { // GET /api/user?username=:un&key=:key path: "/api/user", where: "server", action: function () { @@ -65,6 +65,83 @@ Router.map(function () { }); } }); + + this.route("addSpellsToCharacter", { // POST /api/character/:_id/spellList/:listId + path: "/api/character/:_id/spellList/:listId", + where: "server" + }).post( + function () { + ifPostOK(this, "addSpellsToCharacter", () => { + + }); + } + ); + + this.route("createCharacter", { // POST /api/character + path: "/api/character", + where: "server" + }).post( + function () { + ifPostOK(this, "createCharacter", () => { + + }); + } + ); + + this.route("transferCharacterOwnership", { // POST /api/character/:_id/owner + path: "/api/character/:_id/owner", + where: "server" + }).post( + function () { + ifPostOK(this, "transferCharacterOwnership", () => { + + }); + } + ); + + this.route("insertFeatures", { // POST /api/character/:_id/feature + path: "/api/character/:_id/feature", + where: "server", + }).post( + function () { + ifPostOK(this, "insertFeatures", () => { + + }); + } + ); + + this.route("insertProfs", { // POST /api/character/:_id/prof + path: "/api/character/:_id/prof", + where: "server", + }).post( + function () { + ifPostOK(this, "insertProfs", () => { + + }); + } + ); + + this.route("insertEffects", { // POST /api/character/:_id/effect + path: "/api/character/:_id/effect", + where: "server", + }).post( + function () { + ifPostOK(this, "insertEffects", () => { + + }); + } + ); + + this.route("insertClasses", { // POST /api/character/:_id/class + path: "/api/character/:_id/class", + where: "server", + }).post( + function () { + ifPostOK(this, "insertClasses", () => { + + }); + } + ); }); var ifPostOK = function (router, endpoint, callback) { @@ -113,10 +190,23 @@ var userIdFromKey = function (apiKey) { }; var rateLimiter = new RateLimiter(); -rateLimiter.addRule({apiKey: String}, 5, 5000); +// global limit +rateLimiter.addRule({apiKey: String}, 10, 1000); + +// vmix stuff rateLimiter.addRule({apiKey: String, method: "vmixCharacter"}, 2, 10000); rateLimiter.addRule({apiKey: String, method: "vmixParty"}, 2, 10000); + +// bot API endpoints rateLimiter.addRule({apiKey: String, method: "jsonCharacterSheet"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "getUserId"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "addSpellsToCharacter"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "createCharacter"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "transferCharacterOwnership"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "insertFeatures"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "insertProfs"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "insertEffects"}, 5, 5000); +rateLimiter.addRule({apiKey: String, method: "insertClasses"}, 5, 5000); var isRateLimited = function (apiKey, method) { const limited = !rateLimiter.check({apiKey: apiKey, method: method}).allowed; From fedda62c7c735a522c6471cad5bcbc32d29b9304 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 5 Feb 2019 13:59:55 -0800 Subject: [PATCH 04/10] add endpoint to add spells --- app/Routes/API.js | 28 +++++++++++++++++--- app/lib/constants/characterAssetAllowDeny.js | 7 ++--- app/lib/functions/parenting.js | 3 +++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/app/Routes/API.js b/app/Routes/API.js index 36d6438c..2ca20b7c 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -71,8 +71,30 @@ Router.map(function () { where: "server" }).post( function () { - ifPostOK(this, "addSpellsToCharacter", () => { - + ifPostOK(this, "addSpellsToList", () => { + const spells = this.request.body; + const charId = this.params._id; + const listId = this.params.listId; + let spellIds = []; + let error; + for (let spell of spells) { + spell.parent = {id: listId, collection: "SpellLists"}; + spell.charId = charId; + let id = Spells.insert(spell, (err, _id) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + spellIds.push(id); + } + if (error) { + this.response.writeHead(400, "Failed to insert one or more spells"); + this.response.end(JSON.stringify({err: error, inserted: spellIds})); + } else { + this.response.end(JSON.stringify(spellIds)); + } }); } ); @@ -147,7 +169,7 @@ Router.map(function () { var ifPostOK = function (router, endpoint, callback) { router.response.setHeader("Content-Type", "application/json"); var header = router.request.headers; - var key = header && header['Authorization']; + var key = header && header['authorization']; ifKeyValid(key, router.response, endpoint, () => { if (canEditCharacter(router.params._id, userIdFromKey(key))) { callback(); diff --git a/app/lib/constants/characterAssetAllowDeny.js b/app/lib/constants/characterAssetAllowDeny.js index 65730cfb..70277148 100644 --- a/app/lib/constants/characterAssetAllowDeny.js +++ b/app/lib/constants/characterAssetAllowDeny.js @@ -12,19 +12,20 @@ Meteor.methods({ CHARACTER_SUBSCHEMA_ALLOW = { // the user must be logged in, and the user must be a writer of the character + // or we must be the server insert: function(userId, doc) { var char = Characters.findOne( doc.charId, {fields: {owner: 1, writers: 1}} ); - return (userId && char.owner === userId || _.contains(char.writers, userId)); + return (userId && char.owner === userId || _.contains(char.writers, userId) || Meteor.isServer); }, update: function(userId, doc, fields, modifier) { var char = Characters.findOne( doc.charId, {fields: {owner: 1, writers: 1}} ); - return (userId && char.owner === userId || _.contains(char.writers, userId)); + return (userId && char.owner === userId || _.contains(char.writers, userId) || Meteor.isServer); }, remove: function(userId, doc) { var char = Characters.findOne( @@ -32,7 +33,7 @@ CHARACTER_SUBSCHEMA_ALLOW = { {fields: {owner: 1, writers: 1}} ); if (!char) return true; - return userId && char.owner === userId || _.contains(char.writers, userId); + return userId && char.owner === userId || _.contains(char.writers, userId) || Meteor.isServer; }, fetch: ["charId"], }; diff --git a/app/lib/functions/parenting.js b/app/lib/functions/parenting.js index 82ecf204..d26dc885 100644 --- a/app/lib/functions/parenting.js +++ b/app/lib/functions/parenting.js @@ -132,6 +132,9 @@ makeParent = function(collection, donatedKeys){ }; var checkPermission = function(userId, charId){ + if (Meteor.isServer) { // we always trust server + return true; + } var char = Characters.findOne(charId, {fields: {owner: 1, writers: 1}}); if (!char) throw new Meteor.Error("Access Denied, no charId", From c9242a95f3b1aa753f13cc7960bc2ef011b50d11 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 5 Feb 2019 15:14:11 -0800 Subject: [PATCH 05/10] add createCharacter, transferCharacter endpoints --- app/Routes/API.js | 47 +++++++++++++++++++++++++++++--- app/lib/functions/permissions.js | 7 +++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/app/Routes/API.js b/app/Routes/API.js index 2ca20b7c..f041f37a 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -104,19 +104,58 @@ Router.map(function () { where: "server" }).post( function () { - ifPostOK(this, "createCharacter", () => { + this.response.setHeader("Content-Type", "application/json"); + const header = this.request.headers; + const key = header && header['authorization']; + ifKeyValid(key, this.response, "createCharacter", () => { + const character = this.request.body; + let error; + character.owner = userIdFromKey(key); + let id = Characters.insert(character, (err) => { + if (err) + error = err.message; + }); + + if (error) { + this.response.writeHead(400, "Failed to insert character"); + this.response.end(JSON.stringify({err: error})); + } else { + this.response.end(JSON.stringify({id: id})); + } }); } ); - this.route("transferCharacterOwnership", { // POST /api/character/:_id/owner + this.route("transferCharacterOwnership", { // PUT /api/character/:_id/owner path: "/api/character/:_id/owner", where: "server" - }).post( + }).put( function () { - ifPostOK(this, "transferCharacterOwnership", () => { + this.response.setHeader("Content-Type", "application/json"); + const header = this.request.headers; + const key = header && header['authorization']; + const charId = this.params._id; + ifKeyValid(key, this.response, "transferCharacterOwnership", () => { + if (isOwner(charId, userIdFromKey(key))) { + const newOwner = this.request.body['id']; + let error; + Characters.update({_id: charId}, {"$set": {owner: newOwner}}, null, + (err) => { + if (err) + error = err.message; + }); + if (error) { + this.response.writeHead(400, "Failed to update character"); + this.response.end(JSON.stringify({err: error})); + } else { + this.response.end(JSON.stringify({success: true})); + } + } else { + this.response.writeHead(403, "You do not have permission to transfer this character"); + this.response.end(); + } }); } ); diff --git a/app/lib/functions/permissions.js b/app/lib/functions/permissions.js index 8f160297..495d429b 100644 --- a/app/lib/functions/permissions.js +++ b/app/lib/functions/permissions.js @@ -1,3 +1,10 @@ +isOwner = function(charId, userId) { + userId = userId || Meteor.userId(); + var char = Characters.findOne(charId, {fields: {owner: 1}}); + if (!char) return true; + return (userId === char.owner); +}; + canEditCharacter = function(charId, userId){ userId = userId || Meteor.userId(); var char = Characters.findOne(charId, {fields: {owner: 1, writers: 1}}); From b890a3b11e4eca39ddf04eb37d2a9e1d1705c9e3 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 5 Feb 2019 15:21:32 -0800 Subject: [PATCH 06/10] add feature, effect, prof, class insert --- app/Routes/API.js | 88 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/app/Routes/API.js b/app/Routes/API.js index f041f37a..373f2f83 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -166,7 +166,27 @@ Router.map(function () { }).post( function () { ifPostOK(this, "insertFeatures", () => { - + const features = this.request.body; + const charId = this.params._id; + let ids = []; + let error; + for (let feature of features) { + feature.charId = charId; + let id = Features.insert(feature, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + this.response.writeHead(400, "Failed to insert one or more features"); + this.response.end(JSON.stringify({err: error, inserted: ids})); + } else { + this.response.end(JSON.stringify(ids)); + } }); } ); @@ -177,7 +197,27 @@ Router.map(function () { }).post( function () { ifPostOK(this, "insertProfs", () => { - + const profs = this.request.body; + const charId = this.params._id; + let ids = []; + let error; + for (let prof of profs) { + prof.charId = charId; // we currently rely on the client to supply parent + let id = Proficiencies.insert(prof, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + this.response.writeHead(400, "Failed to insert one or more profs"); + this.response.end(JSON.stringify({err: error, inserted: ids})); + } else { + this.response.end(JSON.stringify(ids)); + } }); } ); @@ -188,7 +228,27 @@ Router.map(function () { }).post( function () { ifPostOK(this, "insertEffects", () => { - + const effects = this.request.body; + const charId = this.params._id; + let ids = []; + let error; + for (let effect of effects) { + effect.charId = charId; // we currently rely on the client to supply parent + let id = Effects.insert(effect, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + this.response.writeHead(400, "Failed to insert one or more effects"); + this.response.end(JSON.stringify({err: error, inserted: ids})); + } else { + this.response.end(JSON.stringify(ids)); + } }); } ); @@ -199,7 +259,27 @@ Router.map(function () { }).post( function () { ifPostOK(this, "insertClasses", () => { - + const klasses = this.request.body; + const charId = this.params._id; + let ids = []; + let error; + for (let klass of klasses) { + klass.charId = charId; // we currently rely on the client to supply parent + let id = Classes.insert(klass, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + this.response.writeHead(400, "Failed to insert one or more classes"); + this.response.end(JSON.stringify({err: error, inserted: ids})); + } else { + this.response.end(JSON.stringify(ids)); + } }); } ); From 40c54524a7dba3dc2cc1f04a08f22a570a0751f6 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 5 Feb 2019 15:46:06 -0800 Subject: [PATCH 07/10] add delete character endpoint --- app/Routes/API.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/Routes/API.js b/app/Routes/API.js index 373f2f83..2f3deaa7 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -127,6 +127,37 @@ Router.map(function () { } ); + this.route("deleteCharacter", { // DELETE /api/character/:_id + path: "/api/character/:_id", + where: "server" + }).delete( + function () { + this.response.setHeader("Content-Type", "application/json"); + const header = this.request.headers; + const key = header && header['authorization']; + const charId = this.params._id; + ifKeyValid(key, this.response, "deleteCharacter", () => { + if (isOwner(charId, userIdFromKey(key))) { + let error; + Characters.remove({_id: charId}, (err) => { + if (err) + error = err.message; + }); + + if (error) { + this.response.writeHead(400, "Failed to delete character"); + this.response.end(JSON.stringify({err: error})); + } else { + this.response.end(JSON.stringify({success: true})); + } + } else { + this.response.writeHead(403, "You do not have permission to delete this character"); + this.response.end(); + } + }); + } + ); + this.route("transferCharacterOwnership", { // PUT /api/character/:_id/owner path: "/api/character/:_id/owner", where: "server" From 2f04d9ec1c52dd89404aad2e479a6b3a06430ab2 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Thu, 7 Feb 2019 15:45:45 -0800 Subject: [PATCH 08/10] remove server check overrides --- app/lib/constants/characterAssetAllowDeny.js | 7 +++---- app/lib/functions/parenting.js | 3 --- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/lib/constants/characterAssetAllowDeny.js b/app/lib/constants/characterAssetAllowDeny.js index 70277148..65730cfb 100644 --- a/app/lib/constants/characterAssetAllowDeny.js +++ b/app/lib/constants/characterAssetAllowDeny.js @@ -12,20 +12,19 @@ Meteor.methods({ CHARACTER_SUBSCHEMA_ALLOW = { // the user must be logged in, and the user must be a writer of the character - // or we must be the server insert: function(userId, doc) { var char = Characters.findOne( doc.charId, {fields: {owner: 1, writers: 1}} ); - return (userId && char.owner === userId || _.contains(char.writers, userId) || Meteor.isServer); + return (userId && char.owner === userId || _.contains(char.writers, userId)); }, update: function(userId, doc, fields, modifier) { var char = Characters.findOne( doc.charId, {fields: {owner: 1, writers: 1}} ); - return (userId && char.owner === userId || _.contains(char.writers, userId) || Meteor.isServer); + return (userId && char.owner === userId || _.contains(char.writers, userId)); }, remove: function(userId, doc) { var char = Characters.findOne( @@ -33,7 +32,7 @@ CHARACTER_SUBSCHEMA_ALLOW = { {fields: {owner: 1, writers: 1}} ); if (!char) return true; - return userId && char.owner === userId || _.contains(char.writers, userId) || Meteor.isServer; + return userId && char.owner === userId || _.contains(char.writers, userId); }, fetch: ["charId"], }; diff --git a/app/lib/functions/parenting.js b/app/lib/functions/parenting.js index d26dc885..82ecf204 100644 --- a/app/lib/functions/parenting.js +++ b/app/lib/functions/parenting.js @@ -132,9 +132,6 @@ makeParent = function(collection, donatedKeys){ }; var checkPermission = function(userId, charId){ - if (Meteor.isServer) { // we always trust server - return true; - } var char = Characters.findOne(charId, {fields: {owner: 1, writers: 1}}); if (!char) throw new Meteor.Error("Access Denied, no charId", From cb71f6d38029298fe0ba73fa33ce41caf64cb7b5 Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Thu, 7 Feb 2019 22:05:24 -0800 Subject: [PATCH 09/10] move everything to Meteor methods --- app/Routes/API.js | 346 ++++++++++------------------- app/lib/functions/api.js | 241 ++++++++++++++++++++ app/lib/functions/characterJSON.js | 17 -- 3 files changed, 353 insertions(+), 251 deletions(-) create mode 100644 app/lib/functions/api.js delete mode 100644 app/lib/functions/characterJSON.js diff --git a/app/Routes/API.js b/app/Routes/API.js index 2f3deaa7..45453351 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -69,266 +69,144 @@ Router.map(function () { this.route("addSpellsToCharacter", { // POST /api/character/:_id/spellList/:listId path: "/api/character/:_id/spellList/:listId", where: "server" - }).post( - function () { - ifPostOK(this, "addSpellsToList", () => { - const spells = this.request.body; - const charId = this.params._id; - const listId = this.params.listId; - let spellIds = []; - let error; - for (let spell of spells) { - spell.parent = {id: listId, collection: "SpellLists"}; - spell.charId = charId; - let id = Spells.insert(spell, (err, _id) => { - if (err) { - error = err.message; - } - }); - if (error) - break; - spellIds.push(id); - } - if (error) { - this.response.writeHead(400, "Failed to insert one or more spells"); - this.response.end(JSON.stringify({err: error, inserted: spellIds})); - } else { - this.response.end(JSON.stringify(spellIds)); - } - }); - } - ); + }).post(function () { + const key = startPOSTResponse(this); + const spells = this.request.body; + const charId = this.params._id; + const listId = this.params.listId; + Meteor.call("insertSpells", key, charId, listId, spells, (err, res) => { + if (err) { + console.log(err); + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); this.route("createCharacter", { // POST /api/character path: "/api/character", where: "server" - }).post( - function () { - this.response.setHeader("Content-Type", "application/json"); - const header = this.request.headers; - const key = header && header['authorization']; - ifKeyValid(key, this.response, "createCharacter", () => { - const character = this.request.body; - let error; - - character.owner = userIdFromKey(key); - let id = Characters.insert(character, (err) => { - if (err) - error = err.message; - }); - - if (error) { - this.response.writeHead(400, "Failed to insert character"); - this.response.end(JSON.stringify({err: error})); - } else { - this.response.end(JSON.stringify({id: id})); - } - }); - } - ); + }).post(function () { + const key = startPOSTResponse(this); + const character = this.request.body; + Meteor.call("insertCharacter", key, character, (err, res) => { + if (err) { + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); this.route("deleteCharacter", { // DELETE /api/character/:_id path: "/api/character/:_id", where: "server" - }).delete( - function () { - this.response.setHeader("Content-Type", "application/json"); - const header = this.request.headers; - const key = header && header['authorization']; - const charId = this.params._id; - ifKeyValid(key, this.response, "deleteCharacter", () => { - if (isOwner(charId, userIdFromKey(key))) { - let error; - Characters.remove({_id: charId}, (err) => { - if (err) - error = err.message; - }); - - if (error) { - this.response.writeHead(400, "Failed to delete character"); - this.response.end(JSON.stringify({err: error})); - } else { - this.response.end(JSON.stringify({success: true})); - } - } else { - this.response.writeHead(403, "You do not have permission to delete this character"); - this.response.end(); - } - }); - } - ); + }).delete(function () { + const key = startPOSTResponse(this); + const charId = this.params._id; + Meteor.call("deleteCharacter", key, charId, (err, res) => { + if (err) { + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); this.route("transferCharacterOwnership", { // PUT /api/character/:_id/owner path: "/api/character/:_id/owner", where: "server" - }).put( - function () { - this.response.setHeader("Content-Type", "application/json"); - const header = this.request.headers; - const key = header && header['authorization']; - const charId = this.params._id; - ifKeyValid(key, this.response, "transferCharacterOwnership", () => { - if (isOwner(charId, userIdFromKey(key))) { - const newOwner = this.request.body['id']; - let error; - Characters.update({_id: charId}, {"$set": {owner: newOwner}}, null, - (err) => { - if (err) - error = err.message; - }); - - if (error) { - this.response.writeHead(400, "Failed to update character"); - this.response.end(JSON.stringify({err: error})); - } else { - this.response.end(JSON.stringify({success: true})); - } - } else { - this.response.writeHead(403, "You do not have permission to transfer this character"); - this.response.end(); - } - }); - } - ); + }).put(function () { + const key = startPOSTResponse(this); + const charId = this.params._id; + const ownerId = this.request.body['id']; + Meteor.call("transferCharacterOwnership", key, charId, ownerId, (err, res) => { + if (err) { + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); this.route("insertFeatures", { // POST /api/character/:_id/feature path: "/api/character/:_id/feature", where: "server", - }).post( - function () { - ifPostOK(this, "insertFeatures", () => { - const features = this.request.body; - const charId = this.params._id; - let ids = []; - let error; - for (let feature of features) { - feature.charId = charId; - let id = Features.insert(feature, (err) => { - if (err) { - error = err.message; - } - }); - if (error) - break; - ids.push(id); - } - if (error) { - this.response.writeHead(400, "Failed to insert one or more features"); - this.response.end(JSON.stringify({err: error, inserted: ids})); - } else { - this.response.end(JSON.stringify(ids)); - } - }); - } - ); + }).post(function () { + const key = startPOSTResponse(this); + const charId = this.params._id; + const features = this.request.body; + Meteor.call("insertFeatures", key, charId, features, (err, res) => { + if (err) { + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); this.route("insertProfs", { // POST /api/character/:_id/prof path: "/api/character/:_id/prof", where: "server", - }).post( - function () { - ifPostOK(this, "insertProfs", () => { - const profs = this.request.body; - const charId = this.params._id; - let ids = []; - let error; - for (let prof of profs) { - prof.charId = charId; // we currently rely on the client to supply parent - let id = Proficiencies.insert(prof, (err) => { - if (err) { - error = err.message; - } - }); - if (error) - break; - ids.push(id); - } - if (error) { - this.response.writeHead(400, "Failed to insert one or more profs"); - this.response.end(JSON.stringify({err: error, inserted: ids})); - } else { - this.response.end(JSON.stringify(ids)); - } - }); - } - ); + }).post(function () { + const key = startPOSTResponse(this); + const charId = this.params._id; + const profs = this.request.body; + Meteor.call("insertProfs", key, charId, profs, (err, res) => { + if (err) { + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); this.route("insertEffects", { // POST /api/character/:_id/effect path: "/api/character/:_id/effect", where: "server", - }).post( - function () { - ifPostOK(this, "insertEffects", () => { - const effects = this.request.body; - const charId = this.params._id; - let ids = []; - let error; - for (let effect of effects) { - effect.charId = charId; // we currently rely on the client to supply parent - let id = Effects.insert(effect, (err) => { - if (err) { - error = err.message; - } - }); - if (error) - break; - ids.push(id); - } - if (error) { - this.response.writeHead(400, "Failed to insert one or more effects"); - this.response.end(JSON.stringify({err: error, inserted: ids})); - } else { - this.response.end(JSON.stringify(ids)); - } - }); - } - ); + }).post(function () { + const key = startPOSTResponse(this); + const charId = this.params._id; + const effects = this.request.body; + Meteor.call("insertEffects", key, charId, effects, (err, res) => { + if (err) { + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); this.route("insertClasses", { // POST /api/character/:_id/class path: "/api/character/:_id/class", where: "server", - }).post( - function () { - ifPostOK(this, "insertClasses", () => { - const klasses = this.request.body; - const charId = this.params._id; - let ids = []; - let error; - for (let klass of klasses) { - klass.charId = charId; // we currently rely on the client to supply parent - let id = Classes.insert(klass, (err) => { - if (err) { - error = err.message; - } - }); - if (error) - break; - ids.push(id); - } - if (error) { - this.response.writeHead(400, "Failed to insert one or more classes"); - this.response.end(JSON.stringify({err: error, inserted: ids})); - } else { - this.response.end(JSON.stringify(ids)); - } - }); - } - ); + }).post(function () { + const key = startPOSTResponse(this); + const charId = this.params._id; + const classes = this.request.body; + Meteor.call("insertClasses", key, charId, classes, (err, res) => { + if (err) { + this.response.writeHead(err.error, err.reason); + this.response.end(err.details); + } else { + this.response.end(JSON.stringify(res)); + } + }); + }); }); -var ifPostOK = function (router, endpoint, callback) { - router.response.setHeader("Content-Type", "application/json"); - var header = router.request.headers; - var key = header && header['authorization']; - ifKeyValid(key, router.response, endpoint, () => { - if (canEditCharacter(router.params._id, userIdFromKey(key))) { - callback(); - } else { - router.response.writeHead(403, "You do not have permission to edit this character"); - router.response.end(); - } - } - ); +const startPOSTResponse = function (request) { + request.response.setHeader("Content-Type", "application/json"); + const header = request.request.headers; + return header && header['authorization']; }; var ifKeyValid = function (apiKey, response, method, callback) { @@ -349,19 +227,19 @@ var ifKeyValid = function (apiKey, response, method, callback) { } }; -var isKeyValid = function (apiKey) { +isKeyValid = function (apiKey) { var user = Meteor.users.findOne({apiKey}); if (!user) return false; var blackListed = Blacklist.findOne({userId: user._id}); return !blackListed; }; -var userIdFromKey = function (apiKey) { +userIdFromKey = function (apiKey) { var user = Meteor.users.findOne({apiKey}); // we know user exists from isKeyValid return user._id; }; -var rateLimiter = new RateLimiter(); +rateLimiter = new RateLimiter(); // global limit rateLimiter.addRule({apiKey: String}, 10, 1000); @@ -380,7 +258,7 @@ rateLimiter.addRule({apiKey: String, method: "insertProfs"}, 5, 5000); rateLimiter.addRule({apiKey: String, method: "insertEffects"}, 5, 5000); rateLimiter.addRule({apiKey: String, method: "insertClasses"}, 5, 5000); -var isRateLimited = function (apiKey, method) { +isRateLimited = function (apiKey, method) { const limited = !rateLimiter.check({apiKey: apiKey, method: method}).allowed; if (limited) { console.log(`Rate limit hit by API key ${apiKey}`); diff --git a/app/lib/functions/api.js b/app/lib/functions/api.js new file mode 100644 index 00000000..e58b5135 --- /dev/null +++ b/app/lib/functions/api.js @@ -0,0 +1,241 @@ +/** + * @return {string} + */ +JSONExport = function (charId) { + const character = { + "attacks": Attacks.find({charId: charId}).fetch(), + "characters": Characters.find({_id: charId}).fetch(), + "classes": Classes.find({charId: charId}).fetch(), + "containers": Containers.find({charId: charId}).fetch(), + "effects": Effects.find({charId: charId}).fetch(), + "experience": Experiences.find({charId: charId}).fetch(), + "features": Features.find({charId: charId}).fetch(), + "items": Items.find({charId: charId}).fetch(), + "notes": Notes.find({charId: charId}).fetch(), + "proficiencies": Proficiencies.find({charId: charId}).fetch(), + "spellLists": SpellLists.find({charId: charId}).fetch(), + "spells": Spells.find({charId: charId}).fetch() + }; + return JSON.stringify(character); +}; + +Meteor.methods({ + "insertSpells": function (key, charId, listId, spells) { + if (Meteor.isClient) return; + ifCanEdit(key, charId, "addSpellsToCharacter", () => { + let ids = []; + let error; + for (let spell of spells) { + spell.parent = {id: listId, collection: "SpellLists"}; + spell.charId = charId; + let id = Spells.insert(spell, (err) => { + if (err) { + error = err.message; + } + }); + ids.push(id); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more spells", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } + }); + }, + + "insertCharacter": function (key, character) { + if (Meteor.isClient) return; + ifAuthorized(key, "createCharacter", () => { + let error; + + character.owner = userIdFromKey(key); + let id = Characters.insert(character, (err) => { + if (err) + error = err.message; + }); + + if (error) { + throw new Meteor.Error(400, "Failed to insert character", JSON.stringify({err: error})); + } else { + return {id: id}; + } + }); + }, + + "deleteCharacter": function (key, charId) { + if (Meteor.isClient) return; + ifAuthorized(key, "deleteCharacter", () => { + if (isOwner(charId, userIdFromKey(key))) { + let error; + + Characters.remove({_id: charId}, (err) => { + if (err) + error = err.message; + }); + if (error) { + throw new Meteor.Error(400, "Failed to delete character", JSON.stringify({err: error})); + } else { + return {success: true}; + } + } else { + throw new Meteor.Error(403, "You do not have permission to delete the requested character"); + } + }); + }, + + "transferCharacterOwnership": function (key, charId, newOwner) { + if (Meteor.isClient) return; + ifAuthorized(key, "transferCharacterOwnership", () => { + if (isOwner(charId, userIdFromKey(key))) { + let error; + Characters.update({_id: charId}, {"$set": {owner: newOwner}}, null, + (err) => { + if (err) + error = err.message; + }); + + if (error) { + throw new Meteor.Error(400, "Failed to update character", JSON.stringify({err: error})); + } else { + return {success: true}; + } + } else { + throw new Meteor.Error(403, "You do not have permission to transfer the requested character"); + } + }); + }, + + "insertFeatures": function (key, charId, features) { + if (Meteor.isClient) return; + ifCanEdit(key, charId, "insertFeatures", () => { + let ids = []; + let error; + for (let feature of features) { + feature.charId = charId; + let id = Features.insert(feature, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more features", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } + }); + }, + + "insertProfs": function (key, charId, profs) { + if (Meteor.isClient) return; + ifCanEdit(key, charId, "insertProfs", () => { + let ids = []; + let error; + for (let prof of profs) { + prof.charId = charId; // we currently rely on the client to supply parent + let id = Proficiencies.insert(prof, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more profs", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } + }); + }, + + "insertEffects": function (key, charId, effects) { + if (Meteor.isClient) return; + ifCanEdit(key, charId, "insertEffects", () => { + let ids = []; + let error; + for (let effect of effects) { + effect.charId = charId; // we currently rely on the client to supply parent + let id = Effects.insert(effect, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more effects", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } + }); + }, + + "insertClasses": function (key, charId, klasses) { + if (Meteor.isClient) return; + ifCanEdit(key, charId, "insertClasses", () => { + let ids = []; + let error; + for (let klass of klasses) { + klass.charId = charId; // we currently rely on the client to supply parent + let id = Classes.insert(klass, (err) => { + if (err) { + error = err.message; + } + }); + if (error) + break; + ids.push(id); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more classes", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } + }); + } +}); + +var ifCanEdit = function (key, charId, method, callback) { + if (canEditCharacter(charId, userIdFromKey(key))) { + ifAuthorized(key, method, callback); + } else { + throw new Meteor.Error(403, "You do not have permission to edit the requested character"); + } +}; + +var ifAuthorized = function (apiKey, method, callback) { + if (!apiKey) { + throw new Meteor.Error(403, "You must use an api key to access this api"); + } else if (!isKeyValid(apiKey)) { + throw new Meteor.Error(403, "API key is invalid"); + } else if (isRateLimited(apiKey, method)) { + throw new Meteor.Error(429, "Too many requests", JSON.stringify({ + "timeToReset": rateLimiter.check({apiKey: apiKey, method: method}).timeToReset + })); + } else { + rateLimiter.increment({apiKey: apiKey, method: method}); + callback(); + } +}; diff --git a/app/lib/functions/characterJSON.js b/app/lib/functions/characterJSON.js deleted file mode 100644 index 861bb796..00000000 --- a/app/lib/functions/characterJSON.js +++ /dev/null @@ -1,17 +0,0 @@ -JSONExport = function(charId) { - var character = { - "attacks": Attacks.find({charId: charId}).fetch(), - "characters": Characters.find({_id: charId}).fetch(), - "classes": Classes.find({charId: charId}).fetch(), - "containers": Containers.find({charId: charId}).fetch(), - "effects": Effects.find({charId: charId}).fetch(), - "experience": Experiences.find({charId: charId}).fetch(), - "features": Features.find({charId: charId}).fetch(), - "items": Items.find({charId: charId}).fetch(), - "notes": Notes.find({charId: charId}).fetch(), - "proficiencies": Proficiencies.find({charId: charId}).fetch(), - "spellLists": SpellLists.find({charId: charId}).fetch(), - "spells": Spells.find({charId: charId}).fetch() - }; - return JSON.stringify(character); -} From 4d642b56bb7e7ed84064900f612dab282b83d37a Mon Sep 17 00:00:00 2001 From: Andrew Zhu Date: Tue, 12 Feb 2019 13:51:39 -0800 Subject: [PATCH 10/10] use direct insert, add schema check --- app/Routes/API.js | 1 - app/lib/functions/api.js | 474 +++++++++++++++++++++++++++++---------- 2 files changed, 352 insertions(+), 123 deletions(-) diff --git a/app/Routes/API.js b/app/Routes/API.js index 45453351..863fdf91 100644 --- a/app/Routes/API.js +++ b/app/Routes/API.js @@ -76,7 +76,6 @@ Router.map(function () { const listId = this.params.listId; Meteor.call("insertSpells", key, charId, listId, spells, (err, res) => { if (err) { - console.log(err); this.response.writeHead(err.error, err.reason); this.response.end(err.details); } else { diff --git a/app/lib/functions/api.js b/app/lib/functions/api.js index e58b5135..34022cd6 100644 --- a/app/lib/functions/api.js +++ b/app/lib/functions/api.js @@ -22,210 +22,248 @@ JSONExport = function (charId) { Meteor.methods({ "insertSpells": function (key, charId, listId, spells) { if (Meteor.isClient) return; - ifCanEdit(key, charId, "addSpellsToCharacter", () => { - let ids = []; - let error; - for (let spell of spells) { + assertCanEdit(key, charId, "addSpellsToCharacter"); + let ids = []; + let error; + for (let spell of spells) { + spell.charId = charId; + try { + Schemas.Spell.clean(spell); + } catch (e) { + // console.log(e); + error = e.error; + } + if (!error) { spell.parent = {id: listId, collection: "SpellLists"}; - spell.charId = charId; - let id = Spells.insert(spell, (err) => { + let id = Spells.direct.insert(spell, (err) => { if (err) { error = err.message; } }); + // console.log(id); ids.push(id); - } - if (error) { - throw new Meteor.Error(400, "Failed to insert one or more spells", JSON.stringify({ - err: error, - inserted: ids - })); } else { - return ids; + break; } - }); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more spells", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } }, "insertCharacter": function (key, character) { if (Meteor.isClient) return; - ifAuthorized(key, "createCharacter", () => { - let error; + assertAuthorized(key, "createCharacter"); + let error, id; - character.owner = userIdFromKey(key); - let id = Characters.insert(character, (err) => { + character.owner = userIdFromKey(key); + try { + Schemas.Character.clean(character); + } catch (e) { + console.log(e); + error = e.error; + } + if (!error) { + id = Characters.direct.insert(character, (err) => { if (err) error = err.message; }); - - if (error) { - throw new Meteor.Error(400, "Failed to insert character", JSON.stringify({err: error})); - } else { - return {id: id}; - } - }); + afterCharacterInsert(id); + return {id: id}; + } else { + throw new Meteor.Error(400, "Failed to insert character", JSON.stringify({err: error})); + } }, "deleteCharacter": function (key, charId) { if (Meteor.isClient) return; - ifAuthorized(key, "deleteCharacter", () => { - if (isOwner(charId, userIdFromKey(key))) { - let error; + assertAuthorized(key, "deleteCharacter"); + if (isOwner(charId, userIdFromKey(key))) { + let error; - Characters.remove({_id: charId}, (err) => { - if (err) - error = err.message; - }); - if (error) { - throw new Meteor.Error(400, "Failed to delete character", JSON.stringify({err: error})); - } else { - return {success: true}; - } + Characters.direct.remove({_id: charId}, (err) => { + if (err) + error = err.message; + }); + if (error) { + throw new Meteor.Error(400, "Failed to delete character", JSON.stringify({err: error})); } else { - throw new Meteor.Error(403, "You do not have permission to delete the requested character"); + return {success: true}; } - }); + } else { + throw new Meteor.Error(403, "You do not have permission to delete the requested character"); + } }, "transferCharacterOwnership": function (key, charId, newOwner) { if (Meteor.isClient) return; - ifAuthorized(key, "transferCharacterOwnership", () => { - if (isOwner(charId, userIdFromKey(key))) { - let error; - Characters.update({_id: charId}, {"$set": {owner: newOwner}}, null, - (err) => { - if (err) - error = err.message; - }); + assertAuthorized(key, "transferCharacterOwnership"); + if (isOwner(charId, userIdFromKey(key))) { + let error; + Characters.direct.update({_id: charId}, {"$set": {owner: newOwner}}, null, + (err) => { + if (err) + error = err.message; + }); - if (error) { - throw new Meteor.Error(400, "Failed to update character", JSON.stringify({err: error})); - } else { - return {success: true}; - } + if (error) { + throw new Meteor.Error(400, "Failed to update character", JSON.stringify({err: error})); } else { - throw new Meteor.Error(403, "You do not have permission to transfer the requested character"); + return {success: true}; } - }); + } else { + throw new Meteor.Error(403, "You do not have permission to transfer the requested character"); + } }, "insertFeatures": function (key, charId, features) { if (Meteor.isClient) return; - ifCanEdit(key, charId, "insertFeatures", () => { - let ids = []; - let error; - for (let feature of features) { - feature.charId = charId; - let id = Features.insert(feature, (err) => { + assertCanEdit(key, charId, "insertFeatures"); + let ids = []; + let error; + for (let feature of features) { + feature.charId = charId; + try { + Schemas.Feature.clean(feature); + } catch (e) { + error = e.error; + } + if (!error) { + let id = Features.direct.insert(feature, (err) => { if (err) { error = err.message; } }); - if (error) - break; ids.push(id); - } - if (error) { - throw new Meteor.Error(400, "Failed to insert one or more features", JSON.stringify({ - err: error, - inserted: ids - })); } else { - return ids; + break; } - }); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more features", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } + }, "insertProfs": function (key, charId, profs) { if (Meteor.isClient) return; - ifCanEdit(key, charId, "insertProfs", () => { - let ids = []; - let error; - for (let prof of profs) { - prof.charId = charId; // we currently rely on the client to supply parent - let id = Proficiencies.insert(prof, (err) => { + assertCanEdit(key, charId, "insertProfs"); + let ids = []; + let error; + for (let prof of profs) { + prof.charId = charId; + try { + Schemas.Proficiency.clean(prof, {filter: false}); + } catch (e) { + error = e.error; + } + if (!error) { + let id = Proficiencies.direct.insert(prof, (err) => { if (err) { error = err.message; } }); - if (error) - break; ids.push(id); - } - if (error) { - throw new Meteor.Error(400, "Failed to insert one or more profs", JSON.stringify({ - err: error, - inserted: ids - })); } else { - return ids; + break; } - }); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more profs", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } }, "insertEffects": function (key, charId, effects) { if (Meteor.isClient) return; - ifCanEdit(key, charId, "insertEffects", () => { - let ids = []; - let error; - for (let effect of effects) { - effect.charId = charId; // we currently rely on the client to supply parent - let id = Effects.insert(effect, (err) => { + assertCanEdit(key, charId, "insertEffects"); + let ids = []; + let error; + for (let effect of effects) { + effect.charId = charId; + try { + Schemas.Effect.clean(effect, {filter: false}); + } catch (e) { + error = e.error; + } + if (!error) { + let id = Effects.direct.insert(effect, (err) => { if (err) { error = err.message; } }); - if (error) - break; ids.push(id); - } - if (error) { - throw new Meteor.Error(400, "Failed to insert one or more effects", JSON.stringify({ - err: error, - inserted: ids - })); } else { - return ids; + break; } - }); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more effects", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } }, "insertClasses": function (key, charId, klasses) { if (Meteor.isClient) return; - ifCanEdit(key, charId, "insertClasses", () => { - let ids = []; - let error; - for (let klass of klasses) { - klass.charId = charId; // we currently rely on the client to supply parent - let id = Classes.insert(klass, (err) => { + assertCanEdit(key, charId, "insertClasses"); + let ids = []; + let error; + for (let klass of klasses) { + klass.charId = charId; + try { + Schemas.Class.clean(klass); + } catch (e) { + error = e.error; + } + if (!error) { + let id = Classes.direct.insert(klass, (err) => { if (err) { error = err.message; } }); - if (error) - break; ids.push(id); - } - if (error) { - throw new Meteor.Error(400, "Failed to insert one or more classes", JSON.stringify({ - err: error, - inserted: ids - })); } else { - return ids; + break; } - }); + } + if (error) { + throw new Meteor.Error(400, "Failed to insert one or more classes", JSON.stringify({ + err: error, + inserted: ids + })); + } else { + return ids; + } } }); -var ifCanEdit = function (key, charId, method, callback) { +var assertCanEdit = function (key, charId, method) { if (canEditCharacter(charId, userIdFromKey(key))) { - ifAuthorized(key, method, callback); + assertAuthorized(key, method); } else { throw new Meteor.Error(403, "You do not have permission to edit the requested character"); } }; -var ifAuthorized = function (apiKey, method, callback) { +var assertAuthorized = function (apiKey, method) { if (!apiKey) { throw new Meteor.Error(403, "You must use an api key to access this api"); } else if (!isKeyValid(apiKey)) { @@ -236,6 +274,198 @@ var ifAuthorized = function (apiKey, method, callback) { })); } else { rateLimiter.increment({apiKey: apiKey, method: method}); - callback(); } }; + +var afterCharacterInsert = function (charId) { + // Effects + Effects.direct.insert({ + charId: charId, + name: "Constitution modifier for each level", + stat: "hitPoints", + operation: "add", + calculation: "level * constitutionMod", + parent: { + id: charId, + collection: "Characters", + group: "Inate", + }, + }); + Effects.direct.insert({ + charId: charId, + name: "Proficiency bonus by level", + stat: "proficiencyBonus", + operation: "add", + calculation: "floor(level / 4 + 1.75)", + parent: { + id: charId, + collection: "Characters", + group: "Inate", + }, + }); + Effects.direct.insert({ + charId: charId, + name: "Dexterity Armor Bonus", + stat: "armor", + operation: "add", + calculation: "dexterityArmor", + parent: { + id: charId, + collection: "Characters", + group: "Inate", + }, + }); + Effects.direct.insert({ + charId: charId, + name: "Natural Armor", + stat: "armor", + operation: "base", + value: 10, + parent: { + id: charId, + collection: "Characters", + group: "Inate", + }, + }); + Effects.direct.insert({ + charId: charId, + name: "Natural Carrying Capacity", + stat: "carryMultiplier", + operation: "base", + value: "1", + parent: { + id: charId, + collection: "Characters", + group: "Inate", + }, + }); + // Features + let featureId = Features.direct.insert({ + name: "Base Ability Scores", + charId: charId, + enabled: true, + alwaysEnabled: true, + }); + Effects.direct.insert({ + stat: "strength", + charId: charId, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.direct.insert({ + stat: "dexterity", + charId: charId, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.direct.insert({ + stat: "constitution", + charId: charId, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.direct.insert({ + stat: "intelligence", + charId: charId, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.direct.insert({ + stat: "wisdom", + charId: charId, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.direct.insert({ + stat: "charisma", + charId: charId, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + // Items + let containerId = Containers.direct.insert({ + name: "Coin Pouch", + charId: charId, + isCarried: true, + description: "A sturdy pouch for coins", + color: "d", + }); + Items.direct.insert({ + name: "Gold piece", + plural: "Gold pieces", + charId: charId, + quantity: 0, + weight: 0.02, + value: 1, + color: "n", + parent: { + id: containerId, + collection: "Containers", + }, + settings: { + showIncrement: true, + }, + }); + Items.direct.insert({ + name: "Silver piece", + plural: "Silver pieces", + charId: charId, + quantity: 0, + weight: 0.02, + value: 0.1, + color: "q", + parent: { + id: containerId, + collection: "Containers", + }, + settings: { + showIncrement: true, + }, + }); + Items.direct.insert({ + name: "Copper piece", + plural: "Copper pieces", + charId: charId, + quantity: 0, + weight: 0.02, + value: 0.01, + color: "s", + parent: { + id: containerId, + collection: "Containers", + }, + settings: { + showIncrement: true, + }, + }); +};