UNPKG

@flowforge/flowforge

Version:

An open source low-code development platform

312 lines (283 loc) 12.7 kB
const cookie = require('@fastify/cookie') const csrf = require('@fastify/csrf-protection') const helmet = require('@fastify/helmet') const { ProfilingIntegration } = require('@sentry/profiling-node') const fastify = require('fastify') const auditLog = require('./auditLog') const comms = require('./comms') const config = require('./config') // eslint-disable-line n/no-unpublished-require const containers = require('./containers') const db = require('./db') const ee = require('./ee') const housekeeper = require('./housekeeper') const license = require('./licensing') const monitor = require('./monitor') const postoffice = require('./postoffice') const routes = require('./routes') const settings = require('./settings') require('dotenv').config() // type defs for JSDoc and VSCode Intellisense /** * @typedef {fastify.FastifyInstance} FastifyInstance * @typedef {fastify.FastifyRequest} FastifyRequest * @typedef {fastify.FastifyReply} FastifyReply */ /** * The Forge/fastify app instance. * @typedef {FastifyInstance} ForgeApplication * @alias app - The Fastify app instance */ /** @type {ForgeApplication} */ module.exports = async (options = {}) => { const runtimeConfig = config.init(options) const loggerConfig = { formatters: { level: (label) => { return { level: label.toUpperCase() } }, bindings: (bindings) => { return { } } }, timestamp: require('pino').stdTimeFunctions.isoTime, level: runtimeConfig.logging.level, serializers: { res (reply) { return { statusCode: reply.statusCode, request: { url: reply.request?.raw?.url, method: reply.request?.method, remoteAddress: reply.request?.ip, remotePort: reply.request?.socket.remotePort } } } } } if (runtimeConfig.logging.pretty !== false) { loggerConfig.transport = { target: 'pino-pretty', options: { translateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss.l'Z'", ignore: 'pid,hostname', singleLine: true } } } const server = fastify({ forceCloseConnections: true, bodyLimit: 5242880, maxParamLength: 500, trustProxy: true, logger: loggerConfig }) if (runtimeConfig.telemetry.backend?.prometheus?.enabled) { const metricsPlugin = require('fastify-metrics') await server.register(metricsPlugin, { endpoint: '/metrics' }) } if (runtimeConfig.telemetry.backend?.sentry?.dsn) { const environment = process.env.SENTRY_ENV ?? (process.env.NODE_ENV ?? 'unknown') server.register(require('@immobiliarelabs/fastify-sentry'), { dsn: runtimeConfig.telemetry.backend.sentry.dsn, environment, release: `flowfuse@${runtimeConfig.version}`, tracesSampleRate: environment === 'production' ? 0.05 : 0.1, profilesSampleRate: environment === 'production' ? 0.05 : 0.1, integrations: [ new ProfilingIntegration() ], extractUserData (request) { const user = request.session?.User || request.user if (!user) { return {} } const extractedUser = { id: user.hashid, username: user.username, email: user.email, name: user.name } return extractedUser } }) } server.addHook('onError', async (request, reply, error) => { // Useful for debugging when a route goes wrong // console.error(error.stack) }) try { // Config : loads environment configuration await server.register(config.attach, options) // Test Only. Permit access to app.routes - for evaluating routes in tests if (options.config?.test?.fastifyRoutes) { // since @fastify/routes is a dev dependency, we only load it when requested in test server.register(require('@fastify/routes')) // eslint-disable-line n/no-unpublished-require } // Rate Limits: rate limiting for the server end points if (server.config.rate_limits?.enabled) { // for rate_limits, see [routes/rateLimits.js].getLimits() await server.register(require('@fastify/rate-limit'), server.config.rate_limits) } // DB : the database connection/models/views/controllers await server.register(db) // Settings await server.register(settings) // License await server.register(license) // Audit Logging await server.register(auditLog) // Housekeeper await server.register(housekeeper) // HTTP Server configuration if (!server.settings.get('cookieSecret')) { await server.settings.set('cookieSecret', server.db.utils.generateToken(12)) } await server.register(cookie, { secret: server.settings.get('cookieSecret') }) await server.register(csrf, { cookieOpts: { _signed: true, _httpOnly: true } }) let contentSecurityPolicy = false if (runtimeConfig.content_security_policy?.enabled) { if (!runtimeConfig.content_security_policy.directives) { contentSecurityPolicy = { directives: { 'base-uri': ["'self'"], 'default-src': ["'self'"], 'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"], 'worker-src': ["'self'", 'blob:'], 'connect-src': ["'self'"], 'img-src': ["'self'", 'data:', 'www.gravatar.com'], 'font-src': ["'self'"], 'style-src': ["'self'", 'https:', "'unsafe-inline'"], 'upgrade-insecure-requests': null } } } else { contentSecurityPolicy = { directives: runtimeConfig.content_security_policy.directives } } if (runtimeConfig.content_security_policy.report_only) { contentSecurityPolicy.reportOnly = true if (runtimeConfig.content_security_policy.report_uri) { contentSecurityPolicy.directives['report-uri'] = runtimeConfig.content_security_policy.report_uri } } if (runtimeConfig.telemetry.frontend?.plausible?.domain) { if (contentSecurityPolicy.directives['script-src'] && Array.isArray(contentSecurityPolicy.directives['script-src'])) { contentSecurityPolicy.directives['script-src'].push('plausible.io') } else { contentSecurityPolicy.directives['script-src'] = ['plausible.io'] } } if (runtimeConfig.telemetry?.frontend?.posthog?.apikey) { let posthogHost = 'app.posthog.com' if (runtimeConfig.telemetry.frontend.posthog.apiurl) { posthogHost = new URL(runtimeConfig.telemetry.frontend.posthog.apiurl).host } if (contentSecurityPolicy.directives['script-src'] && Array.isArray(contentSecurityPolicy.directives['script-src'])) { contentSecurityPolicy.directives['script-src'].push(posthogHost) } else { contentSecurityPolicy.directives['script-src'] = [posthogHost] } if (contentSecurityPolicy.directives['connect-src'] && Array.isArray(contentSecurityPolicy.directives['connect-src'])) { contentSecurityPolicy.directives['connect-src'].push(posthogHost) } else { contentSecurityPolicy.directives['connect-src'] = [posthogHost] } } if (runtimeConfig.telemetry?.frontend?.sentry) { if (contentSecurityPolicy.directives['connect-src'] && Array.isArray(contentSecurityPolicy.directives['connect-src'])) { contentSecurityPolicy.directives['connect-src'].push('*.ingest.sentry.io') } else { contentSecurityPolicy.directives['connect-src'] = ['*.ingest.sentry.io'] } } if (runtimeConfig.support?.enabled && runtimeConfig.support.frontend?.hubspot?.trackingcode) { const hubspotDomains = [ 'js-eu1.hs-analytics.com', 'js-eu1.hs-banner.com', 'js-eu1.hs-scripts.com', 'js-eu1.hscollectedforms.net', 'js-eu1.hubspot.com', 'js-eu1.usemessages.com' ] if (contentSecurityPolicy.directives['script-src'] && Array.isArray(contentSecurityPolicy.directives['script-src'])) { contentSecurityPolicy.directives['script-src'].push(...hubspotDomains) } else { contentSecurityPolicy.directives['script-src'] = hubspotDomains } const hubspotImageDomains = [ 'forms-eu1.hsforms.com', 'track-eu1.hubspot.com', 'perf-eu1.hsforms.com' ] if (contentSecurityPolicy.directives['img-src'] && Array.isArray(contentSecurityPolicy.directives['img-src'])) { contentSecurityPolicy.directives['img-src'].push(...hubspotImageDomains) } else { contentSecurityPolicy.directives['img-src'] = hubspotImageDomains } const hubspotConnectDomains = [ 'api-eu1.hubspot.com', 'cta-eu1.hubspot.com', 'forms-eu1.hscollectedforms.net' ] if (contentSecurityPolicy.directives['connect-src'] && Array.isArray(contentSecurityPolicy.directives['connect-src'])) { contentSecurityPolicy.directives['connect-src'].push(...hubspotConnectDomains) } else { contentSecurityPolicy.directives['connect-src'] = hubspotConnectDomains } const hubspotFrameDomains = [ 'app-eu1.hubspot.com' ] if (contentSecurityPolicy.directives['frame-src'] && Array.isArray(contentSecurityPolicy.directives['frame-src'])) { contentSecurityPolicy.directives['frame-src'].push(...hubspotFrameDomains) } else { contentSecurityPolicy.directives['frame-src'] = hubspotFrameDomains } } } let strictTransportSecurity = false if (runtimeConfig.base_url.startsWith('https://')) { strictTransportSecurity = { includeSubDomains: false, preload: true, maxAge: 3600 } } await server.register(helmet, { global: true, contentSecurityPolicy, crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: false, crossOriginResourcePolicy: false, hidePoweredBy: true, strictTransportSecurity, frameguard: { action: 'deny' } }) // Routes : the HTTP routes await server.register(routes, { logLevel: server.config.logging.http }) // Post Office : handles email await server.register(postoffice) // Comms : real-time communication broker await server.register(comms) // Containers: await server.register(containers) await server.register(ee) // Monitor await server.register(monitor) await server.ready() // NOTE: This is only likely to do anything after a db upgrade where the settingsHashes are cleared. server.db.models.Device.recalculateSettingsHashes(false) // update device.settingsHash if null // Ensure The defaultTeamType is in place await server.db.controllers.TeamType.ensureDefaultTypeExists() return server } catch (err) { console.error(err) server.log.error(`Failed to start: ${err.toString()}`) throw err } }