@nocobase/plugin-multi-portal 2.2.0-alpha.12 → 2.4.0-alpha.2

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 (32) hide show
  1. package/README.md +7 -5
  2. package/dist/client-v2/354.87f18462949b5c23.js +10 -0
  3. package/dist/client-v2/499.245037eea4cc7266.js +10 -0
  4. package/dist/client-v2/556.443e73c9ca4c18b9.js +10 -0
  5. package/dist/client-v2/778.affaa69b23857ef2.js +10 -0
  6. package/dist/client-v2/993.7a1cc7aa6cec0160.js +10 -0
  7. package/dist/client-v2/PortalAccessBoundary.d.ts +49 -0
  8. package/dist/client-v2/RootLanding.d.ts +4 -5
  9. package/dist/client-v2/index.js +1 -1
  10. package/dist/client-v2/interceptor.d.ts +13 -0
  11. package/dist/client-v2/layoutRegistration.d.ts +3 -6
  12. package/dist/client-v2/models/MultiPortalLayoutModels.d.ts +14 -0
  13. package/dist/client-v2/models/MultiPortalMobilePageModels.d.ts +2 -0
  14. package/dist/client-v2/pages/MultiPortalsPage.d.ts +10 -27
  15. package/dist/client-v2/portalAccess.d.ts +73 -0
  16. package/dist/client-v2/routeUrl.d.ts +1 -0
  17. package/dist/constants.d.ts +8 -0
  18. package/dist/constants.js +30 -2
  19. package/dist/externalVersion.js +9 -8
  20. package/dist/locale/en-US.json +45 -14
  21. package/dist/locale/zh-CN.json +43 -12
  22. package/dist/node_modules/tar/package.json +1 -1
  23. package/dist/server/collections/multiPortals.js +12 -11
  24. package/dist/server/plugin.d.ts +6 -4
  25. package/dist/server/plugin.js +487 -312
  26. package/dist/swagger/index.d.ts +152 -0
  27. package/dist/swagger/index.js +142 -0
  28. package/package.json +3 -3
  29. package/dist/client-v2/354.7be267ae5d61a878.js +0 -10
  30. package/dist/client-v2/499.ef2ff0c022687485.js +0 -10
  31. package/dist/client-v2/556.5faf86d4f8faef40.js +0 -10
  32. package/dist/client-v2/778.c98e7ec542d931fe.js +0 -10
@@ -60,9 +60,9 @@ const MULTI_PORTAL_RUNTIME_FIELDS = [
60
60
  "portalName",
61
61
  "routePath",
62
62
  "authCheck",
63
- "enabled"
63
+ "enabled",
64
+ "uiLayoutUid"
64
65
  ];
65
- const MULTI_PORTAL_RUNTIME_QUERY_FIELDS = [...MULTI_PORTAL_RUNTIME_FIELDS, "uiLayoutUid"];
66
66
  const MULTI_PORTAL_ACCESSIBLE_FIELDS = [
67
67
  "uid",
68
68
  "title",
@@ -71,18 +71,15 @@ const MULTI_PORTAL_ACCESSIBLE_FIELDS = [
71
71
  "portalName",
72
72
  "routePath",
73
73
  "authCheck",
74
- "enabled"
74
+ "enabled",
75
+ "uiLayoutUid"
75
76
  ];
76
- const MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS = [...MULTI_PORTAL_ACCESSIBLE_FIELDS, "uiLayoutUid"];
77
- const MULTI_PORTAL_UI_LAYOUT_RUNTIME_FIELDS = ["layoutType"];
78
77
  const DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS = ["id", "title", "hidden", "parentId", "options"];
79
78
  const UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG = "plugin-ui-layout:desktop-route-write-layout";
80
79
  const MAIN_APP_NAME = "main";
81
80
  const UNION_ROLE_KEY = "__union__";
82
81
  const MULTI_PORTAL_MANIFEST_NAMESPACE = "multi-portal";
83
82
  const MULTI_PORTAL_MANIFEST_SYNC_MESSAGE_TYPE = "multi-portal:app-manifest-changed";
84
- const DEFAULT_INIT_PORTAL_TYPE = "ai";
85
- const DEFAULT_INIT_PORTAL_NAME = "main";
86
83
  const DEFAULT_INIT_PORTAL_TEMPLATE = "@nocobase/portal-template-default";
87
84
  const PORTAL_CLIENT_PREFIX = "x";
88
85
  const PORTAL_DEPLOY_UPLOAD_LIMIT = 200 * 1024 * 1024;
@@ -92,7 +89,9 @@ const PORTAL_PUBLIC_FILE_MODE = 420;
92
89
  const PORTAL_TEMPLATE_NPM_PACK_TIMEOUT_MS = 3e4;
93
90
  const DEFAULT_MULTI_PORTAL_UID = "__default_portal__";
94
91
  const MULTI_PORTAL_SLUG_PATTERN = /^[a-z0-9_-]+$/;
95
- const INIT_PORTAL_TYPES = ["no-code", "ai"];
92
+ const PORTAL_ACCESS_DENIED_CODE = "PORTAL_ACCESS_DENIED";
93
+ const PORTAL_CONTEXT_INVALID_CODE = "PORTAL_CONTEXT_INVALID";
94
+ const PORTAL_NOT_FOUND_CODE = "PORTAL_NOT_FOUND";
96
95
  const MULTI_PORTAL_MANAGEMENT_ACTIONS = [
97
96
  "multiPortals:list",
98
97
  "multiPortals:get",
@@ -100,6 +99,7 @@ const MULTI_PORTAL_MANAGEMENT_ACTIONS = [
100
99
  "multiPortals:create",
101
100
  "multiPortals:update",
102
101
  "multiPortals:firstOrCreate",
102
+ "multiPortals:setDefault",
103
103
  "multiPortals:destroy",
104
104
  "multiPortals:deploy",
105
105
  "multiPortals:pullSource",
@@ -119,6 +119,11 @@ const ROLE_MULTI_PORTAL_PERMISSION_ACTIONS = [
119
119
  "rolesMultiPortalDesktopRoutes:*",
120
120
  "rolesMultiPortalRoutePolicies:*"
121
121
  ];
122
+ const MULTI_PORTAL_SEED_TYPES = ["no-code", "ai"];
123
+ const MULTI_PORTAL_SEED_TYPE_SET = new Set(MULTI_PORTAL_SEED_TYPES);
124
+ function isMultiPortalSeedType(value) {
125
+ return typeof value === "string" && MULTI_PORTAL_SEED_TYPE_SET.has(value);
126
+ }
122
127
  const SKIP_CREATE_PORTAL_DIRECTORY = Symbol("skipCreatePortalDirectory");
123
128
  function getRecordField(record, field) {
124
129
  if (!record || typeof record !== "object") {
@@ -144,46 +149,28 @@ async function pathExists(filePath) {
144
149
  function trimString(value) {
145
150
  return String(value ?? "").trim();
146
151
  }
147
- function isInitPortalType(value) {
148
- return INIT_PORTAL_TYPES.includes(value);
149
- }
150
- function getInitPortalType() {
151
- const portalType = trimString(process.env.INIT_PORTAL_TYPE) || DEFAULT_INIT_PORTAL_TYPE;
152
- if (!isInitPortalType(portalType)) {
153
- throw new Error('INIT_PORTAL_TYPE must be either "no-code" or "ai".');
154
- }
155
- return portalType;
156
- }
157
- function getInitPortalName() {
158
- const portalName = trimString(process.env.INIT_PORTAL_NAME) || DEFAULT_INIT_PORTAL_NAME;
159
- if (!MULTI_PORTAL_SLUG_PATTERN.test(portalName)) {
160
- throw new Error("INIT_PORTAL_NAME can only contain lowercase letters, numbers, hyphens, and underscores.");
161
- }
162
- return portalName;
163
- }
164
152
  function getInitPortalTemplate() {
165
153
  return trimString(process.env.INIT_PORTAL_TEMPLATE) || DEFAULT_INIT_PORTAL_TEMPLATE;
166
154
  }
167
- function formatInitPortalTitle(portalName) {
168
- const title = portalName.split(/[-_]/).filter(Boolean).map((segment) => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`).join(" ");
169
- return title || portalName;
155
+ function hasDeprecatedInitPortalEnv() {
156
+ return Boolean(trimString(process.env.INIT_PORTAL_TYPE) || trimString(process.env.INIT_PORTAL_NAME));
170
157
  }
171
- function getDefaultMultiPortalRecord() {
172
- const portalName = getInitPortalName();
158
+ function getDefaultAiMultiPortalRecord(options = {}) {
173
159
  return {
174
160
  uid: DEFAULT_MULTI_PORTAL_UID,
175
- title: formatInitPortalTitle(portalName),
161
+ title: "Main",
176
162
  icon: "DesktopOutlined",
177
- portalType: getInitPortalType(),
178
- portalName,
179
- routePath: `/${portalName}`,
163
+ portalType: "ai",
164
+ portalName: "main",
165
+ routePath: "/main",
180
166
  authCheck: true,
181
167
  enabled: true,
182
- uiLayoutUid: "admin-layout-model"
168
+ ...options.isDefault ? { isDefault: true } : {},
169
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
183
170
  };
184
171
  }
185
172
  function getFreshMultiPortalRecords() {
186
- return [getDefaultMultiPortalRecord()];
173
+ return [getDefaultAiMultiPortalRecord({ isDefault: true }), ...getFixedLayoutMultiPortalRecords()];
187
174
  }
188
175
  function getFixedLayoutMultiPortalRecords() {
189
176
  return [
@@ -196,7 +183,7 @@ function getFixedLayoutMultiPortalRecords() {
196
183
  routePath: "/admin",
197
184
  authCheck: true,
198
185
  enabled: true,
199
- uiLayoutUid: "admin-layout-model"
186
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
200
187
  },
201
188
  {
202
189
  uid: import_constants.DEFAULT_MOBILE_MULTI_PORTAL_UID,
@@ -207,7 +194,7 @@ function getFixedLayoutMultiPortalRecords() {
207
194
  routePath: "/mobile",
208
195
  authCheck: true,
209
196
  enabled: true,
210
- uiLayoutUid: "mobile-layout-model"
197
+ uiLayoutUid: import_constants.MOBILE_UI_LAYOUT_UID
211
198
  }
212
199
  ];
213
200
  }
@@ -292,6 +279,13 @@ function getPortalDeployBasePath(appName, portalName) {
292
279
  const portalPath = appName === MAIN_APP_NAME ? `/${PORTAL_CLIENT_PREFIX}/${portalName}/` : `/${PORTAL_CLIENT_PREFIX}/apps/${appName}/${portalName}/`;
293
280
  return resolvePortalStoragePublicPath(joinPortalStoragePublicPath(process.env.APP_PUBLIC_PATH || "/", portalPath));
294
281
  }
282
+ function getPortalDeployBasePathCandidates(appName, portalName) {
283
+ const expectedBasePaths = [getPortalDeployBasePath(appName, portalName)];
284
+ if (appName !== MAIN_APP_NAME) {
285
+ expectedBasePaths.push(getPortalDeployBasePath(MAIN_APP_NAME, portalName));
286
+ }
287
+ return expectedBasePaths;
288
+ }
295
289
  function createPortalDeployUploadMiddleware() {
296
290
  const storage = import_utils.koaMulter.diskStorage({
297
291
  destination: import_os.default.tmpdir(),
@@ -434,80 +428,13 @@ async function resolvePortalTemplate(templateSource, logPath) {
434
428
  return downloadPortalTemplatePackage(templateSource, logPath);
435
429
  }
436
430
  async function copyPortalTemplate(sourceDir, targetDir) {
437
- const ignoredSegments = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store"]);
431
+ const ignoredSegments = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local"]);
438
432
  await import_fs.default.promises.mkdir(import_path.default.dirname(targetDir), { recursive: true });
439
433
  await import_fs.default.promises.cp(sourceDir, targetDir, {
440
434
  recursive: true,
441
435
  filter: (source) => !import_path.default.relative(sourceDir, source).split(import_path.default.sep).some((segment) => segment.startsWith("._") || ignoredSegments.has(segment))
442
436
  });
443
437
  }
444
- function getPortalStorageConfig(options) {
445
- const sourceOptions = isRecordLike(options) ? options : {};
446
- const sourceStorage = trimString(sourceOptions.sourceStorage);
447
- if (sourceStorage !== "git") {
448
- return { sourceStorage: "nocobase" };
449
- }
450
- const gitOptions = isRecordLike(sourceOptions.git) ? sourceOptions.git : {};
451
- return {
452
- sourceStorage,
453
- git: {
454
- repo: trimString(gitOptions.repo),
455
- branch: trimString(gitOptions.branch) || "main",
456
- path: trimString(gitOptions.path) || "."
457
- }
458
- };
459
- }
460
- function upsertPortalEnvContent(content, values) {
461
- const nextValues = { ...values };
462
- const lines = content ? content.replace(/\r\n/g, "\n").split("\n") : [];
463
- const result = [];
464
- for (const line of lines) {
465
- if (!line && result.length === lines.length - 1) {
466
- continue;
467
- }
468
- const match = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=/);
469
- const key = match == null ? void 0 : match[2];
470
- if (key && Object.prototype.hasOwnProperty.call(nextValues, key)) {
471
- result.push(`${key}=${nextValues[key]}`);
472
- delete nextValues[key];
473
- continue;
474
- }
475
- result.push(line);
476
- }
477
- for (const [key, value] of Object.entries(nextValues)) {
478
- result.push(`${key}=${value}`);
479
- }
480
- return `${result.join("\n").replace(/\n*$/, "")}
481
- `;
482
- }
483
- async function upsertPortalEnvFile(filePath, values) {
484
- let content = "";
485
- try {
486
- content = await import_fs.default.promises.readFile(filePath, "utf-8");
487
- } catch {
488
- content = "";
489
- }
490
- await import_fs.default.promises.writeFile(filePath, upsertPortalEnvContent(content, values), "utf-8");
491
- }
492
- async function ensurePortalStorageConfigFiles(portalDir, item) {
493
- const apiUrl = getPortalStorageApiUrl(item.appName);
494
- const portalBase = getPortalDeployBasePath(item.appName, item.portalName);
495
- await import_fs.default.promises.mkdir(portalDir, { recursive: true });
496
- await upsertPortalEnvFile(import_path.default.join(portalDir, ".env"), {
497
- NOCOBASE_API_URL: resolvePortalStorageEnvApiUrl(apiUrl),
498
- NOCOBASE_PORTAL_BASE: portalBase
499
- });
500
- await upsertPortalEnvFile(import_path.default.join(portalDir, ".env.local"), {
501
- NOCOBASE_API_URL: apiUrl,
502
- NOCOBASE_PORTAL_BASE: portalBase
503
- });
504
- await import_fs.default.promises.writeFile(
505
- import_path.default.join(portalDir, "portal.config.json"),
506
- `${JSON.stringify(item.config, null, 2)}
507
- `,
508
- "utf-8"
509
- );
510
- }
511
438
  function sanitizePortalStorageNodeOptions(value) {
512
439
  return trimString(value).split(/\s+/).filter((option) => option !== "--preserve-symlinks" && option !== "--preserve-symlinks-main").join(" ");
513
440
  }
@@ -592,17 +519,24 @@ async function runPortalStorageCommandOnce(command, args, options) {
592
519
  throw error;
593
520
  }
594
521
  }
595
- async function buildPortalStorageItem(portalDir, item) {
522
+ function hasRequestAppHeader(options) {
523
+ var _a;
524
+ return Boolean(trimString((_a = options == null ? void 0 : options.context) == null ? void 0 : _a.get("X-App")));
525
+ }
526
+ async function buildPortalStorageItem(portalDir, item, options) {
596
527
  const logPath = getPortalStorageLogPath(item);
528
+ const buildAppName = hasRequestAppHeader(options) ? item.appName : MAIN_APP_NAME;
597
529
  const buildEnv = getPortalStorageCommandEnv({
598
- NOCOBASE_API_URL: getPortalStorageApiUrl(item.appName),
599
- NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(item.appName, item.portalName),
530
+ NOCOBASE_API_URL: getPortalStorageApiUrl(buildAppName),
531
+ NOCOBASE_PORTAL_BASE: getPortalDeployBasePath(buildAppName, item.portalName),
532
+ SKIP_YARN_COREPACK_CHECK: "1",
533
+ COREPACK_ENABLE_STRICT: "0",
600
534
  COREPACK_ENABLE_PROJECT_SPEC: "0"
601
535
  });
602
536
  await appendPortalStorageLog(logPath, `Building portal ${item.appName}/${item.portalName}.`);
603
537
  await appendPortalStorageLog(
604
538
  logPath,
605
- `Build environment: NOCOBASE_API_URL=${buildEnv.NOCOBASE_API_URL || ""} NOCOBASE_PORTAL_BASE=${buildEnv.NOCOBASE_PORTAL_BASE || ""} APP_PUBLIC_PATH=${buildEnv.APP_PUBLIC_PATH || ""}`
539
+ `Build environment: NOCOBASE_API_URL=${buildEnv.NOCOBASE_API_URL || ""} NOCOBASE_PORTAL_BASE=${buildEnv.NOCOBASE_PORTAL_BASE || ""} APP_PUBLIC_PATH=${buildEnv.APP_PUBLIC_PATH || ""} SKIP_YARN_COREPACK_CHECK=${buildEnv.SKIP_YARN_COREPACK_CHECK || ""} COREPACK_ENABLE_STRICT=${buildEnv.COREPACK_ENABLE_STRICT || ""} COREPACK_ENABLE_PROJECT_SPEC=${buildEnv.COREPACK_ENABLE_PROJECT_SPEC || ""}`
606
540
  );
607
541
  await runPortalStorageCommandOnce("yarn", ["build:html"], {
608
542
  cwd: portalDir,
@@ -616,9 +550,9 @@ function validatePortalDeployBasePath(appName, portalName, basePath) {
616
550
  if (normalizedBasePath.includes("..")) {
617
551
  throw new Error('basePath cannot contain ".."');
618
552
  }
619
- const expectedBasePath = getPortalDeployBasePath(appName, portalName);
620
- if (normalizedBasePath !== expectedBasePath) {
621
- throw new Error(`basePath must be ${expectedBasePath}`);
553
+ const expectedBasePaths = getPortalDeployBasePathCandidates(appName, portalName);
554
+ if (!expectedBasePaths.includes(normalizedBasePath)) {
555
+ throw new Error(`basePath must be ${expectedBasePaths.join(" or ")}`);
622
556
  }
623
557
  }
624
558
  function isPortalDeployTarEntry(entry) {
@@ -856,19 +790,11 @@ function collectDesktopRouteIds(record) {
856
790
  }
857
791
  return uniqueDesktopRouteIds(routeIds);
858
792
  }
859
- function pickMultiPortalUiLayoutRuntimeFields(record) {
860
- const result = {};
861
- for (const field of MULTI_PORTAL_UI_LAYOUT_RUNTIME_FIELDS) {
862
- result[field] = getRecordField(record, field);
863
- }
864
- return result;
865
- }
866
793
  function pickMultiPortalRuntimeFields(record) {
867
794
  const result = {};
868
795
  for (const field of MULTI_PORTAL_RUNTIME_FIELDS) {
869
796
  result[field] = getRecordField(record, field);
870
797
  }
871
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
872
798
  return result;
873
799
  }
874
800
  function pickMultiPortalAccessibleFields(record) {
@@ -876,7 +802,6 @@ function pickMultiPortalAccessibleFields(record) {
876
802
  for (const field of MULTI_PORTAL_ACCESSIBLE_FIELDS) {
877
803
  result[field] = getRecordField(record, field) ?? null;
878
804
  }
879
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
880
805
  return result;
881
806
  }
882
807
  function getExplicitRequestedLayoutUid(layout) {
@@ -1033,6 +958,37 @@ async function normalizeMultiPortalSlugValues(ctx, next) {
1033
958
  values.portalName = slug;
1034
959
  values.routePath = `/${slug}`;
1035
960
  }
961
+ const repository = ctx.db.getRepository("multiPortals");
962
+ const pendingPortalNames = /* @__PURE__ */ new Set();
963
+ const targets = await getMultiPortalWriteTargets(ctx, ["uid"]);
964
+ for (const { existing, values } of targets) {
965
+ const portalName = values.portalName;
966
+ if (typeof portalName !== "string" || !portalName) {
967
+ continue;
968
+ }
969
+ if (pendingPortalNames.has(portalName)) {
970
+ ctx.throw(409, ctx.t('Portal name "{{portalName}}" already exists', { ns: import_constants.NAMESPACE, portalName }));
971
+ return;
972
+ }
973
+ pendingPortalNames.add(portalName);
974
+ const conflict = await repository.findOne({
975
+ filter: { portalName },
976
+ fields: ["uid"]
977
+ });
978
+ if (conflict && conflict.get("uid") !== (existing == null ? void 0 : existing.get("uid"))) {
979
+ ctx.throw(409, ctx.t('Portal name "{{portalName}}" already exists', { ns: import_constants.NAMESPACE, portalName }));
980
+ return;
981
+ }
982
+ }
983
+ await next();
984
+ }
985
+ async function preventDirectDefaultPortalMutation(ctx, next) {
986
+ for (const values of getMultiPortalWriteRecords(ctx)) {
987
+ if (Object.prototype.hasOwnProperty.call(values, "isDefault")) {
988
+ ctx.throw(400, ctx.t("Use multiPortals:setDefault to update the default Portal", { ns: import_constants.NAMESPACE }));
989
+ return;
990
+ }
991
+ }
1036
992
  await next();
1037
993
  }
1038
994
  function isUniqueConstraintViolation(error) {
@@ -1087,55 +1043,33 @@ async function seedFreshMultiPortals(db) {
1087
1043
  }
1088
1044
  }
1089
1045
  async function seedHistoricalMultiPortals(db) {
1090
- if (getInitPortalType() === "ai") {
1091
- await createDefaultMultiPortalBestEffort(db, getDefaultMultiPortalRecord());
1092
- }
1046
+ await createDefaultMultiPortalBestEffort(db, getDefaultAiMultiPortalRecord());
1093
1047
  for (const portal of getFixedLayoutMultiPortalRecords()) {
1094
1048
  await createDefaultMultiPortalBestEffort(db, portal);
1095
1049
  }
1096
1050
  await repairFixedLayoutMultiPortalRecords(db);
1097
1051
  }
1098
- async function preventUiLayoutRouteNameConflict(ctx, next) {
1099
- const targets = await getMultiPortalWriteTargets(ctx, ["uid", "portalName", "uiLayoutUid"]);
1052
+ async function validateMultiPortalUiLayoutUidWrite(ctx, next) {
1053
+ var _a;
1054
+ const targets = await getMultiPortalWriteTargets(ctx, ["uiLayoutUid"]);
1055
+ const actionName = (_a = ctx.action) == null ? void 0 : _a.actionName;
1056
+ const createsWhenMissing = actionName === "create" || actionName === "firstOrCreate" || actionName === "updateOrCreate";
1100
1057
  for (const { existing, values } of targets) {
1101
- const existingUid = trimString(existing == null ? void 0 : existing.get("uid"));
1102
- const uid = trimString(values.uid ?? existingUid);
1103
- const portalName = trimString(values.portalName ?? (existing == null ? void 0 : existing.get("portalName")));
1104
- const uiLayoutUid = trimString(values.uiLayoutUid ?? (existing == null ? void 0 : existing.get("uiLayoutUid")));
1105
- const fixedPortal = getFixedLayoutMultiPortalRecord(existingUid) || getFixedLayoutMultiPortalRecord(uid);
1106
- const canonicalPortal = (fixedPortal == null ? void 0 : fixedPortal.uid) === uid && (!existingUid || fixedPortal.uid === existingUid) && fixedPortal.portalName === portalName && fixedPortal.uiLayoutUid === uiLayoutUid;
1107
- if (fixedPortal && !canonicalPortal) {
1108
- ctx.throw(400, "Reserved Portal UID must match its canonical route and UI layout");
1058
+ const hasUiLayoutUid = Object.prototype.hasOwnProperty.call(values, "uiLayoutUid");
1059
+ if (!existing && createsWhenMissing && !hasUiLayoutUid) {
1060
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1109
1061
  return;
1110
1062
  }
1111
- if (!portalName) {
1112
- continue;
1113
- }
1114
- if (portalName === "admin" || portalName === "mobile") {
1063
+ if (!hasUiLayoutUid) {
1115
1064
  continue;
1116
1065
  }
1117
- const existingInitPortal = (existing == null ? void 0 : existing.get("uid")) === DEFAULT_MULTI_PORTAL_UID && existing.get("portalName") === portalName;
1118
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1119
- filter: {
1120
- routeName: portalName
1121
- },
1122
- fields: ["uid"]
1123
- });
1124
- if (uiLayout && !existingInitPortal) {
1125
- ctx.throw(400, "Portal route name conflicts with an existing UI layout");
1066
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(values.uiLayoutUid)) {
1067
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1126
1068
  return;
1127
1069
  }
1128
- }
1129
- await next();
1130
- }
1131
- async function preventMultiPortalBackingLayoutChange(ctx, next) {
1132
- const targets = await getMultiPortalWriteTargets(ctx, ["uiLayoutUid"]);
1133
- for (const { existing, values } of targets) {
1134
- if (!Object.prototype.hasOwnProperty.call(values, "uiLayoutUid")) {
1135
- continue;
1136
- }
1137
- if (existing && existing.get("uiLayoutUid") !== values.uiLayoutUid) {
1138
- ctx.throw(400, "Portal backing UI layout cannot be changed");
1070
+ const existingUiLayoutUid = existing == null ? void 0 : existing.get("uiLayoutUid");
1071
+ if (existing && existingUiLayoutUid !== values.uiLayoutUid) {
1072
+ ctx.throw(400, "Portal UI layout cannot be changed");
1139
1073
  return;
1140
1074
  }
1141
1075
  }
@@ -1163,20 +1097,8 @@ async function findRequestedMultiPortal(ctx, transaction) {
1163
1097
  ctx.throw(400, `Portal '${portalUid}' does not support desktop routes`);
1164
1098
  }
1165
1099
  const uiLayoutUid = portal.get("uiLayoutUid");
1166
- if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
1167
- ctx.throw(400, `Portal '${portalUid}' has no backing UI layout`);
1168
- }
1169
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1170
- filter: {
1171
- uid: uiLayoutUid,
1172
- enabled: true
1173
- },
1174
- fields: ["uid"],
1175
- ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1176
- transaction
1177
- });
1178
- if (!uiLayout) {
1179
- ctx.throw(400, `Portal '${portalUid}' has no enabled backing UI layout`);
1100
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(uiLayoutUid)) {
1101
+ ctx.throw(400, `Portal '${portalUid}' has an unsupported UI layout UID`);
1180
1102
  }
1181
1103
  const usesLayoutPermissions = (0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid);
1182
1104
  return {
@@ -1242,7 +1164,6 @@ async function assertDesktopRouteParentsBelongToScope(ctx, scope, values = ((_a)
1242
1164
  id: parentIds,
1243
1165
  ...scope.filter
1244
1166
  },
1245
- ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1246
1167
  transaction
1247
1168
  });
1248
1169
  if (new Set(normalizeDesktopRouteTargetIds(parents)).size !== new Set(parentIds.map(String)).size) {
@@ -1276,7 +1197,6 @@ async function assertDesktopRouteUpsertMatchesBelongToScope(ctx, scope, values =
1276
1197
  ...filter,
1277
1198
  ...scope.filter
1278
1199
  },
1279
- ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1280
1200
  transaction
1281
1201
  });
1282
1202
  const allMatchIds = normalizeDesktopRouteTargetIds(allMatches);
@@ -1294,13 +1214,77 @@ async function canAccessMultiPortal(ctx, multiPortalUid) {
1294
1214
  if (!currentRoles.length) {
1295
1215
  return false;
1296
1216
  }
1297
- const count = await ctx.db.getRepository("rolesMultiPortals").count({
1217
+ const access = await ctx.db.getRepository("rolesMultiPortals").findOne({
1218
+ fields: ["id"],
1298
1219
  filter: {
1299
1220
  roleName: currentRoles,
1300
1221
  multiPortalUid
1301
1222
  }
1302
1223
  });
1303
- return count > 0;
1224
+ return access !== null;
1225
+ }
1226
+ function throwPortalAccessGateError(ctx, status, code, message) {
1227
+ ctx.throw(status, ctx.t(message, { ns: import_constants.NAMESPACE }), { code });
1228
+ throw new Error(message);
1229
+ }
1230
+ function getRequestedPortalNameFromHeader(ctx) {
1231
+ var _a;
1232
+ const headers = (_a = ctx.request) == null ? void 0 : _a.headers;
1233
+ if (!isRecordLike(headers) || !Object.prototype.hasOwnProperty.call(headers, "x-portal")) {
1234
+ return;
1235
+ }
1236
+ const rawPortalName = headers["x-portal"];
1237
+ if (typeof rawPortalName !== "string") {
1238
+ throwPortalAccessGateError(ctx, 400, PORTAL_CONTEXT_INVALID_CODE, "Invalid Portal context");
1239
+ }
1240
+ const portalName = rawPortalName.startsWith("/x/") ? rawPortalName.slice("/x/".length) : rawPortalName;
1241
+ if (!MULTI_PORTAL_SLUG_PATTERN.test(portalName)) {
1242
+ throwPortalAccessGateError(ctx, 400, PORTAL_CONTEXT_INVALID_CODE, "Invalid Portal context");
1243
+ }
1244
+ return portalName;
1245
+ }
1246
+ function pickPortalAccessDeniedData(ctx, portalName) {
1247
+ const roleCheckBody = isRecordLike(ctx.body) ? ctx.body : {};
1248
+ return {
1249
+ portalName,
1250
+ role: typeof roleCheckBody.role === "string" ? roleCheckBody.role : "",
1251
+ roles: Array.isArray(roleCheckBody.roles) ? roleCheckBody.roles.filter((role) => typeof role === "string") : [],
1252
+ roleMode: typeof roleCheckBody.roleMode === "string" ? roleCheckBody.roleMode : "default",
1253
+ allowAnonymous: roleCheckBody.allowAnonymous === true
1254
+ };
1255
+ }
1256
+ async function checkMultiPortalAccessForRolesCheck(ctx, next) {
1257
+ const portalName = getRequestedPortalNameFromHeader(ctx);
1258
+ if (portalName === void 0) {
1259
+ await next();
1260
+ return;
1261
+ }
1262
+ const portal = await ctx.db.getRepository("multiPortals").findOne({
1263
+ filter: {
1264
+ portalName
1265
+ },
1266
+ fields: ["uid", "portalType", "enabled", "uiLayoutUid"]
1267
+ });
1268
+ if (!portal || portal.get("enabled") !== true || !getDefaultMultiPortalType(portal) || !(0, import_constants.isMultiPortalUiLayoutUid)(portal.get("uiLayoutUid"))) {
1269
+ throwPortalAccessGateError(ctx, 404, PORTAL_NOT_FOUND_CODE, "Portal not found");
1270
+ }
1271
+ const portalUid = String(portal.get("uid"));
1272
+ if ((0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid) || await canAccessMultiPortal(ctx, portalUid)) {
1273
+ await next();
1274
+ return;
1275
+ }
1276
+ await next();
1277
+ ctx.status = 403;
1278
+ ctx.withoutDataWrapping = true;
1279
+ ctx.body = {
1280
+ errors: [
1281
+ {
1282
+ code: PORTAL_ACCESS_DENIED_CODE,
1283
+ message: ctx.t("You do not have access to this Portal", { ns: import_constants.NAMESPACE })
1284
+ }
1285
+ ],
1286
+ data: pickPortalAccessDeniedData(ctx, portalName)
1287
+ };
1304
1288
  }
1305
1289
  async function listCurrentRoleAccessibleMultiPortalUids(ctx) {
1306
1290
  const currentRoles = getCurrentRoles(ctx);
@@ -1363,7 +1347,7 @@ async function removeRouteIdsWithUnauthorizedAncestors(ctx, routeIds) {
1363
1347
  }
1364
1348
  }
1365
1349
  }
1366
- async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1350
+ async function getMultiPortalAccessibleRouteIds(ctx, portalContext) {
1367
1351
  const currentRoles = getCurrentRoles(ctx);
1368
1352
  if (currentRoles.includes("root")) {
1369
1353
  return;
@@ -1371,11 +1355,11 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1371
1355
  if (!currentRoles.length) {
1372
1356
  return /* @__PURE__ */ new Set();
1373
1357
  }
1374
- const routePermissions = await ctx.db.getRepository("rolesMultiPortalDesktopRoutes").find({
1358
+ const routePermissions = await ctx.db.getRepository(portalContext.relation === "uiLayouts" ? "rolesDesktopRoutes" : "rolesMultiPortalDesktopRoutes").find({
1375
1359
  fields: ["desktopRouteId"],
1376
1360
  filter: {
1377
1361
  roleName: currentRoles,
1378
- multiPortalUid
1362
+ ...portalContext.relation === "multiPortals" ? { multiPortalUid: portalContext.portalUid } : {}
1379
1363
  }
1380
1364
  });
1381
1365
  const routeIds = /* @__PURE__ */ new Set();
@@ -1391,7 +1375,7 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1391
1375
  const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1392
1376
  fields: ["id"],
1393
1377
  filter: {
1394
- ...getDesktopRoutePortalFilter(multiPortalUid),
1378
+ ...portalContext.filter,
1395
1379
  id: Array.from(routeIds)
1396
1380
  }
1397
1381
  });
@@ -1421,9 +1405,107 @@ function setDesktopRouteChildren(route, children) {
1421
1405
  const maybeModel = route;
1422
1406
  if (typeof maybeModel.setDataValue === "function") {
1423
1407
  maybeModel.setDataValue("children", children);
1408
+ } else {
1409
+ maybeModel.children = children;
1410
+ }
1411
+ if (!maybeModel._options) {
1412
+ return;
1413
+ }
1414
+ if (!maybeModel._options.includeNames) {
1415
+ maybeModel._options.includeNames = ["children"];
1424
1416
  return;
1425
1417
  }
1426
- route.children = children;
1418
+ if (!maybeModel._options.includeNames.includes("children")) {
1419
+ maybeModel._options.includeNames.push("children");
1420
+ }
1421
+ }
1422
+ function getDesktopRouteId(route) {
1423
+ const id = getRecordField(route, "id");
1424
+ return id === null || id === void 0 ? void 0 : String(id);
1425
+ }
1426
+ function getDesktopRouteParentId(route) {
1427
+ const parentId = getRecordField(route, "parentId");
1428
+ return parentId === null || parentId === void 0 ? void 0 : String(parentId);
1429
+ }
1430
+ function collectDesktopRouteStringIds(routes, routeIds) {
1431
+ for (const route of routes) {
1432
+ const routeId = getDesktopRouteId(route);
1433
+ if (routeId) {
1434
+ routeIds.add(routeId);
1435
+ }
1436
+ const children = getRecordField(route, "children");
1437
+ if (Array.isArray(children)) {
1438
+ collectDesktopRouteStringIds(children, routeIds);
1439
+ }
1440
+ }
1441
+ }
1442
+ function removeNestedRootDesktopRoutes(routes) {
1443
+ if (!Array.isArray(routes)) {
1444
+ return [];
1445
+ }
1446
+ const routeIds = /* @__PURE__ */ new Set();
1447
+ collectDesktopRouteStringIds(routes, routeIds);
1448
+ return routes.filter((route) => {
1449
+ const parentId = getDesktopRouteParentId(route);
1450
+ return !parentId || !routeIds.has(parentId);
1451
+ });
1452
+ }
1453
+ function buildAccessibleDesktopRouteTreeWithAncestors(routes, accessibleRouteIds) {
1454
+ const routeById = /* @__PURE__ */ new Map();
1455
+ const childrenByParentId = /* @__PURE__ */ new Map();
1456
+ const roots = [];
1457
+ for (const route of routes) {
1458
+ const routeId = getDesktopRouteId(route);
1459
+ if (!routeId) {
1460
+ continue;
1461
+ }
1462
+ setDesktopRouteChildren(route, void 0);
1463
+ routeById.set(routeId, route);
1464
+ }
1465
+ for (const route of routes) {
1466
+ const routeId = getDesktopRouteId(route);
1467
+ if (!routeId || !routeById.has(routeId)) {
1468
+ continue;
1469
+ }
1470
+ const parentId = getDesktopRouteParentId(route);
1471
+ if (!parentId || !routeById.has(parentId)) {
1472
+ roots.push(route);
1473
+ continue;
1474
+ }
1475
+ const children = childrenByParentId.get(parentId) ?? [];
1476
+ children.push(route);
1477
+ childrenByParentId.set(parentId, children);
1478
+ }
1479
+ const visitRoute = (route, visitingRouteIds) => {
1480
+ const routeId = getDesktopRouteId(route);
1481
+ if (!routeId || visitingRouteIds.has(routeId)) {
1482
+ return void 0;
1483
+ }
1484
+ visitingRouteIds.add(routeId);
1485
+ const visibleChildren = (childrenByParentId.get(routeId) ?? []).map((child) => visitRoute(child, visitingRouteIds)).filter((child) => child !== void 0);
1486
+ visitingRouteIds.delete(routeId);
1487
+ if (!accessibleRouteIds.has(routeId) && visibleChildren.length === 0) {
1488
+ return void 0;
1489
+ }
1490
+ setDesktopRouteChildren(route, visibleChildren.length ? visibleChildren : void 0);
1491
+ return route;
1492
+ };
1493
+ return roots.map((route) => visitRoute(route, /* @__PURE__ */ new Set())).filter((route) => route !== void 0);
1494
+ }
1495
+ async function includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalFilter) {
1496
+ if (!Array.isArray(routes)) {
1497
+ return routes;
1498
+ }
1499
+ const accessibleRouteIds = /* @__PURE__ */ new Set();
1500
+ collectDesktopRouteStringIds(routes, accessibleRouteIds);
1501
+ if (!accessibleRouteIds.size) {
1502
+ return routes;
1503
+ }
1504
+ const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1505
+ sort: "sort",
1506
+ filter: portalFilter
1507
+ });
1508
+ return buildAccessibleDesktopRouteTreeWithAncestors(portalRoutes, accessibleRouteIds);
1427
1509
  }
1428
1510
  function removeDesktopRoutesByIds(routes, routeIds) {
1429
1511
  return routes.map((route) => {
@@ -1490,23 +1572,26 @@ async function removeMultiPortalOwnedRouteFromGetResponse(ctx) {
1490
1572
  ctx.body = void 0;
1491
1573
  }
1492
1574
  async function replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext) {
1493
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1575
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1494
1576
  if (routeIds && routeIds.size === 0) {
1495
1577
  ctx.body = [];
1496
1578
  return;
1497
1579
  }
1498
- ctx.body = await ctx.db.getRepository("desktopRoutes").find({
1580
+ const routes = await ctx.db.getRepository("desktopRoutes").find({
1499
1581
  tree: true,
1500
1582
  sort: "sort",
1501
1583
  filter: {
1502
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1584
+ ...portalContext.filter,
1503
1585
  ...routeIds ? { id: Array.from(routeIds) } : {}
1504
1586
  }
1505
1587
  });
1588
+ ctx.body = removeNestedRootDesktopRoutes(
1589
+ await includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalContext.filter)
1590
+ );
1506
1591
  }
1507
1592
  async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext) {
1508
1593
  var _a;
1509
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1594
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1510
1595
  if (routeIds && routeIds.size === 0) {
1511
1596
  ctx.status = 204;
1512
1597
  ctx.body = void 0;
@@ -1516,7 +1601,7 @@ async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext
1516
1601
  sort: "sort",
1517
1602
  filterByTk: (_a = ctx.action) == null ? void 0 : _a.params.filterByTk,
1518
1603
  filter: {
1519
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1604
+ ...portalContext.filter,
1520
1605
  ...routeIds ? { id: Array.from(routeIds) } : {}
1521
1606
  }
1522
1607
  });
@@ -1553,13 +1638,10 @@ async function addMultiPortalListAccessibleGuard(ctx, next) {
1553
1638
  return;
1554
1639
  }
1555
1640
  await next();
1556
- if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1641
+ if (portalContext) {
1557
1642
  await replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext);
1558
1643
  return;
1559
1644
  }
1560
- if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1561
- return;
1562
- }
1563
1645
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1564
1646
  }
1565
1647
  async function addMultiPortalGetAccessibleGuard(ctx, next) {
@@ -1570,17 +1652,13 @@ async function addMultiPortalGetAccessibleGuard(ctx, next) {
1570
1652
  return;
1571
1653
  }
1572
1654
  await next();
1573
- if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1655
+ if (portalContext) {
1574
1656
  await replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext);
1575
1657
  return;
1576
1658
  }
1577
- if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1578
- return;
1579
- }
1580
1659
  await removeMultiPortalOwnedRouteFromGetResponse(ctx);
1581
1660
  }
1582
1661
  async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next) {
1583
- var _a;
1584
1662
  const portalRequest = await findRequestedMultiPortal(ctx);
1585
1663
  if (portalRequest.requested && !portalRequest.portal) {
1586
1664
  ctx.status = 200;
@@ -1588,22 +1666,19 @@ async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next)
1588
1666
  return;
1589
1667
  }
1590
1668
  const scope = portalRequest.scope;
1591
- if ((scope == null ? void 0 : scope.relation) === "uiLayouts") {
1592
- if ((_a = ctx.action) == null ? void 0 : _a.params) {
1593
- ctx.action.params.layout = scope.uiLayoutUid;
1594
- }
1595
- await next();
1596
- return;
1597
- }
1598
- if ((scope == null ? void 0 : scope.relation) === "multiPortals") {
1669
+ if (scope) {
1599
1670
  const routes = await ctx.db.getRepository("desktopRoutes").find({
1600
- tree: true,
1601
1671
  sort: "sort",
1602
1672
  filter: scope.filter,
1603
1673
  fields: [...DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS]
1604
1674
  });
1675
+ const routeIds = /* @__PURE__ */ new Set();
1676
+ collectDesktopRouteStringIds(routes, routeIds);
1677
+ const routeTree = buildAccessibleDesktopRouteTreeWithAncestors(routes, routeIds);
1605
1678
  ctx.status = 200;
1606
- ctx.body = routes.map((route) => pickDesktopRouteRolePermissionTargetFields(route));
1679
+ ctx.body = removeNestedRootDesktopRoutes(routeTree).map(
1680
+ (route) => pickDesktopRouteRolePermissionTargetFields(route)
1681
+ );
1607
1682
  return;
1608
1683
  }
1609
1684
  await next();
@@ -1691,7 +1766,7 @@ async function findDesktopRouteMutationTargets(ctx, scopeFilter, transaction, se
1691
1766
  ...filter ?? {},
1692
1767
  ...scopeFilter ?? {}
1693
1768
  },
1694
- ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1769
+ ...transaction && !scopeFilter ? { lock: transaction.LOCK.UPDATE } : {},
1695
1770
  transaction
1696
1771
  });
1697
1772
  }
@@ -1843,7 +1918,6 @@ async function guardDesktopRouteMoveScope(ctx, next) {
1843
1918
  id: routeIds,
1844
1919
  ...scope.filter
1845
1920
  },
1846
- lock: transaction.LOCK.UPDATE,
1847
1921
  transaction
1848
1922
  });
1849
1923
  if (normalizeDesktopRouteTargetIds(allRoutes).join(",") !== normalizeDesktopRouteTargetIds(scopedRoutes).join(",") || scopedRoutes.length !== routeIds.length) {
@@ -1873,7 +1947,6 @@ async function guardDesktopRouteMoveScope(ctx, next) {
1873
1947
  id: parentIds,
1874
1948
  ...scope.filter
1875
1949
  },
1876
- lock: transaction.LOCK.UPDATE,
1877
1950
  transaction
1878
1951
  });
1879
1952
  if (normalizeDesktopRouteTargetIds(parents).join(",") !== parentIds.map(String).sort().join(",")) {
@@ -1921,7 +1994,6 @@ async function guardDesktopRouteMoveScope(ctx, next) {
1921
1994
  $gte: targetSort
1922
1995
  }
1923
1996
  },
1924
- lock: transaction.LOCK.UPDATE,
1925
1997
  transaction
1926
1998
  });
1927
1999
  const affectedRouteIds = affectedRoutes.map((route) => route.get("id"));
@@ -2019,7 +2091,7 @@ async function findDesktopRouteDestroyRoots(ctx, transaction, scopeFilter) {
2019
2091
  ...scopeFilter ?? {},
2020
2092
  ...filterByTk.length ? { id: filterByTk } : {}
2021
2093
  },
2022
- lock: transaction.LOCK.UPDATE,
2094
+ ...scopeFilter ? {} : { lock: transaction.LOCK.UPDATE },
2023
2095
  transaction
2024
2096
  });
2025
2097
  return collectDesktopRouteIds(records);
@@ -2085,13 +2157,20 @@ async function detachDesktopRouteTreeOwner(ctx, routeIds, scope, transaction) {
2085
2157
  }
2086
2158
  async function detachOwnerAndFindDestroyRoots(ctx, routeIds, scope, transaction) {
2087
2159
  var _a;
2160
+ await ctx.db.getRepository("desktopRoutes").find({
2161
+ fields: ["id"],
2162
+ filter: {
2163
+ id: routeIds
2164
+ },
2165
+ lock: transaction.LOCK.UPDATE,
2166
+ transaction
2167
+ });
2088
2168
  const routes = await ctx.db.getRepository("desktopRoutes").find({
2089
2169
  fields: ["id", "parentId"],
2090
2170
  appends: ["multiPortals", "uiLayouts"],
2091
2171
  filter: {
2092
2172
  id: routeIds
2093
2173
  },
2094
- lock: transaction.LOCK.UPDATE,
2095
2174
  transaction
2096
2175
  });
2097
2176
  const routesById = new Map(routes.map((route) => [String(route.get("id")), route]));
@@ -2187,24 +2266,119 @@ async function listEnabledMultiPortals(ctx, next) {
2187
2266
  const records = await ctx.db.getRepository("multiPortals").find({
2188
2267
  filter: {
2189
2268
  enabled: true,
2190
- "uiLayout.enabled": true
2269
+ uiLayoutUid: {
2270
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2271
+ }
2191
2272
  },
2192
- fields: [...MULTI_PORTAL_RUNTIME_QUERY_FIELDS],
2193
- appends: ["uiLayout"],
2273
+ fields: [...MULTI_PORTAL_RUNTIME_FIELDS],
2194
2274
  sort: ["uid"]
2195
2275
  });
2196
2276
  ctx.body = records.map((record) => pickMultiPortalRuntimeFields(record));
2197
2277
  await next();
2198
2278
  }
2279
+ const DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS = ["uid", "portalType", "routePath"];
2280
+ function getDefaultMultiPortalType(record) {
2281
+ const portalType = record.get("portalType");
2282
+ if (portalType === null || portalType === void 0) {
2283
+ return "no-code";
2284
+ }
2285
+ return isMultiPortalSeedType(portalType) ? portalType : null;
2286
+ }
2287
+ function pickDefaultMultiPortalFields(record) {
2288
+ return {
2289
+ uid: String(record.get("uid")),
2290
+ portalType: getDefaultMultiPortalType(record) || "no-code",
2291
+ routePath: String(record.get("routePath"))
2292
+ };
2293
+ }
2294
+ async function findEnabledDefaultMultiPortal(ctx, transaction) {
2295
+ const record = await ctx.db.getRepository("multiPortals").findOne({
2296
+ filter: {
2297
+ enabled: true,
2298
+ isDefault: true
2299
+ },
2300
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS, "uiLayoutUid"],
2301
+ transaction
2302
+ });
2303
+ if (!record) {
2304
+ return null;
2305
+ }
2306
+ const portalType = getDefaultMultiPortalType(record);
2307
+ if (!portalType) {
2308
+ return null;
2309
+ }
2310
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(record.get("uiLayoutUid"))) {
2311
+ return null;
2312
+ }
2313
+ return record;
2314
+ }
2315
+ async function getDefaultMultiPortal(ctx, next) {
2316
+ const record = await findEnabledDefaultMultiPortal(ctx);
2317
+ ctx.body = record ? pickDefaultMultiPortalFields(record) : null;
2318
+ ctx.status = 200;
2319
+ await next();
2320
+ }
2321
+ async function setDefaultMultiPortal(ctx, next) {
2322
+ var _a;
2323
+ const filterByTk = (_a = ctx.action) == null ? void 0 : _a.params.filterByTk;
2324
+ if (filterByTk === void 0 || filterByTk === null || filterByTk === "") {
2325
+ ctx.throw(400, ctx.t("filterByTk is required", { ns: import_constants.NAMESPACE }));
2326
+ return;
2327
+ }
2328
+ const repository = ctx.db.getRepository("multiPortals");
2329
+ const updated = await ctx.db.sequelize.transaction(async (transaction) => {
2330
+ const target = await repository.findOne({
2331
+ filterByTk,
2332
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS, "enabled", "uiLayoutUid"],
2333
+ lock: transaction.LOCK.UPDATE,
2334
+ transaction
2335
+ });
2336
+ if (!target) {
2337
+ ctx.throw(404, ctx.t("Portal not found", { ns: import_constants.NAMESPACE }));
2338
+ return null;
2339
+ }
2340
+ if (target.get("enabled") !== true) {
2341
+ ctx.throw(400, ctx.t("Disabled Portal cannot be set as default", { ns: import_constants.NAMESPACE }));
2342
+ return null;
2343
+ }
2344
+ const portalType = getDefaultMultiPortalType(target);
2345
+ if (!portalType) {
2346
+ ctx.throw(400, ctx.t("Unsupported Portal type cannot be set as default", { ns: import_constants.NAMESPACE }));
2347
+ return null;
2348
+ }
2349
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(target.get("uiLayoutUid"))) {
2350
+ ctx.throw(400, ctx.t("Portal device configuration is invalid", { ns: import_constants.NAMESPACE }));
2351
+ return null;
2352
+ }
2353
+ await repository.update({
2354
+ filter: { isDefault: true },
2355
+ values: { isDefault: null },
2356
+ transaction
2357
+ });
2358
+ await repository.update({
2359
+ filterByTk,
2360
+ values: { isDefault: true },
2361
+ transaction
2362
+ });
2363
+ return repository.findOne({
2364
+ filterByTk,
2365
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS],
2366
+ transaction
2367
+ });
2368
+ });
2369
+ ctx.body = updated ? pickDefaultMultiPortalFields(updated) : null;
2370
+ await next();
2371
+ }
2199
2372
  async function listAccessibleMultiPortals(ctx, next) {
2200
2373
  const accessiblePortalUids = await listCurrentRoleAccessibleMultiPortalUids(ctx);
2201
2374
  const records = await ctx.db.getRepository("multiPortals").find({
2202
2375
  filter: {
2203
2376
  enabled: true,
2204
- "uiLayout.enabled": true
2377
+ uiLayoutUid: {
2378
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2379
+ }
2205
2380
  },
2206
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2207
- appends: ["uiLayout"],
2381
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
2208
2382
  sort: ["uid"]
2209
2383
  });
2210
2384
  const accessiblePortalUidSet = Array.isArray(accessiblePortalUids) ? new Set(accessiblePortalUids) : void 0;
@@ -2343,11 +2517,15 @@ async function grantDefaultAccessToNewMultiPortal(db, multiPortal, options) {
2343
2517
  class PluginMultiPortalServer extends import_server.Plugin {
2344
2518
  portalStorageTaskKeys = /* @__PURE__ */ new Set();
2345
2519
  portalStorageTasks = /* @__PURE__ */ new Map();
2520
+ deprecatedInitPortalEnvWarningEmitted = false;
2346
2521
  async afterAdd() {
2347
2522
  }
2348
2523
  getAppName() {
2349
2524
  return this.app.name || MAIN_APP_NAME;
2350
2525
  }
2526
+ getCurrentStorageAppName() {
2527
+ return normalizePortalStorageName(this.getAppName()) || MAIN_APP_NAME;
2528
+ }
2351
2529
  getMultiPortalStorageItem(multiPortal, previous = false) {
2352
2530
  const record = multiPortal;
2353
2531
  const readField = (field) => previous && typeof record.previous === "function" ? record.previous(field) : getRecordField(record, field);
@@ -2362,8 +2540,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
2362
2540
  return {
2363
2541
  appName: this.getAppName(),
2364
2542
  portalName,
2365
- enabled: readField("enabled") === true,
2366
- config: getPortalStorageConfig(readField("options"))
2543
+ enabled: readField("enabled") === true
2367
2544
  };
2368
2545
  }
2369
2546
  warnPortalStorageSyncFailed(error) {
@@ -2373,6 +2550,14 @@ class PluginMultiPortalServer extends import_server.Plugin {
2373
2550
  error
2374
2551
  });
2375
2552
  }
2553
+ logPortalBuildHtml(item, status, reason) {
2554
+ var _a, _b;
2555
+ (_b = (_a = this.app.logger) == null ? void 0 : _a.info) == null ? void 0 : _b.call(_a, `Portal yarn build:html ${status} for ${item.appName}/${item.portalName}`, {
2556
+ appName: item.appName,
2557
+ portalName: item.portalName,
2558
+ reason
2559
+ });
2560
+ }
2376
2561
  getPortalStorageTaskKey(item) {
2377
2562
  return `${item.appName}/${item.portalName}`;
2378
2563
  }
@@ -2391,10 +2576,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2391
2576
  }
2392
2577
  await run();
2393
2578
  }
2394
- async removePortalStorageDist(item) {
2395
- await import_fs.default.promises.rm((0, import_utils.storagePathJoin)("portals", item.appName, item.portalName, "dist"), {
2396
- force: true,
2397
- recursive: true
2579
+ async removePortalStorageIndexHtml(item) {
2580
+ await import_fs.default.promises.rm((0, import_utils.storagePathJoin)("portals", item.appName, item.portalName, "dist", "index.html"), {
2581
+ force: true
2398
2582
  });
2399
2583
  }
2400
2584
  waitForPortalStorageTasks() {
@@ -2404,10 +2588,11 @@ class PluginMultiPortalServer extends import_server.Plugin {
2404
2588
  }
2405
2589
  return Promise.allSettled(tasks).then(() => void 0);
2406
2590
  }
2407
- async schedulePortalTemplateCopyAndBuild(item, template, portalDir) {
2591
+ async schedulePortalTemplateCopyAndBuild(item, template, portalDir, options) {
2408
2592
  var _a;
2409
2593
  const taskKey = this.getPortalStorageTaskKey(item);
2410
2594
  if (this.portalStorageTaskKeys.has(taskKey)) {
2595
+ this.logPortalBuildHtml(item, "skipped", "a storage task is already running");
2411
2596
  await ((_a = template.cleanup) == null ? void 0 : _a.call(template));
2412
2597
  return;
2413
2598
  }
@@ -2428,14 +2613,13 @@ class PluginMultiPortalServer extends import_server.Plugin {
2428
2613
  await copyPortalTemplate(template.dir, portalDir);
2429
2614
  await appendPortalStorageLog(logPath, `Default portal template copied to ${portalDir}.`);
2430
2615
  }
2431
- await ensurePortalStorageConfigFiles(portalDir, item);
2432
- await appendPortalStorageLog(logPath, `Portal configuration files updated in ${portalDir}.`);
2433
2616
  if (item.enabled) {
2434
- await buildPortalStorageItem(portalDir, item);
2617
+ this.logPortalBuildHtml(item, "requested", "storage directory was initialized");
2618
+ await buildPortalStorageItem(portalDir, item, options);
2619
+ this.logPortalBuildHtml(item, "completed", "yarn build:html finished successfully");
2435
2620
  return;
2436
2621
  }
2437
- await this.removePortalStorageDist(item);
2438
- await appendPortalStorageLog(logPath, `Portal dist directory removed for ${item.appName}/${item.portalName}.`);
2622
+ this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
2439
2623
  } catch (error) {
2440
2624
  await appendPortalStorageLog(
2441
2625
  logPath,
@@ -2451,32 +2635,41 @@ class PluginMultiPortalServer extends import_server.Plugin {
2451
2635
  this.portalStorageTasks.set(taskKey, task);
2452
2636
  await task;
2453
2637
  }
2454
- async ensurePortalStorageItem(item) {
2638
+ async ensurePortalStorageItem(item, options = {}) {
2455
2639
  const portalDir = (0, import_utils.storagePathJoin)("portals", item.appName, item.portalName);
2456
2640
  const portalIndex = import_path.default.join(portalDir, "dist", "index.html");
2641
+ const logPath = getPortalStorageLogPath(item);
2457
2642
  if (!await pathExists(portalDir)) {
2458
- const template = await resolvePortalTemplate(getInitPortalTemplate(), getPortalStorageLogPath(item));
2459
- await this.schedulePortalTemplateCopyAndBuild(item, template, portalDir);
2643
+ const template = await resolvePortalTemplate(getInitPortalTemplate(), logPath);
2644
+ await this.schedulePortalTemplateCopyAndBuild(item, template, portalDir, options);
2460
2645
  return;
2461
2646
  }
2462
- await ensurePortalStorageConfigFiles(portalDir, item);
2463
2647
  if (item.enabled) {
2464
- if (!await pathExists(portalIndex)) {
2465
- await buildPortalStorageItem(portalDir, item);
2648
+ const hasPortalIndex = await pathExists(portalIndex);
2649
+ if (options.forceBuild || !hasPortalIndex) {
2650
+ this.logPortalBuildHtml(
2651
+ item,
2652
+ "requested",
2653
+ options.forceBuild ? "forceBuild is enabled" : "dist/index.html does not exist"
2654
+ );
2655
+ await buildPortalStorageItem(portalDir, item, options);
2656
+ this.logPortalBuildHtml(item, "completed", "yarn build:html finished successfully");
2657
+ } else {
2658
+ this.logPortalBuildHtml(item, "skipped", "dist/index.html already exists");
2466
2659
  }
2467
2660
  return;
2468
2661
  }
2469
- await this.removePortalStorageDist(item);
2662
+ this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
2470
2663
  }
2471
- async syncMultiPortalStorageItem(multiPortal, options, syncPrevious = false) {
2664
+ async syncMultiPortalStorageItem(multiPortal, options, syncPrevious = false, forceBuild = false) {
2472
2665
  const currentItem = this.getMultiPortalStorageItem(multiPortal);
2473
2666
  const previousItem = syncPrevious ? this.getMultiPortalStorageItem(multiPortal, true) : null;
2474
2667
  await this.runPortalStorageTask(async () => {
2475
- if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName || !currentItem.enabled)) {
2476
- await this.removePortalStorageDist(previousItem);
2668
+ if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName)) {
2669
+ await this.removePortalStorageIndexHtml(previousItem);
2477
2670
  }
2478
2671
  if (currentItem) {
2479
- await this.ensurePortalStorageItem(currentItem);
2672
+ await this.ensurePortalStorageItem(currentItem, { ...options, forceBuild });
2480
2673
  }
2481
2674
  }, options);
2482
2675
  }
@@ -2519,12 +2712,12 @@ class PluginMultiPortalServer extends import_server.Plugin {
2519
2712
  await next();
2520
2713
  }
2521
2714
  async deployPortalDist(ctx, next) {
2522
- var _a, _b, _c, _d;
2715
+ var _a, _b, _c;
2523
2716
  const deployCtx = ctx;
2524
- const appName = normalizePortalStorageName(((_a = deployCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2525
- const portalName = normalizePortalStorageName((_b = deployCtx.request.body) == null ? void 0 : _b.portal);
2526
- const basePath = trimString((_c = deployCtx.request.body) == null ? void 0 : _c.basePath);
2527
- const filePath = trimString((_d = deployCtx.request.file) == null ? void 0 : _d.path);
2717
+ const appName = this.getCurrentStorageAppName();
2718
+ const portalName = normalizePortalStorageName((_a = deployCtx.request.body) == null ? void 0 : _a.portal);
2719
+ const basePath = trimString((_b = deployCtx.request.body) == null ? void 0 : _b.basePath);
2720
+ const filePath = trimString((_c = deployCtx.request.file) == null ? void 0 : _c.path);
2528
2721
  try {
2529
2722
  if (!isValidPortalDeploySegment(appName)) {
2530
2723
  throw new Error("Invalid app");
@@ -2561,9 +2754,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2561
2754
  }
2562
2755
  }
2563
2756
  async pullPortalSource(ctx, next) {
2564
- var _a, _b;
2565
- const appName = normalizePortalStorageName(((_a = ctx.action.params.values) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2566
- const portalName = normalizePortalStorageName((_b = ctx.action.params.values) == null ? void 0 : _b.portal);
2757
+ var _a;
2758
+ const appName = this.getCurrentStorageAppName();
2759
+ const portalName = normalizePortalStorageName((_a = ctx.action.params.values) == null ? void 0 : _a.portal);
2567
2760
  try {
2568
2761
  if (!isValidPortalDeploySegment(appName)) {
2569
2762
  throw new Error("Invalid app");
@@ -2583,11 +2776,11 @@ class PluginMultiPortalServer extends import_server.Plugin {
2583
2776
  }
2584
2777
  }
2585
2778
  async pushPortalSource(ctx, next) {
2586
- var _a, _b, _c;
2779
+ var _a, _b;
2587
2780
  const sourceCtx = ctx;
2588
- const appName = normalizePortalStorageName(((_a = sourceCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2589
- const portalName = normalizePortalStorageName((_b = sourceCtx.request.body) == null ? void 0 : _b.portal);
2590
- const filePath = trimString((_c = sourceCtx.request.file) == null ? void 0 : _c.path);
2781
+ const appName = this.getCurrentStorageAppName();
2782
+ const portalName = normalizePortalStorageName((_a = sourceCtx.request.body) == null ? void 0 : _a.portal);
2783
+ const filePath = trimString((_b = sourceCtx.request.file) == null ? void 0 : _b.path);
2591
2784
  try {
2592
2785
  if (!isValidPortalDeploySegment(appName)) {
2593
2786
  throw new Error("Invalid app");
@@ -2641,38 +2834,20 @@ class PluginMultiPortalServer extends import_server.Plugin {
2641
2834
  const records = await this.db.getRepository("multiPortals").find({
2642
2835
  filter: {
2643
2836
  enabled: true,
2644
- "uiLayout.enabled": true
2837
+ uiLayoutUid: {
2838
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2839
+ }
2645
2840
  },
2646
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2647
- appends: ["uiLayout"],
2841
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
2648
2842
  sort: ["uid"],
2649
2843
  transaction: options == null ? void 0 : options.transaction
2650
2844
  });
2651
- return records.map((record) => {
2652
- const portal = pickMultiPortalAccessibleFields(record);
2653
- const uiLayout = portal.uiLayout;
2654
- const uid = typeof portal.uid === "string" ? portal.uid : "";
2655
- const title = typeof portal.title === "string" ? portal.title : "";
2656
- return {
2657
- uid,
2658
- title,
2659
- icon: typeof portal.icon === "string" ? portal.icon : null,
2660
- portalType: typeof portal.portalType === "string" ? portal.portalType : null,
2661
- routePath: String(portal.routePath || ""),
2662
- layout: typeof (uiLayout == null ? void 0 : uiLayout.layoutType) === "string" ? uiLayout.layoutType : null
2663
- };
2845
+ return records.flatMap((record) => {
2846
+ const item = this.toAppPortalManifestItem(record);
2847
+ return item ? [item] : [];
2664
2848
  });
2665
2849
  }
2666
- async getUiLayout(uid, options) {
2667
- if (typeof uid !== "string" || !uid) {
2668
- return null;
2669
- }
2670
- return this.db.getRepository("uiLayouts").findOne({
2671
- filterByTk: uid,
2672
- transaction: options == null ? void 0 : options.transaction
2673
- });
2674
- }
2675
- async toAppPortalManifestItem(multiPortal, options) {
2850
+ toAppPortalManifestItem(multiPortal) {
2676
2851
  const uid = getRecordField(multiPortal, "uid");
2677
2852
  const title = getRecordField(multiPortal, "title");
2678
2853
  const portalType = getRecordField(multiPortal, "portalType");
@@ -2682,19 +2857,18 @@ class PluginMultiPortalServer extends import_server.Plugin {
2682
2857
  return null;
2683
2858
  }
2684
2859
  const uiLayoutUid = getRecordField(multiPortal, "uiLayoutUid");
2685
- const uiLayout = getRecordField(multiPortal, "uiLayout") || await this.getUiLayout(uiLayoutUid, options);
2686
- if (!uiLayout || getRecordField(uiLayout, "enabled") !== true) {
2860
+ const layout = (0, import_constants.getMultiPortalLayoutType)(uiLayoutUid);
2861
+ if (!layout) {
2687
2862
  return null;
2688
2863
  }
2689
2864
  const icon = getRecordField(multiPortal, "icon");
2690
- const layoutType = getRecordField(uiLayout, "layoutType");
2691
2865
  return {
2692
2866
  uid,
2693
2867
  title,
2694
2868
  icon: typeof icon === "string" ? icon : null,
2695
2869
  portalType: typeof portalType === "string" ? portalType : null,
2696
2870
  routePath,
2697
- layout: typeof layoutType === "string" ? layoutType : null
2871
+ layout
2698
2872
  };
2699
2873
  }
2700
2874
  async setAppManifestItem(item, options) {
@@ -2737,30 +2911,13 @@ class PluginMultiPortalServer extends import_server.Plugin {
2737
2911
  if (typeof uid !== "string" || !uid) {
2738
2912
  return;
2739
2913
  }
2740
- const item = await this.toAppPortalManifestItem(multiPortal, options);
2914
+ const item = this.toAppPortalManifestItem(multiPortal);
2741
2915
  if (item) {
2742
2916
  await this.setAppManifestItem(item, options);
2743
2917
  return;
2744
2918
  }
2745
2919
  await this.removeAppManifestItem(uid, options);
2746
2920
  }
2747
- async publishUiLayoutManifestItems(uiLayout, options) {
2748
- const uiLayoutUid = getRecordField(uiLayout, "uid");
2749
- if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
2750
- return;
2751
- }
2752
- const records = await this.db.getRepository("multiPortals").find({
2753
- filter: {
2754
- uiLayoutUid
2755
- },
2756
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2757
- transaction: options == null ? void 0 : options.transaction
2758
- });
2759
- for (const record of records) {
2760
- record.set("uiLayout", uiLayout);
2761
- await this.publishAppManifestItem(record, options);
2762
- }
2763
- }
2764
2921
  async reconcilePortalStorage(options) {
2765
2922
  const records = await this.db.getRepository("multiPortals").find({
2766
2923
  filter: {
@@ -2812,6 +2969,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
2812
2969
  }
2813
2970
  async beforeLoad() {
2814
2971
  this.app.db.registerRepositories({ MultiPortalDesktopRouteRepository });
2972
+ this.app.resourceManager.registerPreActionHandler("roles:check", checkMultiPortalAccessForRolesCheck);
2815
2973
  this.app.resourceManager.registerPreActionHandler("desktopRoutes:list", addDesktopRouteEffectiveScopeFilter);
2816
2974
  this.app.resourceManager.registerPreActionHandler("desktopRoutes:get", addDesktopRouteEffectiveScopeFilter);
2817
2975
  this.app.resourceManager.registerPreActionHandler(
@@ -2823,26 +2981,30 @@ class PluginMultiPortalServer extends import_server.Plugin {
2823
2981
  "desktopRoutes:listRolePermissionTargets",
2824
2982
  mapMultiPortalLayoutToUiLayoutForRolePermissionTargets
2825
2983
  );
2984
+ this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventDirectDefaultPortalMutation);
2985
+ this.app.resourceManager.registerPreActionHandler("multiPortals:create", validateMultiPortalUiLayoutUidWrite);
2826
2986
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", captureSkipCreatePortalDirectory);
2827
2987
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", normalizeMultiPortalSlugValues);
2828
- this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventUiLayoutRouteNameConflict);
2829
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventMultiPortalBackingLayoutChange);
2988
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventDirectDefaultPortalMutation);
2989
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", validateMultiPortalUiLayoutUidWrite);
2830
2990
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", normalizeMultiPortalSlugValues);
2831
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventUiLayoutRouteNameConflict);
2991
+ this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", preventDirectDefaultPortalMutation);
2832
2992
  this.app.resourceManager.registerPreActionHandler(
2833
2993
  "multiPortals:firstOrCreate",
2834
- preventMultiPortalBackingLayoutChange
2994
+ validateMultiPortalUiLayoutUidWrite
2835
2995
  );
2836
2996
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", captureSkipCreatePortalDirectory);
2837
2997
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", normalizeMultiPortalSlugValues);
2838
- this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", preventUiLayoutRouteNameConflict);
2839
2998
  this.app.resourceManager.registerPreActionHandler(
2840
2999
  "multiPortals:updateOrCreate",
2841
- preventMultiPortalBackingLayoutChange
3000
+ preventDirectDefaultPortalMutation
3001
+ );
3002
+ this.app.resourceManager.registerPreActionHandler(
3003
+ "multiPortals:updateOrCreate",
3004
+ validateMultiPortalUiLayoutUidWrite
2842
3005
  );
2843
3006
  this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", captureSkipCreatePortalDirectory);
2844
3007
  this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", normalizeMultiPortalSlugValues);
2845
- this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", preventUiLayoutRouteNameConflict);
2846
3008
  this.app.resourceManager.registerPreActionHandler("desktopRoutes:create", addDesktopRouteCreateMultiPortal, {
2847
3009
  after: UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG
2848
3010
  });
@@ -2867,12 +3029,15 @@ class PluginMultiPortalServer extends import_server.Plugin {
2867
3029
  actions: ROLE_MULTI_PORTAL_PERMISSION_ACTIONS
2868
3030
  });
2869
3031
  this.app.acl.allow("multiPortals", "listEnabled", "public");
3032
+ this.app.acl.allow("multiPortals", "getDefault", "public");
2870
3033
  this.app.acl.allow("multiPortals", "listAccessible", "loggedIn");
2871
3034
  this.app.resourceManager.use(createPortalDeployUploadMiddleware(), {
2872
3035
  tag: "multiPortalDeployUpload",
2873
3036
  after: "acl"
2874
3037
  });
2875
3038
  this.app.resourceManager.registerActionHandler("multiPortals:listEnabled", listEnabledMultiPortals);
3039
+ this.app.resourceManager.registerActionHandler("multiPortals:getDefault", getDefaultMultiPortal);
3040
+ this.app.resourceManager.registerActionHandler("multiPortals:setDefault", setDefaultMultiPortal);
2876
3041
  this.app.resourceManager.registerActionHandler("multiPortals:listAccessible", listAccessibleMultiPortals);
2877
3042
  this.app.resourceManager.registerActionHandler("multiPortals:deploy", async (ctx, next) => {
2878
3043
  await this.deployPortalDist(ctx, next);
@@ -2897,13 +3062,18 @@ class PluginMultiPortalServer extends import_server.Plugin {
2897
3062
  (0, import_ensureDefaultRoleMultiPortalAccess.applyDefaultRoleMultiPortalAccess)(role);
2898
3063
  });
2899
3064
  this.app.db.on("desktopRoutes.afterCreate", reconcileCreatedDesktopRoutePermissions);
3065
+ this.app.db.on("multiPortals.beforeUpdate", (multiPortal) => {
3066
+ if (multiPortal.get("enabled") === false && multiPortal.get("isDefault") === true) {
3067
+ multiPortal.set("isDefault", null);
3068
+ }
3069
+ });
2900
3070
  this.app.db.on("multiPortals.afterCreate", async (multiPortal, options) => {
2901
3071
  await grantDefaultAccessToNewMultiPortal(this.app.db, multiPortal, options);
2902
3072
  await this.publishAppManifestItem(multiPortal, options);
2903
3073
  if (shouldSkipCreatePortalDirectory(options)) {
2904
3074
  return;
2905
3075
  }
2906
- await this.syncMultiPortalStorageItem(multiPortal, options);
3076
+ await this.syncMultiPortalStorageItem(multiPortal, options, false, true);
2907
3077
  });
2908
3078
  this.app.db.on("multiPortals.afterUpdate", async (multiPortal, options) => {
2909
3079
  await this.publishAppManifestItem(multiPortal, options);
@@ -2916,11 +3086,16 @@ class PluginMultiPortalServer extends import_server.Plugin {
2916
3086
  }
2917
3087
  await this.removeMultiPortalStorageItem(multiPortal, options);
2918
3088
  });
2919
- this.app.db.on("uiLayouts.afterUpdate", async (uiLayout, options) => {
2920
- await this.publishUiLayoutManifestItems(uiLayout, options);
2921
- });
2922
3089
  }
2923
3090
  async install() {
3091
+ var _a, _b;
3092
+ if (!this.deprecatedInitPortalEnvWarningEmitted && hasDeprecatedInitPortalEnv()) {
3093
+ this.deprecatedInitPortalEnvWarningEmitted = true;
3094
+ (_b = (_a = this.app.logger) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(
3095
+ _a,
3096
+ 'INIT_PORTAL_TYPE and INIT_PORTAL_NAME are deprecated and no longer affect multi-portal seeding; NocoBase now creates the AI Portal "main" plus the fixed no-code "admin" and "mobile" portals by default.'
3097
+ );
3098
+ }
2924
3099
  const version = await this.app.version.get();
2925
3100
  if (!version) {
2926
3101
  await (0, import_ensureDefaultRoleMultiPortalAccess.ensureDefaultRoleMultiPortalAccess)(this.db);