Invites can now be managed to some extent
This commit is contained in:
@@ -15,6 +15,7 @@ let InviteSchema = new SimpleSchema({
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
index: 1,
|
||||
unique: 1,
|
||||
},
|
||||
inviteToken: {
|
||||
type: String,
|
||||
@@ -25,10 +26,6 @@ let InviteSchema = new SimpleSchema({
|
||||
isFunded: {
|
||||
type: Boolean,
|
||||
},
|
||||
isRedundant: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// The timestamp of when the invitee was confirmed
|
||||
// Older invites have priority over newer ones
|
||||
dateConfirmed: {
|
||||
@@ -119,14 +116,15 @@ const acceptInviteToken = new ValidatedMethod({
|
||||
},
|
||||
}).validator(),
|
||||
run({inviteToken}) {
|
||||
if (this.userId) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('Invites.methods.acceptToken.denied',
|
||||
'You need to be the logged in to accept a token');
|
||||
}
|
||||
if (Meteor.isClient) return;
|
||||
let invite = Invites.findOne({inviteToken});
|
||||
if (!invite){
|
||||
throw new Meteor.Error('Invites.methods.acceptToken.notFound',
|
||||
'No invite could be found for this token');
|
||||
'No invite could be found for this link, maybe it has already been claimed');
|
||||
}
|
||||
// If the invitee is already filled, fix unexpected case by deleting the token
|
||||
if (invite.invitee){
|
||||
@@ -134,7 +132,7 @@ const acceptInviteToken = new ValidatedMethod({
|
||||
$unset: {inviteToken: 1}
|
||||
});
|
||||
throw new Meteor.Error('Invites.methods.acceptToken.alreadyAccepted',
|
||||
'This invite already has an invitee, and shouldn\'t have a token');
|
||||
'This invite has already been claimed');
|
||||
}
|
||||
if (this.userId === invite.inviter){
|
||||
throw new Meteor.Error('Invites.methods.acceptToken.ownToken',
|
||||
|
||||
@@ -3,6 +3,7 @@ import CreaturePropertyCreationDialog from '/imports/ui/creature/creaturePropert
|
||||
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue'
|
||||
import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue'
|
||||
import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.vue';
|
||||
import InviteDialog from '/imports/ui/user/InviteDialog.vue';
|
||||
import LibraryCreationDialog from '/imports/ui/library/LibraryCreationDialog.vue';
|
||||
import LibraryEditDialog from '/imports/ui/library/LibraryEditDialog.vue';
|
||||
import LibraryNodeCreationDialog from '/imports/ui/library/LibraryNodeCreationDialog.vue';
|
||||
@@ -16,6 +17,7 @@ export default {
|
||||
CreaturePropertyDialog,
|
||||
CreaturePropertyFromLibraryDialog,
|
||||
DeleteConfirmationDialog,
|
||||
InviteDialog,
|
||||
LibraryCreationDialog,
|
||||
LibraryEditDialog,
|
||||
LibraryNodeCreationDialog,
|
||||
|
||||
@@ -74,16 +74,48 @@
|
||||
</v-btn>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-layout
|
||||
row
|
||||
justify-end
|
||||
>
|
||||
<v-btn
|
||||
flat
|
||||
color="accent"
|
||||
@click="signOut"
|
||||
>
|
||||
Sign Out
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-layout>
|
||||
<template v-if="invites.length">
|
||||
<v-divider class="mt-3 mb-3" />
|
||||
<v-subheader>
|
||||
<h1>
|
||||
Invites
|
||||
</h1>
|
||||
</v-subheader>
|
||||
<v-list>
|
||||
<template
|
||||
v-for="(invite, index) in invites"
|
||||
>
|
||||
<v-list-tile
|
||||
:key="invite._id"
|
||||
:data-id="invite._id"
|
||||
@click="clickInvite(invite)"
|
||||
>
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
{{ invite.inviteeName || invite.invitee || 'Available' }}
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
<v-list-tile-action>
|
||||
<v-icon>mail_outline</v-icon>
|
||||
</v-list-tile-action>
|
||||
</v-list-tile>
|
||||
<v-divider
|
||||
:key="index"
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -114,6 +146,20 @@
|
||||
darkMode(){
|
||||
return this.user && this.user.darkMode;
|
||||
},
|
||||
invites(){
|
||||
let usernames = {};
|
||||
Meteor.users.find({}).forEach(user => {
|
||||
usernames[user._id] = user.username;
|
||||
});
|
||||
return Invites.find({
|
||||
inviter: Meteor.userId(),
|
||||
}, {
|
||||
sort: {dateConfirmed: 1},
|
||||
}).map(invite => {
|
||||
invite.inviteeName = usernames[invite.invitee];
|
||||
return invite;
|
||||
});
|
||||
}
|
||||
},
|
||||
data(){ return {
|
||||
showApiKey: false,
|
||||
@@ -154,6 +200,13 @@
|
||||
if(error) this.emailVerificationError = error.reason;
|
||||
});
|
||||
},
|
||||
clickInvite(invite){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'invite-dialog',
|
||||
elementId: invite._id,
|
||||
data: {inviteId: invite._id},
|
||||
});
|
||||
},
|
||||
linkWithPatreon,
|
||||
},
|
||||
}
|
||||
|
||||
22
app/imports/ui/pages/InviteError.vue
Normal file
22
app/imports/ui/pages/InviteError.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
Invite Error!
|
||||
<p>
|
||||
{{ error.reason || error.message || error }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
error: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
13
app/imports/ui/pages/InviteSuccess.vue
Normal file
13
app/imports/ui/pages/InviteSuccess.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
Invite Success! You can now use DiceCloud.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -85,7 +85,6 @@
|
||||
|
||||
<script>
|
||||
import { Meteor } from 'meteor/meteor'
|
||||
import router from '/imports/ui/router.js';
|
||||
export default{
|
||||
data: () => ({
|
||||
valid: true,
|
||||
@@ -108,7 +107,7 @@
|
||||
if (error){
|
||||
this.error = error.reason;
|
||||
} else {
|
||||
router.push('characterList');
|
||||
this.$router.push(this.$route.query.redirect || 'characterList');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -119,7 +118,7 @@
|
||||
console.error(error);
|
||||
this.googleError = error.message;
|
||||
} else {
|
||||
router.push('characterList');
|
||||
this.$router.push(this.$route.query.redirect || 'characterList');
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -129,7 +128,7 @@
|
||||
console.error(error);
|
||||
this.patreonError = error.message;
|
||||
} else {
|
||||
router.push('characterList');
|
||||
this.$router.push(this.$route.query.redirect || 'characterList');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RouterFactory, nativeScrollBehavior } from 'meteor/akryum:vue-router2';
|
||||
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
|
||||
import LAUNCH_DATE from '/imports/constants/LAUNCH_DATE.js';
|
||||
import { acceptInviteToken } from '/imports/api/users/Invites.js';
|
||||
|
||||
// Components
|
||||
import Home from '/imports/ui/pages/Home.vue';
|
||||
@@ -16,6 +17,8 @@ import Register from '/imports/ui/pages/Register.vue';
|
||||
import Friends from '/imports/ui/pages/Friends.vue' ;
|
||||
import Feedback from '/imports/ui/pages/Feedback.vue' ;
|
||||
import Account from '/imports/ui/pages/Account.vue' ;
|
||||
import InviteSuccess from '/imports/ui/pages/InviteSuccess.vue' ;
|
||||
import InviteError from '/imports/ui/pages/InviteError.vue' ;
|
||||
import NotImplemented from '/imports/ui/pages/NotImplemented.vue';
|
||||
import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
|
||||
import LaunchCountdown from '/imports/ui/pages/LaunchCountdown.vue';
|
||||
@@ -39,7 +42,7 @@ function ensureLoggedIn(to, from, next){
|
||||
if (user){
|
||||
next()
|
||||
} else {
|
||||
next('/sign-in');
|
||||
next({ name: 'signIn', query: { redirect: to.path} });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -51,7 +54,7 @@ function ensurePatronTier5(to, from, next){
|
||||
computation.stop();
|
||||
const user = Meteor.user();
|
||||
if (!user){
|
||||
next('/sign-in');
|
||||
next({ name: 'signIn', query: { redirect: to.path} });
|
||||
return;
|
||||
}
|
||||
let entitledCents = getEntitledCents(user);
|
||||
@@ -64,6 +67,29 @@ function ensurePatronTier5(to, from, next){
|
||||
});
|
||||
}
|
||||
|
||||
function claimInvite(to, from, next){
|
||||
Tracker.autorun((computation) => {
|
||||
if (userSubscription.ready()){
|
||||
computation.stop();
|
||||
const user = Meteor.user();
|
||||
if (user){
|
||||
let inviteToken = to.params.inviteToken;
|
||||
acceptInviteToken.call({
|
||||
inviteToken
|
||||
}, (error) => {
|
||||
if (error){
|
||||
next({name: 'inviteError', params: {error}});
|
||||
} else {
|
||||
next('/invite-success')
|
||||
}
|
||||
});
|
||||
} else {
|
||||
next({ name: 'signIn', query: { redirect: to.path} });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RouterFactory.configure(factory => {
|
||||
factory.addRoutes([
|
||||
{
|
||||
@@ -145,6 +171,7 @@ RouterFactory.configure(factory => {
|
||||
},
|
||||
beforeEnter: ensureLoggedIn,
|
||||
},{
|
||||
name: 'signIn',
|
||||
path: '/sign-in',
|
||||
components: {
|
||||
default: SignIn,
|
||||
@@ -177,6 +204,29 @@ RouterFactory.configure(factory => {
|
||||
meta: {
|
||||
title: 'Feedback',
|
||||
},
|
||||
},{
|
||||
path: '/invite/:inviteToken',
|
||||
beforeEnter: claimInvite,
|
||||
},{
|
||||
name: 'inviteError',
|
||||
path: '/invite-error',
|
||||
components: {
|
||||
default: InviteError,
|
||||
},
|
||||
props: {
|
||||
default: true,
|
||||
},
|
||||
meta: {
|
||||
title: 'Invite Error',
|
||||
},
|
||||
},{
|
||||
path: '/invite-success',
|
||||
components: {
|
||||
default: InviteSuccess,
|
||||
},
|
||||
meta: {
|
||||
title: 'Invite Success',
|
||||
},
|
||||
},{
|
||||
path: '/patreon-level-too-low',
|
||||
components: {
|
||||
|
||||
76
app/imports/ui/user/InviteDialog.vue
Normal file
76
app/imports/ui/user/InviteDialog.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template lang="html">
|
||||
<dialog-base>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
Invite
|
||||
</v-toolbar-title>
|
||||
</template>
|
||||
<div v-if="invite.invitee" />
|
||||
<div v-else>
|
||||
<p>This invite is available</p>
|
||||
<v-fade-transition mode="out-in">
|
||||
<v-btn
|
||||
v-if="!inviteLink"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="getInviteLink"
|
||||
>
|
||||
Get Invite Link
|
||||
</v-btn>
|
||||
<h3 v-else>
|
||||
{{ inviteLink }}
|
||||
</h3>
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import Invites, { getInviteToken } from '/imports/api/users/Invites.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
},
|
||||
props: {
|
||||
inviteId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
inviteToken: '',
|
||||
error: '',
|
||||
loading: false,
|
||||
}},
|
||||
meteor: {
|
||||
invite(){
|
||||
return Invites.findOne(this.inviteId);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inviteLink(){
|
||||
let token = this.inviteToken;
|
||||
return token && `https://beta.dicecloud.com/invite/${token}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getInviteLink(){
|
||||
this.loading = true;
|
||||
getInviteToken.call({inviteId: this.inviteId}, (error, result) => {
|
||||
this.loading = false;
|
||||
if (error){
|
||||
this.error = error.message || error;
|
||||
} else {
|
||||
this.error = '',
|
||||
this.inviteToken = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user