Compare commits

...

14 Commits
1.8.2 ... 1.9.0

Author SHA1 Message Date
Stefan Zermatten
3fbb006783 Added Patreon notification badge for new patreon posts 2019-02-21 11:51:46 +02:00
Stefan Zermatten
2253672f43 Merge pull request #202 from mommothazaz123/master
Add multiple new API endpoints
2019-02-21 10:58:19 +02:00
Andrew Zhu
ed6d557f8a Merge branch 'master' into master 2019-02-12 13:51:20 -08:00
Andrew Zhu
4d642b56bb use direct insert, add schema check 2019-02-12 13:51:39 -08:00
Stefan Zermatten
436c5bb785 Fixed bug in view permission causing 500 errors for Avrae 2019-02-12 09:53:29 +02:00
Andrew Zhu
cb71f6d380 move everything to Meteor methods 2019-02-07 22:05:24 -08:00
Andrew Zhu
2f04d9ec1c remove server check overrides 2019-02-07 15:45:45 -08:00
Andrew Zhu
40c54524a7 add delete character endpoint 2019-02-05 15:46:06 -08:00
Andrew Zhu
b890a3b11e add feature, effect, prof, class insert 2019-02-05 15:21:32 -08:00
Andrew Zhu
c9242a95f3 add createCharacter, transferCharacter endpoints 2019-02-05 15:14:11 -08:00
Andrew Zhu
fedda62c7c add endpoint to add spells 2019-02-05 13:59:55 -08:00
Andrew Zhu
612575d0e6 add skeletons, ratelimits for endpoints 2019-02-05 13:14:09 -08:00
Andrew Zhu
d1d22c0d89 formatting, add helper func for POST endpoints 2019-02-05 13:09:56 -08:00
Andrew Zhu
b94f5ebb4b add getUserId API endpoint 2019-02-05 13:08:28 -08:00
13 changed files with 816 additions and 114 deletions

1
app/.gitignore vendored
View File

@@ -8,4 +8,5 @@ private/oldClient
nohup.out
node_modules
dump
.idea/
.cache

View File

@@ -0,0 +1,26 @@
PatreonPosts = new Mongo.Collection("patreonPosts");
Schemas.PatreonPosts = new SimpleSchema({
link: {
type: String,
},
dateAdded: {
type: Date,
autoValue(){
return new Date();
},
},
});
PatreonPosts.attachSchema(Schemas.PatreonPosts);
PatreonPosts.allow({
insert: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
remove: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
});

View File

@@ -70,6 +70,10 @@ Schemas.User = new SimpleSchema({
index: 1,
optional: true,
},
lastPatreonPostClicked: {
type: String,
optional: true,
},
});
Meteor.users.attachSchema(Schemas.User);
@@ -107,3 +111,11 @@ if (Meteor.isServer) Meteor.methods({
Meteor.users.update(this.userId, {$set: {apiKey}});
},
});
Meteor.methods({
clickPatreonPost(link) {
Meteor.users.update(this.userId, {$set: {
lastPatreonPostClicked: link
}});
},
});

View File

@@ -1,91 +1,268 @@
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", { // GET /character/:_id/json?key=:key
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&key=:key
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}));
}
});
});
}
});
this.route("addSpellsToCharacter", { // POST /api/character/:_id/spellList/:listId
path: "/api/character/:_id/spellList/:listId",
where: "server"
}).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) {
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 () {
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 () {
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 () {
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 () {
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 () {
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 () {
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 () {
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 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();
}
const startPOSTResponse = function (request) {
request.response.setHeader("Content-Type", "application/json");
const header = request.request.headers;
return header && header['authorization'];
};
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;
}
isKeyValid = function (apiKey) {
var user = Meteor.users.findOne({apiKey});
if (!user) return false;
var blackListed = Blacklist.findOne({userId: user._id});
return !blackListed;
};
var rateLimiter = new RateLimiter();
rateLimiter.addRule({apiKey: String}, 5, 5000);
userIdFromKey = function (apiKey) {
var user = Meteor.users.findOne({apiKey}); // we know user exists from isKeyValid
return user._id;
};
rateLimiter = new RateLimiter();
// 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);
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;
}
// 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);
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;
}
};

View File

@@ -51,9 +51,16 @@
<iron-icon icon="bug-report" item-icon></iron-icon>
Send Feedback
</paper-icon-item>
<a class="patreon" href="https://www.patreon.com/dicecloud" target="_blank" tabindex="-1">
<a class="patreon" href="{{patreonLink}}" target="_blank" tabindex="-1">
<paper-icon-item>
<iron-icon icon="dicecloud:patreon" item-icon></iron-icon>
<iron-icon id="patreon-link-icon" icon="dicecloud:patreon" item-icon></iron-icon>
{{#if showPatreonBadge}}
<paper-badge
icon="av:new-releases"
for="patreon-link-icon"
label="New post">
</paper-badge>
{{/if}}
Patreon
</paper-icon-item>
</a>

View File

@@ -7,6 +7,16 @@ Template.appDrawer.helpers({
var user = Meteor.user();
return user.profile && user.profile.username || user.username || "My Account";
},
showPatreonBadge: function(){
let post = PatreonPosts.findOne({}, {sort: {date: -1}});
let user = Meteor.user();
if (!post || !user) return false;
return post.link !== user.lastPatreonPostClicked;
},
patreonLink: function(){
let post = PatreonPosts.findOne({}, {sort: {date: -1}});
return (post && post.link) || 'https://www.patreon.com/dicecloud';
},
});
let drawerLayout;
@@ -37,6 +47,9 @@ Template.appDrawer.events({
closeDrawer(instance);
},
"click .patreon": function(event, instance){
let post = PatreonPosts.findOne({}, {sort: {date: -1}});
let link = (post && post.link) || 'https://www.patreon.com/dicecloud';
Meteor.call('clickPatreonPost', link);
ga("send", "event", "externalLink", "patreon");
},
"click .github": function(event, instance){

View File

@@ -2,13 +2,13 @@
"polyfill": "/components/webcomponentsjs/webcomponents.min.js",
"useShadowDom": true,
"imports": [
"/components/app-layout/app-layout.html",
"/components/app-layout/app-layout.html",
"/components/app-layout/app-layout.html",
"/components/app-layout/app-layout.html",
"/components/app-layout/app-scroll-effects/effects/waterfall.html",
"/components/app-layout/app-scroll-effects/effects/parallax-background.html",
"/components/app-layout/app-scroll-effects/effects/resize-title.html",
"/components/iron-collapse/iron-collapse.html",
"/components/iron-collapse/iron-collapse.html",
"/components/iron-icon/iron-icon.html",
"/components/iron-icons/av-icons.html",
"/components/iron-icons/editor-icons.html",
@@ -21,7 +21,8 @@
"/components/neon-animation/neon-animation.html",
"/components/paper-button/paper-button.html",
"/components/paper-button/paper-button.html",
"/components/paper-badge/paper-badge.html",
"/components/paper-swatch-picker/paper-swatch-picker.html",
"/components/paper-dialog/paper-dialog.html",
"/components/paper-dropdown-menu/paper-dropdown-menu.html",

471
app/lib/functions/api.js Normal file
View File

@@ -0,0 +1,471 @@
/**
* @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;
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"};
let id = Spells.direct.insert(spell, (err) => {
if (err) {
error = err.message;
}
});
// console.log(id);
ids.push(id);
} else {
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;
assertAuthorized(key, "createCharacter");
let error, id;
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;
});
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;
assertAuthorized(key, "deleteCharacter");
if (isOwner(charId, userIdFromKey(key))) {
let error;
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 {
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;
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};
}
} 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;
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;
}
});
ids.push(id);
} else {
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;
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;
}
});
ids.push(id);
} else {
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;
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;
}
});
ids.push(id);
} else {
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;
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;
}
});
ids.push(id);
} else {
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 assertCanEdit = function (key, charId, method) {
if (canEditCharacter(charId, userIdFromKey(key))) {
assertAuthorized(key, method);
} else {
throw new Meteor.Error(403, "You do not have permission to edit the requested character");
}
};
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)) {
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});
}
};
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,
},
});
};

View File

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

View File

@@ -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}});
@@ -9,7 +16,7 @@ canViewCharacter = function(char, userId){
userId = userId || Meteor.userId();
if (typeof char !== 'object'){
char = Characters.findOne(
charId,
char,
{fields: {owner: 1, writers: 1, readers: 1, "settings.viewPermission": 1}}
);
}

6
app/package-lock.json generated
View File

@@ -216,9 +216,9 @@
"integrity": "sha512-HfVRxhYG7C8Jl9FqtrlElMR2z/8YiLQVDKf67MLY25Ic+ILx3ecmklfT1v3u+7P5/4vEFjuxaAFXhr2/Afwk5g=="
},
"file-saver": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.0.tgz",
"integrity": "sha512-cYM1ic5DAkg25pHKgi5f10ziAM7RJU37gaH1XQlyNDrtUnzhC/dfoV9zf2OmF0RMKi42jG5B0JWBnPQqyj/G6g=="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.1.tgz",
"integrity": "sha512-dCB3K7/BvAcUmtmh1DzFdv0eXSVJ9IAFt1mw3XZfAexodNRoE29l3xB2EX4wH2q8m/UTzwzEPq/ArYk98kUkBQ=="
},
"find-up": {
"version": "2.1.0",

View File

@@ -17,7 +17,7 @@
"bower": "^1.7.9",
"core-js": "^2.5.7",
"fibers": "^2.0.2",
"file-saver": "^2.0.0",
"file-saver": "^2.0.1",
"meteor-node-stubs": "^0.3.3",
"qrcode": "^1.3.0",
"source-map-support": "^0.5.9",

View File

@@ -1,9 +1,13 @@
Meteor.publish("user", function(){
return Meteor.users.find(this.userId, {fields: {
roles: 1,
username: 1,
profile: 1,
apiKey: 1,
librarySubscriptions: 1,
}});
return [
Meteor.users.find(this.userId, {fields: {
roles: 1,
username: 1,
profile: 1,
apiKey: 1,
librarySubscriptions: 1,
lastPatreonPostClicked: 1,
}}),
PatreonPosts.find({},{sort: {dateAdded: -1}, limit: 1})
];
});