adminizer 5.1.0-build.28 → 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.
Files changed (118) hide show
  1. package/assets/{abnfDiagram-VCTEODGH-uWOVpoSR.js → abnfDiagram-VCTEODGH-BhZn7BLv.js} +1 -1
  2. package/assets/{add-A_fIlNKI.js → add-DDfJt70P.js} +1 -1
  3. package/assets/{add-group-C2x6K3tV.js → add-group-Cs3S9VVn.js} +1 -1
  4. package/assets/{add-user-u61kYlEm.js → add-user-DxI_sJLw.js} +1 -1
  5. package/assets/ai-assistant/agent.es.js +14736 -11737
  6. package/assets/app-DM8KzeFz.css +1 -0
  7. package/assets/app-DOMFUTFw.js +138 -0
  8. package/assets/{arc--ZX64bmY.js → arc-BRY7wCfa.js} +1 -1
  9. package/assets/{architectureDiagram-5GKGNRK7-W9AzLZvl.js → architectureDiagram-5GKGNRK7-DfBT7jAW.js} +1 -1
  10. package/assets/{blockDiagram-I7D4REHJ-BpK_yEa7.js → blockDiagram-I7D4REHJ-CW2mcR7Z.js} +1 -1
  11. package/assets/{c4Diagram-7LVT6UL2-DiQzU1s0.js → c4Diagram-7LVT6UL2-BGsyO-4Y.js} +1 -1
  12. package/assets/{catalog-dSCv3Szm.js → catalog-cyCZOCXs.js} +3 -3
  13. package/assets/channel-Bd-bsjgh.js +1 -0
  14. package/assets/{chunk-2Q5K7J3B-GPHETbCq.js → chunk-2Q5K7J3B-4EnjsYhG.js} +1 -1
  15. package/assets/{chunk-5VM5RSS4-bLovkogd.js → chunk-5VM5RSS4-BVzmz0IS.js} +1 -1
  16. package/assets/{chunk-F27PBJKO-DbkTY41z.js → chunk-F27PBJKO-Bkf9Z4lt.js} +1 -1
  17. package/assets/{chunk-IMKFNOWR-CL0VAxX7.js → chunk-IMKFNOWR-Dg6Y3xGC.js} +1 -1
  18. package/assets/{chunk-JWPE2WC7-d1Grk14b.js → chunk-JWPE2WC7-BdVNpqE2.js} +1 -1
  19. package/assets/{chunk-POPQ4Y6H-CAUhiiNt.js → chunk-POPQ4Y6H-0R-etdBM.js} +1 -1
  20. package/assets/{chunk-SVP7TREG-C8ADZUEr.js → chunk-SVP7TREG-CdABJwLU.js} +1 -1
  21. package/assets/{chunk-TICWLB2K-oRqFZabo.js → chunk-TICWLB2K-Da-oSw37.js} +1 -1
  22. package/assets/{chunk-XXDRQBXY-C3zo4c2P.js → chunk-XXDRQBXY-Ba8BsH2Z.js} +1 -1
  23. package/assets/classDiagram-ZZMXUADV-BftNBmvr.js +1 -0
  24. package/assets/classDiagram-v2-VYDZK3BY-BftNBmvr.js +1 -0
  25. package/assets/controls/ckeditor.es.js +10659 -10551
  26. package/assets/controls/handsontable.es.js +9053 -8479
  27. package/assets/controls/jsoneditor.es.js +641 -614
  28. package/assets/controls/leaflet.es.js +672 -663
  29. package/assets/controls/toast-ui.es.js +13696 -13676
  30. package/assets/{cose-bilkent-JH36ORCC-ZPvVaI6n.js → cose-bilkent-JH36ORCC-BYrWDdVx.js} +1 -1
  31. package/assets/{cynefin-OW5HDTMX-EEL4eqU7.js → cynefin-OW5HDTMX-CwHzXvDB.js} +1 -1
  32. package/assets/{cynefinDiagram-5FMLGOSQ-DpDFbbrn.js → cynefinDiagram-5FMLGOSQ-V6tH6arn.js} +1 -1
  33. package/assets/{dagre-GXQ25YYZ-Dtjy1xN1.js → dagre-GXQ25YYZ-CaBvafdG.js} +1 -1
  34. package/assets/{dashboard-DXs-vdJG.js → dashboard-C27hB1GI.js} +1 -1
  35. package/assets/{diagram-S7CK7UJ4-BhWisCOy.js → diagram-S7CK7UJ4-85jkE9hd.js} +1 -1
  36. package/assets/{diagram-UQ7AKVKN-BBH2Mm33.js → diagram-UQ7AKVKN-DHJRq1TM.js} +1 -1
  37. package/assets/{diagram-VSXAHHWV-B1Xc2cPz.js → diagram-VSXAHHWV-C74VLfSc.js} +1 -1
  38. package/assets/{diagram-VX7I27RA-CTTYzy1s.js → diagram-VX7I27RA-CVJcG5hn.js} +1 -1
  39. package/assets/{diagram-Z3DM3KII-DdH0yWH0.js → diagram-Z3DM3KII-B8ybg5RA.js} +1 -1
  40. package/assets/{docs-BZYlfOON.js → docs-CK6W0Jko.js} +1 -1
  41. package/assets/{ebnfDiagram-PWID7BFC-R9cSqf0y.js → ebnfDiagram-PWID7BFC-D_gz_iz6.js} +1 -1
  42. package/assets/{erDiagram-RLTQ6QDP-Bpu-Pm95.js → erDiagram-RLTQ6QDP-D3W95aGH.js} +1 -1
  43. package/assets/{flowDiagram-HODETNUW-NdXf-fRN.js → flowDiagram-HODETNUW-D2PTWIEg.js} +1 -1
  44. package/assets/{ganttDiagram-EL5Y4UJY-itQHv_dH.js → ganttDiagram-EL5Y4UJY-TFBUh0C6.js} +1 -1
  45. package/assets/{gitGraphDiagram-WWUBYQGX-DorHK4Wx.js → gitGraphDiagram-WWUBYQGX-BfFJzeqx.js} +1 -1
  46. package/assets/{history-DB48MCa8.js → history-DX6CEQqv.js} +1 -1
  47. package/assets/{infoDiagram-27XIBGKW-FgFATKk4.js → infoDiagram-27XIBGKW-esGmrWiw.js} +1 -1
  48. package/assets/{init-user-DAXb55S_.js → init-user-D1G_dbcZ.js} +1 -1
  49. package/assets/{ishikawaDiagram-5VMMS53U-Bjl1a496.js → ishikawaDiagram-5VMMS53U--aGHPnva.js} +1 -1
  50. package/assets/{journeyDiagram-3NMN7TZE-DU1TmnbI.js → journeyDiagram-3NMN7TZE-03BXeE8O.js} +1 -1
  51. package/assets/{kanban-definition-UXKFOSKX-BsrlKdcU.js → kanban-definition-UXKFOSKX-JdDv2WHg.js} +1 -1
  52. package/assets/{layout-C68duuC7.js → layout-Bz6WKClu.js} +1 -1
  53. package/assets/{linear-DdadsTLS.js → linear-Chzx_9mb.js} +1 -1
  54. package/assets/{list-D3R3q3Ti.js → list-BuSdZ2Ic.js} +1 -1
  55. package/assets/{login-DwxC2WD6.js → login-DFWfnrmE.js} +1 -1
  56. package/assets/manifest.json +277 -277
  57. package/assets/media-manager-CT4LZgo_.js +7 -0
  58. package/assets/mermaid.core-Dp-0i8FJ.js +310 -0
  59. package/assets/{mindmap-definition-YA3MSWOX-BfdP8hOy.js → mindmap-definition-YA3MSWOX-BJSEYobY.js} +1 -1
  60. package/assets/{module-DKdDlng0.js → module-Cgn-q5gZ.js} +1 -1
  61. package/assets/{notification-DDWbaooT.js → notification-BrfoqVVT.js} +1 -1
  62. package/assets/{pegDiagram-XKGWAZYB-BO9cFGCI.js → pegDiagram-XKGWAZYB-ccVsKwl1.js} +1 -1
  63. package/assets/{pieDiagram-E7YTZNPT-B4KrTpto.js → pieDiagram-E7YTZNPT-B1XZVgNq.js} +1 -1
  64. package/assets/{quadrantDiagram-AXDQQJYC-BumScZvv.js → quadrantDiagram-AXDQQJYC-CdABEYmF.js} +1 -1
  65. package/assets/{railroadDiagram-O6MQD6OU-DUDQQC-p.js → railroadDiagram-O6MQD6OU-OX83MXuO.js} +1 -1
  66. package/assets/{register-5rs1OSaq.js → register-Cxf3Pow-.js} +1 -1
  67. package/assets/{requirementDiagram-BXWQKSXE-BtNsxNt1.js → requirementDiagram-BXWQKSXE-CD7swf9u.js} +1 -1
  68. package/assets/{ru-Cf1WWSt0.js → ru-DxiFV21N.js} +1 -1
  69. package/assets/{sankeyDiagram-P5KCCOFB-xjPcSTcN.js → sankeyDiagram-P5KCCOFB-Cgwq2DVJ.js} +1 -1
  70. package/assets/{sequenceDiagram-WJ2MYXX4-BVOgA3-B.js → sequenceDiagram-WJ2MYXX4-BgjP07Zh.js} +1 -1
  71. package/assets/{sizeCapture-INFHLROL-BDI5FsKO.js → sizeCapture-INFHLROL-B-jQ1aKT.js} +1 -1
  72. package/assets/{stateDiagram-D77RDMKH-DreIJby8.js → stateDiagram-D77RDMKH-Drot14NK.js} +1 -1
  73. package/assets/stateDiagram-v2-MP3YSRHH-C5VLF0H1.js +1 -0
  74. package/assets/{swimlanes-42K2YHIH-J_CNnPZv.js → swimlanes-42K2YHIH-lCqHhK1l.js} +1 -1
  75. package/assets/swimlanesDiagram-VR7AAH4N-4yyX1ZbA.js +8 -0
  76. package/assets/{timeline-definition-24CTP7MA-BXGN0YmT.js → timeline-definition-24CTP7MA-B-QLcvwc.js} +1 -1
  77. package/assets/{use-appearance-B2YFalXG.js → use-appearance-B0ck1CR_.js} +1 -1
  78. package/assets/{user-filters-list-D8WohjvX.js → user-filters-list-ce2cmS5Z.js} +1 -1
  79. package/assets/{vennDiagram-4TSXK5OY-Mu0lMKUo.js → vennDiagram-4TSXK5OY-2DN2uspP.js} +1 -1
  80. package/assets/{wardleyDiagram-VM6X3IG4-CXFMI74r.js → wardleyDiagram-VM6X3IG4-C_GODc0p.js} +1 -1
  81. package/assets/{welcome-DHQx7R11.js → welcome-DaAj6jOI.js} +1 -1
  82. package/assets/with-app-layout-CwAECGwY.js +34 -0
  83. package/assets/{xychartDiagram-S5SC5T6Z-DCr7j1YG.js → xychartDiagram-S5SC5T6Z-Cq8htGoW.js} +1 -1
  84. package/controllers/catalog/FrontendCatalogAdapter.d.ts +5 -5
  85. package/controllers/catalog/FrontentCatalogAdapter.d.ts +5 -5
  86. package/controllers/initUser.js +1 -1
  87. package/helpers/inertiaAddHelper.js +10 -1
  88. package/helpers/inertiaMenuHelper.d.ts +3 -2
  89. package/helpers/inertiaMenuHelper.js +9 -4
  90. package/helpers/navigationAccessHelper.d.ts +5 -0
  91. package/helpers/navigationAccessHelper.js +13 -0
  92. package/index.d.ts +1 -0
  93. package/index.js +1 -0
  94. package/interfaces/adminpanelConfig.d.ts +7 -0
  95. package/lib/Adminizer.d.ts +2 -0
  96. package/lib/Adminizer.js +3 -0
  97. package/lib/admin-links/AdminLinkHandler.js +5 -1
  98. package/lib/app-manager/AdminizerApp.d.ts +34 -0
  99. package/lib/app-manager/AppManager.js +35 -0
  100. package/lib/app-manager/ContextHandler.d.ts +240 -0
  101. package/lib/app-manager/ContextHandler.js +446 -0
  102. package/lib/app-manager/ControllerHandler.d.ts +2 -0
  103. package/lib/app-manager/ControllerHandler.js +7 -0
  104. package/lib/media-manager/helpers/MediaManagerHelper.d.ts +1 -1
  105. package/lib/notifications/AbstractNotificationService.d.ts +3 -3
  106. package/package.json +3 -2
  107. package/system/bindDocs.js +1 -1
  108. package/system/bindInertia.js +34 -9
  109. package/assets/app-CRa5Kokq.js +0 -138
  110. package/assets/app-TCCHARSx.css +0 -1
  111. package/assets/channel-DRO88SVn.js +0 -1
  112. package/assets/classDiagram-ZZMXUADV-Cb-leHEm.js +0 -1
  113. package/assets/classDiagram-v2-VYDZK3BY-Cb-leHEm.js +0 -1
  114. package/assets/media-manager-mt4Ykki7.js +0 -6
  115. package/assets/mermaid.core-DxLHD4PF.js +0 -310
  116. package/assets/stateDiagram-v2-MP3YSRHH-BNzSJJYe.js +0 -1
  117. package/assets/swimlanesDiagram-VR7AAH4N-DN5N1rG7.js +0 -8
  118. package/assets/with-app-layout-Cvg36gxr.js +0 -31
@@ -0,0 +1,240 @@
1
+ import { Adminizer } from "../Adminizer.js";
2
+ import type { HrefConfig, NavbarSectionConfig } from "../../interfaces/adminpanelConfig.js";
3
+ import type { User } from "../../models/User.js";
4
+ import type { MenuItem } from "../../helpers/menuHelper.js";
5
+ import type { AppController, AppControllerMethod, AppControllerPolicy } from "./AdminizerApp.js";
6
+ export declare const DEFAULT_ADMIN_CONTEXT_ID = "default";
7
+ /**
8
+ * Shell of a context.
9
+ * - `inherit` — the layout of the default context (the stock one for the
10
+ * default context itself);
11
+ * - `stock` — the stock sidebar + header, without any override;
12
+ * - `none` — no shell at all, the page fills the window;
13
+ * - `{module}` — URL of an ES module whose named exports ({@link ShellSlotName})
14
+ * replace parts of the inherited layout; each receives the replaced part as
15
+ * `Default`. The `Layout` export replaces the whole layout. `base: "stock"`
16
+ * applies the module over the stock layout instead of the inherited one.
17
+ *
18
+ * Kept for compatibility: `default` is `stock`, `bare` is `none`,
19
+ * `{overrides}` is `{module}`, and `{component}` is a module whose default
20
+ * export is the whole layout, applied over the stock layout.
21
+ */
22
+ export type AdminContextLayout = "inherit" | "stock" | "none" | {
23
+ module: string;
24
+ stylesheet?: string;
25
+ base?: "inherit" | "stock";
26
+ } | "default" | "bare" | {
27
+ overrides: string;
28
+ stylesheet?: string;
29
+ } | {
30
+ component: string;
31
+ stylesheet?: string;
32
+ };
33
+ /** Parts of the stock layout a context may override; `Layout` is the whole of it. */
34
+ export type ShellSlotName = "Layout" | "Sidebar" | "SidebarHeader" | "SidebarContent" | "SidebarFooter" | "Header" | "HeaderStart" | "HeaderActions";
35
+ export interface AdminContextNavbar {
36
+ /** Items of the context's own menu; `page()` and `menuItem()` append to them. */
37
+ items?: HrefConfig[];
38
+ sections?: Record<string, NavbarSectionConfig>;
39
+ }
40
+ /**
41
+ * A layout context: a part of the panel living under its own URL prefix
42
+ * with its own navbar and, optionally, its own layout.
43
+ */
44
+ export interface AdminContext {
45
+ id: string;
46
+ title: string;
47
+ icon?: string;
48
+ /** Path below `routePrefix`, e.g. `/support`. Every URL under it belongs to the context. */
49
+ prefix: string;
50
+ /** Where the switcher leads; defaults to the prefix itself. A path below `routePrefix`. */
51
+ home?: string;
52
+ /** @default "inherit" */
53
+ layout?: AdminContextLayout;
54
+ /**
55
+ * `inherit` shows the menu of the default context. Left out, the menu is
56
+ * inherited as well until `page()` or `menuItem()` adds an item to it.
57
+ */
58
+ navbar?: "inherit" | AdminContextNavbar;
59
+ /**
60
+ * Users lacking the token neither see the context in the switcher nor get
61
+ * its shell, and may not open the pages declared to belong to it.
62
+ */
63
+ accessRightsToken?: string;
64
+ /** Show in the context switcher. @default true */
65
+ switcher?: boolean;
66
+ }
67
+ /** What may be changed about the built-in context mounted at `routePrefix`. */
68
+ export type DefaultAdminContextOptions = Partial<Pick<AdminContext, "title" | "icon" | "layout" | "navbar" | "switcher" | "home">>;
69
+ /** A page of a context: a route under its prefix, a menu item and breadcrumbs in one call. */
70
+ export interface AdminContextPage<Component = string> {
71
+ /** Unique within the context; also the id of the menu item. */
72
+ id: string;
73
+ /** Path below the context prefix; `""` is the prefix itself. */
74
+ path: string;
75
+ title: string;
76
+ icon?: string;
77
+ /** ES module whose default export renders the page; it receives `props`. */
78
+ component?: Component;
79
+ /** Props of `component`: values, or a function computing them per request. */
80
+ props?: Record<string, unknown> | ((req: ReqType) => Record<string, unknown> | Promise<Record<string, unknown>>);
81
+ /** A handler of its own instead of `component`. */
82
+ middleware?: MiddlewareType;
83
+ /** @default "get" */
84
+ method?: AppControllerMethod;
85
+ /** @default `auth`. The context token is checked for every page of the context anyway. */
86
+ policies?: AppControllerPolicy[];
87
+ /** Add a menu item. @default true, false when the context's navbar is `"inherit"` */
88
+ menu?: boolean;
89
+ section?: string;
90
+ badge?: HrefConfig["badge"];
91
+ accessRightsToken?: string;
92
+ }
93
+ /** A route under the context prefix without a menu item, e.g. an API of the context. */
94
+ export interface AdminContextController extends Omit<AppController, "route" | "policies" | "context"> {
95
+ /** Path below the context prefix. */
96
+ path: string;
97
+ /** @default `auth`. The context token is checked anyway. */
98
+ policies?: AppControllerPolicy[];
99
+ }
100
+ /** A registered context: adds pages, routes and menu items to it. */
101
+ export interface AdminContextScope<Component = string> {
102
+ readonly id: string;
103
+ /** Registers a page under the context prefix and returns its menu item. */
104
+ page(page: AdminContextPage<Component>): HrefConfig;
105
+ /** Registers a route under the context prefix and returns its full path. */
106
+ controller(controller: AdminContextController): string;
107
+ /** Appends an item to the context's own menu. */
108
+ menuItem(item: HrefConfig): void;
109
+ }
110
+ /** How a scope registers what its pages need: the host's handlers or an app's setup context. */
111
+ export interface AdminContextRegistrar<Component = string> {
112
+ controller(controller: AppController): string;
113
+ /** Turns a page component into the URL of its module. */
114
+ component(component: Component): string;
115
+ }
116
+ /** Context description as the browser receives it. */
117
+ export interface AdminContextProps {
118
+ id: string;
119
+ title: string;
120
+ icon: string | null;
121
+ /** Full URL prefix of the context, `routePrefix` included. */
122
+ prefix: string;
123
+ link: string;
124
+ layout: AdminContextLayoutProps;
125
+ }
126
+ /** A module of the layout chain. */
127
+ export interface AdminContextLayerProps {
128
+ /** URL of the ES module; its named exports are slot overrides. */
129
+ module: string;
130
+ /** `Layout`: the default export is the whole layout (`{component}`). */
131
+ default?: "Layout";
132
+ }
133
+ /**
134
+ * Resolved layout: the layers applied in order over the root, the default
135
+ * context's ones first.
136
+ */
137
+ export interface AdminContextLayoutProps {
138
+ /** What the layers are applied over: the stock shell or nothing at all. */
139
+ root: "stock" | "none";
140
+ layers: AdminContextLayerProps[];
141
+ stylesheets: string[];
142
+ }
143
+ /** Registry of layout contexts contributed by the host and by apps. */
144
+ export declare class ContextHandler {
145
+ private readonly adminizer;
146
+ private readonly contexts;
147
+ private readonly assignments;
148
+ private readonly reportedMissing;
149
+ private defaultOptions;
150
+ constructor(adminizer: Adminizer);
151
+ /**
152
+ * Registers a context. The returned scope adds pages to it; they are
153
+ * registered as `owner`'s controllers.
154
+ */
155
+ register(context: AdminContext, owner?: string): AdminContextScope;
156
+ unregister(id: string, owner?: string): void;
157
+ /** A scope adding pages to a registered context through the given registrar. */
158
+ scope<Component>(id: string, registrar: AdminContextRegistrar<Component>): AdminContextScope<Component>;
159
+ /** Overrides title, icon, layout or navbar of the built-in context. */
160
+ configureDefault(options: DefaultAdminContextOptions): void;
161
+ getDefault(): AdminContext;
162
+ get(id: string): AdminContext | undefined;
163
+ list(): AdminContext[];
164
+ /** Contexts this user may enter, the default one first. */
165
+ listAccessible(user: User): Promise<AdminContext[]>;
166
+ /**
167
+ * Declares that the pages at `path` (below `routePrefix`, `:params`
168
+ * allowed) belong to a context: they render in it wherever the user came
169
+ * from, and only users who may enter the context may open them. Covers
170
+ * everything below `path` too unless `exact`. Returns a function removing
171
+ * the declaration.
172
+ */
173
+ assign(path: string, contextId: string, options?: {
174
+ owner?: string;
175
+ exact?: boolean;
176
+ }): () => void;
177
+ /**
178
+ * The context the page at `url` is declared to belong to: by `assign()`,
179
+ * a controller's `context` or a model's `context`. The most specific
180
+ * declaration wins. Login, logout, registration and the first-user setup
181
+ * never belong to a context.
182
+ */
183
+ declaredFor(url: string): string | undefined;
184
+ /** True when the page at `url` belongs to a context this user may not enter. */
185
+ isBlocked(url: string, user: User | undefined): Promise<boolean>;
186
+ /**
187
+ * Blocks the pages declared to belong to a context the user may not
188
+ * enter, before any handler and for every method: not logged in — the
189
+ * login page (401 for API calls), no context token — 403, a context that
190
+ * is not registered (a typo, a disabled app) — 404. Pages without a
191
+ * declaration pass untouched.
192
+ */
193
+ guard(): MiddlewareType;
194
+ /**
195
+ * The context a request belongs to.
196
+ *
197
+ * A page declared to belong to a context renders in it. A URL under a
198
+ * context prefix belongs to that context. Any other URL (the built-in
199
+ * `/model/...` pages, for one) stays in the context the user came from —
200
+ * remembered in a cookie — as long as that context's navbar links to it;
201
+ * otherwise it falls back to the default context.
202
+ */
203
+ resolve(req: ReqType): Promise<AdminContext>;
204
+ /** Remembers the context of a page visit; see {@link resolve}. */
205
+ remember(req: ReqType, res: ResType, context: AdminContext): void;
206
+ /** The untranslated menu of a context for this user. */
207
+ listMenuItems(context: AdminContext, user: User): Promise<MenuItem[]>;
208
+ getSections(context: AdminContext): Record<string, NavbarSectionConfig>;
209
+ /**
210
+ * The layout of a context with inheritance from the default context
211
+ * applied. Layers always add up along the chain: a module over a parent
212
+ * keeps the parent's layers, whatever they replace.
213
+ */
214
+ resolveLayout(context: AdminContext): AdminContextLayoutProps;
215
+ toProps(context: AdminContext): AdminContextProps;
216
+ private addPage;
217
+ private addController;
218
+ private addMenuItem;
219
+ /** Renders a `component` page: the module page with context breadcrumbs. */
220
+ private renderPage;
221
+ private require;
222
+ private isAccessible;
223
+ private claims;
224
+ /** A model page `/model/<name>/...` of a model whose config declares a context. */
225
+ private modelDeclaration;
226
+ /** Model config by resource name, matched the way the model routes match it. */
227
+ private findModelConfig;
228
+ private isAuthPath;
229
+ /** A route pattern as a regexp: `:param`, `*splat` and `{optional}` parts of Express routes. */
230
+ private compilePattern;
231
+ /** Static segments weigh more than parameters. */
232
+ private specificity;
233
+ private isPageRequest;
234
+ private root;
235
+ private fullPrefix;
236
+ private normalizePrefix;
237
+ private normalizePath;
238
+ private requestPath;
239
+ private readCookie;
240
+ }
@@ -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
+ }