// A Config object provides information about how a specific app is // configured. // mount is the URL for the root of the API; includes http, domain, etc. import AppCache from './cache'; import SchemaCache from './Controllers/SchemaCache'; import DatabaseController from './Controllers/DatabaseController'; import net from 'net'; import { IdempotencyOptions, FileUploadOptions, } from './Options/Definitions'; function removeTrailingSlash(str) { if (!str) { return str; } if (str.endsWith('/')) { str = str.substr(0, str.length - 1); } return str; } export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); if (!cacheInfo) { return; } const config = new Config(); config.applicationId = applicationId; Object.keys(cacheInfo).forEach(key => { if (key == 'databaseController') { const schemaCache = new SchemaCache( cacheInfo.cacheController, cacheInfo.schemaCacheTTL, cacheInfo.enableSingleSchemaCache ); config.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache); } else { config[key] = cacheInfo[key]; } }); config.mount = removeTrailingSlash(mount); config.generateSessionExpiresAt = config.generateSessionExpiresAt.bind(config); config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind( config ); return config; } static put(serverConfiguration) { Config.validate(serverConfiguration); AppCache.put(serverConfiguration.appId, serverConfiguration); Config.setupPasswordValidator(serverConfiguration.passwordPolicy); return serverConfiguration; } static validate({ verifyUserEmails, userController, appName, publicServerURL, revokeSessionOnPasswordReset, expireInactiveSessions, sessionLength, maxLimit, emailVerifyTokenValidityDuration, accountLockout, passwordPolicy, masterKeyIps, masterKey, readOnlyMasterKey, allowHeaders, idempotencyOptions, emailVerifyTokenReuseIfValid, fileUpload, }) { if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); } const emailAdapter = userController.adapter; if (verifyUserEmails) { this.validateEmailConfiguration({ emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, }); } this.validateAccountLockoutPolicy(accountLockout); this.validatePasswordPolicy(passwordPolicy); this.validateFileUploadOptions(fileUpload); if (typeof revokeSessionOnPasswordReset !== 'boolean') { throw 'revokeSessionOnPasswordReset must be a boolean value'; } if (publicServerURL) { if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) { throw 'publicServerURL should be a valid HTTPS URL starting with https://'; } } this.validateSessionConfiguration(sessionLength, expireInactiveSessions); this.validateMasterKeyIps(masterKeyIps); this.validateMaxLimit(maxLimit); this.validateAllowHeaders(allowHeaders); this.validateIdempotencyOptions(idempotencyOptions); } static validateIdempotencyOptions(idempotencyOptions) { if (!idempotencyOptions) { return; } if (idempotencyOptions.ttl === undefined) { idempotencyOptions.ttl = IdempotencyOptions.ttl.default; } else if (!isNaN(idempotencyOptions.ttl) && idempotencyOptions.ttl <= 0) { throw 'idempotency TTL value must be greater than 0 seconds'; } else if (isNaN(idempotencyOptions.ttl)) { throw 'idempotency TTL value must be a number'; } if (!idempotencyOptions.paths) { idempotencyOptions.paths = IdempotencyOptions.paths.default; } else if (!(idempotencyOptions.paths instanceof Array)) { throw 'idempotency paths must be of an array of strings'; } } static validateAccountLockoutPolicy(accountLockout) { if (accountLockout) { if ( typeof accountLockout.duration !== 'number' || accountLockout.duration <= 0 || accountLockout.duration > 99999 ) { throw 'Account lockout duration should be greater than 0 and less than 100000'; } if ( !Number.isInteger(accountLockout.threshold) || accountLockout.threshold < 1 || accountLockout.threshold > 999 ) { throw 'Account lockout threshold should be an integer greater than 0 and less than 1000'; } } } static validatePasswordPolicy(passwordPolicy) { if (passwordPolicy) { if ( passwordPolicy.maxPasswordAge !== undefined && (typeof passwordPolicy.maxPasswordAge !== 'number' || passwordPolicy.maxPasswordAge < 0) ) { throw 'passwordPolicy.maxPasswordAge must be a positive number'; } if ( passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0) ) { throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; } if (passwordPolicy.validatorPattern) { if (typeof passwordPolicy.validatorPattern === 'string') { passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern); } else if (!(passwordPolicy.validatorPattern instanceof RegExp)) { throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.'; } } if ( passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function' ) { throw 'passwordPolicy.validatorCallback must be a function.'; } if ( passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean' ) { throw 'passwordPolicy.doNotAllowUsername must be a boolean value.'; } if ( passwordPolicy.maxPasswordHistory && (!Number.isInteger(passwordPolicy.maxPasswordHistory) || passwordPolicy.maxPasswordHistory <= 0 || passwordPolicy.maxPasswordHistory > 20) ) { throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'; } if ( passwordPolicy.resetTokenReuseIfValid && typeof passwordPolicy.resetTokenReuseIfValid !== 'boolean' ) { throw 'resetTokenReuseIfValid must be a boolean value'; } if (passwordPolicy.resetTokenReuseIfValid && !passwordPolicy.resetTokenValidityDuration) { throw 'You cannot use resetTokenReuseIfValid without resetTokenValidityDuration'; } } } // if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern static setupPasswordValidator(passwordPolicy) { if (passwordPolicy && passwordPolicy.validatorPattern) { passwordPolicy.patternValidator = value => { return passwordPolicy.validatorPattern.test(value); }; } } static validateEmailConfiguration({ emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, }) { if (!emailAdapter) { throw 'An emailAdapter is required for e-mail verification and password resets.'; } if (typeof appName !== 'string') { throw 'An app name is required for e-mail verification and password resets.'; } if (typeof publicServerURL !== 'string') { throw 'A public server url is required for e-mail verification and password resets.'; } if (emailVerifyTokenValidityDuration) { if (isNaN(emailVerifyTokenValidityDuration)) { throw 'Email verify token validity duration must be a valid number.'; } else if (emailVerifyTokenValidityDuration <= 0) { throw 'Email verify token validity duration must be a value greater than 0.'; } } if (emailVerifyTokenReuseIfValid && typeof emailVerifyTokenReuseIfValid !== 'boolean') { throw 'emailVerifyTokenReuseIfValid must be a boolean value'; } if (emailVerifyTokenReuseIfValid && !emailVerifyTokenValidityDuration) { throw 'You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration'; } } static validateFileUploadOptions(fileUpload) { if (!fileUpload) { fileUpload = {}; } if (typeof fileUpload !== 'object' || fileUpload instanceof Array) { throw 'fileUpload must be an object value.'; } if (fileUpload.enableForAnonymousUser === undefined) { fileUpload.enableForAnonymousUser = FileUploadOptions.enableForAnonymousUser.default; } else if (typeof fileUpload.enableForAnonymousUser !== 'boolean') { throw 'fileUpload.enableForAnonymousUser must be a boolean value.'; } if (fileUpload.enableForPublic === undefined) { fileUpload.enableForPublic = FileUploadOptions.enableForPublic.default; } else if (typeof fileUpload.enableForPublic !== 'boolean') { throw 'fileUpload.enableForPublic must be a boolean value.'; } if (fileUpload.enableForAuthenticatedUser === undefined) { fileUpload.enableForAuthenticatedUser = FileUploadOptions.enableForAuthenticatedUser.default; } else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') { throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.'; } } static validateMasterKeyIps(masterKeyIps) { for (const ip of masterKeyIps) { if (!net.isIP(ip)) { throw `Invalid ip in masterKeyIps: ${ip}`; } } } get mount() { var mount = this._mount; if (this.publicServerURL) { mount = this.publicServerURL; } return mount; } set mount(newValue) { this._mount = newValue; } static validateSessionConfiguration(sessionLength, expireInactiveSessions) { if (expireInactiveSessions) { if (isNaN(sessionLength)) { throw 'Session length must be a valid number.'; } else if (sessionLength <= 0) { throw 'Session length must be a value greater than 0.'; } } } static validateMaxLimit(maxLimit) { if (maxLimit <= 0) { throw 'Max limit must be a value greater than 0.'; } } static validateAllowHeaders(allowHeaders) { if (![null, undefined].includes(allowHeaders)) { if (Array.isArray(allowHeaders)) { allowHeaders.forEach(header => { if (typeof header !== 'string') { throw 'Allow headers must only contain strings'; } else if (!header.trim().length) { throw 'Allow headers must not contain empty strings'; } }); } else { throw 'Allow headers must be an array'; } } } generateEmailVerifyTokenExpiresAt() { if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) { return undefined; } var now = new Date(); return new Date(now.getTime() + this.emailVerifyTokenValidityDuration * 1000); } generatePasswordResetTokenExpiresAt() { if (!this.passwordPolicy || !this.passwordPolicy.resetTokenValidityDuration) { return undefined; } const now = new Date(); return new Date(now.getTime() + this.passwordPolicy.resetTokenValidityDuration * 1000); } generateSessionExpiresAt() { if (!this.expireInactiveSessions) { return undefined; } var now = new Date(); return new Date(now.getTime() + this.sessionLength * 1000); } get invalidLinkURL() { return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; } get invalidVerificationLinkURL() { return ( this.customPages.invalidVerificationLink || `${this.publicServerURL}/apps/invalid_verification_link.html` ); } get linkSendSuccessURL() { return ( this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html` ); } get linkSendFailURL() { return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html`; } get verifyEmailSuccessURL() { return ( this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html` ); } get choosePasswordURL() { return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`; } get requestResetPasswordURL() { return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`; } get passwordResetSuccessURL() { return ( this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html` ); } get parseFrameURL() { return this.customPages.parseFrameURL; } get verifyEmailURL() { return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`; } } export default Config; module.exports = Config;