adminizer 5.1.0-build.29 → 5.2.0-build.30
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/assets/{abnfDiagram-VCTEODGH-CpSXY7qH.js → abnfDiagram-VCTEODGH-BhZn7BLv.js} +1 -1
- package/assets/{add-DQWffo5j.js → add-DDfJt70P.js} +1 -1
- package/assets/{add-group-OdAd30ej.js → add-group-Cs3S9VVn.js} +1 -1
- package/assets/{add-user-C6cqZSzp.js → add-user-DxI_sJLw.js} +1 -1
- package/assets/ai-assistant/agent.es.js +14201 -11686
- package/assets/app-DM8KzeFz.css +1 -0
- package/assets/app-DOMFUTFw.js +138 -0
- package/assets/{arc-6oG0N2EU.js → arc-BRY7wCfa.js} +1 -1
- package/assets/{architectureDiagram-5GKGNRK7-BO5E3Osk.js → architectureDiagram-5GKGNRK7-DfBT7jAW.js} +1 -1
- package/assets/{blockDiagram-I7D4REHJ-DpKAEeRQ.js → blockDiagram-I7D4REHJ-CW2mcR7Z.js} +1 -1
- package/assets/{c4Diagram-7LVT6UL2-K4ilHoqv.js → c4Diagram-7LVT6UL2-BGsyO-4Y.js} +1 -1
- package/assets/{catalog-BaSowP5U.js → catalog-cyCZOCXs.js} +3 -3
- package/assets/channel-Bd-bsjgh.js +1 -0
- package/assets/{chunk-2Q5K7J3B-CFvbnPgN.js → chunk-2Q5K7J3B-4EnjsYhG.js} +1 -1
- package/assets/{chunk-5VM5RSS4-Cxb3_K7a.js → chunk-5VM5RSS4-BVzmz0IS.js} +1 -1
- package/assets/{chunk-F27PBJKO-Bp-e-RqW.js → chunk-F27PBJKO-Bkf9Z4lt.js} +1 -1
- package/assets/{chunk-IMKFNOWR-D03Bbv3p.js → chunk-IMKFNOWR-Dg6Y3xGC.js} +1 -1
- package/assets/{chunk-JWPE2WC7-BQzIFXbt.js → chunk-JWPE2WC7-BdVNpqE2.js} +1 -1
- package/assets/{chunk-POPQ4Y6H-DGPhwQEk.js → chunk-POPQ4Y6H-0R-etdBM.js} +1 -1
- package/assets/{chunk-SVP7TREG-CFG2uzdK.js → chunk-SVP7TREG-CdABJwLU.js} +1 -1
- package/assets/{chunk-TICWLB2K-BRoATx6O.js → chunk-TICWLB2K-Da-oSw37.js} +1 -1
- package/assets/{chunk-XXDRQBXY-MTSPV1jX.js → chunk-XXDRQBXY-Ba8BsH2Z.js} +1 -1
- package/assets/classDiagram-ZZMXUADV-BftNBmvr.js +1 -0
- package/assets/classDiagram-v2-VYDZK3BY-BftNBmvr.js +1 -0
- package/assets/controls/ckeditor.es.js +10659 -10551
- package/assets/controls/handsontable.es.js +9053 -8479
- package/assets/controls/jsoneditor.es.js +479 -449
- package/assets/controls/leaflet.es.js +672 -663
- package/assets/controls/toast-ui.es.js +13696 -13676
- package/assets/{cose-bilkent-JH36ORCC-Cd04LiHO.js → cose-bilkent-JH36ORCC-BYrWDdVx.js} +1 -1
- package/assets/{cynefin-OW5HDTMX-Cvk-t4EK.js → cynefin-OW5HDTMX-CwHzXvDB.js} +1 -1
- package/assets/{cynefinDiagram-5FMLGOSQ-CHBsEINZ.js → cynefinDiagram-5FMLGOSQ-V6tH6arn.js} +1 -1
- package/assets/{dagre-GXQ25YYZ-Dq-0d5Tl.js → dagre-GXQ25YYZ-CaBvafdG.js} +1 -1
- package/assets/{dashboard-BZo6F7y9.js → dashboard-C27hB1GI.js} +1 -1
- package/assets/{diagram-S7CK7UJ4-BKDTPndI.js → diagram-S7CK7UJ4-85jkE9hd.js} +1 -1
- package/assets/{diagram-UQ7AKVKN-DZYq9mm9.js → diagram-UQ7AKVKN-DHJRq1TM.js} +1 -1
- package/assets/{diagram-VSXAHHWV-CVOsZwCq.js → diagram-VSXAHHWV-C74VLfSc.js} +1 -1
- package/assets/{diagram-VX7I27RA-BCBXgENw.js → diagram-VX7I27RA-CVJcG5hn.js} +1 -1
- package/assets/{diagram-Z3DM3KII-ByNHDStX.js → diagram-Z3DM3KII-B8ybg5RA.js} +1 -1
- package/assets/{docs-CDmOUM3P.js → docs-CK6W0Jko.js} +1 -1
- package/assets/{ebnfDiagram-PWID7BFC-WrVgmCqC.js → ebnfDiagram-PWID7BFC-D_gz_iz6.js} +1 -1
- package/assets/{erDiagram-RLTQ6QDP-1QXTF1US.js → erDiagram-RLTQ6QDP-D3W95aGH.js} +1 -1
- package/assets/{flowDiagram-HODETNUW-DWcydZT-.js → flowDiagram-HODETNUW-D2PTWIEg.js} +1 -1
- package/assets/{ganttDiagram-EL5Y4UJY-BDrQ3uCR.js → ganttDiagram-EL5Y4UJY-TFBUh0C6.js} +1 -1
- package/assets/{gitGraphDiagram-WWUBYQGX-oD2bpMWh.js → gitGraphDiagram-WWUBYQGX-BfFJzeqx.js} +1 -1
- package/assets/{history-CTbCqtJA.js → history-DX6CEQqv.js} +1 -1
- package/assets/{infoDiagram-27XIBGKW-C8opSvmW.js → infoDiagram-27XIBGKW-esGmrWiw.js} +1 -1
- package/assets/{init-user-DDmp5FmX.js → init-user-D1G_dbcZ.js} +1 -1
- package/assets/{ishikawaDiagram-5VMMS53U-CqOCcw38.js → ishikawaDiagram-5VMMS53U--aGHPnva.js} +1 -1
- package/assets/{journeyDiagram-3NMN7TZE-B1OlZWx5.js → journeyDiagram-3NMN7TZE-03BXeE8O.js} +1 -1
- package/assets/{kanban-definition-UXKFOSKX-DIEdmOZD.js → kanban-definition-UXKFOSKX-JdDv2WHg.js} +1 -1
- package/assets/{layout-Cznoi1Xi.js → layout-Bz6WKClu.js} +1 -1
- package/assets/{linear-DbhwLrTs.js → linear-Chzx_9mb.js} +1 -1
- package/assets/{list-X8LYs75t.js → list-BuSdZ2Ic.js} +1 -1
- package/assets/{login-D0flIAhA.js → login-DFWfnrmE.js} +1 -1
- package/assets/manifest.json +277 -277
- package/assets/media-manager-CT4LZgo_.js +7 -0
- package/assets/mermaid.core-Dp-0i8FJ.js +310 -0
- package/assets/{mindmap-definition-YA3MSWOX-G8OJl_uT.js → mindmap-definition-YA3MSWOX-BJSEYobY.js} +1 -1
- package/assets/{module-D347sEFr.js → module-Cgn-q5gZ.js} +1 -1
- package/assets/{notification-C7usJcF-.js → notification-BrfoqVVT.js} +1 -1
- package/assets/{pegDiagram-XKGWAZYB-CqOymfUM.js → pegDiagram-XKGWAZYB-ccVsKwl1.js} +1 -1
- package/assets/{pieDiagram-E7YTZNPT-C_nbNUey.js → pieDiagram-E7YTZNPT-B1XZVgNq.js} +1 -1
- package/assets/{quadrantDiagram-AXDQQJYC-RDCWL_jp.js → quadrantDiagram-AXDQQJYC-CdABEYmF.js} +1 -1
- package/assets/{railroadDiagram-O6MQD6OU-Bi_j7Fb_.js → railroadDiagram-O6MQD6OU-OX83MXuO.js} +1 -1
- package/assets/{register-DhdaEsna.js → register-Cxf3Pow-.js} +1 -1
- package/assets/{requirementDiagram-BXWQKSXE-Cg6IzoFA.js → requirementDiagram-BXWQKSXE-CD7swf9u.js} +1 -1
- package/assets/{ru-DMAUOdKr.js → ru-DxiFV21N.js} +1 -1
- package/assets/{sankeyDiagram-P5KCCOFB-BNN3S4hm.js → sankeyDiagram-P5KCCOFB-Cgwq2DVJ.js} +1 -1
- package/assets/{sequenceDiagram-WJ2MYXX4-C9UEgNLg.js → sequenceDiagram-WJ2MYXX4-BgjP07Zh.js} +1 -1
- package/assets/{sizeCapture-INFHLROL-DH9gM8J8.js → sizeCapture-INFHLROL-B-jQ1aKT.js} +1 -1
- package/assets/{stateDiagram-D77RDMKH-CzuNaJnp.js → stateDiagram-D77RDMKH-Drot14NK.js} +1 -1
- package/assets/stateDiagram-v2-MP3YSRHH-C5VLF0H1.js +1 -0
- package/assets/{swimlanes-42K2YHIH-DlfHkCIp.js → swimlanes-42K2YHIH-lCqHhK1l.js} +1 -1
- package/assets/swimlanesDiagram-VR7AAH4N-4yyX1ZbA.js +8 -0
- package/assets/{timeline-definition-24CTP7MA-B45WKY9z.js → timeline-definition-24CTP7MA-B-QLcvwc.js} +1 -1
- package/assets/{use-appearance-DPeAg6kc.js → use-appearance-B0ck1CR_.js} +1 -1
- package/assets/{user-filters-list-B-txVF_a.js → user-filters-list-ce2cmS5Z.js} +1 -1
- package/assets/{vennDiagram-4TSXK5OY-BARlq8EX.js → vennDiagram-4TSXK5OY-2DN2uspP.js} +1 -1
- package/assets/{wardleyDiagram-VM6X3IG4-DSGKYXrr.js → wardleyDiagram-VM6X3IG4-C_GODc0p.js} +1 -1
- package/assets/{welcome-Cdhwf3y4.js → welcome-DaAj6jOI.js} +1 -1
- package/assets/with-app-layout-CwAECGwY.js +34 -0
- package/assets/{xychartDiagram-S5SC5T6Z-DPA8cj4H.js → xychartDiagram-S5SC5T6Z-Cq8htGoW.js} +1 -1
- package/controllers/initUser.js +1 -1
- package/helpers/inertiaAddHelper.js +10 -1
- package/helpers/inertiaMenuHelper.d.ts +3 -2
- package/helpers/inertiaMenuHelper.js +9 -4
- package/helpers/navigationAccessHelper.d.ts +5 -0
- package/helpers/navigationAccessHelper.js +13 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/interfaces/adminpanelConfig.d.ts +7 -0
- package/lib/Adminizer.d.ts +2 -0
- package/lib/Adminizer.js +3 -0
- package/lib/admin-links/AdminLinkHandler.js +5 -1
- package/lib/app-manager/AdminizerApp.d.ts +34 -0
- package/lib/app-manager/AppManager.js +35 -0
- package/lib/app-manager/ContextHandler.d.ts +240 -0
- package/lib/app-manager/ContextHandler.js +446 -0
- package/lib/app-manager/ControllerHandler.d.ts +2 -0
- package/lib/app-manager/ControllerHandler.js +7 -0
- package/package.json +3 -2
- package/system/bindInertia.js +34 -9
- package/assets/app-DMN-4aCp.js +0 -138
- package/assets/app-TCCHARSx.css +0 -1
- package/assets/channel-Dh5UPZKs.js +0 -1
- package/assets/classDiagram-ZZMXUADV-a_pfQoRf.js +0 -1
- package/assets/classDiagram-v2-VYDZK3BY-a_pfQoRf.js +0 -1
- package/assets/media-manager-cyrZjDu3.js +0 -6
- package/assets/mermaid.core-DtG9mjld.js +0 -310
- package/assets/stateDiagram-v2-MP3YSRHH-Cm0oV4uL.js +0 -1
- package/assets/swimlanesDiagram-VR7AAH4N-Cc5GTkO2.js +0 -8
- package/assets/with-app-layout-CcoboJFc.js +0 -31
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { Adminizer } from "../Adminizer.js";
|
|
2
|
+
import { filterAccessibleHrefItems, listAccessibleMenuItems, resolveMenuBadge } from "../../helpers/navigationAccessHelper.js";
|
|
3
|
+
import { requirePermission } from "../../policies/authPolicies.js";
|
|
4
|
+
export const DEFAULT_ADMIN_CONTEXT_ID = "default";
|
|
5
|
+
const CONTEXT_COOKIE = "adminizer_context";
|
|
6
|
+
/** Registry of layout contexts contributed by the host and by apps. */
|
|
7
|
+
export class ContextHandler {
|
|
8
|
+
adminizer;
|
|
9
|
+
contexts = new Map();
|
|
10
|
+
assignments = new Set();
|
|
11
|
+
reportedMissing = new Set();
|
|
12
|
+
defaultOptions = {};
|
|
13
|
+
constructor(adminizer) {
|
|
14
|
+
this.adminizer = adminizer;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Registers a context. The returned scope adds pages to it; they are
|
|
18
|
+
* registered as `owner`'s controllers.
|
|
19
|
+
*/
|
|
20
|
+
register(context, owner = "host") {
|
|
21
|
+
const id = context.id?.trim();
|
|
22
|
+
if (!id || !context.title)
|
|
23
|
+
throw new Error("Admin context requires id and title");
|
|
24
|
+
if (id === DEFAULT_ADMIN_CONTEXT_ID)
|
|
25
|
+
throw new Error(`Admin context id "${id}" is reserved; use configureDefault()`);
|
|
26
|
+
if (this.contexts.has(id))
|
|
27
|
+
throw new Error(`Admin context "${id}" is already registered`);
|
|
28
|
+
const prefix = this.normalizePrefix(context.prefix);
|
|
29
|
+
if (!prefix)
|
|
30
|
+
throw new Error(`Admin context "${id}" requires a non-empty prefix`);
|
|
31
|
+
for (const other of this.contexts.values()) {
|
|
32
|
+
if (other.prefix === prefix)
|
|
33
|
+
throw new Error(`Admin context prefix "${prefix}" is already used by "${other.id}"`);
|
|
34
|
+
}
|
|
35
|
+
// Own copy: page() and menuItem() append to the items.
|
|
36
|
+
const navbar = typeof context.navbar === "object"
|
|
37
|
+
? { ...context.navbar, items: [...(context.navbar.items ?? [])] }
|
|
38
|
+
: context.navbar;
|
|
39
|
+
this.contexts.set(id, { ...context, id, prefix, navbar, owner });
|
|
40
|
+
this.adminizer.emitter.emit("app:context:registered", { appName: owner, resourceId: `${owner}:${id}`, id, prefix });
|
|
41
|
+
return this.scope(id, {
|
|
42
|
+
controller: (controller) => this.adminizer.controllerHandler.register(owner, controller),
|
|
43
|
+
component: (url) => url,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
unregister(id, owner) {
|
|
47
|
+
const record = this.contexts.get(id);
|
|
48
|
+
if (!record || (owner && record.owner !== owner))
|
|
49
|
+
return;
|
|
50
|
+
this.contexts.delete(id);
|
|
51
|
+
this.adminizer.emitter.emit("app:context:unregistered", { appName: record.owner, resourceId: `${record.owner}:${id}`, id });
|
|
52
|
+
}
|
|
53
|
+
/** A scope adding pages to a registered context through the given registrar. */
|
|
54
|
+
scope(id, registrar) {
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
page: (page) => this.addPage(id, page, registrar),
|
|
58
|
+
controller: (controller) => this.addController(id, controller, registrar),
|
|
59
|
+
menuItem: (item) => this.addMenuItem(id, item),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Overrides title, icon, layout or navbar of the built-in context. */
|
|
63
|
+
configureDefault(options) {
|
|
64
|
+
this.defaultOptions = { ...this.defaultOptions, ...options };
|
|
65
|
+
}
|
|
66
|
+
getDefault() {
|
|
67
|
+
return {
|
|
68
|
+
title: this.adminizer.menuHelper.getBrandTitle(),
|
|
69
|
+
...this.defaultOptions,
|
|
70
|
+
id: DEFAULT_ADMIN_CONTEXT_ID,
|
|
71
|
+
prefix: "",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
get(id) {
|
|
75
|
+
return id === DEFAULT_ADMIN_CONTEXT_ID ? this.getDefault() : this.contexts.get(id);
|
|
76
|
+
}
|
|
77
|
+
list() {
|
|
78
|
+
return [this.getDefault(), ...this.contexts.values()];
|
|
79
|
+
}
|
|
80
|
+
/** Contexts this user may enter, the default one first. */
|
|
81
|
+
async listAccessible(user) {
|
|
82
|
+
const accessible = [];
|
|
83
|
+
for (const context of this.list()) {
|
|
84
|
+
if (await this.isAccessible(context, user))
|
|
85
|
+
accessible.push(context);
|
|
86
|
+
}
|
|
87
|
+
return accessible;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Declares that the pages at `path` (below `routePrefix`, `:params`
|
|
91
|
+
* allowed) belong to a context: they render in it wherever the user came
|
|
92
|
+
* from, and only users who may enter the context may open them. Covers
|
|
93
|
+
* everything below `path` too unless `exact`. Returns a function removing
|
|
94
|
+
* the declaration.
|
|
95
|
+
*/
|
|
96
|
+
assign(path, contextId, options = {}) {
|
|
97
|
+
const relative = this.normalizePrefix(path);
|
|
98
|
+
const assignment = {
|
|
99
|
+
contextId,
|
|
100
|
+
owner: options.owner ?? "host",
|
|
101
|
+
matcher: this.compilePattern(`${this.root()}${relative}`, options.exact ?? false),
|
|
102
|
+
specificity: this.specificity(relative),
|
|
103
|
+
};
|
|
104
|
+
this.assignments.add(assignment);
|
|
105
|
+
return () => {
|
|
106
|
+
this.assignments.delete(assignment);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The context the page at `url` is declared to belong to: by `assign()`,
|
|
111
|
+
* a controller's `context` or a model's `context`. The most specific
|
|
112
|
+
* declaration wins. Login, logout, registration and the first-user setup
|
|
113
|
+
* never belong to a context.
|
|
114
|
+
*/
|
|
115
|
+
declaredFor(url) {
|
|
116
|
+
const path = this.normalizePath(url);
|
|
117
|
+
if (this.isAuthPath(path))
|
|
118
|
+
return undefined;
|
|
119
|
+
let best;
|
|
120
|
+
for (const assignment of this.assignments) {
|
|
121
|
+
if (assignment.matcher.test(path) && (!best || assignment.specificity > best.specificity))
|
|
122
|
+
best = assignment;
|
|
123
|
+
}
|
|
124
|
+
const model = this.modelDeclaration(path);
|
|
125
|
+
if (model && (!best || model.specificity > best.specificity))
|
|
126
|
+
best = model;
|
|
127
|
+
return best?.contextId;
|
|
128
|
+
}
|
|
129
|
+
/** True when the page at `url` belongs to a context this user may not enter. */
|
|
130
|
+
async isBlocked(url, user) {
|
|
131
|
+
const contextId = this.declaredFor(url);
|
|
132
|
+
if (contextId === undefined)
|
|
133
|
+
return false;
|
|
134
|
+
const context = this.get(contextId);
|
|
135
|
+
if (!context)
|
|
136
|
+
return true;
|
|
137
|
+
if (!this.adminizer.config.auth?.enable || !context.accessRightsToken)
|
|
138
|
+
return false;
|
|
139
|
+
return !user || !await this.isAccessible(context, user);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Blocks the pages declared to belong to a context the user may not
|
|
143
|
+
* enter, before any handler and for every method: not logged in — the
|
|
144
|
+
* login page (401 for API calls), no context token — 403, a context that
|
|
145
|
+
* is not registered (a typo, a disabled app) — 404. Pages without a
|
|
146
|
+
* declaration pass untouched.
|
|
147
|
+
*/
|
|
148
|
+
guard() {
|
|
149
|
+
return (req, res, next) => {
|
|
150
|
+
const path = this.requestPath(req);
|
|
151
|
+
const contextId = this.declaredFor(path);
|
|
152
|
+
if (contextId === undefined)
|
|
153
|
+
return next();
|
|
154
|
+
const context = this.get(contextId);
|
|
155
|
+
const mode = this.isPageRequest(req) ? "ui" : "api";
|
|
156
|
+
if (!context) {
|
|
157
|
+
if (!this.reportedMissing.has(contextId)) {
|
|
158
|
+
this.reportedMissing.add(contextId);
|
|
159
|
+
Adminizer.log.error(`Layout context "${contextId}" is declared for ${path} but not registered; its pages answer 404`);
|
|
160
|
+
}
|
|
161
|
+
if (mode === "ui")
|
|
162
|
+
return res.sendStatus(404);
|
|
163
|
+
return res.status(404).json({ error: req.i18n?.__("Not found") || "Not found" });
|
|
164
|
+
}
|
|
165
|
+
if (!context.accessRightsToken)
|
|
166
|
+
return next();
|
|
167
|
+
return requirePermission(context.accessRightsToken, { mode })(req, res, next);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The context a request belongs to.
|
|
172
|
+
*
|
|
173
|
+
* A page declared to belong to a context renders in it. A URL under a
|
|
174
|
+
* context prefix belongs to that context. Any other URL (the built-in
|
|
175
|
+
* `/model/...` pages, for one) stays in the context the user came from —
|
|
176
|
+
* remembered in a cookie — as long as that context's navbar links to it;
|
|
177
|
+
* otherwise it falls back to the default context.
|
|
178
|
+
*/
|
|
179
|
+
async resolve(req) {
|
|
180
|
+
const path = this.requestPath(req);
|
|
181
|
+
const user = req.user;
|
|
182
|
+
const declared = this.declaredFor(path);
|
|
183
|
+
if (declared !== undefined) {
|
|
184
|
+
// A declared context the user may not enter never renders: guard() answers first.
|
|
185
|
+
const context = this.get(declared);
|
|
186
|
+
return context && user && await this.isAccessible(context, user) ? context : this.getDefault();
|
|
187
|
+
}
|
|
188
|
+
let byPrefix;
|
|
189
|
+
for (const context of this.contexts.values()) {
|
|
190
|
+
const full = this.fullPrefix(context);
|
|
191
|
+
if ((path === full || path.startsWith(`${full}/`)) && (!byPrefix || context.prefix.length > byPrefix.prefix.length)) {
|
|
192
|
+
byPrefix = context;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (byPrefix) {
|
|
196
|
+
return user && await this.isAccessible(byPrefix, user) ? byPrefix : this.getDefault();
|
|
197
|
+
}
|
|
198
|
+
const remembered = this.contexts.get(this.readCookie(req) ?? "");
|
|
199
|
+
if (remembered && user && typeof remembered.navbar === "object"
|
|
200
|
+
&& await this.isAccessible(remembered, user) && this.claims(remembered.navbar.items ?? [], path)) {
|
|
201
|
+
return remembered;
|
|
202
|
+
}
|
|
203
|
+
return this.getDefault();
|
|
204
|
+
}
|
|
205
|
+
/** Remembers the context of a page visit; see {@link resolve}. */
|
|
206
|
+
remember(req, res, context) {
|
|
207
|
+
if (req.method !== "GET" || (this.readCookie(req) ?? DEFAULT_ADMIN_CONTEXT_ID) === context.id)
|
|
208
|
+
return;
|
|
209
|
+
res.cookie(CONTEXT_COOKIE, context.id, { path: this.root() || "/", sameSite: "lax", httpOnly: true });
|
|
210
|
+
}
|
|
211
|
+
/** The untranslated menu of a context for this user. */
|
|
212
|
+
async listMenuItems(context, user) {
|
|
213
|
+
if (!context.navbar || context.navbar === "inherit") {
|
|
214
|
+
return context.id === DEFAULT_ADMIN_CONTEXT_ID
|
|
215
|
+
? listAccessibleMenuItems(this.adminizer, user)
|
|
216
|
+
: this.listMenuItems(this.getDefault(), user);
|
|
217
|
+
}
|
|
218
|
+
const menu = [];
|
|
219
|
+
for (const item of context.navbar.items ?? []) {
|
|
220
|
+
if (await this.isBlocked(item.link, user))
|
|
221
|
+
continue;
|
|
222
|
+
const actions = await filterAccessibleHrefItems(this.adminizer, user, item.subItems ?? []);
|
|
223
|
+
const tokens = actions.map((action) => action.accessRightsToken).filter(Boolean);
|
|
224
|
+
if (item.accessRightsToken)
|
|
225
|
+
tokens.push(item.accessRightsToken);
|
|
226
|
+
if (!await this.adminizer.accessRightsHelper.checkAnyPermission(tokens, user))
|
|
227
|
+
continue;
|
|
228
|
+
menu.push({
|
|
229
|
+
id: item.id || item.title.replace(" ", "_"),
|
|
230
|
+
link: item.link,
|
|
231
|
+
title: item.title,
|
|
232
|
+
type: item.type,
|
|
233
|
+
actions: item.subItems ? actions : null,
|
|
234
|
+
icon: item.icon || null,
|
|
235
|
+
accessRightsToken: item.accessRightsToken || null,
|
|
236
|
+
section: item.section || context.title,
|
|
237
|
+
badge: await resolveMenuBadge(user, item.badge, item.id),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return menu;
|
|
241
|
+
}
|
|
242
|
+
getSections(context) {
|
|
243
|
+
if (typeof context.navbar === "object")
|
|
244
|
+
return { ...(context.navbar.sections ?? {}) };
|
|
245
|
+
return context.id === DEFAULT_ADMIN_CONTEXT_ID
|
|
246
|
+
? this.adminizer.menuHelper.getSections()
|
|
247
|
+
: this.getSections(this.getDefault());
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The layout of a context with inheritance from the default context
|
|
251
|
+
* applied. Layers always add up along the chain: a module over a parent
|
|
252
|
+
* keeps the parent's layers, whatever they replace.
|
|
253
|
+
*/
|
|
254
|
+
resolveLayout(context) {
|
|
255
|
+
const stock = { root: "stock", layers: [], stylesheets: [] };
|
|
256
|
+
const parent = context.id === DEFAULT_ADMIN_CONTEXT_ID ? stock : this.resolveLayout(this.getDefault());
|
|
257
|
+
const layout = context.layout ?? "inherit";
|
|
258
|
+
if (layout === "inherit")
|
|
259
|
+
return parent;
|
|
260
|
+
if (layout === "stock" || layout === "default")
|
|
261
|
+
return stock;
|
|
262
|
+
if (layout === "none" || layout === "bare")
|
|
263
|
+
return { root: "none", layers: [], stylesheets: [] };
|
|
264
|
+
if ("component" in layout) {
|
|
265
|
+
// A whole layout of its own: nothing of the parent is drawn, as before.
|
|
266
|
+
return {
|
|
267
|
+
root: "stock",
|
|
268
|
+
layers: [{ module: layout.component, default: "Layout" }],
|
|
269
|
+
stylesheets: layout.stylesheet ? [layout.stylesheet] : [],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const module = "module" in layout ? layout.module : layout.overrides;
|
|
273
|
+
const base = "module" in layout && layout.base === "stock" ? stock : parent;
|
|
274
|
+
return {
|
|
275
|
+
root: base.root,
|
|
276
|
+
layers: [...base.layers, { module }],
|
|
277
|
+
stylesheets: layout.stylesheet ? [...base.stylesheets, layout.stylesheet] : base.stylesheets,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
toProps(context) {
|
|
281
|
+
return {
|
|
282
|
+
id: context.id,
|
|
283
|
+
title: context.title,
|
|
284
|
+
icon: context.icon ?? null,
|
|
285
|
+
prefix: this.fullPrefix(context),
|
|
286
|
+
link: `${this.root()}${context.home ? this.normalizePrefix(context.home) : context.prefix}` || "/",
|
|
287
|
+
layout: this.resolveLayout(context),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
addPage(id, page, registrar) {
|
|
291
|
+
const record = this.require(id);
|
|
292
|
+
if (!page.id || !page.title)
|
|
293
|
+
throw new Error(`A page of admin context "${id}" requires id and title`);
|
|
294
|
+
if ((page.component === undefined) === (page.middleware === undefined)) {
|
|
295
|
+
throw new Error(`Page "${page.id}" of admin context "${id}" requires either component or middleware`);
|
|
296
|
+
}
|
|
297
|
+
const menu = page.menu ?? record.navbar !== "inherit";
|
|
298
|
+
const moduleComponent = page.component === undefined ? undefined : registrar.component(page.component);
|
|
299
|
+
const link = this.addController(id, {
|
|
300
|
+
id: `context:${id}:page:${page.id}`,
|
|
301
|
+
method: page.method ?? "get",
|
|
302
|
+
path: page.path,
|
|
303
|
+
policies: page.policies,
|
|
304
|
+
middleware: page.middleware ?? this.renderPage(id, page, moduleComponent),
|
|
305
|
+
}, registrar);
|
|
306
|
+
const item = { id: page.id, title: page.title, link, type: "self" };
|
|
307
|
+
if (page.icon)
|
|
308
|
+
item.icon = page.icon;
|
|
309
|
+
if (page.section)
|
|
310
|
+
item.section = page.section;
|
|
311
|
+
if (page.badge !== undefined)
|
|
312
|
+
item.badge = page.badge;
|
|
313
|
+
if (page.accessRightsToken)
|
|
314
|
+
item.accessRightsToken = page.accessRightsToken;
|
|
315
|
+
if (menu)
|
|
316
|
+
this.addMenuItem(id, item);
|
|
317
|
+
return item;
|
|
318
|
+
}
|
|
319
|
+
addController(id, controller, registrar) {
|
|
320
|
+
const record = this.require(id);
|
|
321
|
+
const { path, policies, ...rest } = controller;
|
|
322
|
+
return registrar.controller({
|
|
323
|
+
...rest,
|
|
324
|
+
route: `${record.prefix}${this.normalizePrefix(path)}`,
|
|
325
|
+
policies: policies ?? [{ type: "auth", mode: controller.method === "get" ? "ui" : "api" }],
|
|
326
|
+
context: id,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
addMenuItem(id, item) {
|
|
330
|
+
const record = this.require(id);
|
|
331
|
+
if (record.navbar === "inherit") {
|
|
332
|
+
throw new Error(`Admin context "${id}" inherits its navbar; give it a navbar of its own to add "${item.id}"`);
|
|
333
|
+
}
|
|
334
|
+
record.navbar ??= { items: [] };
|
|
335
|
+
(record.navbar.items ??= []).push(item);
|
|
336
|
+
}
|
|
337
|
+
/** Renders a `component` page: the module page with context breadcrumbs. */
|
|
338
|
+
renderPage(id, page, moduleComponent) {
|
|
339
|
+
return async (req) => {
|
|
340
|
+
const context = this.require(id);
|
|
341
|
+
const props = typeof page.props === "function" ? await page.props(req) : page.props ?? {};
|
|
342
|
+
const contextTitle = req.i18n.__(context.title);
|
|
343
|
+
const title = req.i18n.__(page.title);
|
|
344
|
+
return req.Inertia.render({
|
|
345
|
+
component: "module",
|
|
346
|
+
props: {
|
|
347
|
+
moduleComponent,
|
|
348
|
+
title: `${title} · ${contextTitle}`,
|
|
349
|
+
breadcrumbs: [{ title: contextTitle, href: this.fullPrefix(context) }, { title }],
|
|
350
|
+
...props,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
require(id) {
|
|
356
|
+
const record = this.contexts.get(id);
|
|
357
|
+
if (!record)
|
|
358
|
+
throw new Error(`Admin context "${id}" is not registered`);
|
|
359
|
+
return record;
|
|
360
|
+
}
|
|
361
|
+
async isAccessible(context, user) {
|
|
362
|
+
return !context.accessRightsToken
|
|
363
|
+
|| await this.adminizer.accessRightsHelper.checkPermission(context.accessRightsToken, user);
|
|
364
|
+
}
|
|
365
|
+
claims(items, path) {
|
|
366
|
+
return items.some((item) => {
|
|
367
|
+
const link = typeof item.link === "string" ? item.link.split(/[?#]/)[0].replace(/\/+$/, "") : "";
|
|
368
|
+
return (link.startsWith("/") && (path === link || path.startsWith(`${link}/`)))
|
|
369
|
+
|| this.claims(item.subItems ?? [], path);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/** A model page `/model/<name>/...` of a model whose config declares a context. */
|
|
373
|
+
modelDeclaration(path) {
|
|
374
|
+
const prefix = `${this.root()}/model/`;
|
|
375
|
+
if (!path.startsWith(prefix))
|
|
376
|
+
return undefined;
|
|
377
|
+
let name;
|
|
378
|
+
try {
|
|
379
|
+
name = decodeURIComponent(path.slice(prefix.length).split("/")[0]);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const contextId = name ? this.findModelConfig(name)?.context : undefined;
|
|
385
|
+
return contextId ? { contextId, specificity: this.specificity(`/model/${name}`) } : undefined;
|
|
386
|
+
}
|
|
387
|
+
/** Model config by resource name, matched the way the model routes match it. */
|
|
388
|
+
findModelConfig(name) {
|
|
389
|
+
const models = this.adminizer.config.models ?? {};
|
|
390
|
+
const key = Object.prototype.hasOwnProperty.call(models, name)
|
|
391
|
+
? name
|
|
392
|
+
: (() => {
|
|
393
|
+
const keys = Object.keys(models).filter((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
394
|
+
return keys.length === 1 ? keys[0] : undefined;
|
|
395
|
+
})();
|
|
396
|
+
const config = key ? models[key] : undefined;
|
|
397
|
+
return config && typeof config === "object" ? config : undefined;
|
|
398
|
+
}
|
|
399
|
+
isAuthPath(path) {
|
|
400
|
+
const root = this.root();
|
|
401
|
+
if (path === `${root}/init_user` || path.startsWith(`${root}/init_user/`))
|
|
402
|
+
return true;
|
|
403
|
+
if (!path.startsWith(`${root}/model/`))
|
|
404
|
+
return false;
|
|
405
|
+
const action = path.slice(root.length + "/model/".length).split("/")[1];
|
|
406
|
+
return action === "login" || action === "logout" || action === "register";
|
|
407
|
+
}
|
|
408
|
+
/** A route pattern as a regexp: `:param`, `*splat` and `{optional}` parts of Express routes. */
|
|
409
|
+
compilePattern(pattern, exact) {
|
|
410
|
+
const source = pattern
|
|
411
|
+
.replace(/[.+?^$()|[\]\\]/g, "\\$&")
|
|
412
|
+
.replace(/:[A-Za-z0-9_]+/g, "[^/]+")
|
|
413
|
+
.replace(/\*[A-Za-z0-9_]*/g, ".+")
|
|
414
|
+
.replace(/\{/g, "(?:")
|
|
415
|
+
.replace(/\}/g, ")?");
|
|
416
|
+
return new RegExp(`^${source}${exact ? "" : "(?:/.*)?"}$`);
|
|
417
|
+
}
|
|
418
|
+
/** Static segments weigh more than parameters. */
|
|
419
|
+
specificity(relativePath) {
|
|
420
|
+
return relativePath.split("/").filter(Boolean)
|
|
421
|
+
.reduce((score, segment) => score + (/^[:*]/.test(segment) ? 1 : 2), 0);
|
|
422
|
+
}
|
|
423
|
+
isPageRequest(req) {
|
|
424
|
+
return req.headers["x-inertia"] !== undefined || (req.headers.accept ?? "").includes("text/html");
|
|
425
|
+
}
|
|
426
|
+
root() {
|
|
427
|
+
return (this.adminizer.config.routePrefix || "").replace(/\/+$/, "");
|
|
428
|
+
}
|
|
429
|
+
fullPrefix(context) {
|
|
430
|
+
return `${this.root()}${context.prefix}`;
|
|
431
|
+
}
|
|
432
|
+
normalizePrefix(prefix) {
|
|
433
|
+
const trimmed = (prefix ?? "").trim().replace(/\/+$/, "");
|
|
434
|
+
return trimmed && !trimmed.startsWith("/") ? `/${trimmed}` : trimmed;
|
|
435
|
+
}
|
|
436
|
+
normalizePath(url) {
|
|
437
|
+
return (url || "").split(/[?#]/)[0].replace(/\/+$/, "");
|
|
438
|
+
}
|
|
439
|
+
requestPath(req) {
|
|
440
|
+
return this.normalizePath(req.originalUrl || req.url || "");
|
|
441
|
+
}
|
|
442
|
+
readCookie(req) {
|
|
443
|
+
const value = req.cookies?.[CONTEXT_COOKIE];
|
|
444
|
+
return typeof value === "string" ? value : undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -19,6 +19,12 @@ export class ControllerHandler {
|
|
|
19
19
|
path: fullPath,
|
|
20
20
|
enabled: true,
|
|
21
21
|
};
|
|
22
|
+
if (controller.context) {
|
|
23
|
+
record.unassign = this.adminizer.contextHandler.assign(controller.route, controller.context, {
|
|
24
|
+
owner: appName,
|
|
25
|
+
exact: true,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
22
28
|
const middleware = bindWithMiddlewares(this.adminizer, this.adminizer.config.middlewares ?? [], (req, res, next) => {
|
|
23
29
|
if (!record.enabled) {
|
|
24
30
|
return next();
|
|
@@ -69,6 +75,7 @@ export class ControllerHandler {
|
|
|
69
75
|
return;
|
|
70
76
|
}
|
|
71
77
|
this.removeRoute(record.path, record.method);
|
|
78
|
+
record.unassign?.();
|
|
72
79
|
this.controllers.delete(id);
|
|
73
80
|
this.adminizer.emitter.emit("app:controller:unregistered", {
|
|
74
81
|
appName: record.appName,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adminizer",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.2.0-build.30",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.js",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"build": "npm run copy:backend && npm run compile:backend && npm run compile:ui && npm run build:assets",
|
|
31
31
|
"build:catalog-modules": "cross-env CATALOG_COMPONENT=catalogAction vite build --config fixture/virtual-catalog/vite.config.module.ts && cross-env CATALOG_COMPONENT=Group vite build --config fixture/virtual-catalog/vite.config.module.ts",
|
|
32
32
|
"start:typeorm:seed": "cross-env ORM=typeorm tsx --tsconfig ./fixture/tsconfig.json ./fixture/index.ts",
|
|
33
|
-
"build:apps": "
|
|
33
|
+
"build:context-apps": "cross-env CONTEXT_ENTRY=ContextDemoPage vite build --config fixture/apps/contexts/vite.config.module.ts && cross-env CONTEXT_ENTRY=SupportLayout vite build --config fixture/apps/contexts/vite.config.module.ts && cross-env CONTEXT_ENTRY=StudioLayout vite build --config fixture/apps/contexts/vite.config.module.ts && cross-env CONTEXT_ENTRY=DefaultShellOverrides vite build --config fixture/apps/contexts/vite.config.module.ts && cross-env CONTEXT_ENTRY=SystemShellOverrides vite build --config fixture/apps/contexts/vite.config.module.ts && cross-env CONTEXT_ENTRY=ReportsShellOverrides vite build --config fixture/apps/contexts/vite.config.module.ts",
|
|
34
|
+
"build:apps": "npm run build:context-apps && vite build --config fixture/apps/notification-sender/vite.config.module.ts && vite build --config fixture/apps/module-manager/vite.config.module.ts && vite build --config fixture/apps/navigation/vite.config.module.ts && vite build --config fixture/apps/quill-editor/vite.config.module.ts && vite build --config fixture/apps/widgets/vite.config.module.ts && vite build --config fixture/apps/handsontable-test/vite.config.module.ts && vite build --config fixture/apps/record-scope-test/vite.config.module.ts && npm run build:catalog-modules",
|
|
34
35
|
"build:ai-assistant": "vite build --config vite.config.ai-assistant.ts",
|
|
35
36
|
"lint": "eslint src",
|
|
36
37
|
"lint:async": "eslint src -c eslint.config.async.mjs"
|
package/system/bindInertia.js
CHANGED
|
@@ -132,7 +132,7 @@ export function bindInertia(adminizer) {
|
|
|
132
132
|
headerName: 'x-xsrf-token'
|
|
133
133
|
}
|
|
134
134
|
}));
|
|
135
|
-
adminizer.app.use(async (req,
|
|
135
|
+
adminizer.app.use(async (req, res, next) => {
|
|
136
136
|
checkAuth(req, adminizer);
|
|
137
137
|
const defaultLocale = typeof req.adminizer.config.translation !== "boolean"
|
|
138
138
|
? req.adminizer.config.translation.defaultLocale
|
|
@@ -143,13 +143,30 @@ export function bindInertia(adminizer) {
|
|
|
143
143
|
lang: locale,
|
|
144
144
|
});
|
|
145
145
|
const menuHelper = new InertiaMenuHelper(adminizer);
|
|
146
|
-
//
|
|
147
|
-
// the
|
|
148
|
-
//
|
|
149
|
-
|
|
146
|
+
// The layout context of the request decides which menu and which
|
|
147
|
+
// layout the page gets; the default context reproduces the plain panel.
|
|
148
|
+
// Context, menu and switcher are computed once, and only when a page
|
|
149
|
+
// is rendered: API calls and partial reloads that skip them pay nothing.
|
|
150
|
+
let resolvedContext;
|
|
151
|
+
const resolveContext = () => resolvedContext ??= (async () => {
|
|
152
|
+
const context = await adminizer.contextHandler.resolve(req);
|
|
153
|
+
const contexts = req.user ? await adminizer.contextHandler.listAccessible(req.user) : [];
|
|
154
|
+
// Only page visits move the user between contexts: a rendered page
|
|
155
|
+
// asked for by the browser, never an asset or an API call.
|
|
156
|
+
const isPageVisit = req.headers["x-inertia"] !== undefined || (req.headers.accept ?? "").includes("text/html");
|
|
157
|
+
if (req.user && contexts.length > 1 && isPageVisit)
|
|
158
|
+
adminizer.contextHandler.remember(req, res, context);
|
|
159
|
+
return { context, contexts };
|
|
160
|
+
})();
|
|
161
|
+
let resolvedMenu;
|
|
162
|
+
const resolveMenu = () => resolvedMenu ??= (async () => req.user ? menuHelper.getMenuItems(req, (await resolveContext()).context) : null)();
|
|
163
|
+
const contextProps = (context) => adminizer.contextHandler.toProps({ ...context, title: req.i18n.__(context.title) });
|
|
150
164
|
const historyAccess = req.user && req.adminizer.config.history.enabled
|
|
151
165
|
? await req.adminizer.accessRightsHelper.checkPermission(`history-${req.adminizer.config.history?.adapter ?? 'default'}`, req.user)
|
|
152
166
|
: false;
|
|
167
|
+
// Contextual documentation of this page: metadata only, and null when
|
|
168
|
+
// the subsystem is off or the user may not read documentation.
|
|
169
|
+
// A broken implementation must never take the panel down with it.
|
|
153
170
|
let docs = null;
|
|
154
171
|
try {
|
|
155
172
|
docs = req.user ? await adminizer.documentationHandler.forContextMeta(req.user, req.path) : null;
|
|
@@ -166,16 +183,21 @@ export function bindInertia(adminizer) {
|
|
|
166
183
|
locale,
|
|
167
184
|
common: commonMessages,
|
|
168
185
|
},
|
|
169
|
-
menu,
|
|
170
|
-
menuSections: req.user ? menuHelper.getSections(req) : null,
|
|
186
|
+
menu: resolveMenu,
|
|
187
|
+
menuSections: async () => req.user ? menuHelper.getSections(req, (await resolveContext()).context) : null,
|
|
188
|
+
adminContext: async () => contextProps((await resolveContext()).context),
|
|
189
|
+
adminContexts: async () => (await resolveContext()).contexts
|
|
190
|
+
.filter((item) => item.switcher !== false)
|
|
191
|
+
.map(contextProps),
|
|
171
192
|
// Auto breadcrumbs. A function prop is evaluated by the Inertia
|
|
172
193
|
// adapter only when the key is rendered and only when the page did
|
|
173
194
|
// not send `breadcrumbs` of its own — page props override shared
|
|
174
195
|
// props before evaluation.
|
|
175
|
-
...(
|
|
196
|
+
...(req.user && adminizer.config.navbar?.breadcrumbs === 'auto' ? {
|
|
176
197
|
breadcrumbs: async () => {
|
|
177
198
|
try {
|
|
178
|
-
const
|
|
199
|
+
const menu = await resolveMenu();
|
|
200
|
+
const crumbs = await adminizer.adminLinkHandler.locate(req.user, req.originalUrl || req.url, menu ?? undefined);
|
|
179
201
|
return crumbs.map((crumb) => ({ ...crumb, title: req.i18n.__(crumb.title) }));
|
|
180
202
|
}
|
|
181
203
|
catch (error) {
|
|
@@ -224,6 +246,9 @@ export function bindInertia(adminizer) {
|
|
|
224
246
|
});
|
|
225
247
|
next();
|
|
226
248
|
});
|
|
249
|
+
// Pages declared to belong to a layout context the user may not enter are
|
|
250
|
+
// blocked here, before any route; `req.user` is known by now.
|
|
251
|
+
adminizer.app.use(adminizer.contextHandler.guard());
|
|
227
252
|
}
|
|
228
253
|
function checkAuth(req, adminizer) {
|
|
229
254
|
let locale = "";
|