Invites can now be managed to some extent

This commit is contained in:
Thaum Rystra
2020-05-12 14:11:43 +02:00
parent 47206ccfc4
commit bbda0ea1b6
8 changed files with 230 additions and 17 deletions

View File

@@ -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',

View File

@@ -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,

View File

@@ -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,
},
}

View 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>

View 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>

View File

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

View File

@@ -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: {

View 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>