| 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.)`);    }  }};
 |