Initial project structure with sails.js

This commit is contained in:
2023-11-21 21:57:32 -05:00
commit 523978e520
197 changed files with 76740 additions and 0 deletions

View File

@ -0,0 +1,55 @@
module.exports = {
friendlyName: 'Logout',
description: 'Log out of this app.',
extendedDescription:
`This action deletes the \`req.session.userId\` key from the session of the requesting user agent.
Actual garbage collection of session data depends on this app's session store, and
potentially also on the [TTL configuration](https://sailsjs.com/docs/reference/configuration/sails-config-session)
you provided for it.
Note that this action does not check to see whether or not the requesting user was
actually logged in. (If they weren't, then this action is just a no-op.)`,
exits: {
success: {
description: 'The requesting user agent has been successfully logged out.'
},
redirect: {
description: 'The requesting user agent looks to be a web browser.',
extendedDescription: 'After logging out from a web browser, the user is redirected away.',
responseType: 'redirect'
}
},
fn: async function () {
// Clear the `userId` property from this session.
delete this.req.session.userId;
// Broadcast a message that we can display in other open tabs.
if (sails.hooks.sockets) {
await sails.helpers.broadcastSessionChange(this.req);
}
// Then finish up, sending an appropriate response.
// > Under the covers, this persists the now-logged-out session back
// > to the underlying session store.
if (!this.req.wantsJSON) {
throw {redirect: '/login'};
}
}
};

View File

@ -0,0 +1,79 @@
module.exports = {
friendlyName: 'Update billing card',
description: 'Update the credit card for the logged-in user.',
inputs: {
stripeToken: {
type: 'string',
example: 'tok_199k3qEXw14QdSnRwmsK99MH',
description: 'The single-use Stripe Checkout token identifier representing the user\'s payment source (i.e. credit card.)',
extendedDescription: 'Omit this (or use "") to remove this user\'s payment source.',
whereToGet: {
description: 'This Stripe.js token is provided to the front-end (client-side) code after completing a Stripe Checkout or Stripe Elements flow.'
}
},
billingCardLast4: {
type: 'string',
example: '4242',
description: 'Omit if removing card info.',
whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
},
billingCardBrand: {
type: 'string',
example: 'visa',
description: 'Omit if removing card info.',
whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
},
billingCardExpMonth: {
type: 'string',
example: '08',
description: 'Omit if removing card info.',
whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
},
billingCardExpYear: {
type: 'string',
example: '2023',
description: 'Omit if removing card info.',
whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
},
},
fn: async function ({stripeToken, billingCardLast4, billingCardBrand, billingCardExpMonth, billingCardExpYear}) {
// Add, update, or remove the default payment source for the logged-in user's
// customer entry in Stripe.
var stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
stripeCustomerId: this.req.me.stripeCustomerId,
token: stripeToken || '',
}).timeout(5000).retry();
// Update (or clear) the card info we have stored for this user in our database.
// > Remember, never store complete card numbers-- only the last 4 digits + expiration!
// > Storing (or even receiving) complete, unencrypted card numbers would require PCI
// > compliance in the U.S.
await User.updateOne({ id: this.req.me.id })
.set({
stripeCustomerId,
hasBillingCard: stripeToken ? true : false,
billingCardBrand: stripeToken ? billingCardBrand : '',
billingCardLast4: stripeToken ? billingCardLast4 : '',
billingCardExpMonth: stripeToken ? billingCardExpMonth : '',
billingCardExpYear: stripeToken ? billingCardExpYear : ''
});
}
};

View File

@ -0,0 +1,35 @@
module.exports = {
friendlyName: 'Update password',
description: 'Update the password for the logged-in user.',
inputs: {
password: {
description: 'The new, unencrypted password.',
example: 'abc123v2',
required: true
}
},
fn: async function ({password}) {
// Hash the new password.
var hashed = await sails.helpers.passwords.hashPassword(password);
// Update the record for the logged-in user.
await User.updateOne({ id: this.req.me.id })
.set({
password: hashed
});
}
};

View File

@ -0,0 +1,160 @@
module.exports = {
friendlyName: 'Update profile',
description: 'Update the profile for the logged-in user.',
inputs: {
fullName: {
type: 'string'
},
emailAddress: {
type: 'string'
},
},
exits: {
emailAlreadyInUse: {
statusCode: 409,
description: 'The provided email address is already in use.',
},
},
fn: async function ({fullName, emailAddress}) {
var newEmailAddress = emailAddress;
if (newEmailAddress !== undefined) {
newEmailAddress = newEmailAddress.toLowerCase();
}
// Determine if this request wants to change the current user's email address,
// revert her pending email address change, modify her pending email address
// change, or if the email address won't be affected at all.
var desiredEmailEffect;// ('change-immediately', 'begin-change', 'cancel-pending-change', 'modify-pending-change', or '')
if (
newEmailAddress === undefined ||
(this.req.me.emailStatus !== 'change-requested' && newEmailAddress === this.req.me.emailAddress) ||
(this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailChangeCandidate)
) {
desiredEmailEffect = '';
} else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailAddress) {
desiredEmailEffect = 'cancel-pending-change';
} else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress !== this.req.me.emailAddress) {
desiredEmailEffect = 'modify-pending-change';
} else if (!sails.config.custom.verifyEmailAddresses || this.req.me.emailStatus === 'unconfirmed') {
desiredEmailEffect = 'change-immediately';
} else {
desiredEmailEffect = 'begin-change';
}
// If the email address is changing, make sure it is not already being used.
if (_.contains(['begin-change', 'change-immediately', 'modify-pending-change'], desiredEmailEffect)) {
let conflictingUser = await User.findOne({
or: [
{ emailAddress: newEmailAddress },
{ emailChangeCandidate: newEmailAddress }
]
});
if (conflictingUser) {
throw 'emailAlreadyInUse';
}
}
// Start building the values to set in the db.
// (We always set the fullName if provided.)
var valuesToSet = {
fullName,
};
switch (desiredEmailEffect) {
// Change now
case 'change-immediately':
_.extend(valuesToSet, {
emailAddress: newEmailAddress,
emailChangeCandidate: '',
emailProofToken: '',
emailProofTokenExpiresAt: 0,
emailStatus: this.req.me.emailStatus === 'unconfirmed' ? 'unconfirmed' : 'confirmed'
});
break;
// Begin new email change, or modify a pending email change
case 'begin-change':
case 'modify-pending-change':
_.extend(valuesToSet, {
emailChangeCandidate: newEmailAddress,
emailProofToken: await sails.helpers.strings.random('url-friendly'),
emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL,
emailStatus: 'change-requested'
});
break;
// Cancel pending email change
case 'cancel-pending-change':
_.extend(valuesToSet, {
emailChangeCandidate: '',
emailProofToken: '',
emailProofTokenExpiresAt: 0,
emailStatus: 'confirmed'
});
break;
// Otherwise, do nothing re: email
}
// Save to the db
await User.updateOne({id: this.req.me.id })
.set(valuesToSet);
// If this is an immediate change, and billing features are enabled,
// then also update the billing email for this user's linked customer entry
// in the Stripe API to make sure they receive email receipts.
// > Note: If there was not already a Stripe customer entry for this user,
// > then one will be set up implicitly, so we'll need to persist it to our
// > database. (This could happen if Stripe credentials were not configured
// > at the time this user was originally created.)
if(desiredEmailEffect === 'change-immediately' && sails.config.custom.enableBillingFeatures) {
let didNotAlreadyHaveCustomerId = (! this.req.me.stripeCustomerId);
let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
stripeCustomerId: this.req.me.stripeCustomerId,
emailAddress: newEmailAddress
}).timeout(5000).retry();
if (didNotAlreadyHaveCustomerId){
await User.updateOne({ id: this.req.me.id })
.set({
stripeCustomerId
});
}
}
// 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
}
});
}
}
};

View File

@ -0,0 +1,30 @@
module.exports = {
friendlyName: 'View account overview',
description: 'Display "Account Overview" page.',
exits: {
success: {
viewTemplatePath: 'pages/account/account-overview',
}
},
fn: async function () {
// If billing features are enabled, include our configured Stripe.js
// public key in the view locals. Otherwise, leave it as undefined.
return {
stripePublishableKey: sails.config.custom.enableBillingFeatures? sails.config.custom.stripePublishableKey : undefined,
};
}
};

View File

@ -0,0 +1,26 @@
module.exports = {
friendlyName: 'View edit password',
description: 'Display "Edit password" page.',
exits: {
success: {
viewTemplatePath: 'pages/account/edit-password'
}
},
fn: async function () {
return {};
}
};

View File

@ -0,0 +1,26 @@
module.exports = {
friendlyName: 'View edit profile',
description: 'Display "Edit profile" page.',
exits: {
success: {
viewTemplatePath: 'pages/account/edit-profile',
}
},
fn: async function () {
return {};
}
};

View File

@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'View welcome page',
description: 'Display the dashboard "Welcome" page.',
exits: {
success: {
viewTemplatePath: 'pages/dashboard/welcome',
description: 'Display the welcome page for authenticated users.'
},
},
fn: async function () {
return {};
}
};

View File

@ -0,0 +1,79 @@
module.exports = {
friendlyName: 'Deliver contact form message',
description: 'Deliver a contact form message to the appropriate internal channel(s).',
inputs: {
emailAddress: {
required: true,
type: 'string',
description: 'A return email address where we can respond.',
example: 'hermione@hogwarts.edu'
},
topic: {
required: true,
type: 'string',
description: 'The topic from the contact form.',
example: 'I want to buy stuff.'
},
fullName: {
required: true,
type: 'string',
description: 'The full name of the human sending this message.',
example: 'Hermione Granger'
},
message: {
required: true,
type: 'string',
description: 'The custom message, in plain text.'
}
},
exits: {
success: {
description: 'The message was sent successfully.'
}
},
fn: async function({emailAddress, topic, fullName, message}) {
if (!sails.config.custom.internalEmailAddress) {
throw new Error(
`Cannot deliver incoming message from contact form because there is no internal
email address (\`sails.config.custom.internalEmailAddress\`) configured for this
app. To enable contact form emails, you'll need to add this missing setting to
your custom config -- usually in \`config/custom.js\`, \`config/staging.js\`,
\`config/production.js\`, or via system environment variables.`
);
}
await sails.helpers.sendTemplateEmail.with({
to: sails.config.custom.internalEmailAddress,
subject: 'New contact form message',
template: 'internal/email-contact-form',
layout: false,
templateData: {
contactName: fullName,
contactEmail: emailAddress,
topic,
message,
}
});
}
};

View File

@ -0,0 +1,160 @@
module.exports = {
friendlyName: 'Confirm email',
description:
`Confirm a new user's email address, or an existing user's request for an email address change,
then redirect to either a special landing page (for newly-signed up users), or the account page
(for existing users who just changed their email address).`,
inputs: {
token: {
description: 'The confirmation token from the email.',
example: '4-32fad81jdaf$329'
}
},
exits: {
success: {
description: 'Email address confirmed and requesting user logged in.'
},
redirect: {
description: 'Email address confirmed and requesting user logged in. Since this looks like a browser, redirecting...',
responseType: 'redirect'
},
invalidOrExpiredToken: {
responseType: 'expired',
description: 'The provided token is expired, invalid, or already used up.',
},
emailAddressNoLongerAvailable: {
statusCode: 409,
viewTemplatePath: '500',
description: 'The email address is no longer available.',
extendedDescription: 'This is an edge case that is not always anticipated by websites and APIs. Since it is pretty rare, the 500 server error page is used as a simple catch-all. If this becomes important in the future, this could easily be expanded into a custom error page or resolution flow. But for context: this behavior of showing the 500 server error page mimics how popular apps like Slack behave under the same circumstances.',
}
},
fn: async function ({token}) {
// If no token was provided, this is automatically invalid.
if (!token) {
throw 'invalidOrExpiredToken';
}
// Get the user with the matching email token.
var user = await User.findOne({ emailProofToken: token });
// If no such user exists, or their token is expired, bail.
if (!user || user.emailProofTokenExpiresAt <= Date.now()) {
throw 'invalidOrExpiredToken';
}
if (user.emailStatus === 'unconfirmed') {
// ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦╦═╗╔═╗╔╦╗ ╔╦╗╦╔╦╗╔═╗ ╦ ╦╔═╗╔═╗╦═╗ ┌─┐┌┬┐┌─┐┬┬
// │ │ ││││├┤ │├┬┘││││││││ ┬ ╠╣ ║╠╦╝╚═╗ ║───║ ║║║║║╣ ║ ║╚═╗║╣ ╠╦╝ ├┤ │││├─┤││
// └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚ ╩╩╚═╚═╝ ╩ ╩ ╩╩ ╩╚═╝ ╚═╝╚═╝╚═╝╩╚═ └─┘┴ ┴┴ ┴┴┴─┘
// If this is a new user confirming their email for the first time,
// then just update the state of their user record in the database,
// store their user id in the session (just in case they aren't logged
// in already), and then redirect them to the "email confirmed" page.
await User.updateOne({ id: user.id }).set({
emailStatus: 'confirmed',
emailProofToken: '',
emailProofTokenExpiresAt: 0
});
this.req.session.userId = user.id;
// In case there was an existing session, broadcast a message that we can
// display in other open tabs.
if (sails.hooks.sockets) {
await sails.helpers.broadcastSessionChange(this.req);
}
if (this.req.wantsJSON) {
return;
} else {
throw { redirect: '/email/confirmed' };
}
} else if (user.emailStatus === 'change-requested') {
// ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗ ┌─┐┌┬┐┌─┐┬┬
// │ │ ││││├┤ │├┬┘││││││││ ┬ ║ ╠═╣╠═╣║║║║ ╦║╣ ║║ ├┤ │││├─┤││
// └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚═╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ └─┘┴ ┴┴ ┴┴┴─┘
if (!user.emailChangeCandidate){
throw new Error(`Consistency violation: Could not update Stripe customer because this user record's emailChangeCandidate ("${user.emailChangeCandidate}") is missing. (This should never happen.)`);
}
// Last line of defense: since email change candidates are not protected
// by a uniqueness constraint in the database, it's important that we make
// sure no one else managed to grab this email in the mean time since we
// last checked its availability. (This is a relatively rare edge case--
// see exit description.)
if (await User.count({ emailAddress: user.emailChangeCandidate }) > 0) {
throw 'emailAddressNoLongerAvailable';
}
// If billing features are enabled, also update the billing email for this
// user's linked customer entry in the Stripe API to make sure they receive
// email receipts.
// > Note: If there was not already a Stripe customer entry for this user,
// > then one will be set up implicitly, so we'll need to persist it to our
// > database. (This could happen if Stripe credentials were not configured
// > at the time this user was originally created.)
if(sails.config.custom.enableBillingFeatures) {
let didNotAlreadyHaveCustomerId = (! user.stripeCustomerId);
let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
stripeCustomerId: user.stripeCustomerId,
emailAddress: user.emailChangeCandidate
}).timeout(5000).retry();
if (didNotAlreadyHaveCustomerId){
await User.updateOne({ id: user.id }).set({
stripeCustomerId
});
}
}
// Finally update the user in the database, store their id in the session
// (just in case they aren't logged in already), then redirect them to
// their "my account" page so they can see their updated email address.
await User.updateOne({ id: user.id })
.set({
emailStatus: 'confirmed',
emailProofToken: '',
emailProofTokenExpiresAt: 0,
emailAddress: user.emailChangeCandidate,
emailChangeCandidate: '',
});
this.req.session.userId = user.id;
// In case there was an existing session, broadcast a message that we can
// display in other open tabs.
if (sails.hooks.sockets) {
await sails.helpers.broadcastSessionChange(this.req);
}
if (this.req.wantsJSON) {
return;
} else {
throw { redirect: '/account' };
}
} else {
throw new Error(`Consistency violation: User ${user.id} has an email proof token, but somehow also has an emailStatus of "${user.emailStatus}"! (This should never happen.)`);
}
}
};

View File

@ -0,0 +1,119 @@
module.exports = {
friendlyName: 'Login',
description: 'Log in using the provided email and password combination.',
extendedDescription:
`This action attempts to look up the user record in the database with the
specified email address. Then, if such a user exists, it uses
bcrypt to compare the hashed password from the database with the provided
password attempt.`,
inputs: {
emailAddress: {
description: 'The email to try in this attempt, e.g. "irl@example.com".',
type: 'string',
required: true
},
password: {
description: 'The unencrypted password to try in this attempt, e.g. "passwordlol".',
type: 'string',
required: true
},
rememberMe: {
description: 'Whether to extend the lifetime of the user\'s session.',
extendedDescription:
`Note that this is NOT SUPPORTED when using virtual requests (e.g. sending
requests over WebSockets instead of HTTP).`,
type: 'boolean'
}
},
exits: {
success: {
description: 'The requesting user agent has been successfully logged in.',
extendedDescription:
`Under the covers, this stores the id of the logged-in user in the session
as the \`userId\` key. The next time this user agent sends a request, assuming
it includes a cookie (like a web browser), Sails will automatically make this
user id available as req.session.userId in the corresponding action. (Also note
that, thanks to the included "custom" hook, when a relevant request is received
from a logged-in user, that user's entire record from the database will be fetched
and exposed as \`req.me\`.)`
},
badCombo: {
description: `The provided email and password combination does not
match any user in the database.`,
responseType: 'unauthorized'
// ^This uses the custom `unauthorized` response located in `api/responses/unauthorized.js`.
// To customize the generic "unauthorized" response across this entire app, change that file
// (see api/responses/unauthorized).
//
// To customize the response for _only this_ action, replace `responseType` with
// something else. For example, you might set `statusCode: 498` and change the
// implementation below accordingly (see http://sailsjs.com/docs/concepts/controllers).
}
},
fn: async function ({emailAddress, password, rememberMe}) {
// Look up by the email address.
// (note that we lowercase it to ensure the lookup is always case-insensitive,
// regardless of which database we're using)
var userRecord = await User.findOne({
emailAddress: emailAddress.toLowerCase(),
});
// If there was no matching user, respond thru the "badCombo" exit.
if(!userRecord) {
throw 'badCombo';
}
// If the password doesn't match, then also exit thru "badCombo".
await sails.helpers.passwords.checkPassword(password, userRecord.password)
.intercept('incorrect', 'badCombo');
// If "Remember Me" was enabled, then keep the session alive for
// a longer amount of time. (This causes an updated "Set Cookie"
// response header to be sent as the result of this request -- thus
// we must be dealing with a traditional HTTP request in order for
// this to work.)
if (rememberMe) {
if (this.req.isSocket) {
sails.log.warn(
'Received `rememberMe: true` from a virtual request, but it was ignored\n'+
'because a browser\'s session cookie cannot be reset over sockets.\n'+
'Please use a traditional HTTP request instead.'
);
} else {
this.req.session.cookie.maxAge = sails.config.custom.rememberMeCookieMaxAge;
}
}//fi
// Modify the active session instance.
// (This will be persisted when the response is sent.)
this.req.session.userId = userRecord.id;
// In case there was an existing session (e.g. if we allow users to go to the login page
// when they're already logged in), broadcast a message that we can display in other open tabs.
if (sails.hooks.sockets) {
await sails.helpers.broadcastSessionChange(this.req);
}
}
};

View File

@ -0,0 +1,66 @@
module.exports = {
friendlyName: 'Send password recovery email',
description: 'Send a password recovery notification to the user with the specified email address.',
inputs: {
emailAddress: {
description: 'The email address of the alleged user who wants to recover their password.',
example: 'rydahl@example.com',
type: 'string',
required: true
}
},
exits: {
success: {
description: 'The email address might have matched a user in the database. (If so, a recovery email was sent.)'
},
},
fn: async function ({emailAddress}) {
// Find the record for this user.
// (Even if no such user exists, pretend it worked to discourage sniffing.)
var userRecord = await User.findOne({ emailAddress });
if (!userRecord) {
return;
}//•
// Come up with a pseudorandom, probabilistically-unique token for use
// in our password recovery email.
var token = await sails.helpers.strings.random('url-friendly');
// Store the token on the user record
// (This allows us to look up the user when the link from the email is clicked.)
await User.updateOne({ id: userRecord.id })
.set({
passwordResetToken: token,
passwordResetTokenExpiresAt: Date.now() + sails.config.custom.passwordResetTokenTTL,
});
// Send recovery email
await sails.helpers.sendTemplateEmail.with({
to: emailAddress,
subject: 'Password reset instructions',
template: 'email-reset-password',
templateData: {
fullName: userRecord.fullName,
token: token
}
});
}
};

View File

@ -0,0 +1,127 @@
module.exports = {
friendlyName: 'Signup',
description: 'Sign up for a new user account.',
extendedDescription:
`This creates a new user record in the database, signs in the requesting user agent
by modifying its [session](https://sailsjs.com/documentation/concepts/sessions), and
(if emailing with Mailgun is enabled) sends an account verification email.
If a verification email is sent, the new user's account is put in an "unconfirmed" state
until they confirm they are using a legitimate email address (by clicking the link in
the account verification message.)`,
inputs: {
emailAddress: {
required: true,
type: 'string',
isEmail: true,
description: 'The email address for the new account, e.g. m@example.com.',
extendedDescription: 'Must be a valid email address.',
},
password: {
required: true,
type: 'string',
maxLength: 200,
example: 'passwordlol',
description: 'The unencrypted password to use for the new account.'
},
fullName: {
required: true,
type: 'string',
example: 'Frida Kahlo de Rivera',
description: 'The user\'s full name.',
}
},
exits: {
success: {
description: 'New user account was created successfully.'
},
invalid: {
responseType: 'badRequest',
description: 'The provided fullName, 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.'
},
emailAlreadyInUse: {
statusCode: 409,
description: 'The provided email address is already in use.',
},
},
fn: async function ({emailAddress, password, fullName}) {
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,
emailAddress: newEmailAddress,
password: await sails.helpers.passwords.hashPassword(password),
tosAcceptedByIp: this.req.ip
}, sails.config.custom.verifyEmailAddresses? {
emailProofToken: await sails.helpers.strings.random('url-friendly'),
emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL,
emailStatus: 'unconfirmed'
}:{}))
.intercept('E_UNIQUE', 'emailAlreadyInUse')
.intercept({name: 'UsageError'}, 'invalid')
.fetch();
// If billing feaures are enabled, save a new customer entry in the Stripe API.
// Then persist the Stripe customer id in the database.
if (sails.config.custom.enableBillingFeatures) {
let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
emailAddress: newEmailAddress
}).timeout(5000).retry();
await User.updateOne({id: newUserRecord.id})
.set({
stripeCustomerId
});
}
// Store the user's new id in their session.
this.req.session.userId = newUserRecord.id;
// In case there was an existing session (e.g. if we allow users to go to the signup page
// when they're already logged in), broadcast a message that we can display in other open tabs.
if (sails.hooks.sockets) {
await sails.helpers.broadcastSessionChange(this.req);
}
if (sails.config.custom.verifyEmailAddresses) {
// Send "confirm account" email
await sails.helpers.sendTemplateEmail.with({
to: newEmailAddress,
subject: 'Please confirm your account',
template: 'email-verify-account',
templateData: {
fullName,
token: newUserRecord.emailProofToken
}
});
} else {
sails.log.info('Skipping new account email verification... (since `verifyEmailAddresses` is disabled)');
}
}
};

View File

@ -0,0 +1,80 @@
module.exports = {
friendlyName: 'Update password and login',
description: 'Finish the password recovery flow by setting the new password and '+
'logging in the requesting user, based on the authenticity of their token.',
inputs: {
password: {
description: 'The new, unencrypted password.',
example: 'abc123v2',
required: true
},
token: {
description: 'The password token that was generated by the `sendPasswordRecoveryEmail` endpoint.',
example: 'gwa8gs8hgw9h2g9hg29hgwh9asdgh9q34$$$$$asdgasdggds',
required: true
}
},
exits: {
success: {
description: 'Password successfully updated, and requesting user agent is now logged in.'
},
invalidToken: {
description: 'The provided password token is invalid, expired, or has already been used.',
responseType: 'expired'
}
},
fn: async function ({password, token}) {
if(!token) {
throw 'invalidToken';
}
// Look up the user with this reset token.
var userRecord = await User.findOne({ passwordResetToken: token });
// If no such user exists, or their token is expired, bail.
if (!userRecord || userRecord.passwordResetTokenExpiresAt <= Date.now()) {
throw 'invalidToken';
}
// Hash the new password.
var hashed = await sails.helpers.passwords.hashPassword(password);
// Store the user's new password and clear their reset token so it can't be used again.
await User.updateOne({ id: userRecord.id })
.set({
password: hashed,
passwordResetToken: '',
passwordResetTokenExpiresAt: 0
});
// Log the user in.
// (This will be persisted when the response is sent.)
this.req.session.userId = userRecord.id;
// In case there was an existing session, broadcast a message that we can
// display in other open tabs.
if (sails.hooks.sockets) {
await sails.helpers.broadcastSessionChange(this.req);
}
}
};

View File

@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'View confirmed email',
description: 'Display "Confirmed email" page.',
exits: {
success: {
viewTemplatePath: 'pages/entrance/confirmed-email'
}
},
fn: async function () {
// Respond with view.
return {};
}
};

View File

@ -0,0 +1,36 @@
module.exports = {
friendlyName: 'View forgot password',
description: 'Display "Forgot password" page.',
exits: {
success: {
viewTemplatePath: 'pages/entrance/forgot-password',
},
redirect: {
description: 'The requesting user is already logged in.',
extendedDescription: 'Logged-in users should change their password in "Account settings."',
responseType: 'redirect',
}
},
fn: async function () {
if (this.req.me) {
throw {redirect: '/'};
}
return {};
}
};

View File

@ -0,0 +1,35 @@
module.exports = {
friendlyName: 'View login',
description: 'Display "Login" page.',
exits: {
success: {
viewTemplatePath: 'pages/entrance/login',
},
redirect: {
description: 'The requesting user is already logged in.',
responseType: 'redirect'
}
},
fn: async function () {
if (this.req.me) {
throw {redirect: '/'};
}
return {};
}
};

View File

@ -0,0 +1,57 @@
module.exports = {
friendlyName: 'View new password',
description: 'Display "New password" page.',
inputs: {
token: {
description: 'The password reset token from the email.',
example: '4-32fad81jdaf$329'
}
},
exits: {
success: {
viewTemplatePath: 'pages/entrance/new-password'
},
invalidOrExpiredToken: {
responseType: 'expired',
description: 'The provided token is expired, invalid, or has already been used.',
}
},
fn: async function ({token}) {
// If password reset token is missing, display an error page explaining that the link is bad.
if (!token) {
sails.log.warn('Attempting to view new password (recovery) page, but no reset password token included in request! Displaying error page...');
throw 'invalidOrExpiredToken';
}//•
// Look up the user with this reset token.
var userRecord = await User.findOne({ passwordResetToken: token });
// If no such user exists, or their token is expired, display an error page explaining that the link is bad.
if (!userRecord || userRecord.passwordResetTokenExpiresAt <= Date.now()) {
throw 'invalidOrExpiredToken';
}
// Grab token and include it in view locals
return {
token,
};
}
};

View File

@ -0,0 +1,35 @@
module.exports = {
friendlyName: 'View signup',
description: 'Display "Signup" page.',
exits: {
success: {
viewTemplatePath: 'pages/entrance/signup',
},
redirect: {
description: 'The requesting user is already logged in.',
responseType: 'redirect'
}
},
fn: async function () {
if (this.req.me) {
throw {redirect: '/'};
}
return {};
}
};

View File

@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'View privacy',
description: 'Display "Privacy policy" page.',
exits: {
success: {
viewTemplatePath: 'pages/legal/privacy'
}
},
fn: async function () {
// All done.
return;
}
};

View File

@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'View terms',
description: 'Display "Legal terms" page.',
exits: {
success: {
viewTemplatePath: 'pages/legal/terms'
}
},
fn: async function () {
// All done.
return;
}
};

View File

@ -0,0 +1,32 @@
module.exports = {
friendlyName: 'Observe my session',
description: 'Subscribe to the logged-in user\'s session so that you receive socket broadcasts when logged out in another tab.',
exits: {
success: {
description: 'The requesting socket is now subscribed to socket broadcasts about the logged-in user\'s session.',
},
},
fn: async function ({}) {
if (!this.req.isSocket) {
throw new Error('This action is designed for use with the virtual request interpreter (over sockets, not traditional HTTP).');
}
let roomName = `session${_.deburr(this.req.sessionID)}`;
sails.sockets.join(this.req, roomName);
}
};

View File

@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'View contact',
description: 'Display "Contact" page.',
exits: {
success: {
viewTemplatePath: 'pages/contact'
}
},
fn: async function () {
// Respond with view.
return {};
}
};

View File

@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'View faq',
description: 'Display "FAQ" page.',
exits: {
success: {
viewTemplatePath: 'pages/faq'
}
},
fn: async function () {
// Respond with view.
return {};
}
};

View File

@ -0,0 +1,37 @@
module.exports = {
friendlyName: 'View homepage or redirect',
description: 'Display or redirect to the appropriate homepage, depending on login status.',
exits: {
success: {
statusCode: 200,
description: 'Requesting user is a guest, so show the public landing page.',
viewTemplatePath: 'pages/homepage'
},
redirect: {
responseType: 'redirect',
description: 'Requesting user is logged in, so redirect to the internal welcome page.'
},
},
fn: async function () {
if (this.req.me) {
throw {redirect:'/welcome'};
}
return {};
}
};