diff --git a/app/.meteor/packages b/app/.meteor/packages
index 1df88a4f..8a53403c 100644
--- a/app/.meteor/packages
+++ b/app/.meteor/packages
@@ -45,3 +45,5 @@ akryum:vue-router2
percolate:migrations
meteortesting:mocha
ostrio:files
+simple:rest-bearer-token-parser
+simple:rest-json-error-handler
diff --git a/app/.meteor/versions b/app/.meteor/versions
index 00189406..e9557bec 100644
--- a/app/.meteor/versions
+++ b/app/.meteor/versions
@@ -108,6 +108,8 @@ sha@1.0.9
shell-server@0.5.0
simple:json-routes@2.3.1
simple:rest@1.2.1
+simple:rest-bearer-token-parser@1.1.1
+simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js
index 3cb0027b..3661df4a 100644
--- a/app/imports/api/engine/computation/writeComputation/writeScope.js
+++ b/app/imports/api/engine/computation/writeComputation/writeScope.js
@@ -1,5 +1,10 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function writeScope(creatureId, scope){
+ // Remove large properties that aren't likely to be accessed
+ for (const key in scope){
+ delete scope[key].parent;
+ delete scope[key].ancestors;
+ }
Creatures.update(creatureId, {$set: {variables: scope}});
}
diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js
index 16831209..6f48ed4b 100644
--- a/app/imports/api/sharing/sharingPermissions.js
+++ b/app/imports/api/sharing/sharingPermissions.js
@@ -4,7 +4,7 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){
throw new Meteor.Error('Permission denied',
- 'No user ID given for edit permission check');
+ 'No user ID. Are you logged in?');
}
}
diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js
index 268b8d99..be3fd3a8 100644
--- a/app/imports/api/users/Users.js
+++ b/app/imports/api/users/Users.js
@@ -1,7 +1,10 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
-import '/imports/api/users/deleteMyAccount.js';
+import '/imports/api/users/methods/deleteMyAccount.js';
+import '/imports/api/users/methods/addEmail.js';
+import '/imports/api/users/methods/removeEmail.js';
+
import { some } from 'lodash';
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
diff --git a/app/imports/api/users/methods/addEmail.js b/app/imports/api/users/methods/addEmail.js
new file mode 100644
index 00000000..af8e81f6
--- /dev/null
+++ b/app/imports/api/users/methods/addEmail.js
@@ -0,0 +1,34 @@
+import SimpleSchema from 'simpl-schema';
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+
+const addEmail = new ValidatedMethod({
+ name: 'users.addEmail',
+ validate: new SimpleSchema({
+ email: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Email,
+ },
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 1,
+ timeInterval: 5000,
+ },
+ run({email}){
+ const userId = Meteor.userId();
+ const user = Meteor.users.findOne(userId);
+ if (!user) throw new Meteor.Error('No user',
+ 'You must be logged in to add an email address');
+ if (user.emails && user.emails.length >= 2){
+ throw new Meteor.Error('Emails full',
+ 'You may only have up to 2 email addresses per account');
+ }
+ if (Meteor.isServer){
+ Accounts.addEmail(userId, email);
+ Accounts.sendVerificationEmail(userId, email);
+ }
+ }
+});
+
+export default addEmail;
diff --git a/app/imports/api/users/deleteMyAccount.js b/app/imports/api/users/methods/deleteMyAccount.js
similarity index 96%
rename from app/imports/api/users/deleteMyAccount.js
rename to app/imports/api/users/methods/deleteMyAccount.js
index d887b8c0..f6947bfc 100644
--- a/app/imports/api/users/deleteMyAccount.js
+++ b/app/imports/api/users/methods/deleteMyAccount.js
@@ -15,7 +15,7 @@ Meteor.users.deleteMyAccount = new ValidatedMethod({
run(){
let userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user',
- 'You must be logged into to delete your account');
+ 'You must be logged in to delete your account');
// Delete all creatures
let creatures = Creatures.find({owner: userId}, {fields: {_id: 1}}).fetch();
diff --git a/app/imports/api/users/linkWithPatreon.js b/app/imports/api/users/methods/linkWithPatreon.js
similarity index 100%
rename from app/imports/api/users/linkWithPatreon.js
rename to app/imports/api/users/methods/linkWithPatreon.js
diff --git a/app/imports/api/users/methods/removeEmail.js b/app/imports/api/users/methods/removeEmail.js
new file mode 100644
index 00000000..86ffb822
--- /dev/null
+++ b/app/imports/api/users/methods/removeEmail.js
@@ -0,0 +1,37 @@
+import SimpleSchema from 'simpl-schema';
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+
+const removeEmail = new ValidatedMethod({
+ name: 'users.removeEmail',
+ validate: new SimpleSchema({
+ email: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Email,
+ },
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 1,
+ timeInterval: 5000,
+ },
+ run({email}){
+ const userId = Meteor.userId();
+ const user = Meteor.users.findOne(userId);
+ if (!user) throw new Meteor.Error('No user',
+ 'You must be logged in to remove an email address');
+ if (!user.emails){
+ throw new Meteor.Error('No email to remove',
+ 'No email addresses are associated with this account');
+ }
+ if (user.emails.length == 1){
+ throw new Meteor.Error('Can\'t remove last email',
+ 'You may not remove the last email address from your account');
+ }
+ if (Meteor.isServer){
+ Accounts.removeEmail(userId, email);
+ }
+ }
+});
+
+export default removeEmail;
diff --git a/app/imports/server/config/SimpleRestConfig.js b/app/imports/server/config/SimpleRestConfig.js
index c633c6a2..72f63b40 100644
--- a/app/imports/server/config/SimpleRestConfig.js
+++ b/app/imports/server/config/SimpleRestConfig.js
@@ -1,8 +1,7 @@
-import { JsonRoutes } from 'meteor/simple:json-routes';
+import { JsonRoutes, RestMiddleware } from 'meteor/simple:json-routes';
import { SimpleRest } from 'meteor/simple:rest';
Meteor.startup(() => {
- //
// Enable cross origin requests for all endpoints
JsonRoutes.setResponseHeaders({
'Cache-Control': 'no-store',
@@ -17,3 +16,18 @@ SimpleRest.configure({
// No default collection methods get end points
collections: [],
});
+
+// All errors are handled as JSON
+JsonRoutes.ErrorMiddleware.use(RestMiddleware.handleErrorAsJson);
+
+// Hack to stop simple:rest adding routes automatically unless their URL
+// has been explicitly set to 'api/...'
+const oldAdd = JsonRoutes.add;
+JsonRoutes.add = function(method, path, handler){
+ if (path.substring(0,4) !== 'api/'){
+ return;
+ }
+ oldAdd(method, path, handler);
+}
+
+import '/imports/server/rest/restLogin.js';
diff --git a/app/imports/server/config/accountsEmailConfig.js b/app/imports/server/config/accountsEmailConfig.js
index b1526d82..e6853515 100644
--- a/app/imports/server/config/accountsEmailConfig.js
+++ b/app/imports/server/config/accountsEmailConfig.js
@@ -1,4 +1,35 @@
import { Accounts } from 'meteor/accounts-base'
+import emailTemplate from './emailTemplate.js';
Accounts.emailTemplates.from = 'no-reply@dicecloud.com';
Accounts.emailTemplates.siteName = 'DiceCloud';
+
+Accounts.emailTemplates.enrollAccount = {
+ subject: () => 'DiceCloud Invite',
+ html: (user, url) => emailTemplate({
+ heading: 'DiceCloud Invite',
+ text: 'You have been invited to DiceCloud, click the button below to begin.',
+ buttonText: 'Get Started',
+ url: url.replace( '#/', '' ),
+ }),
+};
+
+Accounts.emailTemplates.resetPassword = {
+ subject: () => 'DiceCloud Password Reset',
+ html: (user, url) => emailTemplate({
+ heading: 'Password Reset',
+ text: 'If you did not request this password reset, please ignore this email.',
+ buttonText: 'Reset Password',
+ url: url.replace( '#/', '' ),
+ }),
+};
+
+Accounts.emailTemplates.verifyEmail = {
+ subject: () => 'DiceCloud Email Verification',
+ html: (user, url) => emailTemplate({
+ heading: 'DiceCloud Email Verification',
+ text: 'Click below to verify your email address',
+ buttonText: 'Verify Email',
+ url: url.replace( '#/', '' ),
+ }),
+};
diff --git a/app/imports/server/config/emailTemplate.js b/app/imports/server/config/emailTemplate.js
new file mode 100644
index 00000000..80f6d6a4
--- /dev/null
+++ b/app/imports/server/config/emailTemplate.js
@@ -0,0 +1,167 @@
+export default function emailTemplate({heading, text, buttonText, url}){
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DiceCloud
+
+ ${heading}
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+`
+}
diff --git a/app/imports/server/config/limitLoginTokens.js b/app/imports/server/config/limitLoginTokens.js
new file mode 100644
index 00000000..594cb14c
--- /dev/null
+++ b/app/imports/server/config/limitLoginTokens.js
@@ -0,0 +1,21 @@
+const MAX_LOGIN_TOKENS = 20;
+
+Accounts._insertHashedLoginToken = function(userId, hashedToken, query) {
+ query = query ? { ...query } : {};
+ query._id = userId;
+ const user = Accounts.users.findOne(query);
+ let loginTokenLength = user?.services?.resume?.loginTokens?.length;
+ while (loginTokenLength >= MAX_LOGIN_TOKENS){
+ loginTokenLength -=1;
+ Accounts.users.update(query, {
+ $pop: {
+ 'services.resume.loginTokens': -1
+ }
+ });
+ }
+ Accounts.users.update(query, {
+ $addToSet: {
+ 'services.resume.loginTokens': hashedToken
+ }
+ });
+};
diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js
index 69fff4f7..611f1d23 100644
--- a/app/imports/server/publications/singleCharacter.js
+++ b/app/imports/server/publications/singleCharacter.js
@@ -14,7 +14,11 @@ let schema = new SimpleSchema({
});
Meteor.publish('singleCharacter', function(creatureId){
- schema.validate({ creatureId });
+ try {
+ schema.validate({ creatureId });
+ } catch (e){
+ this.error(e);
+ }
this.autorun(function (computation){
let userId = this.userId;
let creatureCursor
diff --git a/app/imports/server/rest/apiPublications/creature.js b/app/imports/server/rest/apiPublications/creature.js
new file mode 100644
index 00000000..f805c92b
--- /dev/null
+++ b/app/imports/server/rest/apiPublications/creature.js
@@ -0,0 +1,46 @@
+import SimpleSchema from 'simpl-schema';
+import Creatures from '/imports/api/creature/creatures/Creatures.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
+import computeCreature from '/imports/api/engine/computeCreature.js';
+import VERSION from '/imports/constants/VERSION.js';
+
+Meteor.publish('api-creature', function(creatureId){
+ try {
+ new SimpleSchema({
+ creatureId: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ },
+ }).validate({ creatureId });
+ } catch (e){
+ this.error(e);
+ return;
+ }
+ const userId = this.userId;
+ const creatureCursor = Creatures.find({
+ _id: creatureId,
+ });
+ const creature = creatureCursor.fetch()[0];
+ try {
+ assertViewPermission(creature, userId)
+ } catch(e){
+ this.error(e);
+ return;
+ }
+ if (creature.computeVersion !== VERSION){
+ try {
+ computeCreature(creatureId)
+ } catch(e){
+ console.error(e)
+ }
+ }
+ return [
+ creatureCursor,
+ CreatureProperties.find({
+ 'ancestors.id': creatureId,
+ }),
+ ];
+}, {
+ url: 'api/creature/:0'
+});
diff --git a/app/imports/server/rest/apiPublications/index.js b/app/imports/server/rest/apiPublications/index.js
new file mode 100644
index 00000000..765d7988
--- /dev/null
+++ b/app/imports/server/rest/apiPublications/index.js
@@ -0,0 +1 @@
+import './creature.js';
diff --git a/app/imports/server/rest/index.js b/app/imports/server/rest/index.js
new file mode 100644
index 00000000..b552e02a
--- /dev/null
+++ b/app/imports/server/rest/index.js
@@ -0,0 +1,2 @@
+import './restLogin.js';
+import './apiPublications/index.js';
diff --git a/app/imports/server/rest/middleware/authenticateUserByToken.js b/app/imports/server/rest/middleware/authenticateUserByToken.js
new file mode 100644
index 00000000..576f5430
--- /dev/null
+++ b/app/imports/server/rest/middleware/authenticateUserByToken.js
@@ -0,0 +1,57 @@
+var Fiber = Npm.require('fibers');
+import { RestMiddleware } from 'meteor/simple:json-routes';
+
+/**
+ * SimpleRest middleware for validating a Meteor.user's login token
+ *
+ * This middleware must be processed after the request.token has been set to a
+ * valid login token for a Meteor.user account (from a separate layer of
+ * middleware). If authentication is successful, the request.userId will be set
+ * to the ID of the authenticated user. An invalid token will result in a error.
+ *
+ * @middleware
+ */
+const authenticateMeteorUserByToken =
+ function (req, res, next) {
+ Fiber(function () {
+ let userId;
+ try {
+ userId = getUserIdFromAuthToken(req.authToken);
+ } catch (e){
+ RestMiddleware.handleErrorAsJson(e, req, res, next);
+ return;
+ }
+ if (userId) {
+ req.userId = userId;
+ }
+
+ next();
+ }).run();
+ };
+
+/**
+ * Retrieves the ID of the Meteor.user that the given auth token belongs to
+ *
+ * @param token An unhashed auth token
+ * @returns {String} The ID of the authenticated Meteor.user, or null if token
+ * is invalid
+ */
+function getUserIdFromAuthToken(token) {
+ if (!token) {
+ return null;
+ }
+
+ var user = Meteor.users.findOne({
+ 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(token),
+ });
+ if (user) {
+ return user._id;
+ } else {
+ const error = new Meteor.Error('Permission denied',
+ 'Invalid authentication token');
+ error.statusCode = 403;
+ throw error;
+ }
+}
+
+export default authenticateMeteorUserByToken;
diff --git a/app/imports/server/rest/restLogin.js b/app/imports/server/rest/restLogin.js
new file mode 100644
index 00000000..1099bc37
--- /dev/null
+++ b/app/imports/server/rest/restLogin.js
@@ -0,0 +1,85 @@
+import { JsonRoutes } from 'meteor/simple:json-routes';
+import authenticateMeteorUserByToken from './middleware/authenticateUserByToken.js';
+/**
+ * Login with username/email and password:
+ * POST /api/login
+ * Body: {"username": "", "password": ""}
+ * Alternative Body: {"email": "", "password": ""}
+ * Successful response:
+ * {
+ * "id": "",
+ * "token": "",
+ * "tokenExpires": ""
+ * }
+ *
+ * Warning: Your token may expire before the given date.
+ * Since each user has a limited pool of login tokens. If you get a permission
+ * error, you may need to login again to refresh your token
+ *
+ * Once you have your token, you can use it as a standard bearer token header
+ * in other API endpoints:
+ * HTTP.post("/methods/return-five-auth", {
+ * headers: { Authorization: "Bearer " }
+ * }, callback);
+**/
+
+JsonRoutes.Middleware.use(JsonRoutes.Middleware.parseBearerToken);
+JsonRoutes.Middleware.use(authenticateMeteorUserByToken);
+
+JsonRoutes.add('options', 'api/login', function (req, res) {
+ JsonRoutes.sendResult(res);
+});
+
+JsonRoutes.add('post', 'api/login', function (req, res) {
+ var options = req.body;
+
+ var user;
+ if (options.email) {
+ check(options, {
+ email: String,
+ password: String,
+ });
+ user = Accounts.findUserByEmail(options.email);
+ } else {
+ check(options, {
+ username: String,
+ password: String,
+ });
+ user = Accounts.findUserByUsername(options.username);
+ }
+
+ if (!user) {
+ throw new Meteor.Error('not-found',
+ 'User with that username or email address not found.');
+ }
+
+ var result = Accounts._checkPassword(user, options.password);
+ check(result, {
+ userId: String,
+ error: Match.Optional(Meteor.Error),
+ });
+
+ if (result.error) {
+ throw result.error;
+ }
+
+ var stampedLoginToken = Accounts._generateStampedLoginToken();
+ check(stampedLoginToken, {
+ token: String,
+ when: Date,
+ });
+
+ Accounts._insertLoginToken(result.userId, stampedLoginToken);
+
+ var tokenExpiration = Accounts._tokenExpiration(stampedLoginToken.when);
+ check(tokenExpiration, Date);
+
+ JsonRoutes.sendResult(res, {
+ data: {
+ id: result.userId,
+ token: stampedLoginToken.token,
+ tokenExpires: tokenExpiration,
+ },
+ });
+
+});
diff --git a/app/imports/ui/pages/Account.vue b/app/imports/ui/pages/Account.vue
index 122ae952..bbd25a06 100644
--- a/app/imports/ui/pages/Account.vue
+++ b/app/imports/ui/pages/Account.vue
@@ -62,10 +62,60 @@
v-for="email in emails"
:key="email.address"
>
+
+
+ mdi-delete
+
+
{{ email.address }}
+
+
+ {{ removeEmailError }}
+
+
+
+
+
+ mdi-close
+
+
+ mdi-send
+
+
+
+ mdi-plus
+
+
Patreon
@@ -167,8 +217,10 @@
import router from '/imports/ui/router.js';
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
import Invites from '/imports/api/users/Invites.js';
- import linkWithPatreon from '/imports/api/users/linkWithPatreon.js'
+ import linkWithPatreon from '/imports/api/users/methods/linkWithPatreon.js'
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
+ import addEmail from '/imports/api/users/methods/addEmail.js';
+ import removeEmail from '/imports/api/users/methods/removeEmail.js';
export default {
meteor: {
@@ -216,6 +268,14 @@
linkPatreonError: '',
updatePatreonError: '',
updatePatreonLoading: false,
+ // Add email
+ showEmailInput: false,
+ addEmailLoading: false,
+ inputEmail: '',
+ addEmailError: undefined,
+ // Remove email
+ removeEmailLoading: undefined,
+ removeEmailError: undefined,
}},
computed: {
entitledCents(){
@@ -233,6 +293,33 @@
elementId: 'username',
});
},
+ clearEmailInput(){
+ this.showEmailInput = false;
+ this.addEmailError = undefined;
+ this.inputEmail = '';
+ },
+ addEmail(){
+ this.addEmailLoading = true;
+ addEmail.call({email: this.inputEmail}, error => {
+ this.addEmailError = error && error.message;
+ this.addEmailLoading = false;
+ if (!error){
+ this.showEmailInput = false;
+ this.inputEmail = '';
+ }
+ });
+ },
+ removeEmail(address){
+ this.removeEmailLoading = address;
+ removeEmail.call({email: address}, error => {
+ this.removeEmailError = error && error.message;
+ this.removeEmailLoading = undefined;
+ if (!error){
+ this.showEmailInput = false;
+ this.inputEmail = '';
+ }
+ });
+ },
signOut(){
Meteor.logout();
router.push('/');
diff --git a/app/imports/ui/pages/EmailVerificationError.vue b/app/imports/ui/pages/EmailVerificationError.vue
new file mode 100644
index 00000000..e01d0090
--- /dev/null
+++ b/app/imports/ui/pages/EmailVerificationError.vue
@@ -0,0 +1,30 @@
+
+
+
+
+ Email Verification Error
+
+
+ {{ error.reason || error.message || error }}
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/pages/EmailVerificationSuccess.vue b/app/imports/ui/pages/EmailVerificationSuccess.vue
new file mode 100644
index 00000000..759e998f
--- /dev/null
+++ b/app/imports/ui/pages/EmailVerificationSuccess.vue
@@ -0,0 +1,24 @@
+
+
+
+
+ Email Verified
+
+
+ Your email address has been verified
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/pages/ResetPassword.vue b/app/imports/ui/pages/ResetPassword.vue
new file mode 100644
index 00000000..f901e3a5
--- /dev/null
+++ b/app/imports/ui/pages/ResetPassword.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+ {{ info }}
+
+
+
+
+ Reset Password
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/pages/SignIn.vue b/app/imports/ui/pages/SignIn.vue
index 5bb545cd..87f83356 100644
--- a/app/imports/ui/pages/SignIn.vue
+++ b/app/imports/ui/pages/SignIn.vue
@@ -33,7 +33,10 @@
required
@keyup.enter="submit"
/>
-
+
Reset Password
@@ -50,7 +53,7 @@
Register
diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js
index 347f3eb7..f8fe9442 100644
--- a/app/imports/ui/router.js
+++ b/app/imports/ui/router.js
@@ -19,6 +19,9 @@ const Feedback = () => import('/imports/ui/pages/Feedback.vue' );
const Account = () => import('/imports/ui/pages/Account.vue' );
const InviteSuccess = () => import('/imports/ui/pages/InviteSuccess.vue' );
const InviteError = () => import('/imports/ui/pages/InviteError.vue' );
+const EmailVerificationSuccess = () => import('/imports/ui/pages/EmailVerificationSuccess.vue' );
+const EmailVerificationError = () => import('/imports/ui/pages/EmailVerificationError.vue' );
+const ResetPassword = () => import('/imports/ui/pages/ResetPassword.vue' );
const NotImplemented = () => import('/imports/ui/pages/NotImplemented.vue');
const PatreonLevelTooLow = () => import('/imports/ui/pages/PatreonLevelTooLow.vue');
const Tabletops = () => import('/imports/ui/pages/Tabletops.vue');
@@ -94,6 +97,17 @@ function claimInvite(to, from, next){
});
}
+function verifyEmail(to, from, next){
+ const token = to.params.token;
+ Accounts.verifyEmail(token, error => {
+ if (error){
+ next({name: 'emailVerificationError', params: {error}});
+ } else {
+ next('/email-verification-success')
+ }
+ });
+}
+
RouterFactory.configure(router => {
router.addRoutes([{
path: '/',
@@ -213,6 +227,9 @@ RouterFactory.configure(router => {
},{
path: '/invite/:inviteToken',
beforeEnter: claimInvite,
+ },{
+ path: '/verify-email/:token',
+ beforeEnter: verifyEmail,
},{
name: 'inviteError',
path: '/invite-error',
@@ -233,6 +250,34 @@ RouterFactory.configure(router => {
meta: {
title: 'Invite Success',
},
+ },{
+ name: 'emailVerificationError',
+ path: '/email-verification-error',
+ components: {
+ default: EmailVerificationError,
+ },
+ props: {
+ default: true,
+ },
+ meta: {
+ title: 'Email Verification Error',
+ },
+ },{
+ path: '/email-verification-success',
+ components: {
+ default: EmailVerificationSuccess,
+ },
+ meta: {
+ title: 'Email Verification Success',
+ },
+ },{
+ path: '/reset-password/:token?',
+ components: {
+ default: ResetPassword,
+ },
+ meta: {
+ title: 'Reset Password',
+ },
},{
path: '/patreon-level-too-low',
components: {
diff --git a/app/imports/ui/user/TierTooLowDialog.vue b/app/imports/ui/user/TierTooLowDialog.vue
index d1831c81..bd6eeb9a 100644
--- a/app/imports/ui/user/TierTooLowDialog.vue
+++ b/app/imports/ui/user/TierTooLowDialog.vue
@@ -48,7 +48,7 @@