index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. /**
  2. * custom hook
  3. *
  4. * @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions, and/or initialization logic.
  5. * @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
  6. */
  7. module.exports = function defineCustomHook(sails) {
  8. return {
  9. /**
  10. * Runs when a Sails app loads/lifts.
  11. *
  12. * @param {Function} done
  13. */
  14. initialize: async function (done) {
  15. sails.log.info('Initializing hook... (`api/hooks/custom`)');
  16. // Check Stripe/Mailgun configuration (for billing and emails).
  17. var IMPORTANT_STRIPE_CONFIG = ['stripeSecret', 'stripePublishableKey'];
  18. var IMPORTANT_MAILGUN_CONFIG = ['mailgunSecret', 'mailgunDomain', 'internalEmailAddress'];
  19. var isMissingStripeConfig = _.difference(IMPORTANT_STRIPE_CONFIG, Object.keys(sails.config.custom)).length > 0;
  20. var isMissingMailgunConfig = _.difference(IMPORTANT_MAILGUN_CONFIG, Object.keys(sails.config.custom)).length > 0;
  21. if (isMissingStripeConfig || isMissingMailgunConfig) {
  22. let missingFeatureText = isMissingStripeConfig && isMissingMailgunConfig ? 'billing and email' : isMissingStripeConfig ? 'billing' : 'email';
  23. let suffix = '';
  24. if (_.contains(['silly'], sails.config.log.level)) {
  25. suffix =
  26. `
  27. > Tip: To exclude sensitive credentials from source control, use:
  28. > • config/local.js (for local development)
  29. > • environment variables (for production)
  30. >
  31. > If you want to check them in to source control, use:
  32. > • config/custom.js (for development)
  33. > • config/env/staging.js (for staging)
  34. > • config/env/production.js (for production)
  35. >
  36. > (See https://sailsjs.com/docs/concepts/configuration for help configuring Sails.)
  37. `;
  38. }
  39. let problems = [];
  40. if (sails.config.custom.stripeSecret === undefined) {
  41. problems.push('No `sails.config.custom.stripeSecret` was configured.');
  42. }
  43. if (sails.config.custom.stripePublishableKey === undefined) {
  44. problems.push('No `sails.config.custom.stripePublishableKey` was configured.');
  45. }
  46. if (sails.config.custom.mailgunSecret === undefined) {
  47. problems.push('No `sails.config.custom.mailgunSecret` was configured.');
  48. }
  49. if (sails.config.custom.mailgunDomain === undefined) {
  50. problems.push('No `sails.config.custom.mailgunDomain` was configured.');
  51. }
  52. if (sails.config.custom.internalEmailAddress === undefined) {
  53. problems.push('No `sails.config.custom.internalEmailAddress` was configured.');
  54. }
  55. sails.log.verbose(
  56. `Some optional settings have not been configured yet:
  57. ---------------------------------------------------------------------
  58. ${problems.join('\n')}
  59. Until this is addressed, this app's ${missingFeatureText} features
  60. will be disabled and/or hidden in the UI.
  61. [?] If you're unsure or need advice, come by https://sailsjs.com/support
  62. ---------------------------------------------------------------------${suffix}`);
  63. }//fi
  64. // Set an additional config keys based on whether Stripe config is available.
  65. // This will determine whether or not to enable various billing features.
  66. sails.config.custom.enableBillingFeatures = !isMissingStripeConfig;
  67. // After "sails-hook-organics" finishes initializing, configure Stripe
  68. // and Mailgun packs with any available credentials.
  69. sails.after('hook:organics:loaded', ()=>{
  70. sails.helpers.stripe.configure({
  71. secret: sails.config.custom.stripeSecret
  72. });
  73. sails.helpers.mailgun.configure({
  74. secret: sails.config.custom.mailgunSecret,
  75. domain: sails.config.custom.mailgunDomain,
  76. from: sails.config.custom.fromEmailAddress,
  77. fromName: sails.config.custom.fromName,
  78. });
  79. });//_∏_
  80. // ... Any other app-specific setup code that needs to run on lift,
  81. // even in production, goes here ...
  82. return done();
  83. },
  84. routes: {
  85. /**
  86. * Runs before every matching route.
  87. *
  88. * @param {Ref} req
  89. * @param {Ref} res
  90. * @param {Function} next
  91. */
  92. before: {
  93. '/*': {
  94. skipAssets: true,
  95. fn: async function(req, res, next){
  96. // First, if this is a GET request (and thus potentially a view),
  97. // attach a couple of guaranteed locals.
  98. if (req.method === 'GET') {
  99. // The `_environment` local lets us do a little workaround to make Vue.js
  100. // run in "production mode" without unnecessarily involving complexities
  101. // with webpack et al.)
  102. if (res.locals._environment !== undefined) {
  103. throw new Error('Cannot attach Sails environment as the view local `_environment`, because this view local already exists! (Is it being attached somewhere else?)');
  104. }
  105. res.locals._environment = sails.config.environment;
  106. // The `me` local is set explicitly to `undefined` here just to avoid having to
  107. // do `typeof me !== 'undefined'` checks in our views/layouts/partials.
  108. // > Note that, depending on the request, this may or may not be set to the
  109. // > logged-in user record further below.
  110. if (res.locals.me !== undefined) {
  111. throw new Error('Cannot attach view local `me`, because this view local already exists! (Is it being attached somewhere else?)');
  112. }
  113. res.locals.me = undefined;
  114. }//fi
  115. // No session? Proceed as usual.
  116. // (e.g. request for a static asset)
  117. if (!req.session) { return next(); }
  118. // Not logged in? Proceed as usual.
  119. if (!req.session.userId) { return next(); }
  120. // Otherwise, look up the logged-in user.
  121. var loggedInUser = await User.findOne({
  122. id: req.session.userId
  123. });
  124. // If the logged-in user has gone missing, log a warning,
  125. // wipe the user id from the requesting user agent's session,
  126. // and then send the "unauthorized" response.
  127. if (!loggedInUser) {
  128. sails.log.warn('Somehow, the user record for the logged-in user (`'+req.session.userId+'`) has gone missing....');
  129. delete req.session.userId;
  130. return res.unauthorized();
  131. }
  132. // Add additional information for convenience when building top-level navigation.
  133. // (i.e. whether to display "Dashboard", "My Account", etc.)
  134. if (!loggedInUser.password || loggedInUser.emailStatus === 'unconfirmed') {
  135. loggedInUser.dontDisplayAccountLinkInNav = true;
  136. }
  137. // Expose the user record as an extra property on the request object (`req.me`).
  138. // > Note that we make sure `req.me` doesn't already exist first.
  139. if (req.me !== undefined) {
  140. throw new Error('Cannot attach logged-in user as `req.me` because this property already exists! (Is it being attached somewhere else?)');
  141. }
  142. req.me = loggedInUser;
  143. // If our "lastSeenAt" attribute for this user is at least a few seconds old, then set it
  144. // to the current timestamp.
  145. //
  146. // (Note: As an optimization, this is run behind the scenes to avoid adding needless latency.)
  147. var MS_TO_BUFFER = 60*1000;
  148. var now = Date.now();
  149. if (loggedInUser.lastSeenAt < now - MS_TO_BUFFER) {
  150. User.update({id: loggedInUser.id})
  151. .set({ lastSeenAt: now })
  152. .exec((err)=>{
  153. if (err) {
  154. sails.log.error('Background task failed: Could not update user (`'+loggedInUser.id+'`) with a new `lastSeenAt` timestamp. Error details: '+err.stack);
  155. return;
  156. }//•
  157. sails.log.verbose('Updated the `lastSeenAt` timestamp for user `'+loggedInUser.id+'`.');
  158. // Nothing else to do here.
  159. });//_∏_ (Meanwhile...)
  160. }//fi
  161. // If this is a GET request, then also expose an extra view local (`<%= me %>`).
  162. // > Note that we make sure a local named `me` doesn't already exist first.
  163. // > Also note that we strip off any properties that correspond with protected attributes.
  164. if (req.method === 'GET') {
  165. if (res.locals.me !== undefined) {
  166. throw new Error('Cannot attach logged-in user as the view local `me`, because this view local already exists! (Is it being attached somewhere else?)');
  167. }
  168. // Exclude any fields corresponding with attributes that have `protect: true`.
  169. var sanitizedUser = _.extend({}, loggedInUser);
  170. for (let attrName in User.attributes) {
  171. if (User.attributes[attrName].protect) {
  172. delete sanitizedUser[attrName];
  173. }
  174. }//∞
  175. // If there is still a "password" in sanitized user data, then delete it just to be safe.
  176. // (But also log a warning so this isn't hopelessly confusing.)
  177. if (sanitizedUser.password) {
  178. sails.log.warn('The logged in user record has a `password` property, but it was still there after pruning off all properties that match `protect: true` attributes in the User model. So, just to be safe, removing the `password` property anyway...');
  179. delete sanitizedUser.password;
  180. }//fi
  181. res.locals.me = sanitizedUser;
  182. // Include information on the locals as to whether billing features
  183. // are enabled for this app, and whether email verification is required.
  184. res.locals.isBillingEnabled = sails.config.custom.enableBillingFeatures;
  185. res.locals.isEmailVerificationRequired = sails.config.custom.verifyEmailAddresses;
  186. }//fi
  187. // Prevent the browser from caching logged-in users' pages.
  188. // (including w/ the Chrome back button)
  189. // > • https://mixmax.com/blog/chrome-back-button-cache-no-store
  190. // > • https://madhatted.com/2013/6/16/you-do-not-understand-browser-history
  191. res.setHeader('Cache-Control', 'no-cache, no-store');
  192. return next();
  193. }
  194. }
  195. }
  196. }
  197. };
  198. };