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,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
optional: true,
|
optional: true,
|
||||||
index: 1,
|
index: 1,
|
||||||
|
unique: 1,
|
||||||
},
|
},
|
||||||
inviteToken: {
|
inviteToken: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -25,10 +26,6 @@ let InviteSchema = new SimpleSchema({
|
|||||||
isFunded: {
|
isFunded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
isRedundant: {
|
|
||||||
type: Boolean,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
// The timestamp of when the invitee was confirmed
|
// The timestamp of when the invitee was confirmed
|
||||||
// Older invites have priority over newer ones
|
// Older invites have priority over newer ones
|
||||||
dateConfirmed: {
|
dateConfirmed: {
|
||||||
@@ -119,14 +116,15 @@ const acceptInviteToken = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
}).validator(),
|
}).validator(),
|
||||||
run({inviteToken}) {
|
run({inviteToken}) {
|
||||||
if (this.userId) {
|
if (!this.userId) {
|
||||||
throw new Meteor.Error('Invites.methods.acceptToken.denied',
|
throw new Meteor.Error('Invites.methods.acceptToken.denied',
|
||||||
'You need to be the logged in to accept a token');
|
'You need to be the logged in to accept a token');
|
||||||
}
|
}
|
||||||
|
if (Meteor.isClient) return;
|
||||||
let invite = Invites.findOne({inviteToken});
|
let invite = Invites.findOne({inviteToken});
|
||||||
if (!invite){
|
if (!invite){
|
||||||
throw new Meteor.Error('Invites.methods.acceptToken.notFound',
|
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 the invitee is already filled, fix unexpected case by deleting the token
|
||||||
if (invite.invitee){
|
if (invite.invitee){
|
||||||
@@ -134,7 +132,7 @@ const acceptInviteToken = new ValidatedMethod({
|
|||||||
$unset: {inviteToken: 1}
|
$unset: {inviteToken: 1}
|
||||||
});
|
});
|
||||||
throw new Meteor.Error('Invites.methods.acceptToken.alreadyAccepted',
|
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){
|
if (this.userId === invite.inviter){
|
||||||
throw new Meteor.Error('Invites.methods.acceptToken.ownToken',
|
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 CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue'
|
||||||
import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue'
|
import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue'
|
||||||
import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.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 LibraryCreationDialog from '/imports/ui/library/LibraryCreationDialog.vue';
|
||||||
import LibraryEditDialog from '/imports/ui/library/LibraryEditDialog.vue';
|
import LibraryEditDialog from '/imports/ui/library/LibraryEditDialog.vue';
|
||||||
import LibraryNodeCreationDialog from '/imports/ui/library/LibraryNodeCreationDialog.vue';
|
import LibraryNodeCreationDialog from '/imports/ui/library/LibraryNodeCreationDialog.vue';
|
||||||
@@ -16,6 +17,7 @@ export default {
|
|||||||
CreaturePropertyDialog,
|
CreaturePropertyDialog,
|
||||||
CreaturePropertyFromLibraryDialog,
|
CreaturePropertyFromLibraryDialog,
|
||||||
DeleteConfirmationDialog,
|
DeleteConfirmationDialog,
|
||||||
|
InviteDialog,
|
||||||
LibraryCreationDialog,
|
LibraryCreationDialog,
|
||||||
LibraryEditDialog,
|
LibraryEditDialog,
|
||||||
LibraryNodeCreationDialog,
|
LibraryNodeCreationDialog,
|
||||||
|
|||||||
@@ -74,16 +74,48 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
</v-list>
|
</v-list>
|
||||||
<v-card-actions>
|
<v-layout
|
||||||
<v-spacer />
|
row
|
||||||
|
justify-end
|
||||||
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
flat
|
|
||||||
color="accent"
|
color="accent"
|
||||||
@click="signOut"
|
@click="signOut"
|
||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
</v-btn>
|
</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>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -114,6 +146,20 @@
|
|||||||
darkMode(){
|
darkMode(){
|
||||||
return this.user && this.user.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 {
|
data(){ return {
|
||||||
showApiKey: false,
|
showApiKey: false,
|
||||||
@@ -154,6 +200,13 @@
|
|||||||
if(error) this.emailVerificationError = error.reason;
|
if(error) this.emailVerificationError = error.reason;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
clickInvite(invite){
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'invite-dialog',
|
||||||
|
elementId: invite._id,
|
||||||
|
data: {inviteId: invite._id},
|
||||||
|
});
|
||||||
|
},
|
||||||
linkWithPatreon,
|
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>
|
<script>
|
||||||
import { Meteor } from 'meteor/meteor'
|
import { Meteor } from 'meteor/meteor'
|
||||||
import router from '/imports/ui/router.js';
|
|
||||||
export default{
|
export default{
|
||||||
data: () => ({
|
data: () => ({
|
||||||
valid: true,
|
valid: true,
|
||||||
@@ -108,7 +107,7 @@
|
|||||||
if (error){
|
if (error){
|
||||||
this.error = error.reason;
|
this.error = error.reason;
|
||||||
} else {
|
} else {
|
||||||
router.push('characterList');
|
this.$router.push(this.$route.query.redirect || 'characterList');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
this.googleError = error.message;
|
this.googleError = error.message;
|
||||||
} else {
|
} else {
|
||||||
router.push('characterList');
|
this.$router.push(this.$route.query.redirect || 'characterList');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -129,7 +128,7 @@
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
this.patreonError = error.message;
|
this.patreonError = error.message;
|
||||||
} else {
|
} 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 { RouterFactory, nativeScrollBehavior } from 'meteor/akryum:vue-router2';
|
||||||
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
|
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
|
||||||
import LAUNCH_DATE from '/imports/constants/LAUNCH_DATE.js';
|
import LAUNCH_DATE from '/imports/constants/LAUNCH_DATE.js';
|
||||||
|
import { acceptInviteToken } from '/imports/api/users/Invites.js';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Home from '/imports/ui/pages/Home.vue';
|
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 Friends from '/imports/ui/pages/Friends.vue' ;
|
||||||
import Feedback from '/imports/ui/pages/Feedback.vue' ;
|
import Feedback from '/imports/ui/pages/Feedback.vue' ;
|
||||||
import Account from '/imports/ui/pages/Account.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 NotImplemented from '/imports/ui/pages/NotImplemented.vue';
|
||||||
import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
|
import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
|
||||||
import LaunchCountdown from '/imports/ui/pages/LaunchCountdown.vue';
|
import LaunchCountdown from '/imports/ui/pages/LaunchCountdown.vue';
|
||||||
@@ -39,7 +42,7 @@ function ensureLoggedIn(to, from, next){
|
|||||||
if (user){
|
if (user){
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
next('/sign-in');
|
next({ name: 'signIn', query: { redirect: to.path} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -51,7 +54,7 @@ function ensurePatronTier5(to, from, next){
|
|||||||
computation.stop();
|
computation.stop();
|
||||||
const user = Meteor.user();
|
const user = Meteor.user();
|
||||||
if (!user){
|
if (!user){
|
||||||
next('/sign-in');
|
next({ name: 'signIn', query: { redirect: to.path} });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let entitledCents = getEntitledCents(user);
|
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 => {
|
RouterFactory.configure(factory => {
|
||||||
factory.addRoutes([
|
factory.addRoutes([
|
||||||
{
|
{
|
||||||
@@ -145,6 +171,7 @@ RouterFactory.configure(factory => {
|
|||||||
},
|
},
|
||||||
beforeEnter: ensureLoggedIn,
|
beforeEnter: ensureLoggedIn,
|
||||||
},{
|
},{
|
||||||
|
name: 'signIn',
|
||||||
path: '/sign-in',
|
path: '/sign-in',
|
||||||
components: {
|
components: {
|
||||||
default: SignIn,
|
default: SignIn,
|
||||||
@@ -177,6 +204,29 @@ RouterFactory.configure(factory => {
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'Feedback',
|
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',
|
path: '/patreon-level-too-low',
|
||||||
components: {
|
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