2021-07-19 13:46:38 +03:00
const errors = require ( '@tryghost/errors' ) ;
const tpl = require ( '@tryghost/tpl' ) ;
2021-05-12 15:02:27 +03:00
const MembersSSR = require ( '@tryghost/members-ssr' ) ;
const db = require ( '../../data/db' ) ;
const MembersConfigProvider = require ( './config' ) ;
2021-07-21 18:34:11 +03:00
const MembersCSVImporter = require ( '@tryghost/members-importer' ) ;
2021-08-18 10:48:07 +03:00
const MembersStats = require ( './stats/members-stats' ) ;
2021-05-12 15:02:27 +03:00
const createMembersApiInstance = require ( './api' ) ;
const createMembersSettingsInstance = require ( './settings' ) ;
2021-06-15 17:36:27 +03:00
const logging = require ( '@tryghost/logging' ) ;
2021-05-12 15:02:27 +03:00
const urlUtils = require ( '../../../shared/url-utils' ) ;
2021-07-20 17:42:57 +03:00
const labsService = require ( '../../../shared/labs' ) ;
2021-06-30 16:56:57 +03:00
const settingsCache = require ( '../../../shared/settings-cache' ) ;
2021-05-12 15:02:27 +03:00
const config = require ( '../../../shared/config' ) ;
2021-07-27 12:27:59 +03:00
const models = require ( '../../models' ) ;
2021-06-16 11:36:58 +03:00
const ghostVersion = require ( '@tryghost/version' ) ;
2021-05-12 15:02:27 +03:00
const _ = require ( 'lodash' ) ;
2021-07-20 17:42:26 +03:00
const { GhostMailer } = require ( '../mail' ) ;
2021-07-20 18:21:59 +03:00
const jobsService = require ( '../jobs' ) ;
2021-05-12 15:02:27 +03:00
2021-07-19 13:46:38 +03:00
const messages = {
noLiveKeysInDevelopment : 'Cannot use live stripe keys in development. Please restart in production mode.' ,
sslRequiredForStripe : 'Cannot run Ghost without SSL when Stripe is connected. Please update your url config to use "https://".' ,
2021-07-23 19:37:29 +03:00
remoteWebhooksInDevelopment : 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.' ,
2021-08-20 17:07:20 +03:00
emailVerificationNeeded : ` We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go. ` ,
2021-07-28 18:28:13 +03:00
emailVerificationEmailMessage : ` Email verification needed for site: {siteUrl}, just imported: {importedNumber} members. `
2021-07-19 13:46:38 +03:00
} ;
2021-07-07 17:49:45 +03:00
// Bind to settings.edited to update systems based on settings changes, similar to the bridge and models/base/listeners
const events = require ( '../../lib/common/events' ) ;
2021-07-20 17:42:26 +03:00
const ghostMailer = new GhostMailer ( ) ;
2021-05-12 15:02:27 +03:00
const membersConfig = new MembersConfigProvider ( {
config ,
settingsCache ,
urlUtils ,
logging ,
ghostVersion
} ) ;
let membersApi ;
let membersSettings ;
function reconfigureMembersAPI ( ) {
const reconfiguredMembersAPI = createMembersApiInstance ( membersConfig ) ;
reconfiguredMembersAPI . bus . on ( 'ready' , function ( ) {
membersApi = reconfiguredMembersAPI ;
} ) ;
reconfiguredMembersAPI . bus . on ( 'error' , function ( err ) {
logging . error ( err ) ;
} ) ;
}
2021-07-23 19:37:29 +03:00
2021-08-18 17:39:43 +03:00
/ * *
* @ description Calculates threshold based on following formula
* Threshold = max { [ current number of members ] , [ volume threshold ] }
*
* @ returns { Promise < number > }
* /
const fetchImportThreshold = async ( ) => {
const membersTotal = await membersService . stats . getTotalMembers ( ) ;
const volumeThreshold = _ . get ( config . get ( 'hostSettings' ) , 'emailVerification.importThreshold' ) || Infinity ;
const threshold = Math . max ( membersTotal , volumeThreshold ) ;
return threshold ;
2021-07-23 19:37:29 +03:00
} ;
2021-07-23 15:58:35 +03:00
const membersImporter = new MembersCSVImporter ( {
storagePath : config . getContentPath ( 'data' ) ,
getTimezone : ( ) => settingsCache . get ( 'timezone' ) ,
2021-07-27 12:09:04 +03:00
getMembersApi : ( ) => membersService . api ,
2021-07-23 15:58:35 +03:00
sendEmail : ghostMailer . send . bind ( ghostMailer ) ,
isSet : labsService . isSet . bind ( labsService ) ,
addJob : jobsService . addJob . bind ( jobsService ) ,
knex : db . knex ,
urlFor : urlUtils . urlFor . bind ( urlUtils ) ,
2021-08-18 17:39:43 +03:00
fetchThreshold : fetchImportThreshold
2021-07-23 15:58:35 +03:00
} ) ;
2021-07-28 18:28:13 +03:00
const startEmailVerification = async ( importedNumber ) => {
2021-07-27 14:44:08 +03:00
const isVerifiedEmail = config . get ( 'hostSettings:emailVerification:verified' ) === true ;
2021-07-28 15:36:20 +03:00
if ( ( ! isVerifiedEmail ) ) {
2021-07-28 18:28:13 +03:00
// Only trigger flag change and escalation email the first time
if ( settingsCache . get ( 'email_verification_required' ) !== true ) {
await models . Settings . edit ( [ {
key : 'email_verification_required' ,
value : true
} ] , { context : { internal : true } } ) ;
const escalationAddress = config . get ( 'hostSettings:emailVerification:escalationAddress' ) ;
2021-08-12 13:00:33 +03:00
const fromAddress = config . get ( 'user_email' ) ;
2021-07-28 18:28:13 +03:00
if ( escalationAddress ) {
ghostMailer . send ( {
subject : 'Email needs verification' ,
html : tpl ( messages . emailVerificationEmailMessage , {
importedNumber ,
siteUrl : urlUtils . getSiteUrl ( )
} ) ,
forceTextContent : true ,
2021-08-12 13:00:33 +03:00
from : fromAddress ,
2021-07-28 18:28:13 +03:00
to : escalationAddress
} ) ;
}
}
2021-07-27 12:27:59 +03:00
2021-07-23 19:37:29 +03:00
throw new errors . ValidationError ( {
message : tpl ( messages . emailVerificationNeeded )
} ) ;
}
2021-07-28 15:36:20 +03:00
} ;
const processImport = async ( options ) => {
const result = await membersImporter . process ( options ) ;
const freezeTriggered = result . meta . freeze ;
2021-07-28 18:28:13 +03:00
const importSize = result . meta . originalImportSize ;
2021-07-28 15:36:20 +03:00
delete result . meta . freeze ;
2021-07-28 18:28:13 +03:00
delete result . meta . originalImportSize ;
2021-07-28 15:36:20 +03:00
if ( freezeTriggered ) {
2021-07-28 18:28:13 +03:00
await startEmailVerification ( importSize ) ;
2021-07-28 15:36:20 +03:00
}
2021-07-23 19:37:29 +03:00
return result ;
2021-07-23 15:58:35 +03:00
} ;
2021-05-12 15:02:27 +03:00
const debouncedReconfigureMembersAPI = _ . debounce ( reconfigureMembersAPI , 600 ) ;
// Bind to events to automatically keep subscription info up-to-date from settings
events . on ( 'settings.edited' , function updateSettingFromModel ( settingModel ) {
if ( ! [
'members_signup_access' ,
'members_from_address' ,
'members_support_address' ,
'members_reply_address' ,
'stripe_publishable_key' ,
'stripe_secret_key' ,
'stripe_product_name' ,
'stripe_plans' ,
'stripe_connect_publishable_key' ,
'stripe_connect_secret_key' ,
'stripe_connect_livemode' ,
'stripe_connect_display_name' ,
'stripe_connect_account_id'
] . includes ( settingModel . get ( 'key' ) ) ) {
return ;
}
debouncedReconfigureMembersAPI ( ) ;
} ) ;
const membersService = {
2021-05-12 15:46:05 +03:00
async init ( ) {
const env = config . get ( 'env' ) ;
const paymentConfig = membersConfig . getStripePaymentConfig ( ) ;
if ( env !== 'production' ) {
if ( ! process . env . WEBHOOK _SECRET && membersConfig . isStripeConnected ( ) ) {
2021-07-12 12:48:52 +03:00
process . env . WEBHOOK _SECRET = 'DEFAULT_WEBHOOK_SECRET' ;
2021-07-19 13:46:38 +03:00
logging . warn ( tpl ( messages . remoteWebhooksInDevelopment ) ) ;
2021-05-12 15:46:05 +03:00
}
if ( paymentConfig && paymentConfig . secretKey . startsWith ( 'sk_live' ) ) {
2021-07-19 13:46:38 +03:00
throw new errors . IncorrectUsageError ( tpl ( messages . noLiveKeysInDevelopment ) ) ;
2021-05-12 15:46:05 +03:00
}
} else {
const siteUrl = urlUtils . getSiteUrl ( ) ;
2021-05-27 20:33:03 +03:00
if ( ! /^https/ . test ( siteUrl ) && membersConfig . isStripeConnected ( ) ) {
2021-07-19 13:46:38 +03:00
throw new errors . IncorrectUsageError ( tpl ( messages . sslRequiredForStripe ) ) ;
2021-05-12 15:46:05 +03:00
}
}
} ,
2021-05-12 15:02:27 +03:00
contentGating : require ( './content-gating' ) ,
config : membersConfig ,
get api ( ) {
if ( ! membersApi ) {
membersApi = createMembersApiInstance ( membersConfig ) ;
membersApi . bus . on ( 'error' , function ( err ) {
logging . error ( err ) ;
} ) ;
}
return membersApi ;
} ,
get settings ( ) {
if ( ! membersSettings ) {
membersSettings = createMembersSettingsInstance ( membersConfig ) ;
}
return membersSettings ;
} ,
ssr : MembersSSR ( {
cookieSecure : urlUtils . isSSL ( urlUtils . getSiteUrl ( ) ) ,
cookieKeys : [ settingsCache . get ( 'theme_session_secret' ) ] ,
cookieName : 'ghost-members-ssr' ,
cookieCacheName : 'ghost-members-ssr-cache' ,
getMembersApi : ( ) => membersService . api
} ) ,
stripeConnect : require ( './stripe-connect' ) ,
2021-07-23 15:58:35 +03:00
processImport : processImport ,
2021-05-12 15:02:27 +03:00
stats : new MembersStats ( {
db : db ,
settingsCache : settingsCache ,
isSQLite : config . get ( 'database:client' ) === 'sqlite3'
} )
} ;
module . exports = membersService ;
module . exports . middleware = require ( './middleware' ) ;