Add customer portal and license dispenser to fleetdm.com (#3546)
* Add images for customer portal, dashboard, and email templates * updated email layout and reset password template, new email template * update ajax-button component to have an optional spinner * updated cloud-error & stripe-card-element component styles * updates to user model, add quote and subscription * Login, signup, forgot password, update profile * link to customer portal from pricing * new-license page, bootstrap updates * create quote action, dashboard page, update routes * Add new page styles to importer, update component styles * updates to js-timestamp * update modal styles and layout * using @submitted on ajax form, controller updates * Update create-quote.js * updates to quote model, action updates, truncate license key on dashboard * update email layout, subscribe action, user model * Update importer.less * style updates, order confirmation * use correct font * style updates * create license key * new-license page changes * signup page changes * add billing format to js-timestamp component, dashboard updates, change password * swap get started link for customers * order -> subscription * Update login.ejs * Lint fixes, page updates, mobile styles * remove edit-profile route, update layout, bootstrap, forms * change customer-layout name to match other layout names, update copyright year in layouts * changes requested from code review and #3570 * submit button width, contact font-size * Update dashboard.less * Update bootstrap-overrides.less * slack logo update, login text
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@ actually logged in. (If they weren't, then this action is just a no-op.)`,
|
||||
// > Under the covers, this persists the now-logged-out session back
|
||||
// > to the underlying session store.
|
||||
if (!this.req.wantsJSON) {
|
||||
throw {redirect: '/login'};
|
||||
throw {redirect: '/'};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-3
@@ -9,7 +9,12 @@ module.exports = {
|
||||
|
||||
inputs: {
|
||||
|
||||
password: {
|
||||
oldPassword: {
|
||||
description: 'The new, unencrypted password.',
|
||||
example: 'abc123v2',
|
||||
required: true
|
||||
},
|
||||
newPassword: {
|
||||
description: 'The new, unencrypted password.',
|
||||
example: 'abc123v2',
|
||||
required: true
|
||||
@@ -17,11 +22,25 @@ module.exports = {
|
||||
|
||||
},
|
||||
|
||||
exits: {
|
||||
success: {
|
||||
description: 'The requesting user agent has been successfully changed their password.',
|
||||
},
|
||||
|
||||
fn: async function ({password}) {
|
||||
badPassword: {
|
||||
description: `The provided password does not match the user's current password.`,
|
||||
responseType: 'unauthorized'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
fn: async function (inputs) {
|
||||
|
||||
await sails.helpers.passwords.checkPassword(inputs.oldPassword, this.req.me.password)
|
||||
.intercept('incorrect', 'badPassword');
|
||||
|
||||
// Hash the new password.
|
||||
var hashed = await sails.helpers.passwords.hashPassword(password);
|
||||
var hashed = await sails.helpers.passwords.hashPassword(inputs.newPassword);
|
||||
|
||||
// Update the record for the logged-in user.
|
||||
await User.updateOne({ id: this.req.me.id })
|
||||
|
||||
+23
-12
@@ -9,7 +9,15 @@ module.exports = {
|
||||
|
||||
inputs: {
|
||||
|
||||
fullName: {
|
||||
firstName: {
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
lastName: {
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
organization: {
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
@@ -30,7 +38,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({fullName, emailAddress}) {
|
||||
fn: async function ({firstName, lastName, organization, emailAddress}) {
|
||||
|
||||
var newEmailAddress = emailAddress;
|
||||
if (newEmailAddress !== undefined) {
|
||||
@@ -75,7 +83,9 @@ module.exports = {
|
||||
// Start building the values to set in the db.
|
||||
// (We always set the fullName if provided.)
|
||||
var valuesToSet = {
|
||||
fullName,
|
||||
firstName,
|
||||
lastName,
|
||||
organization,
|
||||
};
|
||||
|
||||
switch (desiredEmailEffect) {
|
||||
@@ -143,15 +153,16 @@ module.exports = {
|
||||
// If an email address change was requested, and re-confirmation is required,
|
||||
// send the "confirm account" email.
|
||||
if (desiredEmailEffect === 'begin-change' || desiredEmailEffect === 'modify-pending-change') {
|
||||
await sails.helpers.sendTemplateEmail.with({
|
||||
to: newEmailAddress,
|
||||
subject: 'Your account has been updated',
|
||||
template: 'email-verify-new-email',
|
||||
templateData: {
|
||||
fullName: fullName||this.req.me.fullName,
|
||||
token: valuesToSet.emailProofToken
|
||||
}
|
||||
});
|
||||
throw new Error('Not yet supported: the email confirmation feature is unused and has not been adapted for fleetdm.com. This error should never be displayed.');
|
||||
// await sails.helpers.sendTemplateEmail.with({
|
||||
// to: newEmailAddress,
|
||||
// subject: 'Your account has been updated',
|
||||
// template: 'email-verify-new-email',
|
||||
// templateData: {
|
||||
// fullName: fullName||this.req.me.fullName,
|
||||
// token: valuesToSet.emailProofToken
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Create quote',
|
||||
|
||||
|
||||
description: '',
|
||||
|
||||
|
||||
inputs: {
|
||||
|
||||
numberOfHosts: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({ numberOfHosts }) {
|
||||
|
||||
// Determine the price, 1 dollar * host * month (Billed anually)
|
||||
let price = 1.00 * numberOfHosts * 12;
|
||||
|
||||
let quote = await Quote.create({
|
||||
numberOfHosts: numberOfHosts,
|
||||
quotedPrice: price,
|
||||
organization: this.req.me.organization,
|
||||
user: this.req.me.id,
|
||||
}).fetch();
|
||||
|
||||
|
||||
return quote;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Save billing info and subscribe',
|
||||
|
||||
|
||||
description: '',
|
||||
|
||||
|
||||
inputs: {
|
||||
|
||||
quoteId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The quote to use (determines the price and number of hosts.)'
|
||||
},
|
||||
|
||||
paymentSource: {
|
||||
required: true,
|
||||
description: 'New payment source info to use (instead of the saved default payment source).',
|
||||
extendedDescription: 'If provided, this will also be saved as the new default payment source for the customer, replacing the existing default payment source (if there is one.)',
|
||||
type: {
|
||||
stripeToken: 'string',
|
||||
billingCardLast4: 'string',
|
||||
billingCardBrand: 'string',
|
||||
billingCardExpMonth: 'string',
|
||||
billingCardExpYear: 'string',
|
||||
},
|
||||
example: {
|
||||
stripeToken: 'tok_199k3qEXw14QdSnRwmsK99MH',
|
||||
billingCardLast4: '4242',
|
||||
billingCardBrand: 'visa',
|
||||
billingCardExpMonth: '08',
|
||||
billingCardExpYear: '2023',
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
|
||||
couldNotSaveBillingInfo: {
|
||||
description: 'The billing information provided could not be saved.',
|
||||
responseType: 'badRequest'
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
fn: async function (inputs) {
|
||||
const stripe = require('stripe')(sails.config.custom.stripeSecret);
|
||||
|
||||
let quoteRecord = await Quote.findOne({id: inputs.quoteId});
|
||||
if(!quoteRecord) {
|
||||
throw new Error(`Consistency violation: The specified quote (${inputs.quoteId}) no longer seems to exist.`);
|
||||
}
|
||||
|
||||
// If this user has a subscription, we'll throw an error.
|
||||
let doesUserHaveAnExistingSubscription = await Subscription.findOne({user: this.req.me.id});
|
||||
if(doesUserHaveAnExistingSubscription) {
|
||||
throw new Error(`Consistency violation: The requesting user (${this.req.me.emailAddress}) already has an existing subscription!`);
|
||||
}
|
||||
|
||||
// What if the stripe customer id doesn't already exist on the user?
|
||||
if (!this.req.me.stripeCustomerId) {
|
||||
throw new Error(`Consistency violation: The logged-in user's (${this.req.me.emailAddress}) Stripe customer id has somehow gone missing!`);
|
||||
}
|
||||
|
||||
// Write new payment card info ("token") to Stripe's API.
|
||||
await sails.helpers.stripe.saveBillingInfo.with({
|
||||
stripeCustomerId: this.req.me.stripeCustomerId,
|
||||
token: inputs.paymentSource.stripeToken
|
||||
})
|
||||
.intercept({ type: 'StripeCardError' }, 'couldNotSaveBillingInfo');
|
||||
|
||||
// Save payment card info to our database.
|
||||
await User.updateOne({ id: this.req.me.id })
|
||||
.set({
|
||||
hasBillingCard: true,
|
||||
billingCardBrand: inputs.paymentSource.billingCardBrand,
|
||||
billingCardLast4: inputs.paymentSource.billingCardLast4,
|
||||
billingCardExpMonth: inputs.paymentSource.billingCardExpMonth,
|
||||
billingCardExpYear: inputs.paymentSource.billingCardExpYear,
|
||||
});
|
||||
|
||||
// Create the subscription for this order in Stripe
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
customer: this.req.me.stripeCustomerId,
|
||||
items: [
|
||||
{
|
||||
price: sails.config.custom.stripeSubscriptionProduct,
|
||||
quantity: quoteRecord.numberOfHosts,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Generate the license key for this subscription
|
||||
let licenseKey = await sails.helpers.createLicenseKey.with({
|
||||
numberOfHosts: quoteRecord.numberOfHosts,
|
||||
organization: this.req.me.organization,
|
||||
validTo: subscription.current_period_end
|
||||
});
|
||||
|
||||
// Create the subscription record for this order.
|
||||
await Subscription.create({
|
||||
organization: this.req.me.organization,
|
||||
numberOfHosts: quoteRecord.numberOfHosts,
|
||||
subscriptionPrice: quoteRecord.quotedPrice,
|
||||
user: this.req.me.id,
|
||||
stripeSubscriptionId: subscription.id,
|
||||
nextBillingAt: subscription.current_period_end * 1000,
|
||||
fleetLicenseKey: licenseKey,
|
||||
});
|
||||
|
||||
// Send the order confirmation template email
|
||||
await sails.helpers.sendTemplateEmail.with({
|
||||
to: this.req.me.emailAddress,
|
||||
from: sails.config.custom.fromEmail,
|
||||
fromName: sails.config.custom.fromName,
|
||||
subject: 'Your Fleet Premium order',
|
||||
template: 'email-order-confirmation',
|
||||
templateData: {
|
||||
firstName: this.req.me.firstName,
|
||||
lastName: this.req.me.lastName,
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'View dashboard',
|
||||
|
||||
|
||||
description: 'Display "Dashboard" page.',
|
||||
|
||||
|
||||
exits: {
|
||||
|
||||
success: {
|
||||
viewTemplatePath: 'pages/customers/dashboard'
|
||||
},
|
||||
|
||||
redirect: {
|
||||
description: 'The requesting user does not have a subscription, redirecting to the new license page.',
|
||||
responseType: 'redirect',
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
fn: async function () {
|
||||
|
||||
// Get subscription Info
|
||||
let thisSubscription = await Subscription.findOne({user: this.req.me.id});
|
||||
// If the user does not have a subscription, then help them subscribe.
|
||||
if(!thisSubscription) {
|
||||
throw {redirect: '/customers/new-license'};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Respond with view.
|
||||
return {
|
||||
stripePublishableKey: sails.config.custom.enableBillingFeatures? sails.config.custom.stripePublishableKey : undefined,
|
||||
thisSubscription,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'View new license',
|
||||
|
||||
|
||||
description: 'Display "New license" page.',
|
||||
|
||||
|
||||
exits: {
|
||||
|
||||
success: {
|
||||
viewTemplatePath: 'pages/customers/new-license'
|
||||
},
|
||||
|
||||
redirect: {
|
||||
description: 'The requesting user already has a subscription, or does not exist',
|
||||
responseType: 'redirect',
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
fn: async function () {
|
||||
|
||||
// if the user isn't logged in, we'll redirect them to the register page.
|
||||
if (!this.req.me) {
|
||||
throw {redirect: '/customers/register'};
|
||||
}
|
||||
|
||||
// If the user has a license key, we'll redirect them to the customer dashboard.
|
||||
let userHasExistingSubscription = await Subscription.findOne({user: this.req.me.id});
|
||||
if (userHasExistingSubscription) {
|
||||
throw {redirect: '/customers/dashboard'};
|
||||
}
|
||||
|
||||
// Respond with view.
|
||||
return { stripePublishableKey: sails.config.custom.stripePublishableKey};
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -55,7 +55,7 @@ module.exports = {
|
||||
subject: 'Password reset instructions',
|
||||
template: 'email-reset-password',
|
||||
templateData: {
|
||||
fullName: userRecord.fullName,
|
||||
firstName: userRecord.firstName,
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
+27
-7
@@ -35,11 +35,26 @@ the account verification message.)`,
|
||||
description: 'The unencrypted password to use for the new account.'
|
||||
},
|
||||
|
||||
fullName: {
|
||||
organization: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'Frida Kahlo de Rivera',
|
||||
description: 'The user\'s full name.',
|
||||
maxLength: 120,
|
||||
example: 'The Sails company',
|
||||
description: 'The organization the user works for'
|
||||
},
|
||||
|
||||
firstName: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'Frida',
|
||||
description: 'The user\'s first name.',
|
||||
},
|
||||
|
||||
lastName: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'Rivera',
|
||||
description: 'The user\'s last name.',
|
||||
}
|
||||
|
||||
},
|
||||
@@ -53,7 +68,7 @@ the account verification message.)`,
|
||||
|
||||
invalid: {
|
||||
responseType: 'badRequest',
|
||||
description: 'The provided fullName, password and/or email address are invalid.',
|
||||
description: 'The provided firstName, lastName, organization, password and/or email address are invalid.',
|
||||
extendedDescription: 'If this request was sent from a graphical user interface, the request '+
|
||||
'parameters should have been validated/coerced _before_ they were sent.'
|
||||
},
|
||||
@@ -66,14 +81,16 @@ the account verification message.)`,
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({emailAddress, password, fullName}) {
|
||||
fn: async function ({emailAddress, password, firstName, lastName, organization}) {
|
||||
|
||||
var newEmailAddress = emailAddress.toLowerCase();
|
||||
|
||||
// Build up data for the new user record and save it to the database.
|
||||
// (Also use `fetch` to retrieve the new ID so that we can use it below.)
|
||||
var newUserRecord = await User.create(_.extend({
|
||||
fullName,
|
||||
firstName,
|
||||
lastName,
|
||||
organization,
|
||||
emailAddress: newEmailAddress,
|
||||
password: await sails.helpers.passwords.hashPassword(password),
|
||||
tosAcceptedByIp: this.req.ip
|
||||
@@ -105,16 +122,19 @@ the account verification message.)`,
|
||||
// Send "confirm account" email
|
||||
await sails.helpers.sendTemplateEmail.with({
|
||||
to: newEmailAddress,
|
||||
from: sails.config.custom.fromEmailAddress,
|
||||
fromName: sails.config.custom.fromName,
|
||||
subject: 'Please confirm your account',
|
||||
template: 'email-verify-account',
|
||||
templateData: {
|
||||
fullName,
|
||||
firstName,
|
||||
token: newUserRecord.emailProofToken
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sails.log.info('Skipping new account email verification... (since `verifyEmailAddresses` is disabled)');
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ module.exports = {
|
||||
fn: async function () {
|
||||
|
||||
if (this.req.me) {
|
||||
throw {redirect: '/'};
|
||||
throw {redirect: '/customers/new-license'};
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ module.exports = {
|
||||
fn: async function () {
|
||||
|
||||
if (this.req.me) {
|
||||
throw {redirect: '/'};
|
||||
throw {redirect: '/customers/new-license'};
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -25,10 +25,6 @@ module.exports = {
|
||||
|
||||
fn: async function () {
|
||||
|
||||
if (this.req.me) {
|
||||
throw {redirect:'/welcome'};
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
}
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Create license key',
|
||||
|
||||
|
||||
description: '',
|
||||
|
||||
|
||||
inputs: {
|
||||
|
||||
numberOfHosts: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
|
||||
validTo: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'A JS Timestamp representing when this license will expire.'
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
|
||||
success: {
|
||||
outputType: 'string',
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
fn: async function (inputs) {
|
||||
|
||||
let jwt = require('jsonwebtoken');
|
||||
|
||||
let token = jwt.sign(
|
||||
{
|
||||
iss: 'Fleet Device Management Inc.',
|
||||
exp: inputs.validTo,
|
||||
sub: inputs.organization,
|
||||
devices: inputs.numberOfHosts,
|
||||
note: 'Created with Fleet License key dispenser',
|
||||
tier: 'premium',
|
||||
},
|
||||
{
|
||||
key: sails.config.custom.licenseKeyGeneratorPrivateKey,
|
||||
passphrase: sails.config.custom.licenseKeyGeneratorPassphrase
|
||||
},
|
||||
{ algorithm: 'ES256' }
|
||||
);
|
||||
|
||||
|
||||
return token;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Quote.js
|
||||
*
|
||||
* @description :: A model definition represents a database table/collection.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
attributes: {
|
||||
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
organization: { // Note: the current organization exists on the user model, this reflects the organization at the time the quote was created.
|
||||
type: 'string',
|
||||
description: 'The organization the user entered when they generated a quote',
|
||||
maxLength: 120,
|
||||
},
|
||||
|
||||
numberOfHosts: {
|
||||
type: 'number',
|
||||
description: 'The number of hosts the user wants a license for',
|
||||
required: true,
|
||||
},
|
||||
|
||||
quotedPrice: {
|
||||
type: 'number',
|
||||
description: 'The price of the Fleet Premium license that was quoted to the user',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
||||
|
||||
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
user: {
|
||||
model: 'User',
|
||||
required: true,
|
||||
description: 'The user who created this quote.'
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Subscription.js
|
||||
*
|
||||
* @description :: A model definition represents a database table/collection.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
attributes: {
|
||||
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
|
||||
nextBillingAt: {
|
||||
type: 'number',
|
||||
description: 'A JS Timestamp representing the next billing date for this subscription',
|
||||
required: true,
|
||||
},
|
||||
|
||||
numberOfHosts: {
|
||||
type: 'number',
|
||||
description: 'The number of hosts this subscription is valid for',
|
||||
required: true,
|
||||
},
|
||||
|
||||
subscriptionPrice: {
|
||||
type: 'number',
|
||||
description: 'The price of this Fleet Premium subscription',
|
||||
required: true,
|
||||
},
|
||||
|
||||
stripeSubscriptionId: {
|
||||
type: 'string',
|
||||
description: 'The stripe id for this subscription',
|
||||
required: true,
|
||||
},
|
||||
|
||||
fleetLicenseKey: {
|
||||
type: 'string',
|
||||
example:'1234 1234 1234 1234 1234',
|
||||
description: 'The user\'s Fleet Premium license key'
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
||||
|
||||
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
user: {
|
||||
model: 'User',
|
||||
description: 'The user who started this subscription.',
|
||||
required: true
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
Vendored
+34
-18
@@ -27,11 +27,11 @@ module.exports = {
|
||||
defaultsTo: 'confirmed',
|
||||
description: 'The confirmation status of the user\'s email address.',
|
||||
extendedDescription:
|
||||
`Users might be created as "unconfirmed" (e.g. normal signup) or as "confirmed" (e.g. hard-coded
|
||||
admin users). When the email verification feature is enabled, new users created via the
|
||||
signup form have \`emailStatus: 'unconfirmed'\` until they click the link in the confirmation email.
|
||||
Similarly, when an existing user changes their email address, they switch to the "change-requested"
|
||||
email status until they click the link in the confirmation email.`
|
||||
`Users might be created as "unconfirmed" (e.g. normal signup) or as "confirmed" (e.g. hard-coded
|
||||
admin users). When the email verification feature is enabled, new users created via the
|
||||
signup form have \`emailStatus: 'unconfirmed'\` until they click the link in the confirmation email.
|
||||
Similarly, when an existing user changes their email address, they switch to the "change-requested"
|
||||
email status until they click the link in the confirmation email.`
|
||||
},
|
||||
|
||||
emailChangeCandidate: {
|
||||
@@ -48,30 +48,46 @@ email status until they click the link in the confirmation email.`
|
||||
example: '2$28a8eabna301089103-13948134nad'
|
||||
},
|
||||
|
||||
fullName: {
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Full representation of the user\'s name.',
|
||||
description: 'The user\'s first name.',
|
||||
maxLength: 120,
|
||||
example: 'Mary Sue van der McHenst'
|
||||
example: 'Mary'
|
||||
},
|
||||
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The user\'s last name.',
|
||||
maxLength: 120,
|
||||
example: 'van der McHenst'
|
||||
},
|
||||
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The organization the user works for.',
|
||||
maxLength: 120,
|
||||
example: 'The Sails Company',
|
||||
},
|
||||
|
||||
isSuperAdmin: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this user is a "super admin" with extra permissions, etc.',
|
||||
extendedDescription:
|
||||
`Super admins might have extra permissions, see a different default home page when they log in,
|
||||
or even have a completely different feature set from normal users. In this app, the \`isSuperAdmin\`
|
||||
flag is just here as a simple way to represent two different kinds of users. Usually, it's a good idea
|
||||
to keep the data model as simple as possible, only adding attributes when you actually need them for
|
||||
features being built right now.
|
||||
`Super admins might have extra permissions, see a different default home page when they log in,
|
||||
or even have a completely different feature set from normal users. In this app, the \`isSuperAdmin\`
|
||||
flag is just here as a simple way to represent two different kinds of users. Usually, it's a good idea
|
||||
to keep the data model as simple as possible, only adding attributes when you actually need them for
|
||||
features being built right now.
|
||||
|
||||
For example, a "super admin" user for a small to medium-sized e-commerce website might be able to
|
||||
change prices, deactivate seasonal categories, add new offerings, and view live orders as they come in.
|
||||
On the other hand, for an e-commerce website like Walmart.com that has undergone years of development
|
||||
by a large team, those administrative features might be split across a few different roles.
|
||||
For example, a "super admin" user for a small to medium-sized e-commerce website might be able to
|
||||
change prices, deactivate seasonal categories, add new offerings, and view live orders as they come in.
|
||||
On the other hand, for an e-commerce website like Walmart.com that has undergone years of development
|
||||
by a large team, those administrative features might be split across a few different roles.
|
||||
|
||||
So, while this \`isSuperAdmin\` demarcation might not be the right approach forever, it's a good place to start.`
|
||||
So, while this \`isSuperAdmin\` demarcation might not be the right approach forever, it's a good place to start.`
|
||||
},
|
||||
|
||||
passwordResetToken: {
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ module.exports = function unauthorized() {
|
||||
delete req.session.userId;
|
||||
}
|
||||
|
||||
return res.redirect('/login');
|
||||
return res.redirect('/customers/login');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user