2020-12-02 20:48:03 +00:00
/ * *
* @ description : : The conventional "custom" hook . Extends this app with custom server - start - time and request - time logic .
* @ docs : : https : //sailsjs.com/docs/concepts/extending-sails/hooks
* /
module . exports = function defineCustomHook ( sails ) {
return {
/ * *
* Runs when a Sails app loads / lifts .
* /
initialize : async function ( ) {
sails . log . info ( 'Initializing project hook... (`api/hooks/custom/`)' ) ;
// Check Stripe/Sendgrid configuration (for billing and emails).
var IMPORTANT _STRIPE _CONFIG = [ 'stripeSecret' , 'stripePublishableKey' ] ;
var IMPORTANT _SENDGRID _CONFIG = [ 'sendgridSecret' , 'internalEmailAddress' ] ;
var isMissingStripeConfig = _ . difference ( IMPORTANT _STRIPE _CONFIG , Object . keys ( sails . config . custom ) ) . length > 0 ;
var isMissingSendgridConfig = _ . difference ( IMPORTANT _SENDGRID _CONFIG , Object . keys ( sails . config . custom ) ) . length > 0 ;
if ( isMissingStripeConfig || isMissingSendgridConfig ) {
let missingFeatureText = isMissingStripeConfig && isMissingSendgridConfig ? 'billing and email' : isMissingStripeConfig ? 'billing' : 'email' ;
let suffix = '' ;
if ( _ . contains ( [ 'silly' ] , sails . config . log . level ) ) {
suffix =
`
> Tip : To exclude sensitive credentials from source control , use :
> • config / local . js ( for local development )
> • environment variables ( for production )
>
> If you want to check them in to source control , use :
> • config / custom . js ( for development )
> • config / env / staging . js ( for staging )
> • config / env / production . js ( for production )
>
> ( See https : //sailsjs.com/docs/concepts/configuration for help configuring Sails.)
` ;
}
let problems = [ ] ;
if ( sails . config . custom . stripeSecret === undefined ) {
problems . push ( 'No `sails.config.custom.stripeSecret` was configured.' ) ;
}
if ( sails . config . custom . stripePublishableKey === undefined ) {
problems . push ( 'No `sails.config.custom.stripePublishableKey` was configured.' ) ;
}
if ( sails . config . custom . sendgridSecret === undefined ) {
problems . push ( 'No `sails.config.custom.sendgridSecret` was configured.' ) ;
}
if ( sails . config . custom . internalEmailAddress === undefined ) {
problems . push ( 'No `sails.config.custom.internalEmailAddress` was configured.' ) ;
}
sails . log . verbose (
` Some optional settings have not been configured yet:
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
$ { problems . join ( '\n' ) }
Until this is addressed , this app ' s $ { missingFeatureText } features
will be disabled and / or hidden in the UI .
[ ? ] If you ' re unsure or need advice , come by https : //sailsjs.com/support
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - $ { suffix } ` );
} //fi
// Set an additional config keys based on whether Stripe config is available.
// This will determine whether or not to enable various billing features.
sails . config . custom . enableBillingFeatures = ! isMissingStripeConfig ;
2025-04-26 20:16:07 +00:00
// After "sails-hook-organics" finishes initializing…
2020-12-02 20:48:03 +00:00
sails . after ( 'hook:organics:loaded' , ( ) => {
2025-04-26 20:16:07 +00:00
// Configure Stripe and Sendgrid packs with any available credentials.
2020-12-02 20:48:03 +00:00
sails . helpers . stripe . configure ( {
secret : sails . config . custom . stripeSecret
} ) ;
sails . helpers . sendgrid . configure ( {
secret : sails . config . custom . sendgridSecret ,
from : sails . config . custom . fromEmailAddress ,
fromName : sails . config . custom . fromName ,
} ) ;
2024-12-09 22:33:20 +00:00
// Validate all values in the githubRepoDRIByPath config variable.
if ( sails . config . custom . githubRepoDRIByPath ) {
if ( ! _ . isObject ( sails . config . custom . githubRepoDRIByPath ) ) {
throw new Error ( ` Invalid configuration! An invalid "sails.config.custom.githubRepoDRIByPath" value was provided. If set, this value should be a dictionary, where each key is a path in the GitHub repo, and each value is a GitHub username. Please change this value to be a dictionary and try running this script again. ` ) ;
}
for ( let path in sails . config . custom . githubRepoDRIByPath ) {
if ( typeof sails . config . custom . githubRepoDRIByPath [ path ] !== 'string' ) {
throw new Error ( ` Invalid configuration! A path ( ${ path } ) in the "sails.config.custom.githubRepoDRIByPath" config value contains a DRI value that is not a string (type: ${ typeof sails . config . custom . githubRepoDRIByPath [ path ] } ). Please change the DRI for this path to be a string containing a single GitHub username and try running this script again. ` ) ;
}
}
}
2024-04-15 19:03:52 +00:00
// Send a request to our Algolia crawler to reindex the website.
2024-04-19 22:40:45 +00:00
// FUTURE: If this breaks again, use the Platform model to store when the website was last crawled
// (platform.algoliaLastCrawledWebsiteAt), and then only send a request if it was <30m ago, then remove dyno check.
2024-04-15 19:03:52 +00:00
if ( sails . config . environment === 'production' && process . env . DYNO === 'web.1' ) {
2023-01-25 16:09:41 +00:00
sails . helpers . http . post . with ( {
url : ` https://crawler.algolia.com/api/1/crawlers/ ${ sails . config . custom . algoliaCrawlerId } /reindex ` ,
headers : { 'Authorization' : sails . config . custom . algoliaCrawlerApiToken }
} ) . exec ( ( err ) => {
if ( err ) {
sails . log . warn ( 'When trying to send a request to Algolia to refresh the Fleet website search index, an error occurred: ' + err ) ;
}
} ) ; //_∏_
2025-04-26 20:16:07 +00:00
} //fi
// Expose `ƒ`, for convenience.
global . ƒ = { } ;
global . ƒ [ require ( 'util' ) . inspect . custom ] = ( unusedDepth , unusedInspectOptions , unusedInspect ) => { // https://nodejs.org/api/util.html#utilinspectcustom
return ` [ƒ (derived from sails.helpers)] ` ;
} ; //ƒ
for ( let keyName of Object . keys ( sails . helpers ) ) {
if ( [ 'mailgun' ] . includes ( keyName ) ) {
continue ;
} //•
if ( _ . isFunction ( sails . helpers [ keyName ] ) ) {
if ( global . ƒ . hasOwnProperty ( keyName ) ) {
sails . log . warn ( ` Overwriting ƒ. ${ keyName } … ` ) ;
}
global . ƒ [ keyName ] = sails . helpers [ keyName ] ;
} else {
for ( let helperMethodName of Object . keys ( sails . helpers [ keyName ] ) ) {
if ( global . ƒ . hasOwnProperty ( helperMethodName ) ) {
sails . log . warn ( ` Overwriting ƒ. ${ helperMethodName } … ` ) ;
}
global . ƒ [ helperMethodName ] = sails . helpers [ keyName ] [ helperMethodName ] ;
} //∞
}
} //∞
2020-12-02 20:48:03 +00:00
} ) ; //_∏_
// ... Any other app-specific setup code that needs to run on lift,
// even in production, goes here ...
} ,
routes : {
/ * *
* Runs before every matching route .
*
* @ param { Ref } req
* @ param { Ref } res
* @ param { Function } next
* /
before : {
'/*' : {
skipAssets : true ,
fn : async function ( req , res , next ) {
var url = require ( 'url' ) ;
2024-04-16 00:51:50 +00:00
// First, if this is a GET request (and thus potentially a view) or a HEAD request,
2020-12-02 20:48:03 +00:00
// attach a couple of guaranteed locals.
2024-04-16 00:51:50 +00:00
if ( req . method === 'GET' || req . method === 'HEAD' ) {
2020-12-02 20:48:03 +00:00
// The `_environment` local lets us do a little workaround to make Vue.js
// run in "production mode" without unnecessarily involving complexities
// with webpack et al.)
if ( res . locals . _environment !== undefined ) {
throw new Error ( 'Cannot attach Sails environment as the view local `_environment`, because this view local already exists! (Is it being attached somewhere else?)' ) ;
}
res . locals . _environment = sails . config . environment ;
// The `me` local is set explicitly to `undefined` here just to avoid having to
// do `typeof me !== 'undefined'` checks in our views/layouts/partials.
// > Note that, depending on the request, this may or may not be set to the
// > logged-in user record further below.
if ( res . locals . me !== undefined ) {
throw new Error ( 'Cannot attach view local `me`, because this view local already exists! (Is it being attached somewhere else?)' ) ;
}
res . locals . me = undefined ;
} //fi
2024-08-24 19:40:12 +00:00
// Check for query parameters set by ad clicks.
// This is used to track the reason behind a psychological stage change.
// If the user performs any action that causes a stage change
// within 30 minutes of visiting the website from an ad, their psychological
// stage change will be attributed to the ad campaign that brought them here.
if ( req . param ( 'utm_source' ) && req . param ( 'creative_id' ) && req . param ( 'campaign_id' ) ) {
req . session . adAttributionString = ` ${ req . param ( 'utm_source' ) } ads - ${ req . param ( 'campaign_id' ) } - ${ _ . trim ( req . param ( 'creative_id' ) , '?' ) } ` ; // Trim questionmarks from the end of creative_id parameters.
// Example adAttributionString: Linkedin - 1245983829 - 41u3985237
req . session . visitedSiteFromAdAt = Date . now ( ) ;
}
2024-04-13 22:36:51 +00:00
// Check for website personalization parameter, and if valid, absorb it in the session.
// (This makes the experience simpler and less confusing for people, prioritizing showing things that matter for them)
2024-03-01 17:42:46 +00:00
// [?] https://en.wikipedia.org/wiki/UTM_parameters
// e.g.
2025-07-10 21:31:07 +00:00
// https://fleetdm.com/device-management?utm_content=it-major-mdm
if ( [ 'clear' , 'security-misc' , 'security-vm' , 'it-major-mdm' , 'it-gap-filler-mdm' , 'it-misc' ] . includes ( req . param ( 'utm_content' ) ) ) {
2024-04-13 22:36:51 +00:00
req . session . primaryBuyingSituation = req . param ( 'utm_content' ) === 'clear' ? undefined : req . param ( 'utm_content' ) ;
2024-05-31 19:31:21 +00:00
// FUTURE: reimplement the following (auto-redirect without querystring to make it prettier in the URL bar), but do it in the client-side JS
// using whatever that poppushstateblah thing is that makes it so you can change the URL bar from the browser-side code without screwing up
// the history stack (i.e. back button)
// ```
// return res.redirect(req.path);
// ```
2024-03-02 00:33:36 +00:00
} //fi
2024-04-13 22:36:51 +00:00
2024-03-03 23:30:21 +00:00
if ( req . method === 'GET' || req . method === 'HEAD' ) {
2024-04-13 22:36:51 +00:00
// Include information about the primary buying situation for use in the HTML layout, views, and page scripts.
2024-03-01 23:47:37 +00:00
res . locals . primaryBuyingSituation = req . session . primaryBuyingSituation || undefined ;
2024-03-02 00:33:36 +00:00
} //fi
2024-03-01 17:42:46 +00:00
2020-12-02 20:48:03 +00:00
// Next, if we're running in our actual "production" or "staging" Sails
// environment, check if this is a GET request via some other host,
// for example a subdomain like `webhooks.` or `click.`. If so, we'll
// automatically go ahead and redirect to the corresponding path under
// our base URL, which is environment-specific.
// > Note that we DO NOT redirect virtual socket requests and we DO NOT
// > redirect non-GET requests (because it can confuse some 3rd party
// > platforms that send webhook requests.) We also DO NOT redirect
// > requests in other environments to allow for flexibility during
// > development (e.g. so you can preview an app running locally on
// > your laptop using a local IP address or a tool like ngrok, in
// > case you want to run it on a real, physical mobile/IoT device)
var configuredBaseHostname ;
try {
configuredBaseHostname = url . parse ( sails . config . custom . baseUrl ) . host ;
} catch ( unusedErr ) { /*…*/ }
if ( ( sails . config . environment === 'staging' || sails . config . environment === 'production' ) && ! req . isSocket && req . method === 'GET' && req . hostname !== configuredBaseHostname ) {
sails . log . info ( 'Redirecting GET request from `' + req . hostname + '` to configured expected host (`' + configuredBaseHostname + '`)...' ) ;
return res . redirect ( sails . config . custom . baseUrl + req . url ) ;
} //•
2022-05-05 19:22:47 +00:00
// Prevent the browser from caching logged-in users' pages.
// (including w/ the Chrome back button)
// > • https://mixmax.com/blog/chrome-back-button-cache-no-store
// > • https://madhatted.com/2013/6/16/you-do-not-understand-browser-history
//
// This also prevents an issue where webpages may be cached by browsers, and thus
// reference an old bundle file (e.g. dist/production.min.js or dist/production.min.css),
// which might have a different hash encoded in its filename. This way, by preventing caching
// of the webpage itself, the HTML is always fresh, and thus always trying to load the latest,
// correct bundle files.
res . setHeader ( 'Cache-Control' , 'no-cache, no-store' ) ;
2020-12-02 20:48:03 +00:00
// No session? Proceed as usual.
// (e.g. request for a static asset)
2022-05-05 19:22:47 +00:00
if ( ! req . session ) {
return next ( ) ;
}
2020-12-02 20:48:03 +00:00
2025-03-06 22:34:43 +00:00
// Not logged in? Set local variables for the start flow CTA.
if ( ! req . session . userId ) {
res . locals . showStartCta = true ;
2025-03-18 22:39:41 +00:00
res . locals . collapseStartCta = true ;
2025-03-06 22:34:43 +00:00
return next ( ) ;
}
2020-12-02 20:48:03 +00:00
// Otherwise, look up the logged-in user.
var loggedInUser = await User . findOne ( {
id : req . session . userId
} ) ;
// If the logged-in user has gone missing, log a warning,
// wipe the user id from the requesting user agent's session,
// and then send the "unauthorized" response.
if ( ! loggedInUser ) {
sails . log . warn ( 'Somehow, the user record for the logged-in user (`' + req . session . userId + '`) has gone missing....' ) ;
delete req . session . userId ;
return res . unauthorized ( ) ;
}
// Add additional information for convenience when building top-level navigation.
// (i.e. whether to display "Dashboard", "My Account", etc.)
if ( ! loggedInUser . password || loggedInUser . emailStatus === 'unconfirmed' ) {
loggedInUser . dontDisplayAccountLinkInNav = true ;
}
// Expose the user record as an extra property on the request object (`req.me`).
// > Note that we make sure `req.me` doesn't already exist first.
if ( req . me !== undefined ) {
throw new Error ( 'Cannot attach logged-in user as `req.me` because this property already exists! (Is it being attached somewhere else?)' ) ;
}
req . me = loggedInUser ;
// If our "lastSeenAt" attribute for this user is at least a few seconds old, then set it
// to the current timestamp.
//
// (Note: As an optimization, this is run behind the scenes to avoid adding needless latency.)
var MS _TO _BUFFER = 60 * 1000 ;
var now = Date . now ( ) ;
if ( loggedInUser . lastSeenAt < now - MS _TO _BUFFER ) {
User . updateOne ( { id : loggedInUser . id } )
. set ( { lastSeenAt : now } )
. exec ( ( err ) => {
if ( err ) {
sails . log . error ( 'Background task failed: Could not update user (`' + loggedInUser . id + '`) with a new `lastSeenAt` timestamp. Error details: ' + err . stack ) ;
return ;
} //•
sails . log . verbose ( 'Updated the `lastSeenAt` timestamp for user `' + loggedInUser . id + '`.' ) ;
// Nothing else to do here.
} ) ; //_∏_ (Meanwhile...)
} //fi
// If this is a GET request, then also expose an extra view local (`<%= me %>`).
// > Note that we make sure a local named `me` doesn't already exist first.
// > Also note that we strip off any properties that correspond with protected attributes.
if ( req . method === 'GET' ) {
if ( res . locals . me !== undefined ) {
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?)' ) ;
}
// Exclude any fields corresponding with attributes that have `protect: true`.
var sanitizedUser = _ . extend ( { } , loggedInUser ) ;
sails . helpers . redactUser ( sanitizedUser ) ;
// If there is still a "password" in sanitized user data, then delete it just to be safe.
// (But also log a warning so this isn't hopelessly confusing.)
if ( sanitizedUser . password ) {
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...' ) ;
delete sanitizedUser . password ;
} //fi
res . locals . me = sanitizedUser ;
2024-10-18 17:22:01 +00:00
// Create a timestamp of thirty seconds ago. We'll use this to check the age of the user account before creating a fleetwebsite page view record in Salesforce.
let thirtySecondsAgoAt = Date . now ( ) - ( 1000 * 30 ) ;
// Start tracking a website page view in the CRM for logged-in users:
2024-10-15 02:13:13 +00:00
res . once ( 'finish' , function onceFinish ( ) {
2024-10-18 17:22:01 +00:00
// Only track a page view if the requested URL is not a redirect and if this user record is over 30 seconds old (To give time for the background task queued by the signup action to create the initial contact record.
if ( res . statusCode === 200 && sanitizedUser . createdAt < thirtySecondsAgoAt ) {
2024-10-15 02:13:13 +00:00
sails . helpers . flow . build ( async ( ) => {
if ( sails . config . environment !== 'production' ) {
sails . log . verbose ( 'Skipping Salesforce integration...' ) ;
return ;
}
let recordIds = await sails . helpers . salesforce . updateOrCreateContactAndAccount . with ( {
emailAddress : sanitizedUser . emailAddress ,
firstName : sanitizedUser . firstName ,
lastName : sanitizedUser . lastName ,
organization : sanitizedUser . organization ,
2024-11-11 18:35:49 +00:00
contactSource : 'Website - Sign up' , // Note: this is only set on new contacts.
2024-10-15 02:13:13 +00:00
} ) ;
2024-10-24 21:39:32 +00:00
let websiteVisitReason ;
2025-08-22 16:36:49 +00:00
if ( req . session . adAttributionString && req . session . visitedSiteFromAdAt ) {
2024-10-24 21:39:32 +00:00
let thirtyMinutesAgoAt = Date . now ( ) - ( 1000 * 60 * 30 ) ;
// If this user visited the website from an ad, set the websiteVisitReason to be the adAttributionString stored in their session.
if ( req . session . visitedSiteFromAdAt > thirtyMinutesAgoAt ) {
websiteVisitReason = this . req . session . adAttributionString ;
}
}
2024-10-15 02:13:13 +00:00
// Create the new Fleet website page view record.
2025-05-23 22:59:58 +00:00
await sails . helpers . salesforce . createHistoricalEvent . with ( {
salesforceContactId : recordIds . salesforceContactId ,
salesforceAccountId : recordIds . salesforceAccountId ,
fleetWebsitePageUrl : ` https://fleetdm.com ${ req . url } ` ,
eventType : 'Website page view' ,
websiteVisitReason : websiteVisitReason
2024-10-15 02:13:13 +00:00
} ) . intercept ( ( err ) => {
return new Error ( ` Could not create new Fleet website page view record. Error: ${ err } ` ) ;
} ) ;
} )
. exec ( ( err ) => {
if ( err && typeof err . errorCode !== 'undefined' && err . errorCode === 'DUPLICATES_DETECTED' ) {
// Swallow errors related to duplicate records.
sails . log . verbose ( ` Background task failed: When a logged-in user (email: ${ sanitizedUser . emailAddress } visited a page, a Contact/Account/website activity record could not be created/updated in the CRM. ` , err ) ;
} else if ( err ) {
sails . log . warn ( ` Background task failed: When a logged-in user (email: ${ sanitizedUser . emailAddress } visited a page, a Contact/Account/website activity record could not be created/updated in the CRM. ` , err ) ;
}
return ;
} ) ; //_∏_
2024-10-11 17:06:35 +00:00
}
2024-10-15 02:13:13 +00:00
} ) ;
2024-10-11 17:06:35 +00:00
2020-12-02 20:48:03 +00:00
// Include information on the locals as to whether billing features
// are enabled for this app, and whether email verification is required.
res . locals . isBillingEnabled = sails . config . custom . enableBillingFeatures ;
res . locals . isEmailVerificationRequired = sails . config . custom . verifyEmailAddresses ;
2024-03-01 23:47:37 +00:00
// Include information about the primary buying situation
// If set in the session (e.g. from an ad), use the primary buying situation for personalization.
res . locals . primaryBuyingSituation = req . session . primaryBuyingSituation || undefined ;
2024-05-31 23:44:13 +00:00
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// FUTURE: Only show this CTA to users who are below psyStage 6.
// > The code below is so we don't bother users who have completed the questionnaire
2024-08-21 17:54:06 +00:00
// Show this logged-in user a CTA to bring them to the /start questionnaire if they do not have billing information saved.
res . locals . showStartCta = ! req . me . hasBillingCard ;
2024-05-31 23:44:13 +00:00
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// If an expandCtaAt timestamp is set in the user's sesssion, check the value to see if we should expand the CTA.
if ( req . session . expandCtaAt && req . session . expandCtaAt > Date . now ( ) ) {
res . locals . collapseStartCta = true ;
} else {
res . locals . collapseStartCta = false ;
}
2020-12-02 20:48:03 +00:00
} //fi
return next ( ) ;
}
}
}
}
} ;
} ;