@directus/api 35.2.0 → 36.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/chat/models/chat-request.js +48 -48
- package/dist/ai/chat/models/object-request.js +6 -6
- package/dist/ai/chat/models/providers.js +14 -14
- package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
- package/dist/ai/mcp/server.js +44 -6
- package/dist/ai/mcp/utils.js +31 -0
- package/dist/ai/tools/assets/index.js +3 -3
- package/dist/ai/tools/collections/index.js +18 -18
- package/dist/ai/tools/fields/index.js +18 -18
- package/dist/ai/tools/files/index.js +18 -18
- package/dist/ai/tools/flows/index.js +16 -16
- package/dist/ai/tools/folders/index.js +18 -18
- package/dist/ai/tools/items/index.js +17 -17
- package/dist/ai/tools/operations/index.js +16 -16
- package/dist/ai/tools/relations/index.js +22 -22
- package/dist/ai/tools/schema/index.js +3 -3
- package/dist/ai/tools/schema.js +159 -159
- package/dist/ai/tools/system/index.js +3 -3
- package/dist/ai/tools/trigger-flow/index.js +3 -3
- package/dist/app.js +35 -11
- package/dist/auth/drivers/ldap.js +3 -1
- package/dist/auth/drivers/local.js +2 -0
- package/dist/auth/drivers/oauth2.js +3 -1
- package/dist/auth/drivers/openid.js +3 -1
- package/dist/auth/drivers/saml.js +2 -0
- package/dist/auth/utils/check-local-disabled.js +16 -0
- package/dist/auth/utils/check-sso-enabled.js +14 -0
- package/dist/auth.js +8 -5
- package/dist/cli/commands/bootstrap/index.js +3 -0
- package/dist/cli/commands/cache/clear.js +6 -1
- package/dist/cli/commands/roles/create.js +4 -1
- package/dist/cli/commands/users/create.js +3 -0
- package/dist/constants.js +8 -1
- package/dist/controllers/access.js +1 -1
- package/dist/controllers/activity.js +2 -1
- package/dist/controllers/assets.js +2 -0
- package/dist/controllers/auth.js +13 -5
- package/dist/controllers/collections.js +1 -1
- package/dist/controllers/comments.js +1 -1
- package/dist/controllers/dashboards.js +1 -1
- package/dist/controllers/fields.js +1 -1
- package/dist/controllers/files.js +3 -1
- package/dist/controllers/flows.js +6 -5
- package/dist/controllers/folders.js +1 -1
- package/dist/controllers/graphql.js +2 -0
- package/dist/controllers/items.js +3 -1
- package/dist/controllers/license.js +119 -0
- package/dist/controllers/mcp/index.js +38 -0
- package/dist/controllers/mcp/oauth-clients.js +68 -0
- package/dist/controllers/mcp/oauth-consent-page.js +316 -0
- package/dist/controllers/mcp/oauth.js +381 -0
- package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
- package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
- package/dist/controllers/notifications.js +1 -1
- package/dist/controllers/operations.js +1 -1
- package/dist/controllers/panels.js +1 -1
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/policies.js +1 -1
- package/dist/controllers/presets.js +1 -1
- package/dist/controllers/revisions.js +3 -2
- package/dist/controllers/roles.js +1 -1
- package/dist/controllers/server.js +38 -10
- package/dist/controllers/shares.js +1 -1
- package/dist/controllers/translations.js +1 -1
- package/dist/controllers/users.js +1 -1
- package/dist/controllers/utils.js +2 -2
- package/dist/controllers/versions.js +12 -5
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
- package/dist/database/helpers/fn/dialects/mysql.js +7 -12
- package/dist/database/helpers/fn/dialects/oracle.js +3 -4
- package/dist/database/helpers/fn/dialects/postgres.js +4 -26
- package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
- package/dist/database/helpers/fn/json/parse-function.js +14 -6
- package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
- package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
- package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
- package/dist/database/migrations/20260507A-add-licensing.js +22 -0
- package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
- package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
- package/dist/database/run-ast/lib/apply-query/index.js +4 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
- package/dist/database/run-ast/lib/get-db-query.js +21 -9
- package/dist/database/run-ast/lib/parse-current-level.js +2 -1
- package/dist/database/run-ast/run-ast.js +2 -1
- package/dist/database/run-ast/utils/get-column.js +2 -1
- package/dist/database/run-ast/utils/merge-with-parent-items.js +5 -3
- package/dist/extensions/lib/installation/manager.js +1 -1
- package/dist/extensions/lib/sandbox/register/operation.js +1 -1
- package/dist/extensions/lib/sync/sync.js +1 -1
- package/dist/extensions/manager.js +3 -3
- package/dist/flows.js +5 -5
- package/dist/license/entitlements/lib/collections.js +37 -0
- package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
- package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
- package/dist/license/entitlements/lib/flows.js +29 -0
- package/dist/license/entitlements/lib/seats.js +103 -0
- package/dist/license/entitlements/lib/sso-enabled.js +45 -0
- package/dist/license/entitlements/manager.js +256 -0
- package/dist/license/index.js +4 -0
- package/dist/license/manager.js +505 -0
- package/dist/license/utils/compute-license-status.js +27 -0
- package/dist/license/utils/get-core-grace-expires-at.js +38 -0
- package/dist/license/utils/get-license-key.js +23 -0
- package/dist/license/utils/get-license-token.js +23 -0
- package/dist/license/utils/handle-license-error.js +41 -0
- package/dist/license/utils/is-in-core-grace-period.js +11 -0
- package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
- package/dist/license/utils/use-rpc.js +33 -0
- package/dist/middleware/cache.js +4 -1
- package/dist/middleware/error-handler.js +11 -0
- package/dist/middleware/extract-token.js +11 -2
- package/dist/middleware/is-admin.js +16 -0
- package/dist/middleware/is-locked.js +16 -0
- package/dist/middleware/mcp-oauth-guard.js +23 -0
- package/dist/middleware/request-counter.js +5 -2
- package/dist/packages/types/dist/index.js +117 -122
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
- package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
- package/dist/request/is-denied-ip.js +2 -0
- package/dist/schedules/license.js +31 -0
- package/dist/schedules/oauth-cleanup.js +26 -0
- package/dist/schedules/retention.js +1 -1
- package/dist/schedules/telemetry.js +4 -1
- package/dist/schedules/tus.js +1 -1
- package/dist/schedules/utils/duration-to-cron.js +36 -0
- package/dist/services/activity.js +15 -0
- package/dist/services/authentication.js +12 -5
- package/dist/services/collections.js +40 -10
- package/dist/services/fields.js +6 -6
- package/dist/services/flows.js +12 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -2
- package/dist/services/graphql/resolvers/system-global.js +1 -1
- package/dist/services/graphql/resolvers/system.js +43 -27
- package/dist/services/graphql/schema/get-types.js +28 -7
- package/dist/services/graphql/schema/parse-query.js +8 -0
- package/dist/services/graphql/schema/read.js +12 -0
- package/dist/services/graphql/types/json-filter.js +30 -0
- package/dist/services/index.js +6 -6
- package/dist/services/items.js +32 -14
- package/dist/services/mcp-oauth/cimd.js +307 -0
- package/dist/services/mcp-oauth/index.js +1185 -0
- package/dist/services/mcp-oauth/types/error.js +22 -0
- package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
- package/dist/services/mcp-oauth/utils/domain.js +21 -0
- package/dist/services/mcp-oauth/utils/loopback.js +11 -0
- package/dist/services/mcp-oauth/utils/redirect.js +84 -0
- package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
- package/dist/services/payload.js +2 -1
- package/dist/services/permissions.js +31 -9
- package/dist/services/revisions.js +15 -0
- package/dist/services/server.js +66 -68
- package/dist/services/settings.js +37 -3
- package/dist/services/users.js +23 -6
- package/dist/services/utils.js +6 -1
- package/dist/services/versions.js +160 -70
- package/dist/utils/calculate-field-depth.js +1 -0
- package/dist/utils/create-admin.js +3 -3
- package/dist/utils/deep-freeze.js +24 -0
- package/dist/utils/extract-function-name.js +13 -0
- package/dist/utils/generate-translations.js +5 -5
- package/dist/utils/get-accountability-for-token.js +13 -1
- package/dist/utils/get-cache-key.js +1 -1
- package/dist/utils/get-history-filter-query.js +22 -0
- package/dist/utils/get-schema.js +2 -2
- package/dist/utils/get-service.js +3 -3
- package/dist/utils/is-admin.js +9 -0
- package/dist/utils/is-unauthenticated.js +15 -0
- package/dist/utils/parse-oauth-scope.js +12 -0
- package/dist/utils/sanitize-query.js +2 -2
- package/dist/utils/split-field-path.js +29 -0
- package/dist/utils/store.js +1 -1
- package/dist/utils/transaction.js +2 -2
- package/dist/utils/translations-validation.js +2 -2
- package/dist/utils/validate-query.js +35 -4
- package/dist/utils/validate-user-count-integrity.js +28 -5
- package/dist/utils/verify-session-jwt.js +5 -2
- package/dist/utils/versioning/handle-version.js +131 -48
- package/dist/utils/versioning/remove-circular.js +17 -0
- package/dist/websocket/authenticate.js +2 -1
- package/dist/websocket/collab/collab.js +1 -1
- package/dist/websocket/collab/room.js +1 -1
- package/dist/websocket/controllers/base.js +12 -0
- package/dist/websocket/controllers/graphql.js +1 -1
- package/dist/websocket/handlers/subscribe.js +1 -1
- package/dist/websocket/messages.js +64 -64
- package/dist/websocket/utils/items.js +2 -2
- package/license +90 -80
- package/package.json +33 -32
- package/dist/controllers/mcp.js +0 -31
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import async_handler_default from "../utils/async-handler.js";
|
|
2
2
|
import { respond } from "../middleware/respond.js";
|
|
3
3
|
import { GraphQLService } from "../services/graphql/index.js";
|
|
4
|
+
import is_locked_default from "../middleware/is-locked.js";
|
|
4
5
|
import { parseGraphQL } from "../middleware/graphql.js";
|
|
5
6
|
import { Router } from "express";
|
|
6
7
|
|
|
7
8
|
//#region src/controllers/graphql.ts
|
|
8
9
|
const router = Router();
|
|
10
|
+
router.use(is_locked_default("graphql"));
|
|
9
11
|
router.use("/system", parseGraphQL, async_handler_default(async (req, res, next) => {
|
|
10
12
|
const service = new GraphQLService({
|
|
11
13
|
accountability: req.accountability,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import async_handler_default from "../utils/async-handler.js";
|
|
2
2
|
import { ItemsService } from "../services/items.js";
|
|
3
|
-
import { sanitizeQuery } from "../utils/sanitize-query.js";
|
|
4
3
|
import { respond } from "../middleware/respond.js";
|
|
4
|
+
import { sanitizeQuery } from "../utils/sanitize-query.js";
|
|
5
5
|
import { MetaService } from "../services/meta.js";
|
|
6
6
|
import { validateBatch } from "../middleware/validate-batch.js";
|
|
7
|
+
import is_locked_default from "../middleware/is-locked.js";
|
|
7
8
|
import collection_exists_default from "../middleware/collection-exists.js";
|
|
8
9
|
import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from "@directus/errors";
|
|
9
10
|
import express from "express";
|
|
@@ -11,6 +12,7 @@ import { isSystemCollection } from "@directus/system-data";
|
|
|
11
12
|
|
|
12
13
|
//#region src/controllers/items.ts
|
|
13
14
|
const router = express.Router();
|
|
15
|
+
router.use(is_locked_default("items"));
|
|
14
16
|
router.post("/:collection", collection_exists_default, async_handler_default(async (req, res, next) => {
|
|
15
17
|
if (isSystemCollection(req.params["collection"])) throw new ForbiddenError();
|
|
16
18
|
if (req.singleton) throw new RouteNotFoundError({ path: req.path });
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import async_handler_default from "../utils/async-handler.js";
|
|
2
|
+
import { getEntitlementManager } from "../license/entitlements/manager.js";
|
|
3
|
+
import { respond } from "../middleware/respond.js";
|
|
4
|
+
import { GRACE_PERIOD_MS, getCoreGraceExpiresAt } from "../license/utils/get-core-grace-expires-at.js";
|
|
5
|
+
import { getLicenseManager } from "../license/manager.js";
|
|
6
|
+
import "../license/index.js";
|
|
7
|
+
import is_admin_default from "../middleware/is-admin.js";
|
|
8
|
+
import { InvalidPayloadError } from "@directus/errors";
|
|
9
|
+
import express from "express";
|
|
10
|
+
import { fromZodError } from "zod-validation-error";
|
|
11
|
+
import { ResolveInput } from "@directus/license";
|
|
12
|
+
|
|
13
|
+
//#region src/controllers/license.ts
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
router.get("/", is_admin_default, async_handler_default(async (_req, res, next) => {
|
|
16
|
+
const licenseManager = getLicenseManager();
|
|
17
|
+
const entitlementManager = getEntitlementManager();
|
|
18
|
+
const [license, status, downgradeReason, seatUsage, collectionUsage, flowUsage] = await Promise.all([
|
|
19
|
+
licenseManager.getLicense(),
|
|
20
|
+
licenseManager.getStatus(),
|
|
21
|
+
licenseManager.getDowngradeReason(),
|
|
22
|
+
entitlementManager.getUsage("seats"),
|
|
23
|
+
entitlementManager.getUsage("collections"),
|
|
24
|
+
entitlementManager.getUsage("flows")
|
|
25
|
+
]);
|
|
26
|
+
const source = licenseManager.getSource();
|
|
27
|
+
let expiresAt = license.meta.expires_at;
|
|
28
|
+
let gracePeriod = license.meta.grace_period;
|
|
29
|
+
if (source === null && status === "grace") {
|
|
30
|
+
const coreGraceExpiresAt = await getCoreGraceExpiresAt();
|
|
31
|
+
if (coreGraceExpiresAt !== null) {
|
|
32
|
+
expiresAt = coreGraceExpiresAt;
|
|
33
|
+
gracePeriod = Math.floor(GRACE_PERIOD_MS / 1e3);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const payload = {
|
|
37
|
+
name: license.meta.name,
|
|
38
|
+
status,
|
|
39
|
+
source,
|
|
40
|
+
downgrade_reason: downgradeReason,
|
|
41
|
+
renews_at: license.meta.renews_at,
|
|
42
|
+
expires_at: expiresAt,
|
|
43
|
+
entitlements: license.entitlements,
|
|
44
|
+
grace_period: gracePeriod,
|
|
45
|
+
offline: license.meta.offline,
|
|
46
|
+
usage: {
|
|
47
|
+
seats: seatUsage,
|
|
48
|
+
collections: collectionUsage,
|
|
49
|
+
flows: flowUsage
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
res.locals["payload"] = { data: payload };
|
|
53
|
+
return next();
|
|
54
|
+
}), respond);
|
|
55
|
+
router.post("/", is_admin_default, async_handler_default(async (req, _res, next) => {
|
|
56
|
+
if (!req.body.license_key) throw new InvalidPayloadError({ reason: "A \"license_key\" is required" });
|
|
57
|
+
await getLicenseManager().activate(req.body.license_key);
|
|
58
|
+
return next();
|
|
59
|
+
}), respond);
|
|
60
|
+
router.patch("/", is_admin_default, async_handler_default(async (req, _res, next) => {
|
|
61
|
+
if (!req.body.license_key) throw new InvalidPayloadError({ reason: "A \"license_key\" is required" });
|
|
62
|
+
await getLicenseManager().update(req.body.license_key);
|
|
63
|
+
return next();
|
|
64
|
+
}), respond);
|
|
65
|
+
router.delete("/", is_admin_default, async_handler_default(async (_req, _res, next) => {
|
|
66
|
+
await getLicenseManager().deactivate();
|
|
67
|
+
return next();
|
|
68
|
+
}), respond);
|
|
69
|
+
router.post("/preview", async_handler_default(async (req, res, next) => {
|
|
70
|
+
if (!req.body.license_key) throw new InvalidPayloadError({ reason: "A \"license_key\" is required" });
|
|
71
|
+
const preview = await getLicenseManager().preview(req.body.license_key);
|
|
72
|
+
const payload = {
|
|
73
|
+
plan_name: preview.plan_name,
|
|
74
|
+
expires_at: preview.expires_at,
|
|
75
|
+
renews_at: preview.renews_at,
|
|
76
|
+
production_enabled: preview.entitlements.production_enabled.override ?? preview.entitlements.production_enabled.default
|
|
77
|
+
};
|
|
78
|
+
res.locals["payload"] = { data: payload };
|
|
79
|
+
return next();
|
|
80
|
+
}), respond);
|
|
81
|
+
router.get("/portal", is_admin_default, async_handler_default(async (_req, res) => {
|
|
82
|
+
const portal = await getLicenseManager().billingPortalUrl();
|
|
83
|
+
res.redirect(portal);
|
|
84
|
+
}));
|
|
85
|
+
router.get("/addons", is_admin_default, async_handler_default(async (_req, res, next) => {
|
|
86
|
+
const payload = await getLicenseManager().availableAddons();
|
|
87
|
+
res.locals["payload"] = { data: payload };
|
|
88
|
+
return next();
|
|
89
|
+
}), respond);
|
|
90
|
+
router.patch("/addons/:id", is_admin_default, async_handler_default(async (req, _res, next) => {
|
|
91
|
+
if (typeof req.body.quantity !== "number") throw new InvalidPayloadError({ reason: "A numbered \"quantity\" is required" });
|
|
92
|
+
await getLicenseManager().setAddonQuantity({
|
|
93
|
+
addonId: req.params["id"],
|
|
94
|
+
quantity: req.body.quantity
|
|
95
|
+
});
|
|
96
|
+
return next();
|
|
97
|
+
}), respond);
|
|
98
|
+
router.delete("/addons/:id", is_admin_default, async_handler_default(async (req, _res, next) => {
|
|
99
|
+
await getLicenseManager().removeAddon(req.params["id"]);
|
|
100
|
+
return next();
|
|
101
|
+
}), respond);
|
|
102
|
+
router.post("/pending-resolution", is_admin_default, async_handler_default(async (req, res, next) => {
|
|
103
|
+
const payload = await getLicenseManager().pendingResolution({
|
|
104
|
+
adminId: req.accountability.user,
|
|
105
|
+
licenseKey: req.body.license_key
|
|
106
|
+
});
|
|
107
|
+
res.locals["payload"] = { data: payload };
|
|
108
|
+
return next();
|
|
109
|
+
}), respond);
|
|
110
|
+
router.post("/resolve", is_admin_default, async_handler_default(async (req, _res, next) => {
|
|
111
|
+
const { error, data } = ResolveInput.safeParse(req.body);
|
|
112
|
+
if (error) throw new InvalidPayloadError({ reason: fromZodError(error).message });
|
|
113
|
+
await getLicenseManager().applyResolution(data, { accountability: req.accountability });
|
|
114
|
+
return next();
|
|
115
|
+
}), respond);
|
|
116
|
+
var license_default = router;
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { license_default as default };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import async_handler_default from "../../utils/async-handler.js";
|
|
2
|
+
import { SettingsService } from "../../services/settings.js";
|
|
3
|
+
import is_locked_default from "../../middleware/is-locked.js";
|
|
4
|
+
import { DirectusMCP } from "../../ai/mcp/server.js";
|
|
5
|
+
import "../../ai/mcp/index.js";
|
|
6
|
+
import { useEnv } from "@directus/env";
|
|
7
|
+
import { ForbiddenError } from "@directus/errors";
|
|
8
|
+
import { toBoolean } from "@directus/utils";
|
|
9
|
+
import { Router } from "express";
|
|
10
|
+
|
|
11
|
+
//#region src/controllers/mcp/index.ts
|
|
12
|
+
const router = Router();
|
|
13
|
+
router.use(is_locked_default("mcp"));
|
|
14
|
+
const mcpHandler = async_handler_default(async (req, res) => {
|
|
15
|
+
const env = useEnv();
|
|
16
|
+
const { mcp_enabled, mcp_oauth_enabled, mcp_allow_deletes, mcp_prompts_collection, mcp_system_prompt, mcp_system_prompt_enabled } = await new SettingsService({ schema: req.schema }).readSingleton({ fields: [
|
|
17
|
+
"mcp_enabled",
|
|
18
|
+
"mcp_oauth_enabled",
|
|
19
|
+
"mcp_allow_deletes",
|
|
20
|
+
"mcp_prompts_collection",
|
|
21
|
+
"mcp_system_prompt",
|
|
22
|
+
"mcp_system_prompt_enabled"
|
|
23
|
+
] });
|
|
24
|
+
if (!mcp_enabled) throw new ForbiddenError({ reason: "MCP must be enabled" });
|
|
25
|
+
if (req.accountability?.oauth && (toBoolean(env["MCP_OAUTH_ENABLED"]) !== true || toBoolean(mcp_oauth_enabled) !== true)) throw new ForbiddenError({ reason: "MCP OAuth must be enabled" });
|
|
26
|
+
new DirectusMCP({
|
|
27
|
+
promptsCollection: mcp_prompts_collection,
|
|
28
|
+
allowDeletes: mcp_allow_deletes,
|
|
29
|
+
systemPromptEnabled: mcp_system_prompt_enabled,
|
|
30
|
+
systemPrompt: mcp_system_prompt
|
|
31
|
+
}).handleRequest(req, res);
|
|
32
|
+
});
|
|
33
|
+
router.get("/", mcpHandler);
|
|
34
|
+
router.post("/", mcpHandler);
|
|
35
|
+
var mcp_default = router;
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { mcp_default as default };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import async_handler_default from "../../utils/async-handler.js";
|
|
2
|
+
import { ItemsService } from "../../services/items.js";
|
|
3
|
+
import { respond } from "../../middleware/respond.js";
|
|
4
|
+
import { sanitizeQuery } from "../../utils/sanitize-query.js";
|
|
5
|
+
import { MetaService } from "../../services/meta.js";
|
|
6
|
+
import use_collection_default from "../../middleware/use-collection.js";
|
|
7
|
+
import { validateBatch } from "../../middleware/validate-batch.js";
|
|
8
|
+
import { ForbiddenError } from "@directus/errors";
|
|
9
|
+
import { Router } from "express";
|
|
10
|
+
|
|
11
|
+
//#region src/controllers/mcp/oauth-clients.ts
|
|
12
|
+
const router = Router();
|
|
13
|
+
router.use(use_collection_default("directus_oauth_clients"));
|
|
14
|
+
router.use((req, _res, next) => {
|
|
15
|
+
if (!req.accountability?.admin) throw new ForbiddenError();
|
|
16
|
+
next();
|
|
17
|
+
});
|
|
18
|
+
const readHandler = async_handler_default(async (req, res, next) => {
|
|
19
|
+
const service = new ItemsService("directus_oauth_clients", {
|
|
20
|
+
accountability: req.accountability,
|
|
21
|
+
schema: req.schema
|
|
22
|
+
});
|
|
23
|
+
const metaService = new MetaService({
|
|
24
|
+
accountability: req.accountability,
|
|
25
|
+
schema: req.schema
|
|
26
|
+
});
|
|
27
|
+
const records = await service.readByQuery(req.sanitizedQuery);
|
|
28
|
+
const meta = await metaService.getMetaForQuery("directus_oauth_clients", req.sanitizedQuery);
|
|
29
|
+
res.locals["payload"] = {
|
|
30
|
+
data: records || null,
|
|
31
|
+
meta
|
|
32
|
+
};
|
|
33
|
+
return next();
|
|
34
|
+
});
|
|
35
|
+
router.get("/", validateBatch("read"), readHandler, respond);
|
|
36
|
+
router.search("/", validateBatch("read"), readHandler, respond);
|
|
37
|
+
router.get("/:id", async_handler_default(async (req, res, next) => {
|
|
38
|
+
const record = await new ItemsService("directus_oauth_clients", {
|
|
39
|
+
accountability: req.accountability,
|
|
40
|
+
schema: req.schema
|
|
41
|
+
}).readOne(req.params["id"], req.sanitizedQuery);
|
|
42
|
+
res.locals["payload"] = { data: record || null };
|
|
43
|
+
return next();
|
|
44
|
+
}), respond);
|
|
45
|
+
router.delete("/", validateBatch("delete"), async_handler_default(async (req, _res, next) => {
|
|
46
|
+
const service = new ItemsService("directus_oauth_clients", {
|
|
47
|
+
accountability: req.accountability,
|
|
48
|
+
schema: req.schema
|
|
49
|
+
});
|
|
50
|
+
if (Array.isArray(req.body)) await service.deleteMany(req.body);
|
|
51
|
+
else if (req.body.keys) await service.deleteMany(req.body.keys);
|
|
52
|
+
else {
|
|
53
|
+
const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
|
|
54
|
+
await service.deleteByQuery(sanitizedQuery);
|
|
55
|
+
}
|
|
56
|
+
return next();
|
|
57
|
+
}), respond);
|
|
58
|
+
router.delete("/:id", async_handler_default(async (req, _res, next) => {
|
|
59
|
+
await new ItemsService("directus_oauth_clients", {
|
|
60
|
+
accountability: req.accountability,
|
|
61
|
+
schema: req.schema
|
|
62
|
+
}).deleteOne(req.params["id"]);
|
|
63
|
+
return next();
|
|
64
|
+
}), respond);
|
|
65
|
+
var oauth_clients_default = router;
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { oauth_clients_default as default };
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { Liquid } from "liquidjs";
|
|
4
|
+
import { flatten } from "flat";
|
|
5
|
+
|
|
6
|
+
//#region src/controllers/mcp/oauth-consent-page.ts
|
|
7
|
+
const liquid = new Liquid({
|
|
8
|
+
root: join(dirname(fileURLToPath(import.meta.url)), "templates"),
|
|
9
|
+
extname: ".liquid",
|
|
10
|
+
outputEscape: "escape"
|
|
11
|
+
});
|
|
12
|
+
const logoDataUri = `data:image/svg+xml,${encodeURIComponent(`<svg width="64" height="39" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" d="M51.18 24.61c-.3-.07-.55-.15-.77-.25-.16-.07-.3-.16-.42-.26a.31.31 0 0 1-.1-.27c.11-1.24-.02-2.33.1-3.56.5-5 3.64-3.41 6.46-4.23 1.62-.45 3.24-1.35 3.83-3.1.1-.3.01-.61-.19-.84a36.2 36.2 0 0 0-6.12-5.54A36.73 36.73 0 0 0 27.94.36a.46.46 0 0 0-.33.7 13.7 13.7 0 0 0 4.31 4.24c.31.19.19.6-.17.52a8.2 8.2 0 0 1-2.92-1.26.35.35 0 0 0-.33-.04l-1.64.67a.45.45 0 0 0-.12.75A13.7 13.7 0 0 0 42.8 7.3c.3-.19.8.2.7.55a27 27 0 0 0-.54 2.37c-1.26 6.37-4.9 5.87-9.4 4.27-9-3.26-14.12-.48-18.66-5.98-.31-.38-.87-.51-1.24-.19a4.25 4.25 0 0 0 .43 6.8c.14.1.33.05.44-.08.28-.35.5-.59.8-.74.3-.16.46.29.2.52-.97.85-1.25 1.87-1.88 3.87-.99 3.13-.57 6.34-5.2 7.18-2.45.12-2.4 1.78-3.29 4.25-1.03 2.98-2.39 4.3-4.9 6.9-.34.36-.36.93.01 1.25 1 .85 2.04.9 3.09.46 2.6-1.08 4.6-4.44 6.48-6.61 2.1-2.42 7.15-1.39 10.97-3.76 2.05-1.25 3.29-2.86 2.9-5.27-.07-.38.37-.62.53-.26.31.68.51 1.4.6 2.16.02.2.2.34.39.33 4.12-.23 9.46 4.3 14.44 5.53.3.08.52-.27.35-.53a9.17 9.17 0 0 1-1.3-3.02c-.1-.39.47-.5.66-.14a9.2 9.2 0 0 0 7.4 4.71c1.2.1 2.54-.05 3.93-.47 1.66-.5 3.19-1.14 5.02-.79 1.36.25 2.63.94 3.42 2.1 1.1 1.62 3.45 2.04 4.7.35a.81.81 0 0 0 .08-.8c-2.76-6.43-9.75-6.88-12.75-7.65Z"/></svg>`)}`;
|
|
13
|
+
const baseRules = {
|
|
14
|
+
borderRadius: "0.3125rem",
|
|
15
|
+
borderWidth: "2px",
|
|
16
|
+
primary: "var(--project-color)",
|
|
17
|
+
primaryBackground: "color-mix(in srgb, var(--theme--background), var(--theme--primary) 10%)",
|
|
18
|
+
primarySubdued: "color-mix(in srgb, var(--theme--background), var(--theme--primary) 50%)",
|
|
19
|
+
danger: "#e35169",
|
|
20
|
+
fonts: {
|
|
21
|
+
display: {
|
|
22
|
+
fontFamily: "\"Inter\", system-ui",
|
|
23
|
+
fontWeight: "700"
|
|
24
|
+
},
|
|
25
|
+
sans: {
|
|
26
|
+
fontFamily: "\"Inter\", system-ui",
|
|
27
|
+
fontWeight: "500"
|
|
28
|
+
},
|
|
29
|
+
monospace: {
|
|
30
|
+
fontFamily: "\"Fira Mono\", monospace",
|
|
31
|
+
fontWeight: "500"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
form: { field: { input: {
|
|
35
|
+
background: "var(--theme--background)",
|
|
36
|
+
foreground: "var(--theme--foreground)",
|
|
37
|
+
foregroundSubdued: "var(--theme--foreground-subdued)",
|
|
38
|
+
borderColor: "var(--theme--border-color)",
|
|
39
|
+
borderColorHover: "var(--theme--border-color-accent)",
|
|
40
|
+
padding: "0.875rem"
|
|
41
|
+
} } },
|
|
42
|
+
public: {
|
|
43
|
+
background: "var(--theme--background)",
|
|
44
|
+
foreground: "var(--theme--foreground)",
|
|
45
|
+
foregroundAccent: "var(--theme--foreground-accent)"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const lightRules = {
|
|
49
|
+
...baseRules,
|
|
50
|
+
foreground: "#4f5464",
|
|
51
|
+
foregroundAccent: "#172940",
|
|
52
|
+
foregroundSubdued: "#a2b5cd",
|
|
53
|
+
background: "#fff",
|
|
54
|
+
backgroundNormal: "#f0f4f9",
|
|
55
|
+
backgroundAccent: "#e4eaf1",
|
|
56
|
+
backgroundSubdued: "#f7fafc",
|
|
57
|
+
borderColor: "#e4eaf1",
|
|
58
|
+
borderColorAccent: "#d3dae4",
|
|
59
|
+
borderColorSubdued: "#f0f4f9",
|
|
60
|
+
primaryAccent: "color-mix(in srgb, var(--theme--primary), #2e3c43 25%)"
|
|
61
|
+
};
|
|
62
|
+
const darkRules = {
|
|
63
|
+
...baseRules,
|
|
64
|
+
foreground: "#c9d1d9",
|
|
65
|
+
foregroundAccent: "#f0f6fc",
|
|
66
|
+
foregroundSubdued: "#666672",
|
|
67
|
+
background: "#0d1117",
|
|
68
|
+
backgroundNormal: "#21262e",
|
|
69
|
+
backgroundAccent: "#30363d",
|
|
70
|
+
backgroundSubdued: "#161b22",
|
|
71
|
+
borderColor: "#21262e",
|
|
72
|
+
borderColorAccent: "#30363d",
|
|
73
|
+
borderColorSubdued: "#21262d",
|
|
74
|
+
primaryAccent: "color-mix(in srgb, var(--theme--primary), #16151a 25%)"
|
|
75
|
+
};
|
|
76
|
+
function camelToKebab(str) {
|
|
77
|
+
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
78
|
+
}
|
|
79
|
+
function rulesToCssVars(rules) {
|
|
80
|
+
const flattened = flatten(rules, { delimiter: "--" });
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const [key, value] of Object.entries(flattened)) result[`--theme--${camelToKebab(key)}`] = value;
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function varsToCss(vars) {
|
|
86
|
+
return Object.entries(vars).map(([key, value]) => `${key}: ${value};`).join("\n ");
|
|
87
|
+
}
|
|
88
|
+
const lightCss = varsToCss(rulesToCssVars(lightRules));
|
|
89
|
+
const darkCss = varsToCss(rulesToCssVars(darkRules));
|
|
90
|
+
/**
|
|
91
|
+
* Generate theme CSS with light/dark mode support.
|
|
92
|
+
* Inlines Directus theme tokens as CSS custom properties (same logic as @directus/themes
|
|
93
|
+
* `rulesToCssVars`). Uses `--project-color` as the primary color seed.
|
|
94
|
+
*
|
|
95
|
+
* @param projectColor - Hex color, already validated by `validateProjectColor`
|
|
96
|
+
*/
|
|
97
|
+
function buildStyles(projectColor) {
|
|
98
|
+
return `
|
|
99
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
100
|
+
|
|
101
|
+
:root {
|
|
102
|
+
--project-color: ${projectColor};
|
|
103
|
+
${lightCss}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
[data-theme="dark"] {
|
|
107
|
+
${darkCss}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (prefers-color-scheme: dark) {
|
|
111
|
+
:root:not([data-theme="light"]) {
|
|
112
|
+
${darkCss}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
body {
|
|
117
|
+
font-family: var(--theme--fonts--sans--font-family);
|
|
118
|
+
font-weight: var(--theme--fonts--sans--font-weight);
|
|
119
|
+
background: var(--theme--background-subdued);
|
|
120
|
+
color: var(--theme--public--foreground-accent);
|
|
121
|
+
display: flex;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
align-items: center;
|
|
124
|
+
min-height: 100vh;
|
|
125
|
+
padding: 1rem;
|
|
126
|
+
-webkit-font-smoothing: antialiased;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.card {
|
|
130
|
+
background: var(--theme--public--background);
|
|
131
|
+
color: var(--theme--public--foreground);
|
|
132
|
+
border-radius: calc(var(--theme--border-radius) * 3);
|
|
133
|
+
padding: 1.5rem;
|
|
134
|
+
max-width: 576px;
|
|
135
|
+
width: 100%;
|
|
136
|
+
box-shadow: 0 0 40px 0 rgb(38 50 56 / 0.1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (min-width: 480px) {
|
|
140
|
+
.card { padding: 2.5rem 2.5rem 2rem; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.logo-row {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
gap: 0.75rem;
|
|
148
|
+
margin-bottom: 1.5rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.logo {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
flex-shrink: 0;
|
|
156
|
+
inline-size: 3.125rem;
|
|
157
|
+
block-size: 3.125rem;
|
|
158
|
+
border-radius: calc(var(--theme--border-radius) - 0.125rem);
|
|
159
|
+
background: var(--theme--primary);
|
|
160
|
+
overflow: hidden;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.logo img {
|
|
164
|
+
object-fit: contain;
|
|
165
|
+
object-position: center center;
|
|
166
|
+
block-size: 2.25rem;
|
|
167
|
+
inline-size: 2.25rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
h1 {
|
|
172
|
+
font-family: var(--theme--fonts--display--font-family);
|
|
173
|
+
font-weight: var(--theme--fonts--display--font-weight);
|
|
174
|
+
font-size: 1.25rem;
|
|
175
|
+
text-align: center;
|
|
176
|
+
margin-bottom: 1.5rem;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
h1 strong { font-weight: 700; }
|
|
180
|
+
|
|
181
|
+
.details {
|
|
182
|
+
border: var(--theme--border-width) solid var(--theme--form--field--input--border-color);
|
|
183
|
+
border-radius: var(--theme--border-radius);
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
margin-bottom: 1.25rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.details-label {
|
|
189
|
+
font-size: 0.75rem;
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
color: var(--theme--foreground-subdued);
|
|
192
|
+
text-transform: uppercase;
|
|
193
|
+
letter-spacing: 0.05em;
|
|
194
|
+
padding: 0.625rem var(--theme--form--field--input--padding);
|
|
195
|
+
border-bottom: var(--theme--border-width) solid var(--theme--form--field--input--border-color);
|
|
196
|
+
background: var(--theme--background-subdued);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.detail-row {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: baseline;
|
|
202
|
+
padding: 0.5rem var(--theme--form--field--input--padding);
|
|
203
|
+
font-size: 0.875rem;
|
|
204
|
+
border-bottom: var(--theme--border-width) solid var(--theme--border-color-subdued);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.detail-row:last-child { border-bottom: none; }
|
|
208
|
+
|
|
209
|
+
.detail-key {
|
|
210
|
+
color: var(--theme--foreground-subdued);
|
|
211
|
+
min-width: 7rem;
|
|
212
|
+
flex-shrink: 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@media (max-width: 479px) {
|
|
216
|
+
.detail-row { flex-direction: column; gap: 0.125rem; }
|
|
217
|
+
.detail-key { min-width: 0; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.detail-value {
|
|
221
|
+
color: var(--theme--foreground-accent);
|
|
222
|
+
word-break: break-all;
|
|
223
|
+
font-family: var(--theme--fonts--monospace--font-family);
|
|
224
|
+
font-size: 0.8125rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.detail-value.name {
|
|
228
|
+
font-family: var(--theme--fonts--sans--font-family);
|
|
229
|
+
font-size: 0.875rem;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.note {
|
|
234
|
+
margin-bottom: 1.5rem;
|
|
235
|
+
line-height: 1.6;
|
|
236
|
+
color: var(--theme--foreground-subdued);
|
|
237
|
+
font-size: 0.875rem;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.note strong { color: var(--theme--foreground); }
|
|
241
|
+
|
|
242
|
+
.actions {
|
|
243
|
+
display: flex;
|
|
244
|
+
justify-content: flex-end;
|
|
245
|
+
gap: 0.625rem;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
button {
|
|
249
|
+
padding: 0.5rem 1.25rem;
|
|
250
|
+
border: var(--theme--border-width) solid transparent;
|
|
251
|
+
border-radius: var(--theme--border-radius);
|
|
252
|
+
font-size: 0.875rem;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
font-family: inherit;
|
|
256
|
+
transition: opacity 0.15s;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
button:hover { opacity: 0.85; }
|
|
260
|
+
button:active { opacity: 0.7; }
|
|
261
|
+
|
|
262
|
+
.btn-approve { background: var(--theme--primary); color: #fff; }
|
|
263
|
+
.btn-cancel { background: var(--theme--background); color: var(--theme--foreground); border-color: var(--theme--border-color-accent); }
|
|
264
|
+
|
|
265
|
+
.redirect-warn {
|
|
266
|
+
font-size: 0.75rem;
|
|
267
|
+
color: var(--theme--danger);
|
|
268
|
+
font-family: var(--theme--fonts--sans--font-family);
|
|
269
|
+
}`;
|
|
270
|
+
}
|
|
271
|
+
const DEFAULT_PROJECT_COLOR = "#6644ff";
|
|
272
|
+
function validateProjectColor(color) {
|
|
273
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(color) ? color : DEFAULT_PROJECT_COLOR;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Build shared template variables from page options.
|
|
277
|
+
*
|
|
278
|
+
* Template variables:
|
|
279
|
+
* - `styles` (string, `| raw`): generated CSS with theme variables
|
|
280
|
+
* - `themeAttr` (string, `| raw`): `data-theme="dark|light"` or empty for auto
|
|
281
|
+
* - `logoUrl` / `logoDataUri`: custom project logo URL or default Directus logo as data URI
|
|
282
|
+
* - `projectName`: escaped by Liquid auto-escape, safe for text content
|
|
283
|
+
*/
|
|
284
|
+
function buildTemplateData(opts) {
|
|
285
|
+
const customLogo = !!opts.logoUrl;
|
|
286
|
+
let themeAttr = "";
|
|
287
|
+
if (opts.appearance === "dark") themeAttr = " data-theme=\"dark\"";
|
|
288
|
+
else if (opts.appearance === "light") themeAttr = " data-theme=\"light\"";
|
|
289
|
+
const validColor = validateProjectColor(opts.projectColor);
|
|
290
|
+
return {
|
|
291
|
+
projectName: opts.projectName,
|
|
292
|
+
styles: buildStyles(validColor),
|
|
293
|
+
themeAttr,
|
|
294
|
+
logoClass: "logo",
|
|
295
|
+
logoUrl: customLogo ? opts.logoUrl : null,
|
|
296
|
+
logoDataUri: !customLogo ? logoDataUri : null,
|
|
297
|
+
logoAlt: customLogo ? opts.projectName : "Directus"
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** Render the OAuth consent approval/deny page. */
|
|
301
|
+
async function renderConsentPage(data, opts) {
|
|
302
|
+
return liquid.renderFile("oauth-consent", {
|
|
303
|
+
...buildTemplateData(opts),
|
|
304
|
+
...data
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** Render the OAuth error page (pre-redirect errors that can't be sent to the client). */
|
|
308
|
+
async function renderErrorPage(description, opts) {
|
|
309
|
+
return liquid.renderFile("oauth-error", {
|
|
310
|
+
...buildTemplateData(opts),
|
|
311
|
+
description
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
//#endregion
|
|
316
|
+
export { renderConsentPage, renderErrorPage };
|