@betterportal/config-manager 10.0.30 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/.bsb/clients/service-betterportal-config-manager.d.ts +1 -1
- package/lib/.bsb/clients/service-betterportal-config-manager.js +1 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.js +188 -32
- package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +13 -12
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/GET.js +121 -6
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/GET.js +36 -2
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.d.ts +6 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.js +3 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/GET.js +115 -32
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js +9 -4
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/index.js +42 -8
- package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/menuEditor.js +7 -1
- package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/routeMounts.d.ts +8 -0
- package/lib/plugins/service-betterportal-config-manager/routeMounts.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/routeMounts.js +22 -0
- package/lib/plugins/service-betterportal-config-manager/routeMounts.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/setupTokens.js +50 -0
- package/lib/plugins/service-betterportal-config-manager/setupTokens.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.js +25 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +2 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.js +56 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
- package/lib/schemas/service-betterportal-config-manager.json +1 -1
- package/lib/schemas/service-betterportal-config-manager.plugin.json +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated BSB virtual client for service-betterportal-config-manager
|
|
3
3
|
* DO NOT EDIT - Regenerated on every build
|
|
4
|
-
* @version 10.0
|
|
4
|
+
* @version 10.1.0
|
|
5
5
|
*/
|
|
6
6
|
import { ServiceClient, BSBService } from "@bsb/base";
|
|
7
7
|
import type { Observable, BSBServiceClientDefinition, EventInputType } from "@bsb/base";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated BSB virtual client for service-betterportal-config-manager
|
|
3
3
|
* DO NOT EDIT - Regenerated on every build
|
|
4
|
-
* @version 10.0
|
|
4
|
+
* @version 10.1.0
|
|
5
5
|
*/
|
|
6
6
|
import { ServiceClient, bsb, createBroadcastEvent, createEventSchemas } from "@bsb/base";
|
|
7
7
|
const _platformConfigChangedSchema = bsb.object({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAEjB,mBAAmB,EAEpB,MAAM,yBAAyB,CAAC;AAUjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAEjB,mBAAmB,EAEpB,MAAM,yBAAyB,CAAC;AAUjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAmyBzD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,IAAI,CAgnCN"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { eventHeaders, htmlResponse, jsonResponse, resolveEmbeddedRequestContext, signServiceConfigTicket, uuidv7 } from "@betterportal/framework";
|
|
2
2
|
import { generateApiKey, hashApiKey } from "./storage/index.js";
|
|
3
3
|
import { getManifestCache } from "./syncApi.js";
|
|
4
|
+
import { apiRoutePath, isApiRoute } from "./routeMounts.js";
|
|
4
5
|
const API_BASE = "/.well-known/bp/admin";
|
|
5
6
|
const CONFIG_TICKET_TTL_SECONDS = 5 * 60;
|
|
6
7
|
// Parse-only base for relative request URLs. Never emit this origin.
|
|
@@ -14,6 +15,24 @@ async function readFormBody(event) {
|
|
|
14
15
|
out[k] = v; });
|
|
15
16
|
return out;
|
|
16
17
|
}
|
|
18
|
+
async function readFormBodyWithArrays(event) {
|
|
19
|
+
const fd = await event.req.formData().catch(() => null);
|
|
20
|
+
if (!fd)
|
|
21
|
+
return {};
|
|
22
|
+
const out = {};
|
|
23
|
+
fd.forEach((v, k) => {
|
|
24
|
+
if (typeof v !== "string")
|
|
25
|
+
return;
|
|
26
|
+
const existing = out[k];
|
|
27
|
+
if (Array.isArray(existing))
|
|
28
|
+
existing.push(v);
|
|
29
|
+
else if (typeof existing === "string")
|
|
30
|
+
out[k] = [existing, v];
|
|
31
|
+
else
|
|
32
|
+
out[k] = v;
|
|
33
|
+
});
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
17
36
|
function escapeHtml(s) {
|
|
18
37
|
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
19
38
|
}
|
|
@@ -26,7 +45,7 @@ async function readJsonBody(event) {
|
|
|
26
45
|
async function readFormOrJsonBody(event) {
|
|
27
46
|
const contentType = event.req.headers.get("content-type") ?? "";
|
|
28
47
|
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
29
|
-
return
|
|
48
|
+
return readFormBodyWithArrays(event);
|
|
30
49
|
}
|
|
31
50
|
return readJsonBody(event);
|
|
32
51
|
}
|
|
@@ -59,6 +78,12 @@ function routeMethodsFromManifest(methods) {
|
|
|
59
78
|
const normalized = (methods ?? []).filter((method) => method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE" || method === "OPTIONS");
|
|
60
79
|
return normalized.length ? normalized : ["GET"];
|
|
61
80
|
}
|
|
81
|
+
function hasPageRenderer(view) {
|
|
82
|
+
const renderers = view.html?.themeRenderers;
|
|
83
|
+
if (!renderers)
|
|
84
|
+
return view.renderable !== false;
|
|
85
|
+
return Object.values(renderers).some((renderer) => Array.isArray(renderer.renderModes) && renderer.renderModes.includes("page"));
|
|
86
|
+
}
|
|
62
87
|
function appMatchesTenantUrl(app, tenantUrl) {
|
|
63
88
|
let host = "";
|
|
64
89
|
try {
|
|
@@ -187,7 +212,9 @@ function parseRouteCreateBody(body) {
|
|
|
187
212
|
return { error: viewId.error };
|
|
188
213
|
const manifestView = getManifestCache().get(serviceId.value)?.viewIndex[viewId.value];
|
|
189
214
|
const renderable = manifestView?.renderable !== false;
|
|
190
|
-
const
|
|
215
|
+
const manifest = getManifestCache().get(serviceId.value);
|
|
216
|
+
const serviceRoutePath = manifestView?.path ?? `/${viewId.value}`;
|
|
217
|
+
const path = renderable ? requiredRouteString(body, "path", "Mount path") : { value: apiRoutePath(manifest?.serviceId ?? serviceId.value, serviceRoutePath) };
|
|
191
218
|
if (path.error)
|
|
192
219
|
return { error: path.error };
|
|
193
220
|
if (!path.value.startsWith("/"))
|
|
@@ -196,6 +223,7 @@ function parseRouteCreateBody(body) {
|
|
|
196
223
|
if (title.error)
|
|
197
224
|
return { error: title.error };
|
|
198
225
|
const route = {
|
|
226
|
+
kind: renderable ? "page" : "api",
|
|
199
227
|
path: path.value,
|
|
200
228
|
serviceId: serviceId.value,
|
|
201
229
|
viewId: viewId.value,
|
|
@@ -281,7 +309,8 @@ function addRouteDependencies(appDef, route) {
|
|
|
281
309
|
continue;
|
|
282
310
|
appDef.routes.push({
|
|
283
311
|
id: uuidv7(),
|
|
284
|
-
|
|
312
|
+
kind: "api",
|
|
313
|
+
path: apiRoutePath(manifest.serviceId, dependency.path),
|
|
285
314
|
serviceId: route.serviceId,
|
|
286
315
|
viewId: dependencyViewId,
|
|
287
316
|
targetPath: dependency.path,
|
|
@@ -445,6 +474,53 @@ function collectServiceDeleteBlockers(config, tenantId, serviceId) {
|
|
|
445
474
|
}
|
|
446
475
|
return blockers;
|
|
447
476
|
}
|
|
477
|
+
function purgeTenantServiceReferences(config, tenantId, serviceId) {
|
|
478
|
+
const summary = {
|
|
479
|
+
routesRemoved: 0,
|
|
480
|
+
menuItemsRemoved: 0,
|
|
481
|
+
slotsRemoved: 0,
|
|
482
|
+
fragmentsRemoved: 0,
|
|
483
|
+
roleGrantsRemoved: 0,
|
|
484
|
+
shellCleared: 0,
|
|
485
|
+
authCleared: 0
|
|
486
|
+
};
|
|
487
|
+
for (const app of config.apps.filter((candidate) => candidate.tenantId === tenantId)) {
|
|
488
|
+
if (app.shell?.serviceId === serviceId) {
|
|
489
|
+
delete app.shell;
|
|
490
|
+
summary.shellCleared += 1;
|
|
491
|
+
}
|
|
492
|
+
if (app.auth?.serviceId === serviceId) {
|
|
493
|
+
delete app.auth;
|
|
494
|
+
summary.authCleared += 1;
|
|
495
|
+
}
|
|
496
|
+
else if (app.auth?.roles) {
|
|
497
|
+
for (const role of app.auth.roles) {
|
|
498
|
+
const before = role.permissions.length;
|
|
499
|
+
role.permissions = role.permissions.filter((grant) => grant.serviceId !== serviceId);
|
|
500
|
+
summary.roleGrantsRemoved += before - role.permissions.length;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const routeIds = new Set(app.routes.filter((route) => route.serviceId === serviceId).map((route) => route.id));
|
|
504
|
+
const routeCountBefore = app.routes.length;
|
|
505
|
+
app.routes = app.routes.filter((route) => route.serviceId !== serviceId);
|
|
506
|
+
summary.routesRemoved += routeCountBefore - app.routes.length;
|
|
507
|
+
const menuBefore = JSON.stringify(app.menu ?? []);
|
|
508
|
+
app.menu = removeMenuRoutes(app.menu, routeIds);
|
|
509
|
+
if (menuBefore !== JSON.stringify(app.menu ?? []))
|
|
510
|
+
summary.menuItemsRemoved += routeIds.size;
|
|
511
|
+
const slotsBefore = app.slots.length;
|
|
512
|
+
app.slots = app.slots.filter((slot) => slot.serviceId !== serviceId);
|
|
513
|
+
summary.slotsRemoved += slotsBefore - app.slots.length;
|
|
514
|
+
for (const [location, fragments] of Object.entries(app.fragments)) {
|
|
515
|
+
const before = fragments.length;
|
|
516
|
+
app.fragments[location] = fragments.filter((fragment) => fragment.serviceId !== serviceId);
|
|
517
|
+
summary.fragmentsRemoved += before - app.fragments[location].length;
|
|
518
|
+
if (app.fragments[location].length === 0)
|
|
519
|
+
delete app.fragments[location];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return summary;
|
|
523
|
+
}
|
|
448
524
|
function sharedServiceIdFor(service, requested) {
|
|
449
525
|
return (requested && requested.trim()) || service.serviceId || service.id;
|
|
450
526
|
}
|
|
@@ -616,6 +692,23 @@ function migrateTenantServiceToShared(config, tenantId, serviceId, options = {})
|
|
|
616
692
|
function linkedServiceError(blockers) {
|
|
617
693
|
return `Service is still linked and cannot be deleted. Remove these references first: ${blockers.slice(0, 8).join("; ")}${blockers.length > 8 ? `; and ${blockers.length - 8} more` : ""}`;
|
|
618
694
|
}
|
|
695
|
+
function linkedServiceHtmxError(tenantId, serviceId, blockers) {
|
|
696
|
+
const items = blockers.slice(0, 8).map((blocker) => `<li>${escapeHtml(blocker)}</li>`).join("");
|
|
697
|
+
const more = blockers.length > 8 ? `<li>${blockers.length - 8} more references</li>` : "";
|
|
698
|
+
return htmlResponse(`
|
|
699
|
+
<div class="alert alert-warning">
|
|
700
|
+
<div class="fw-semibold mb-2">Service is still linked and cannot be deleted.</div>
|
|
701
|
+
<ul class="mb-3">${items}${more}</ul>
|
|
702
|
+
<button
|
|
703
|
+
class="btn btn-sm btn-danger"
|
|
704
|
+
hx-post="${API_BASE}/tenants/${encodeURIComponent(tenantId)}/services/${encodeURIComponent(serviceId)}/purge"
|
|
705
|
+
hx-target="#bp-services-alerts"
|
|
706
|
+
hx-swap="innerHTML"
|
|
707
|
+
hx-confirm="Purge all routes, menu entries, fragments, slots, auth links and role grants for this service, then delete it?"
|
|
708
|
+
>Purge references and delete service</button>
|
|
709
|
+
</div>
|
|
710
|
+
`, 409, "text/html; mode=fragment");
|
|
711
|
+
}
|
|
619
712
|
function parseWizardManifest(raw) {
|
|
620
713
|
const parsed = JSON.parse(raw);
|
|
621
714
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
@@ -1002,7 +1095,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1002
1095
|
const blockers = collectServiceDeleteBlockers(config, tenantId, serviceId);
|
|
1003
1096
|
if (blockers.length > 0) {
|
|
1004
1097
|
const message = linkedServiceError(blockers);
|
|
1005
|
-
return wantsHtmx(event) ?
|
|
1098
|
+
return wantsHtmx(event) ? linkedServiceHtmxError(tenantId, serviceId, blockers) : jsonResponse({ error: message, blockers }, 409);
|
|
1006
1099
|
}
|
|
1007
1100
|
tenant.services = tenant.services.filter((s) => s.id !== serviceId);
|
|
1008
1101
|
await store.saveConfig(config);
|
|
@@ -1014,6 +1107,31 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1014
1107
|
}
|
|
1015
1108
|
return jsonResponse({ ok: true });
|
|
1016
1109
|
});
|
|
1110
|
+
app.post(`${API_BASE}/tenants/:tenantId/services/:serviceId/purge`, async (event) => {
|
|
1111
|
+
const tenantId = getParam(event, "tenantId");
|
|
1112
|
+
const serviceId = getParam(event, "serviceId");
|
|
1113
|
+
if (!tenantId || !serviceId)
|
|
1114
|
+
return jsonResponse({ error: "tenantId and serviceId required" }, 400);
|
|
1115
|
+
const config = await store.loadConfig();
|
|
1116
|
+
const tenant = config.tenants.find((t) => t.id === tenantId);
|
|
1117
|
+
if (!tenant)
|
|
1118
|
+
return wantsHtmx(event) ? htmxError("Tenant not found", 404) : jsonResponse({ error: "Tenant not found" }, 404);
|
|
1119
|
+
const service = tenant.services.find((candidate) => candidate.id === serviceId);
|
|
1120
|
+
if (!service)
|
|
1121
|
+
return wantsHtmx(event) ? htmxError("Service not found", 404) : jsonResponse({ error: "Service not found" }, 404);
|
|
1122
|
+
const summary = purgeTenantServiceReferences(config, tenantId, serviceId);
|
|
1123
|
+
tenant.services = tenant.services.filter((candidate) => candidate.id !== serviceId);
|
|
1124
|
+
await store.saveConfig(config);
|
|
1125
|
+
if (wantsHtmx(event)) {
|
|
1126
|
+
return htmlResponse(`
|
|
1127
|
+
<div class="alert alert-success">
|
|
1128
|
+
Service deleted. Purged ${summary.routesRemoved} route(s), ${summary.slotsRemoved} slot(s), ${summary.fragmentsRemoved} fragment(s), ${summary.roleGrantsRemoved} role grant(s), ${summary.shellCleared} shell binding(s), and ${summary.authCleared} auth binding(s).
|
|
1129
|
+
</div>
|
|
1130
|
+
<div hx-get="/services?tenantId=${encodeURIComponent(tenantId)}" hx-trigger="load" hx-target="#bp-main" hx-swap="innerHTML"></div>
|
|
1131
|
+
`, 200, "text/html; mode=fragment");
|
|
1132
|
+
}
|
|
1133
|
+
return jsonResponse({ ok: true, summary });
|
|
1134
|
+
});
|
|
1017
1135
|
// Activate/deactivate platform services for tenant
|
|
1018
1136
|
app.get(`${API_BASE}/tenants/:tenantId/services/:serviceId/migrate-to-shared/preview`, async (event) => {
|
|
1019
1137
|
const tenantId = getParam(event, "tenantId");
|
|
@@ -1335,15 +1453,20 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1335
1453
|
return validationError(event, "View is required.");
|
|
1336
1454
|
route.viewId = viewId;
|
|
1337
1455
|
}
|
|
1338
|
-
const
|
|
1456
|
+
const manifest = getManifestCache().get(route.serviceId);
|
|
1457
|
+
const manifestView = manifest?.viewIndex[route.viewId];
|
|
1339
1458
|
if (manifestView) {
|
|
1340
1459
|
route.methods = routeMethodsFromManifest(manifestView.methods);
|
|
1341
1460
|
route.targetPath = manifestView.path;
|
|
1342
1461
|
if (manifestView.renderable === false) {
|
|
1343
|
-
route.
|
|
1462
|
+
route.kind = "api";
|
|
1463
|
+
route.path = apiRoutePath(manifest?.serviceId ?? route.serviceId, manifestView.path);
|
|
1344
1464
|
route.title = manifestView.viewId;
|
|
1345
1465
|
delete route.query;
|
|
1346
1466
|
}
|
|
1467
|
+
else {
|
|
1468
|
+
route.kind = "page";
|
|
1469
|
+
}
|
|
1347
1470
|
}
|
|
1348
1471
|
if (body.targetPath !== undefined) {
|
|
1349
1472
|
const targetPath = trimmedString(body, "targetPath");
|
|
@@ -1354,7 +1477,9 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1354
1477
|
}
|
|
1355
1478
|
if (body.query !== undefined) {
|
|
1356
1479
|
const query = trimmedString(body, "query");
|
|
1357
|
-
if (
|
|
1480
|
+
if (isApiRoute(route, manifestView?.renderable))
|
|
1481
|
+
delete route.query;
|
|
1482
|
+
else if (query)
|
|
1358
1483
|
route.query = query.replace(/^\?+/, "");
|
|
1359
1484
|
else
|
|
1360
1485
|
delete route.query;
|
|
@@ -1363,7 +1488,8 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1363
1488
|
const title = trimmedString(body, "title");
|
|
1364
1489
|
if (!title)
|
|
1365
1490
|
return validationError(event, "Display title is required.");
|
|
1366
|
-
route
|
|
1491
|
+
if (!isApiRoute(route, manifestView?.renderable))
|
|
1492
|
+
route.title = title;
|
|
1367
1493
|
}
|
|
1368
1494
|
if (body.enabled !== undefined)
|
|
1369
1495
|
route.enabled = body.enabled === true || body.enabled === "true" || body.enabled === "on";
|
|
@@ -1441,7 +1567,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1441
1567
|
// Browser-mediated: the admin UI fetches the manifest from the service
|
|
1442
1568
|
// (CM cannot reach services) and POSTs the parsed payload here.
|
|
1443
1569
|
app.post(`${API_BASE}/wizard/verify`, async (event) => {
|
|
1444
|
-
const adminApiBase =
|
|
1570
|
+
const adminApiBase = API_BASE;
|
|
1445
1571
|
const form = await readFormBody(event);
|
|
1446
1572
|
const tenantId = form.tenantId ?? "";
|
|
1447
1573
|
const hostname = normalizeHostname(form.hostname);
|
|
@@ -1479,17 +1605,22 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1479
1605
|
});
|
|
1480
1606
|
// HTMX wizard: register service
|
|
1481
1607
|
app.post(`${API_BASE}/wizard/register`, async (event) => {
|
|
1482
|
-
const
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1608
|
+
const body = await readFormOrJsonBody(event);
|
|
1609
|
+
const jsonMode = (event.req.headers.get("accept") ?? "").includes("application/json")
|
|
1610
|
+
|| (event.req.headers.get("content-type") ?? "").includes("application/json");
|
|
1611
|
+
const registerError = (message, status = 400) => jsonMode
|
|
1612
|
+
? jsonResponse({ error: message }, status)
|
|
1613
|
+
: htmlResponse(`<div class="alert alert-danger">${escapeHtml(message)}</div>`, 200, "text/html; mode=fragment");
|
|
1614
|
+
const tenantId = typeof body.tenantId === "string" ? body.tenantId : "";
|
|
1615
|
+
const hostname = (typeof body.hostname === "string" ? body.hostname : "").replace(/\/+$/, "");
|
|
1616
|
+
const title = (typeof body.title === "string" && body.title.trim().length > 0 ? body.title.trim() : hostname);
|
|
1486
1617
|
if (!tenantId || !hostname) {
|
|
1487
|
-
return
|
|
1618
|
+
return registerError("Missing tenantId or hostname");
|
|
1488
1619
|
}
|
|
1489
1620
|
const config = await store.loadConfig();
|
|
1490
1621
|
const tenant = config.tenants.find((t) => t.id === tenantId);
|
|
1491
1622
|
if (!tenant) {
|
|
1492
|
-
return
|
|
1623
|
+
return registerError("Tenant not found", 404);
|
|
1493
1624
|
}
|
|
1494
1625
|
// Schema is fetched by the browser from the service (CM cannot reach services)
|
|
1495
1626
|
// and posted to us as a JSON string in form.schema.
|
|
@@ -1497,19 +1628,27 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1497
1628
|
let capabilities = [];
|
|
1498
1629
|
let serviceRoutes = [];
|
|
1499
1630
|
let viewMeta = new Map();
|
|
1500
|
-
|
|
1631
|
+
const schemaText = typeof body.schema === "string" ? body.schema : "";
|
|
1632
|
+
if (schemaText) {
|
|
1501
1633
|
try {
|
|
1502
|
-
const schema = JSON.parse(
|
|
1634
|
+
const schema = JSON.parse(schemaText);
|
|
1503
1635
|
serviceId = schema.manifest?.pluginId;
|
|
1504
1636
|
capabilities = Array.isArray(schema.manifest?.capabilities) ? schema.manifest.capabilities.filter((value) => typeof value === "string") : [];
|
|
1505
1637
|
serviceRoutes = schema.routes ?? [];
|
|
1506
|
-
viewMeta = new Map((schema.manifest?.views ?? []).map((v) => [v.viewId,
|
|
1638
|
+
viewMeta = new Map((schema.manifest?.views ?? []).map((v) => [v.viewId, {
|
|
1639
|
+
title: v.title,
|
|
1640
|
+
renderable: v.renderable !== false,
|
|
1641
|
+
pageRenderable: hasPageRenderer(v),
|
|
1642
|
+
methods: Array.isArray(v.methods)
|
|
1643
|
+
? v.methods.filter((value) => typeof value === "string")
|
|
1644
|
+
: []
|
|
1645
|
+
}]));
|
|
1507
1646
|
}
|
|
1508
1647
|
catch { /* malformed schema - register without routes */ }
|
|
1509
1648
|
}
|
|
1510
1649
|
const duplicate = duplicateTenantService(tenant, hostname, serviceId);
|
|
1511
1650
|
if (duplicate) {
|
|
1512
|
-
return
|
|
1651
|
+
return registerError(duplicate, 409);
|
|
1513
1652
|
}
|
|
1514
1653
|
const newServiceId = uuidv7();
|
|
1515
1654
|
const service = {
|
|
@@ -1532,30 +1671,37 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1532
1671
|
const groupChildren = [];
|
|
1533
1672
|
const existingPaths = new Set(appDef.routes.map((r) => r.path));
|
|
1534
1673
|
for (const r of serviceRoutes) {
|
|
1674
|
+
const meta = viewMeta.get(r.viewId);
|
|
1675
|
+
const renderable = meta?.renderable ?? r.renderable !== false;
|
|
1535
1676
|
// Default mounting path is the service's own path; skip collisions
|
|
1536
|
-
const mountPath =
|
|
1677
|
+
const mountPath = renderable
|
|
1678
|
+
? (existingPaths.has(r.path) ? `/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}${r.path}` : r.path)
|
|
1679
|
+
: apiRoutePath(serviceId ?? newServiceId, r.path);
|
|
1537
1680
|
if (existingPaths.has(mountPath))
|
|
1538
1681
|
continue;
|
|
1539
1682
|
const routeId = uuidv7();
|
|
1540
|
-
const routeTitle =
|
|
1683
|
+
const routeTitle = meta?.title ?? r.viewId;
|
|
1541
1684
|
appDef.routes.push({
|
|
1542
1685
|
id: routeId,
|
|
1686
|
+
kind: renderable ? "page" : "api",
|
|
1543
1687
|
path: mountPath,
|
|
1544
1688
|
serviceId: newServiceId,
|
|
1545
1689
|
viewId: r.viewId,
|
|
1546
1690
|
targetPath: r.path,
|
|
1547
1691
|
title: routeTitle,
|
|
1548
1692
|
enabled: true,
|
|
1549
|
-
methods:
|
|
1693
|
+
methods: routeMethodsFromManifest(r.methods)
|
|
1550
1694
|
});
|
|
1551
1695
|
existingPaths.add(mountPath);
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1696
|
+
if (meta?.pageRenderable === true && !isApiRoute({ kind: renderable ? "page" : "api", path: mountPath }, renderable)) {
|
|
1697
|
+
groupChildren.push({
|
|
1698
|
+
id: uuidv7(),
|
|
1699
|
+
type: "link",
|
|
1700
|
+
title: routeTitle,
|
|
1701
|
+
routeId,
|
|
1702
|
+
enabled: true
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1559
1705
|
}
|
|
1560
1706
|
if (groupChildren.length > 0) {
|
|
1561
1707
|
const menu = (appDef.menu ?? []);
|
|
@@ -1571,7 +1717,17 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1571
1717
|
}
|
|
1572
1718
|
}
|
|
1573
1719
|
await store.saveConfig(config);
|
|
1574
|
-
|
|
1720
|
+
if (jsonMode) {
|
|
1721
|
+
return jsonResponse({
|
|
1722
|
+
ok: true,
|
|
1723
|
+
tenantId,
|
|
1724
|
+
serviceInstanceId: newServiceId,
|
|
1725
|
+
serviceUrl: hostname,
|
|
1726
|
+
title,
|
|
1727
|
+
deploymentMode: service.deploymentMode
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
const adminApiBase = API_BASE;
|
|
1575
1731
|
return htmlResponse(renderWizardStep3({
|
|
1576
1732
|
apiKey: "",
|
|
1577
1733
|
deploymentMode: service.deploymentMode,
|
|
@@ -1602,7 +1758,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1602
1758
|
const tenantId = url.searchParams.get("tenantId") ?? undefined;
|
|
1603
1759
|
const hostname = url.searchParams.get("hostname") ?? undefined;
|
|
1604
1760
|
const config = await store.loadConfig();
|
|
1605
|
-
return htmlResponse(renderWizardStep1(config, undefined, { tenantId, hostname },
|
|
1761
|
+
return htmlResponse(renderWizardStep1(config, undefined, { tenantId, hostname }, API_BASE), 200, "text/html; mode=fragment");
|
|
1606
1762
|
});
|
|
1607
1763
|
// HTMX configure: load form
|
|
1608
1764
|
app.get(`${API_BASE}/configure`, async (event) => {
|
|
@@ -1771,7 +1927,7 @@ function renderWizardStep2(d) {
|
|
|
1771
1927
|
<div class="form-text">Auto-filled from manifest. Edit if needed.</div>
|
|
1772
1928
|
</div>
|
|
1773
1929
|
<div class="d-flex gap-2">
|
|
1774
|
-
<button type="button" class="btn btn-outline-secondary" hx-get="
|
|
1930
|
+
<button type="button" class="btn btn-outline-secondary" hx-get="${escapeHtml(d.adminApiBase)}/wizard/step1" hx-target="#bp-wizard-step" hx-swap="outerHTML"><- Back</button>
|
|
1775
1931
|
<button type="submit" class="btn btn-primary flex-grow-1">Register Service</button>
|
|
1776
1932
|
</div>
|
|
1777
1933
|
</form>
|