123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- 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 (inputs, exits) {
- // If no token was provided, this is automatically invalid.
- if (!inputs.token) {
- throw 'invalidOrExpiredToken';
- }
- // Get the user with the matching email token.
- var user = await User.findOne({ emailProofToken: inputs.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.update({ id: user.id }).set({
- emailStatus: 'confirmed',
- emailProofToken: '',
- emailProofTokenExpiresAt: 0
- });
- this.req.session.userId = user.id;
- if (this.req.wantsJSON) {
- return exits.success();
- } else {
- throw { redirect: '/email/confirmed' };
- }
- } else if (user.emailStatus === 'changeRequested') {
- // ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗ ┌─┐┌┬┐┌─┐┬┬
- // │ │ ││││├┤ │├┬┘││││││││ ┬ ║ ╠═╣╠═╣║║║║ ╦║╣ ║║ ├┤ │││├─┤││
- // └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚═╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ └─┘┴ ┴┴ ┴┴┴─┘
- 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
- });
- if (didNotAlreadyHaveCustomerId){
- await User.update({ 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.update({ id: user.id }).set({
- emailStatus: 'confirmed',
- emailProofToken: '',
- emailProofTokenExpiresAt: 0,
- emailAddress: user.emailChangeCandidate,
- emailChangeCandidate: '',
- });
- this.req.session.userId = user.id;
- if (this.req.wantsJSON) {
- return exits.success();
- } 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.)`);
- }
- }
- };
|