confirm-email.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. module.exports = {
  2. friendlyName: 'Confirm email',
  3. description:
  4. `Confirm a new user's email address, or an existing user's request for an email address change,
  5. then redirect to either a special landing page (for newly-signed up users), or the account page
  6. (for existing users who just changed their email address).`,
  7. inputs: {
  8. token: {
  9. description: 'The confirmation token from the email.',
  10. example: '4-32fad81jdaf$329'
  11. }
  12. },
  13. exits: {
  14. success: {
  15. description: 'Email address confirmed and requesting user logged in.'
  16. },
  17. redirect: {
  18. description: 'Email address confirmed and requesting user logged in. Since this looks like a browser, redirecting...',
  19. responseType: 'redirect'
  20. },
  21. invalidOrExpiredToken: {
  22. responseType: 'expired',
  23. description: 'The provided token is expired, invalid, or already used up.',
  24. },
  25. emailAddressNoLongerAvailable: {
  26. statusCode: 409,
  27. viewTemplatePath: '500',
  28. description: 'The email address is no longer available.',
  29. 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.',
  30. }
  31. },
  32. fn: async function (inputs, exits) {
  33. // If no token was provided, this is automatically invalid.
  34. if (!inputs.token) {
  35. throw 'invalidOrExpiredToken';
  36. }
  37. // Get the user with the matching email token.
  38. var user = await User.findOne({ emailProofToken: inputs.token });
  39. // If no such user exists, or their token is expired, bail.
  40. if (!user || user.emailProofTokenExpiresAt <= Date.now()) {
  41. throw 'invalidOrExpiredToken';
  42. }
  43. if (user.emailStatus === 'unconfirmed') {
  44. // ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦╦═╗╔═╗╔╦╗ ╔╦╗╦╔╦╗╔═╗ ╦ ╦╔═╗╔═╗╦═╗ ┌─┐┌┬┐┌─┐┬┬
  45. // │ │ ││││├┤ │├┬┘││││││││ ┬ ╠╣ ║╠╦╝╚═╗ ║───║ ║║║║║╣ ║ ║╚═╗║╣ ╠╦╝ ├┤ │││├─┤││
  46. // └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚ ╩╩╚═╚═╝ ╩ ╩ ╩╩ ╩╚═╝ ╚═╝╚═╝╚═╝╩╚═ └─┘┴ ┴┴ ┴┴┴─┘
  47. // If this is a new user confirming their email for the first time,
  48. // then just update the state of their user record in the database,
  49. // store their user id in the session (just in case they aren't logged
  50. // in already), and then redirect them to the "email confirmed" page.
  51. await User.update({ id: user.id }).set({
  52. emailStatus: 'confirmed',
  53. emailProofToken: '',
  54. emailProofTokenExpiresAt: 0
  55. });
  56. this.req.session.userId = user.id;
  57. if (this.req.wantsJSON) {
  58. return exits.success();
  59. } else {
  60. throw { redirect: '/email/confirmed' };
  61. }
  62. } else if (user.emailStatus === 'changeRequested') {
  63. // ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗ ┌─┐┌┬┐┌─┐┬┬
  64. // │ │ ││││├┤ │├┬┘││││││││ ┬ ║ ╠═╣╠═╣║║║║ ╦║╣ ║║ ├┤ │││├─┤││
  65. // └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚═╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ └─┘┴ ┴┴ ┴┴┴─┘
  66. if (!user.emailChangeCandidate){
  67. throw new Error(`Consistency violation: Could not update Stripe customer because this user record's emailChangeCandidate ("${user.emailChangeCandidate}") is missing. (This should never happen.)`);
  68. }
  69. // Last line of defense: since email change candidates are not protected
  70. // by a uniqueness constraint in the database, it's important that we make
  71. // sure no one else managed to grab this email in the mean time since we
  72. // last checked its availability. (This is a relatively rare edge case--
  73. // see exit description.)
  74. if (await User.count({ emailAddress: user.emailChangeCandidate }) > 0) {
  75. throw 'emailAddressNoLongerAvailable';
  76. }
  77. // If billing features are enabled, also update the billing email for this
  78. // user's linked customer entry in the Stripe API to make sure they receive
  79. // email receipts.
  80. // > Note: If there was not already a Stripe customer entry for this user,
  81. // > then one will be set up implicitly, so we'll need to persist it to our
  82. // > database. (This could happen if Stripe credentials were not configured
  83. // > at the time this user was originally created.)
  84. if(sails.config.custom.enableBillingFeatures) {
  85. let didNotAlreadyHaveCustomerId = (! user.stripeCustomerId);
  86. let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
  87. stripeCustomerId: user.stripeCustomerId,
  88. emailAddress: user.emailChangeCandidate
  89. });
  90. if (didNotAlreadyHaveCustomerId){
  91. await User.update({ id: user.id }).set({
  92. stripeCustomerId
  93. });
  94. }
  95. }
  96. // Finally update the user in the database, store their id in the session
  97. // (just in case they aren't logged in already), then redirect them to
  98. // their "my account" page so they can see their updated email address.
  99. await User.update({ id: user.id }).set({
  100. emailStatus: 'confirmed',
  101. emailProofToken: '',
  102. emailProofTokenExpiresAt: 0,
  103. emailAddress: user.emailChangeCandidate,
  104. emailChangeCandidate: '',
  105. });
  106. this.req.session.userId = user.id;
  107. if (this.req.wantsJSON) {
  108. return exits.success();
  109. } else {
  110. throw { redirect: '/account' };
  111. }
  112. } else {
  113. 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.)`);
  114. }
  115. }
  116. };