Made all login Patreon only, limited some functionality to $5 patrons

This commit is contained in:
Thaum Rystra
2020-04-30 22:38:27 +02:00
parent 1ca6bc834a
commit 073578b90d
19 changed files with 654 additions and 314 deletions

View File

@@ -47,3 +47,4 @@ static-html
aldeed:collection2@3.0.0
aldeed:schema-index
akryum:vue-component
accounts-patreon

View File

@@ -2,6 +2,7 @@ accounts-base@1.6.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.0
accounts-patreon@0.1.0
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
akryum:npm-check@0.1.2
@@ -88,6 +89,7 @@ oauth2@1.3.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
ordered-dict@1.1.0
patreon-oauth@0.1.0
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.11.2

View File

@@ -1,15 +1,18 @@
import '/imports/api/users/Users.js';
Meteor.publish("user", function(){
Meteor.publish('user', function(){
return Meteor.users.find(this.userId, {fields: {
roles: 1,
username: 1,
apiKey: 1,
darkMode: 1,
'services.patreon.id': 1,
'services.patreon.entitledCents': 1,
'services.patreon.entitledCentsOverride': 1,
}});
});
Meteor.publish("userPublicProfiles", function(ids){
Meteor.publish('userPublicProfiles', function(ids){
if (!this.userId || !Array.isArray(ids)) return [];
return Meteor.users.find({
_id: {$in: ids}

View File

@@ -1,100 +0,0 @@
<template lang="html">
<toolbar-layout>
<v-layout row slot="toolbar" align-center>
<div>
Storybook
</div>
<v-flex/>
<v-btn flat icon @click="sidebar = !sidebar">
<v-icon>menu</v-icon>
</v-btn>
</v-layout>
<v-navigation-drawer right app v-model="sidebar">
<v-toolbar color="secondary" dark>
Components
<v-switch :input-value="darkMode" @change="setDarkMode" label="Dark mode"/>
</v-toolbar>
<v-list>
<v-list-tile
v-for="(component, componentName) in $options.components"
v-if="componentName !== 'story-book' && componentName !== 'ToolbarLayout'"
:key="componentName"
@click=""
:to="`/storybook/${componentName}`"
>
<v-list-tile-content>
<v-list-tile-title>{{componentName}}</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<div class="content">
<component v-if="dontWrap" :is="$route.params.component"/>
<v-card class="ma-4" v-else="dontWrap">
<component :is="$route.params.component"/>
</v-card>
</div>
</toolbar-layout>
</template>
<script>
import Vue from "vue";
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.Story.vue';
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.Story.vue';
import ColorPicker from '/imports/ui/components/ColorPicker.Story.vue';
import ColumnLayout from "/imports/ui/components/ColumnLayout.Story.vue";
import DialogStack from '/imports/ui/dialogStack/DialogStack.Story.vue';
import EffectEditExpansionList from '/imports/ui/properties/viewers/shared/effects/EffectEditExpansionList.Story.vue';
import FeatureCard from '/imports/ui/properties/components/features/FeatureCard.Story.vue';
import HealthBar from '/imports/ui/properties/components/attributes/HealthBar.Story.vue';
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.Story.vue';
// import IconSearch from '/imports/ui/components/IconSearch.Story.vue';
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.Story.vue';
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.Story.vue';
import SmartInput from '/imports/ui/components/global/SmartInput.Story.vue';
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.Story.vue';
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
export default {
components: {
AbilityListTile,
AttributeCard,
ColorPicker,
ColumnLayout,
DialogStack,
EffectEditExpansionList,
FeatureCard,
HealthBar,
HitDiceListTile,
// IconSearch,
ResourceCard,
SkillListTile,
SmartInput,
SpellSlotListTile,
ToolbarLayout,
},
data(){ return {
sidebar: undefined,
}},
computed: {
dontWrap(){
let component = this.$options.components[this.$route.params.component];
return component && component.dontWrap;
},
},
methods: {
setDarkMode(value){
Meteor.users.setDarkMode.call({darkMode: !!value});
},
},
meteor: {
darkMode(){
let user = Meteor.user();
return user && user.darkMode;
},
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,59 +1,77 @@
<template>
<div class="sidebar">
<v-alert
:value="showWarning"
type="warning"
>
<div>
This is an early build of DiceCloud version 2. Data will be erased
frequently. Don't store anything important here.
</div>
<div class="layout row justify-center">
<v-btn @click="showWarning = false">I won't</v-btn>
</div>
</v-alert>
<v-layout row justify-center v-if="!signedIn">
<v-btn flat to="/sign-in">Sign in</v-btn>
</v-layout>
<v-alert
:value="showWarning"
type="warning"
>
<div>
This is an early build of DiceCloud version 2. Data will be erased
frequently. Don't store anything important here yet.
</div>
<div class="layout row justify-center">
<v-btn @click="showWarning = false">
I won't
</v-btn>
</div>
</v-alert>
<v-layout
v-if="!signedIn"
row
justify-center
>
<v-btn
flat
to="/sign-in"
>
Sign in
</v-btn>
</v-layout>
<v-list>
<v-list-tile v-if="signedIn">
<v-list-tile-content>
<v-list-tile-title>
{{userName}}
</v-list-tile-title>
<v-list-tile v-if="signedIn">
<v-list-tile-content>
<v-list-tile-title>
{{ userName }}
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-tooltip bottom>
<v-btn flat icon slot="activator" to="/account"><v-icon>settings</v-icon></v-btn>
<span>Account Settings</span>
</v-tooltip>
<v-list-tile-action>
<v-tooltip bottom>
<v-btn
slot="activator"
flat
icon
to="/account"
>
<v-icon>settings</v-icon>
</v-btn>
<span>Account Settings</span>
</v-tooltip>
</v-list-tile-action>
</v-list-tile>
</v-list-tile>
<v-list-tile
v-for="(link, i) in links"
v-if="link.vif || link.vif === undefined"
:to="link.to"
:href="link.href"
v-if="link.vif || link.vif === undefined"
:key="i"
:to="link.to"
:href="link.href"
>
<v-list-tile-action>
<v-icon>{{link.icon}}</v-icon>
<v-icon>{{ link.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-title>
{{link.title}}
{{ link.title }}
</v-list-tile-title>
</v-list-tile>
<v-divider></v-divider>
<v-divider />
</v-list>
<v-list dense>
<v-list-tile
v-for="character in CreaturesWithNoParty"
:to="character.url"
:key="character._id"
:to="character.url"
>
<v-list-tile-title>
{{character.name}}
{{ character.name }}
</v-list-tile-title>
</v-list-tile>
<v-list-group
@@ -62,16 +80,16 @@
>
<v-list-tile slot="activator">
<v-list-tile-title>
{{party.name}}
{{ party.name }}
</v-list-tile-title>
</v-list-tile>
<v-list-tile
v-for="character in characterDocs"
:to="character.url"
:key="character._id"
:to="character.url"
>
<v-list-tile-title>
{{character.name}}
{{ character.name }}
</v-list-tile-title>
</v-list-tile>
</v-list-group>
@@ -89,7 +107,7 @@
}},
meteor: {
$subscribe: {
"characterList": [],
'characterList': [],
},
signedIn(){
return Meteor.userId();
@@ -100,16 +118,13 @@
},
links(){
let links = [
{title: "Home", icon: "home", to: "/"},
{title: "Characters", icon: "group", to: "/characterList", vif: Meteor.userId()},
{title: "Library", icon: "book", to: "/library", vif: Meteor.userId()},
{title: "Send Feedback", icon: "bug_report", to: "/feedback"},
{title: "Patreon", icon: "", href: "https://www.patreon.com/dicecloud"},
{title: "Github", icon: "", href: "https://github.com/ThaumRystra/DiceCloud/tree/version-2"},
{title: 'Home', icon: 'home', to: '/'},
{title: 'Characters', icon: 'group', to: '/characterList', vif: Meteor.userId()},
{title: 'Library', icon: 'book', to: '/library', vif: Meteor.userId()},
{title: 'Send Feedback', icon: 'bug_report', to: '/feedback'},
{title: 'Patreon', icon: '', href: 'https://www.patreon.com/dicecloud'},
{title: 'Github', icon: '', href: 'https://github.com/ThaumRystra/DiceCloud/tree/version-2'},
];
if (Meteor.settings.public.showStorybook || Meteor.isDevelopment){
links.push({title: 'Component Previews', icon: "category", to: '/storybook/HealthBar'})
}
return links;
},
parties(){
@@ -126,7 +141,7 @@
fields: {name: 1, urlName: 1},
}
).map(char => {
char.url = `\/character\/${char._id}\/${char.urlName || "-"}`;
char.url = `\/character\/${char._id}\/${char.urlName || '-'}`;
return char;
});
return party;
@@ -143,7 +158,7 @@
},
{sort: {name: 1}}
).map(char => {
char.url = `\/character\/${char._id}\/${char.urlName || "-"}`;
char.url = `\/character\/${char._id}\/${char.urlName || '-'}`;
return char;
});
},

View File

@@ -3,23 +3,34 @@
<span slot="toolbar">
Account
</span>
<v-layout align-center justify-center>
<v-layout
align-center
justify-center
>
<v-card class="ma-4 pa-2">
<v-list>
<v-list-tile>
<v-switch :input-value="darkMode" @change="setDarkMode" label="Dark mode"/>
</v-list-tile>
<v-list-tile>
<v-switch
:input-value="darkMode"
label="Dark mode"
@change="setDarkMode"
/>
</v-list-tile>
<v-subheader>
Username
</v-subheader>
<v-list-tile>
<v-list-tile-title>
{{user.username}}
{{ user && user.username }}
</v-list-tile-title>
<v-list-tile-action>
<v-tooltip left>
<span>Change Username</span>
<v-btn icon flat slot="activator">
<v-btn
slot="activator"
icon
flat
>
<v-icon>create</v-icon>
</v-btn>
</v-tooltip>
@@ -28,21 +39,29 @@
<v-subheader>
Email
</v-subheader>
<v-list-tile v-for="email in emails" :key="email.address">
<v-list-tile
v-for="email in emails"
:key="email.address"
>
<v-list-tile-title>
{{email.address}}
{{ email.address }}
</v-list-tile-title>
<v-list-tile-action>
<v-tooltip left v-if="email.verified">
<v-tooltip
v-if="email.verified"
left
>
<span>Verified</span>
<v-icon slot="activator">assignment_turned_in</v-icon>
<v-icon slot="activator">
assignment_turned_in
</v-icon>
</v-tooltip>
<v-tooltip left v-else="email.verified">
<v-tooltip left>
<span>Verify Account</span>
<v-btn
slot="activator"
flat
icon
slot="activator"
@click="verifyEmail(email.address)"
>
<v-icon>assignment_late</v-icon>
@@ -54,7 +73,11 @@
<v-list-tile-action>
<v-tooltip right>
<span>Add email address</span>
<v-btn flat icon slot="activator">
<v-btn
slot="activator"
flat
icon
>
<v-icon>add</v-icon>
</v-btn>
</v-tooltip>
@@ -63,24 +86,33 @@
<v-subheader>
API Key
</v-subheader>
<v-list-tile v-if="user.apiKey">
<v-list-tile v-if="user && user.apiKey">
<v-list-tile v-if="showApiKey">
{{user.apiKey}}
{{ user.apiKey }}
</v-list-tile>
<v-list-tile-title v-else="showApiKey">
{{"".repeat(user.apiKey.length)}}
<v-list-tile-title>
{{ "•".repeat(user.apiKey.length) }}
</v-list-tile-title>
<v-list-tile-action>
<v-btn flat icon @click="showApiKey=!showApiKey">
<v-icon>{{showApiKey ? 'visibility_off' : 'visibility'}}</v-icon>
<v-btn
flat
icon
@click="showApiKey=!showApiKey"
>
<v-icon>{{ showApiKey ? 'visibility_off' : 'visibility' }}</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
<v-list-tile v-else="user.apiKey">
<v-btn flat color="accent" @click="generateKey">
<v-list-tile v-else>
<v-btn
flat
color="accent"
@click="generateKey"
>
Generate API Key
</v-btn>
</v-list-tile>
<!--
<v-subheader>
Google Account
</v-subheader>
@@ -90,22 +122,30 @@
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{googleAccount.name}}
{{ googleAccount.name }}
</v-list-tile-title>
<v-list-tile-sub-title>
{{googleAccount.email}}
{{ googleAccount.email }}
</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-else="googleAccount">
<v-btn flat color="accent">
<v-btn
flat
color="accent"
>
Connect Google
</v-btn>
</v-list-tile>
-->
</v-list>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn flat color="accent">
<v-spacer />
<v-btn
flat
color="accent"
@click="signOut"
>
Sign Out
</v-btn>
</v-card-actions>
@@ -115,28 +155,32 @@
</template>
<script>
import ToolbarLayout from "/imports/ui/layouts/ToolbarLayout.vue";
import { Accounts } from "meteor/accounts-base";
import router from "/imports/ui/vueSetup.js";
import router from '/imports/ui/router.js';
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
export default {
meteor: {
$subscribe: {
"user": [],
'user': [],
},
user(){
return Meteor.user();
},
googleAccount(){
const user = Meteor.user();
return user.services && user.services.google;
return user && user.services && user.services.google;
},
emails(){
return Meteor.user().emails;
const user = Meteor.user();
return user && user.emails;
},
darkMode(){
return this.user && this.user.darkMode;
},
},
components: {
ToolbarLayout,
},
data(){ return {
showApiKey: false,
signOutBusy: false,
@@ -145,9 +189,8 @@
}},
methods: {
signOut(){
Meteor.logout(function (e) {
});
Meteor.logout();
router.push('/');
},
setDarkMode(value){
Meteor.users.setDarkMode.call({darkMode: !!value});
@@ -156,7 +199,7 @@
Meteor.users.gnerateApiKey.call(error => {
if(error) this.apiKeyGenerationError = error.reason;
});
this.showApiKey = true;
this.showApiKey = true;
},
verifyEmail(address){
Meteor.users.sendVerificationEmail.call({address}, error => {
@@ -164,8 +207,5 @@
});
},
},
components: {
ToolbarLayout,
},
}
</script>

View File

@@ -5,8 +5,16 @@
</div>
<div class="content">
<section>
<v-parallax src="/png/paper-dice-crown.png" height="300">
<v-layout column align-center justify-center class="white--text">
<v-parallax
src="/png/paper-dice-crown.png"
height="300"
>
<v-layout
column
align-center
justify-center
class="white--text"
>
<h1 class="white--text mb-2 display-1 text-xs-center">
DiceCloud - Free, Auditable, real-time character tracking for 5th edition
</h1>
@@ -17,9 +25,22 @@
</v-parallax>
</section>
<section class="text-xs-center">
<v-layout row wrap justify-space-around class="selling-points">
<v-layout column align-center>
<v-icon x-large class="ma-2">money_off</v-icon>
<v-layout
row
wrap
justify-space-around
class="selling-points"
>
<v-layout
column
align-center
>
<v-icon
x-large
class="ma-2"
>
money_off
</v-icon>
<h3 class="mb-2">
Free, open source, community funded
</h3>
@@ -28,8 +49,16 @@
and the source code is available on Github under a GPL license.
</p>
</v-layout>
<v-layout column align-center>
<v-icon x-large class="ma-2">ballot</v-icon>
<v-layout
column
align-center
>
<v-icon
x-large
class="ma-2"
>
ballot
</v-icon>
<h3 class="mb-2">
Character sheets optimised for one ruleset
</h3>
@@ -38,8 +67,16 @@
does: being a fully automated character tracker
</p>
</v-layout>
<v-layout column align-center>
<v-icon x-large class="ma-2">scatter_plot</v-icon>
<v-layout
column
align-center
>
<v-icon
x-large
class="ma-2"
>
scatter_plot
</v-icon>
<h3 class="mb-2">
Inventory manager
</h3>
@@ -51,9 +88,21 @@
</v-layout>
</v-layout>
</section>
<section class="ma-5" v-if="!signedIn">
<v-layout row align-center justify-space-around>
<v-btn color="accent" round large to="/register">
<section
v-if="!signedIn"
class="ma-5"
>
<v-layout
row
align-center
justify-space-around
>
<v-btn
color="accent"
round
large
to="/sign-in"
>
Sign up
</v-btn>
</v-layout>
@@ -62,29 +111,34 @@
<h1 class="mb-2 text-xs-center">
Check out the example characters
</h1>
<v-layout row align-center justify-space-around class="pa-4">
<v-layout
row
align-center
justify-space-around
class="pa-4"
>
<a href="/character/yBWwt5XQTTHZiRQxq">
<v-hover>
<v-card
slot-scope="{ hover }"
:class="`elevation-${hover ? 12 : 2}`"
>
<v-card-text>
Starter set archer
</v-card-text>
</v-card>
<v-card
slot-scope="{ hover }"
:class="`elevation-${hover ? 12 : 2}`"
>
<v-card-text>
Starter set archer
</v-card-text>
</v-card>
</v-hover>
</a>
<a href="/character/yBWwt5XQTTHZiRQxq">
<v-hover>
<v-card
slot-scope="{ hover }"
:class="`elevation-${hover ? 12 : 2}`"
>
<v-card-text>
Starter set wizard
</v-card-text>
</v-card>
<v-card
slot-scope="{ hover }"
:class="`elevation-${hover ? 12 : 2}`"
>
<v-card-text>
Starter set wizard
</v-card-text>
</v-card>
</v-hover>
</a>
</v-layout>
@@ -93,22 +147,48 @@
<h1>
Get involved in the DiceCloud community
</h1>
<v-layout row wrap align-center justify-space-around class="pa-4">
<v-btn href="https://reddit.com/r/dicecloud" flat large color="secondary">
<v-layout
row
wrap
align-center
justify-space-around
class="pa-4"
>
<v-btn
href="https://reddit.com/r/dicecloud"
flat
large
color="secondary"
>
Reddit
</v-btn>
<v-flex>
<v-btn href="https://discord.gg/qEvdfeB" flat large color="secondary">
<v-btn
href="https://discord.gg/qEvdfeB"
flat
large
color="secondary"
>
Discord
</v-btn>
</v-flex>
<v-flex>
<v-btn href="https://www.patreon.com/dicecloud" flat large color="secondary">
<v-btn
href="https://www.patreon.com/dicecloud"
flat
large
color="secondary"
>
Patreon
</v-btn>
</v-flex>
<v-flex>
<v-btn href="https://github.com/ThaumRystra/DiceCloud" flat large color="secondary">
<v-btn
href="https://github.com/ThaumRystra/DiceCloud"
flat
large
color="secondary"
>
Github
</v-btn>
</v-flex>
@@ -119,7 +199,7 @@
</template>
<script>
import ToolbarLayout from "/imports/ui/layouts/ToolbarLayout.vue";
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
export default {
components: {
ToolbarLayout,

View File

@@ -3,10 +3,22 @@
<div slot="toolbar">
Not Found
</div>
<v-layout align-center justify-center>
<v-layout
align-center
justify-center
>
<h1>
No page was found for this address
</h1>
</v-layout>
</toolbar-layout>
</template>
<script>
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
export default {
components: {
ToolbarLayout,
}
}
</script>

View File

@@ -0,0 +1,38 @@
<template>
<toolbar-layout>
<div slot="toolbar">
Patreon tier not high enough
</div>
<v-layout
column
align-center
justify-center
>
<h2 style="margin: 48px 28px 16px">
Your current patreon support is ${{ entitledDollars }}.
</h2>
<h2 style="margin: 16px 28px">
You need to pledge at least $5 to use this beta.
</h2>
</v-layout>
</toolbar-layout>
</template>
<script>
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
export default {
components: {
ToolbarLayout,
},
meteor: {
entitledDollars(){
let user = Meteor.user();
if (!user) return 0;
let entitledCents = user.services.patreon.entitledCents || 0;
let overrideCents = user.services.patreon.entitledCentsOverride || 0;
return Math.max(entitledCents, overrideCents)/100;
}
}
}
</script>

View File

@@ -1,74 +1,112 @@
<template>
<ToolbarLayout>
<div slot="toolbar">Sign In</div>
<v-form ref="form" class="mt-4">
<v-layout column align-center>
<v-img
src="crown-dice-logo-cropped-transparent.png"
width="120px"
class="ma-3"></v-img>
<v-text-field
type="text"
label="Username or email"
v-model="name"
:rules="nameRules"
@keyup.enter="submit"
required
></v-text-field>
<v-text-field
type="password"
label="Password"
v-model="password"
:rules="passwordRules"
@keyup.enter="submit"
required
></v-text-field>
<v-btn flat>Reset Password</v-btn>
<div class="error--text">
{{error}}
</div>
<v-layout row>
<v-btn
:disabled="!valid"
@click="submit"
color="accent"
>
Sign In
</v-btn>
<v-btn color="accent" to="/register">
Register
</v-btn>
</v-layout>
</v-layout>
</v-form>
<v-divider class="ma-4"></v-divider>
<v-layout column align-center>
<div class="error--text">
{{googleError}}
</div>
<v-btn color="accent" @click="googleLogin">
Sign in with Google
</v-btn>
</v-layout>
</ToolbarLayout>
<ToolbarLayout>
<div slot="toolbar">
Sign In
</div>
<v-form
ref="form"
class="mt-4"
>
<v-layout
column
align-center
>
<v-img
src="crown-dice-logo-cropped-transparent.png"
width="120px"
class="ma-3"
/>
<!--
<v-text-field
v-model="name"
type="text"
label="Username or email"
:rules="nameRules"
required
@keyup.enter="submit"
/>
<v-text-field
v-model="password"
type="password"
label="Password"
:rules="passwordRules"
required
@keyup.enter="submit"
/>
<v-btn flat>
Reset Password
</v-btn>
<div class="error--text">
{{ error }}
</div>
<v-layout row>
<v-btn
:disabled="!valid"
color="accent"
@click="submit"
>
Sign In
</v-btn>
<v-btn
color="accent"
to="/register"
>
Register
</v-btn>
</v-layout>
-->
</v-layout>
</v-form>
<v-divider class="ma-4" />
<v-layout
column
align-center
>
<!--
<div class="error--text">
{{ googleError }}
</div>
<v-btn
color="accent"
@click="googleLogin"
>
Sign in with Google
</v-btn>
-->
<div class="error--text">
{{ patreonError }}
</div>
<v-btn
color="accent"
@click="patreonLogin"
>
Sign in with Patreon
</v-btn>
</v-layout>
</ToolbarLayout>
</template>
<script>
import ToolbarLayout from "/imports/ui/layouts/ToolbarLayout.vue";
import router from "/imports/ui/router.js";
import { Meteor } from 'meteor/meteor'
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
import router from '/imports/ui/router.js';
export default{
components: {
ToolbarLayout,
},
data: () => ({
valid: true,
name: "",
name: '',
nameRules: [
v => !!v || "Name is required",
v => !!v || 'Name is required',
],
password: "",
password: '',
passwordRules: [
v => !!v || "Password is required",
v => !!v || 'Password is required',
],
error: "",
googleError: "",
error: '',
googleError: '',
patreonError: '',
}),
methods: {
submit () {
@@ -77,19 +115,31 @@
if (error){
this.error = error.reason;
} else {
router.push("characterList");
router.push('characterList');
}
});
}
},
googleLogin() {
Meteor.loginWithGoogle(error => {
if (error) this.googleError = error.reason;
if (error){
console.error(error);
this.googleError = error.message;
} else {
router.push('characterList');
}
});
},
patreonLogin() {
Meteor.loginWithPatreon(error => {
if (error){
console.error(error);
this.patreonError = error.message;
} else {
router.push('characterList');
}
});
}
},
components: {
ToolbarLayout,
},
}
</script>

View File

@@ -1,5 +1,4 @@
import { RouterFactory, nativeScrollBehavior } from 'meteor/akryum:vue-router2';
import Vue from 'vue';
// Components
import Home from '/imports/ui/pages/Home.vue';
@@ -10,6 +9,7 @@ import SignIn from '/imports/ui/pages/SignIn.vue' ;
import Register from '/imports/ui/pages/Register.vue' ;
import Account from '/imports/ui/pages/Account.vue' ;
import NotImplemented from '/imports/ui/pages/NotImplemented.vue';
import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
// Not found
import NotFound from '/imports/ui/pages/NotFound.vue';
@@ -21,6 +21,22 @@ const routerFactory = new RouterFactory({
scrollBehavior: nativeScrollBehavior,
});
function ensurePatronTier(to, from, next){
let user = Meteor.user();
if (!user){
next('/sign-in');
return;
}
let entitledCents = user.services.patreon.entitledCents || 0;
let overrideCents = user.services.patreon.entitledCentsOverride || 0;
if (entitledCents < 500 && overrideCents < 500){
next('/patreon-level-too-low');
} else {
next();
}
}
RouterFactory.configure(factory => {
factory.addRoutes([
{
@@ -30,48 +46,36 @@ RouterFactory.configure(factory => {
},{
path: '/characterList',
component: CharacterList,
//component: NotImplemented,
beforeEnter: ensurePatronTier,
},{
path: '/library',
component: Library,
beforeEnter: ensurePatronTier,
},{
path: '/character/:id/:urlName',
component: CharacterSheetPage,
//component: NotImplemented,
beforeEnter: ensurePatronTier,
},{
path: '/character/:id',
component: CharacterSheetPage,
//component: NotImplemented,
beforeEnter: ensurePatronTier,
},{
path: '/sign-in',
component: SignIn,
},{
},/*{
path: '/register',
component: Register,
},{
},*/{
path: '/account',
component: Account,
},{
path: '/feedback',
component: NotImplemented,
},{
path: '/patreon-level-too-low',
component: PatreonLevelTooLow,
},
]);
// Storybook routes
if (Meteor.settings.public.showStorybook || Meteor.isDevelopment){
let StoryBook = require('/imports/ui/StoryBook.vue').default;
factory.addRoutes([
{
path: '/storybook/:component',
name: 'componentStory',
component: StoryBook,
},{
path: '/storybook',
name: 'storybook',
component: StoryBook,
},
]);
}
// Icon admin routes
if (Meteor.isDevelopment){
let IconAdmin = require('/imports/ui/icons/IconAdmin.vue').default;

View File

@@ -82,7 +82,8 @@
"env": {
"es6": true,
"browser": true,
"node": true
"node": true,
"meteor": true
},
"rules": {
"quotes": [

View File

@@ -0,0 +1 @@
.build*

View File

@@ -0,0 +1,17 @@
Package.describe({
summary: 'Login service for Patreon accounts',
version: '0.1.0',
});
Package.onUse(api => {
api.use('ecmascript');
api.use('accounts-base', ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('accounts-oauth', ['client', 'server']);
api.use('patreon-oauth');
api.imply('patreon-oauth');
api.addFiles('patreon.js');
});

View File

@@ -0,0 +1,26 @@
Accounts.oauth.registerService('patreon');
console.log('accounts-patreon');
if (Meteor.isClient) {
const loginWithPatreon = (options, callback) => {
// support a callback without options
if (! callback && typeof options === 'function') {
callback = options;
options = null;
}
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Patreon.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('patreon', loginWithPatreon);
Meteor.loginWithPatreon =
(...args) => Accounts.applyLoginFunction('patreon', args);
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on
// localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit
forLoggedInUser: ['services.patreon'],
forOtherUsers: ['services.patreon.id']
});
}

View File

@@ -0,0 +1,3 @@
# patreon-oauth
An implementation of the Patreon OAuth flow. See the [Meteor Guide](https://guide.meteor.com/accounts.html) for more details.

View File

@@ -0,0 +1,18 @@
Package.describe({
summary: 'Patreon OAuth flow',
version: '0.1.0'
});
Package.onUse(api => {
api.use('ecmascript');
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('http', 'server');
api.use('random', 'client');
api.use('service-configuration', ['client', 'server']);
api.addFiles('patreon_server.js', 'server');
api.addFiles('patreon_client.js', 'client');
api.export('Patreon');
});

View File

@@ -0,0 +1,54 @@
Patreon = {};
console.log('patreon-oauth');
// Request Patreon credentials for the user
// @param options {optional}
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
Patreon.requestCredential = (options, credentialRequestCompleteCallback) => {
// support both (options, callback) and (callback).
if (!credentialRequestCompleteCallback && typeof options === 'function') {
credentialRequestCompleteCallback = options;
options = {};
}
const config = ServiceConfiguration.configurations.findOne({service: 'patreon'});
if (!config) {
credentialRequestCompleteCallback && credentialRequestCompleteCallback(
new ServiceConfiguration.ConfigError());
return;
}
// For some reason, meetup converts underscores to spaces in the state
// parameter when redirecting back to the client, so we use
// `Random.id()` here (alphanumerics) instead of `Random.secret()`
// (base 64 characters).
const credentialToken = Random.id();
const scope = (options && options.requestPermissions) || [
'identity',
'identity[email]',
];
const flatScope = scope.map(encodeURIComponent).join(' ');
//const flatScope = encodeURIComponent(scope.join(','));
console.log({flatScope})
const loginStyle = OAuth._loginStyle('patreon', config, options);
const loginUrl =
'https://www.patreon.com/oauth2/authorize' +
`?client_id=${config.clientId}` +
'&response_type=code' +
(flatScope ? `&scope=${flatScope}` : '') +
`&redirect_uri=${OAuth._redirectUri('patreon', config)}` +
`&state=${OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl)}`;
OAuth.launchLogin({
loginService: 'patreon',
loginStyle,
loginUrl,
credentialRequestCompleteCallback,
credentialToken,
});
};

View File

@@ -0,0 +1,75 @@
Patreon = {};
OAuth.registerService('patreon', 2, null, query => {
const response = getAccessToken(query);
const accessToken = response.access_token;
const refreshToken = response.refresh_token;
const scope = response.scope;
const expiresAt = (+new Date) + (1000 * response.expires_in);
const identity = getIdentity(accessToken);
let serviceData = {
id : identity.data.id,
email: identity.data.attributes.email,
entitledCents: identity.included[0] &&
identity.included[0].attributes.currently_entitled_amount_cents || 0,
accessToken,
refreshToken,
scope,
expiresAt,
};
return { serviceData };
});
const getAccessToken = query => {
const config = ServiceConfiguration.configurations.findOne({service: 'patreon'});
if (!config)
throw new ServiceConfiguration.ConfigError();
let response;
try {
response = HTTP.post(
'https://www.patreon.com/api/oauth2/token', {headers: {Accept: 'application/json'}, params: {
code: query.code,
client_id: config.clientId,
client_secret: config.secret,
grant_type: 'authorization_code',
redirect_uri: OAuth._redirectUri('patreon', config),
}});
} catch (err) {
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Patreon. ${err.message}`),
{ response: err.response }
);
}
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error(`Failed to complete OAuth handshake with Patreon. ${response.data.error}`);
} else {
return response.data;
}
};
const getIdentity = accessToken => {
try {
const response = HTTP.get(
'https://www.patreon.com/api/oauth2/v2/identity?' +
'fields%5Buser%5D=email&' +
'fields%5Bmember%5D=currently_entitled_amount_cents&' +
'include=memberships',
{
headers: {authorization: `Bearer ${accessToken}`},
}
);
let data = JSON.parse(response.content);
return data;
} catch (err) {
throw Object.assign(
new Error(`Failed to fetch identity from Patreon. ${err.message}`),
{ response: err.response }
);
}
};
Patreon.retrieveCredential = (credentialToken, credentialSecret) =>
OAuth.retrieveCredential(credentialToken, credentialSecret);