UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

290 lines (288 loc) 13.3 kB
import { getConfigFromEnv } from "./utils/get-config-from-env.js"; import { createExpressLogger, useLogger } from "./logger/index.js"; import { initAIDevTools } from "./ai/devtools/index.js"; import { initAITelemetry } from "./ai/telemetry/index.js"; import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations } from "./database/index.js"; import emitter_default from "./emitter.js"; import schedule from "./schedules/license.js"; import { Url } from "./utils/url.js"; import { getEntitlementManager } from "./license/entitlements/manager.js"; import { getFlowManager } from "./flows.js"; import { getExtensionManager } from "./extensions/index.js"; import { registerAuthProviders } from "./auth.js"; import { ensureDeploymentWebhooks, registerDeploymentDrivers } from "./deployment.js"; import { getLicenseManager } from "./license/manager.js"; import "./license/index.js"; import { aiRouter } from "./ai/chat/router.js"; import { aiFilesRouter } from "./ai/files/router.js"; import access_default from "./controllers/access.js"; import activity_default from "./controllers/activity.js"; import assets_default from "./controllers/assets.js"; import auth_default from "./controllers/auth.js"; import collections_default from "./controllers/collections.js"; import comments_default from "./controllers/comments.js"; import dashboards_default from "./controllers/dashboards.js"; import deployment_webhooks_default from "./controllers/deployment-webhooks.js"; import deployment_default from "./controllers/deployment.js"; import extensions_default from "./controllers/extensions.js"; import fields_default from "./controllers/fields.js"; import files_default from "./controllers/files.js"; import flows_default from "./controllers/flows.js"; import folders_default from "./controllers/folders.js"; import graphql_default from "./controllers/graphql.js"; import items_default from "./controllers/items.js"; import license_default from "./controllers/license.js"; import mcp_default from "./controllers/mcp/index.js"; import oauth_clients_default from "./controllers/mcp/oauth-clients.js"; import { mcpOAuthProtectedRouter, mcpOAuthPublicRouter } from "./controllers/mcp/oauth.js"; import metrics_default from "./controllers/metrics.js"; import not_found_default from "./controllers/not-found.js"; import notifications_default from "./controllers/notifications.js"; import operations_default from "./controllers/operations.js"; import panels_default from "./controllers/panels.js"; import permissions_default from "./controllers/permissions.js"; import policies_default from "./controllers/policies.js"; import presets_default from "./controllers/presets.js"; import relations_default from "./controllers/relations.js"; import revisions_default from "./controllers/revisions.js"; import roles_default from "./controllers/roles.js"; import schema_default from "./controllers/schema.js"; import server_default from "./controllers/server.js"; import settings_default from "./controllers/settings.js"; import shares_default from "./controllers/shares.js"; import translations_default from "./controllers/translations.js"; import tus_default from "./controllers/tus.js"; import users_default from "./controllers/users.js"; import utils_default from "./controllers/utils.js"; import versions_default from "./controllers/versions.js"; import authenticate_default from "./middleware/authenticate.js"; import cache_default from "./middleware/cache.js"; import cors_default from "./middleware/cors.js"; import { errorHandler } from "./middleware/error-handler.js"; import extract_token_default from "./middleware/extract-token.js"; import mcp_oauth_guard_default from "./middleware/mcp-oauth-guard.js"; import rate_limiter_global_default from "./middleware/rate-limiter-global.js"; import rate_limiter_ip_default from "./middleware/rate-limiter-ip.js"; import request_counter_default from "./middleware/request-counter.js"; import sanitize_query_default from "./middleware/sanitize-query.js"; import schema_default$1 from "./middleware/schema.js"; import schedule$1 from "./schedules/metrics.js"; import scheduleOAuthCleanup from "./schedules/oauth-cleanup.js"; import schedule$2 from "./schedules/project.js"; import schedule$3 from "./schedules/retention.js"; import schedule$4 from "./schedules/telemetry.js"; import schedule$5 from "./schedules/tus.js"; import { validateStorage } from "./utils/validate-storage.js"; import { createRequire } from "node:module"; import { readFile } from "node:fs/promises"; import path from "path"; import { useEnv } from "@directus/env"; import { InvalidPayloadError, ServiceUnavailableError } from "@directus/errors"; import { handlePressure } from "@directus/pressure"; import { toBoolean } from "@directus/utils"; import cookieParser from "cookie-parser"; import express from "express"; import { merge } from "lodash-es"; import qs from "qs"; //#region src/app.ts const require = createRequire(import.meta.url); async function createApp() { const env = useEnv(); const logger = useLogger(); const helmet = await import("helmet"); await validateDatabaseConnection(); if (await isInstalled() === false) { logger.error(`Database doesn't have Directus tables installed.`); process.exit(1); } if (await validateMigrations() === false) logger.warn(`Database migrations have not all been run`); if (!env["SECRET"]) logger.warn(`"SECRET" env variable is missing. Using a random value instead. Tokens will not persist between restarts. This is not appropriate for production usage.`); if (typeof env["SECRET"] === "string" && Buffer.byteLength(env["SECRET"]) < 32) logger.warn("\"SECRET\" env variable is shorter than 32 bytes which is insecure. This is not appropriate for production usage."); if (!new Url(env["PUBLIC_URL"]).isAbsolute()) logger.warn("\"PUBLIC_URL\" should be a full URL"); if (env["MCP_OAUTH_ENABLED"] === true) { if (toBoolean(env["MCP_ENABLED"]) !== true) { logger.warn("MCP_OAUTH_ENABLED requires MCP_ENABLED=true. OAuth disabled."); env["MCP_OAUTH_ENABLED"] = false; } } await validateDatabaseExtensions(); await validateStorage(); await getLicenseManager().initialize(); getEntitlementManager().initialize(); await registerAuthProviders(); registerDeploymentDrivers(); await ensureDeploymentWebhooks(); const extensionManager = getExtensionManager(); const flowManager = getFlowManager(); await extensionManager.initialize(); await flowManager.initialize(); const app = express(); app.disable("x-powered-by"); app.set("trust proxy", env["IP_TRUST_PROXY"]); app.set("query parser", (str) => qs.parse(str, { depth: Number(env["QUERYSTRING_MAX_PARSE_DEPTH"]), arrayLimit: Number(env["QUERYSTRING_ARRAY_LIMIT"]) })); if (env["PRESSURE_LIMITER_ENABLED"]) { const sampleInterval = Number(env["PRESSURE_LIMITER_SAMPLE_INTERVAL"]); if (Number.isNaN(sampleInterval) === true || Number.isFinite(sampleInterval) === false) throw new Error(`Invalid value for PRESSURE_LIMITER_SAMPLE_INTERVAL environment variable`); app.use(handlePressure({ sampleInterval, maxEventLoopUtilization: env["PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION"], maxEventLoopDelay: env["PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY"], maxMemoryRss: env["PRESSURE_LIMITER_MAX_MEMORY_RSS"], maxMemoryHeapUsed: env["PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED"], error: new ServiceUnavailableError({ service: "api", reason: "Under pressure" }), retryAfter: env["PRESSURE_LIMITER_RETRY_AFTER"] })); } app.use(helmet.contentSecurityPolicy(merge({ useDefaults: true, directives: { scriptSrc: ["'self'", "'unsafe-eval'"], upgradeInsecureRequests: null, workerSrc: ["'self'", "blob:"], childSrc: ["'self'", "blob:"], imgSrc: [ "'self'", "data:", "blob:", "https://raw.githubusercontent.com", "https://avatars.githubusercontent.com" ], mediaSrc: ["'self'"], connectSrc: [ "'self'", "https://*", "wss://*" ] } }, getConfigFromEnv("CONTENT_SECURITY_POLICY_")))); if (env["CROSS_ORIGIN_OPENER_POLICY_ENABLED"]) app.use(helmet.crossOriginOpenerPolicy({ policy: env["CROSS_ORIGIN_OPENER_POLICY"] ?? "same-origin-allow-popups" })); if (env["HSTS_ENABLED"]) app.use(helmet.hsts(getConfigFromEnv("HSTS_", { omitPrefix: "HSTS_ENABLED" }))); await emitter_default.emitInit("app.before", { app }); await emitter_default.emitInit("middlewares.before", { app }); app.use(createExpressLogger()); app.use((_req, res, next) => { res.setHeader("X-Powered-By", "Directus"); next(); }); if (env["CORS_ENABLED"] === true) app.use(cors_default); app.use((req, res, next) => { express.json({ limit: env["MAX_PAYLOAD_SIZE"], verify: (req$1, _res, buf) => { req$1.rawBody = buf; } })(req, res, (err) => { if (err) return next(new InvalidPayloadError({ reason: err.message })); return next(); }); }); app.use(cookieParser()); app.use(extract_token_default); app.get("/", (_req, res, next) => { if (env["ROOT_REDIRECT"]) res.redirect(env["ROOT_REDIRECT"]); else next(); }); app.get("/robots.txt", (_, res) => { res.set("Content-Type", "text/plain"); res.status(200); res.send(env["ROBOTS_TXT"]); }); if (env["SERVE_APP"]) { const adminPath = require.resolve("@directus/app"); const adminUrl = new Url(env["PUBLIC_URL"]).addPath("admin"); const embeds = extensionManager.getEmbeds(); const htmlWithVars = (await readFile(adminPath, "utf8")).replace(/<base \/>/, `<base href="${adminUrl.toString({ rootRelative: true })}/" />`).replace("<!-- directus-embed-head -->", embeds.head).replace("<!-- directus-embed-body -->", embeds.body); const sendHtml = (_req, res) => { res.setHeader("Cache-Control", "no-cache"); res.setHeader("Vary", "Origin, Cache-Control"); res.send(htmlWithVars); }; const setStaticHeaders = (res) => { res.setHeader("Cache-Control", "max-age=31536000, immutable"); res.setHeader("Vary", "Origin, Cache-Control"); }; app.get("/admin", sendHtml); app.use("/admin", express.static(path.join(adminPath, ".."), { setHeaders: setStaticHeaders })); app.use("/admin/*", sendHtml); } if (env["RATE_LIMITER_GLOBAL_ENABLED"] === true) app.use(rate_limiter_global_default); if (env["RATE_LIMITER_ENABLED"] === true) app.use(rate_limiter_ip_default); app.get("/server/ping", (_req, res) => res.send("pong")); app.use("/deployments/webhooks", deployment_webhooks_default); if (env["MCP_OAUTH_ENABLED"] === true) app.use(mcpOAuthPublicRouter); app.use(authenticate_default); app.use(mcp_oauth_guard_default); if (env["MCP_OAUTH_ENABLED"] === true) app.use(mcpOAuthProtectedRouter); app.use(schema_default$1); app.use(sanitize_query_default); app.use(request_counter_default); app.use(cache_default); await emitter_default.emitInit("middlewares.after", { app }); await emitter_default.emitInit("routes.before", { app }); app.use("/auth", auth_default); app.use("/graphql", graphql_default); app.use("/activity", activity_default); app.use("/access", access_default); app.use("/assets", assets_default); app.use("/collections", collections_default); app.use("/comments", comments_default); app.use("/dashboards", dashboards_default); app.use("/deployments", deployment_default); app.use("/extensions", extensions_default); app.use("/fields", fields_default); if (env["TUS_ENABLED"] === true) app.use("/files/tus", tus_default); app.use("/files", files_default); app.use("/flows", flows_default); app.use("/folders", folders_default); app.use("/items", items_default); app.use("/license", license_default); if (toBoolean(env["MCP_ENABLED"]) === true) app.use("/mcp", mcp_default); if (toBoolean(env["AI_ENABLED"]) === true) { await initAIDevTools(); await initAITelemetry(); app.use("/ai", aiRouter); app.use("/ai/files", aiFilesRouter); } if (env["METRICS_ENABLED"] === true) app.use("/metrics", metrics_default); app.use("/notifications", notifications_default); app.use("/operations", operations_default); app.use("/panels", panels_default); app.use("/permissions", permissions_default); app.use("/policies", policies_default); app.use("/presets", presets_default); app.use("/translations", translations_default); app.use("/relations", relations_default); app.use("/revisions", revisions_default); app.use("/roles", roles_default); if (toBoolean(env["MCP_OAUTH_ENABLED"]) === true) app.use("/mcp-oauth/clients", oauth_clients_default); app.use("/schema", schema_default); app.use("/server", server_default); app.use("/settings", settings_default); app.use("/shares", shares_default); app.use("/users", users_default); app.use("/utils", utils_default); app.use("/versions", versions_default); await emitter_default.emitInit("routes.custom.before", { app }); app.use(extensionManager.getEndpointRouter()); await emitter_default.emitInit("routes.custom.after", { app }); app.use(not_found_default); app.use(errorHandler); await emitter_default.emitInit("routes.after", { app }); await schedule$3(); await schedule$4(); await schedule$5(); await schedule$1(); await schedule$2(); await schedule(); if (env["MCP_OAUTH_ENABLED"] === true) await scheduleOAuthCleanup(); await emitter_default.emitInit("app.after", { app }); return app; } //#endregion export { createApp as default };