@betterportal/config-manager 10.1.52 → 10.1.54
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/bp-contracts/org.betterportal.config-manager.json +129 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts +13 -0
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.js +242 -51
- package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.js +129 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.d.ts +8 -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 +4 -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/_renderer.bootstrap5/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js +36 -8
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_renderer.bootstrap5/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_renderer.bootstrap5/GET.js +8 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_renderer.bootstrap5/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.d.ts +30 -0
- 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 +41 -0
- 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 +4 -0
- package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.js +38 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/file.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/file.js +2 -2
- package/lib/plugins/service-betterportal-config-manager/storage/file.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +2 -2
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +14 -0
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.js +32 -5
- 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
|
@@ -90,6 +90,76 @@ function requiredRouteString(body, key, label) {
|
|
|
90
90
|
return { error: `${label} is required.` };
|
|
91
91
|
return { value };
|
|
92
92
|
}
|
|
93
|
+
function routeParamNames(path) {
|
|
94
|
+
return [...new Set(path.split("/").flatMap((segment) => {
|
|
95
|
+
const match = segment.match(/^:([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
96
|
+
return match ? [match[1]] : [];
|
|
97
|
+
}))];
|
|
98
|
+
}
|
|
99
|
+
function validateCanonicalRoutePath(path, label) {
|
|
100
|
+
if (!path.startsWith("/"))
|
|
101
|
+
return `${label} must start with /.`;
|
|
102
|
+
if (/\{[^}]+\}/.test(path))
|
|
103
|
+
return `${label} must use :param syntax; {param} is no longer supported.`;
|
|
104
|
+
for (const segment of path.split("/")) {
|
|
105
|
+
if (segment.startsWith(":") && !/^:[A-Za-z_][A-Za-z0-9_]*$/.test(segment)) {
|
|
106
|
+
return `${label} contains an invalid parameter segment: ${segment}.`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function fixedParamsFromBody(body) {
|
|
112
|
+
const fixed = {};
|
|
113
|
+
for (const [key, value] of Object.entries(body)) {
|
|
114
|
+
if (!key.startsWith("fixedParam."))
|
|
115
|
+
continue;
|
|
116
|
+
const name = key.slice("fixedParam.".length);
|
|
117
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || typeof value !== "string")
|
|
118
|
+
continue;
|
|
119
|
+
const normalized = value.trim();
|
|
120
|
+
if (normalized)
|
|
121
|
+
fixed[name] = normalized;
|
|
122
|
+
}
|
|
123
|
+
return fixed;
|
|
124
|
+
}
|
|
125
|
+
function validateFixedParamValue(name, value, paramsSchema) {
|
|
126
|
+
const properties = paramsSchema?.properties;
|
|
127
|
+
const schema = properties && typeof properties === "object" && !Array.isArray(properties)
|
|
128
|
+
? properties[name]
|
|
129
|
+
: undefined;
|
|
130
|
+
const rule = schema && typeof schema === "object" && !Array.isArray(schema)
|
|
131
|
+
? schema
|
|
132
|
+
: {};
|
|
133
|
+
const minLength = typeof rule.minLength === "number" ? rule.minLength : 1;
|
|
134
|
+
const maxLength = Math.min(typeof rule.maxLength === "number" ? rule.maxLength : 100, 100);
|
|
135
|
+
if (value.length < minLength || value.length > maxLength) {
|
|
136
|
+
return `${name} must be between ${minLength} and ${maxLength} characters.`;
|
|
137
|
+
}
|
|
138
|
+
if (typeof rule.pattern === "string") {
|
|
139
|
+
try {
|
|
140
|
+
if (!new RegExp(rule.pattern).test(value))
|
|
141
|
+
return `${name} does not match its service validation pattern.`;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return `${name} has an invalid validation pattern in the service manifest.`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function validateRouteParamMapping(appPath, servicePath, fixedParams, paramsSchema) {
|
|
150
|
+
const appParams = new Set(routeParamNames(appPath));
|
|
151
|
+
for (const name of routeParamNames(servicePath)) {
|
|
152
|
+
if (appParams.has(name))
|
|
153
|
+
continue;
|
|
154
|
+
const value = fixedParams[name];
|
|
155
|
+
if (!value)
|
|
156
|
+
return `Service parameter :${name} must appear in the app path or have a fixed value.`;
|
|
157
|
+
const valueError = validateFixedParamValue(name, value, paramsSchema);
|
|
158
|
+
if (valueError)
|
|
159
|
+
return valueError;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
93
163
|
function routeMethodsFromManifest(methods) {
|
|
94
164
|
const normalized = (methods ?? []).filter((method) => method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE" || method === "OPTIONS");
|
|
95
165
|
return normalized.length ? normalized : ["GET"];
|
|
@@ -266,12 +336,24 @@ function parseRouteCreateBody(body) {
|
|
|
266
336
|
const manifestView = getManifestCache().get(serviceId.value)?.viewIndex[viewId.value];
|
|
267
337
|
const renderable = manifestView?.renderable !== false;
|
|
268
338
|
const manifest = getManifestCache().get(serviceId.value);
|
|
269
|
-
const
|
|
339
|
+
const servicePathVariant = trimmedString(body, "servicePathVariant") ?? manifestView?.path ?? `/${viewId.value}`;
|
|
340
|
+
if (manifestView && ![manifestView.path, ...manifestView.pathVariants].includes(servicePathVariant)) {
|
|
341
|
+
return { error: "Selected service path is not available for this view." };
|
|
342
|
+
}
|
|
343
|
+
const serviceRoutePath = servicePathVariant;
|
|
270
344
|
const path = renderable ? requiredRouteString(body, "path", "Mount path") : { value: apiRoutePath(manifest?.serviceId ?? serviceId.value, serviceRoutePath) };
|
|
271
345
|
if (path.error)
|
|
272
346
|
return { error: path.error };
|
|
273
|
-
|
|
274
|
-
|
|
347
|
+
const pathError = validateCanonicalRoutePath(path.value, "Mount path");
|
|
348
|
+
if (pathError)
|
|
349
|
+
return { error: pathError };
|
|
350
|
+
const servicePathError = validateCanonicalRoutePath(serviceRoutePath, "Service path");
|
|
351
|
+
if (servicePathError)
|
|
352
|
+
return { error: servicePathError };
|
|
353
|
+
const fixedParams = fixedParamsFromBody(body);
|
|
354
|
+
const mappingError = validateRouteParamMapping(path.value, serviceRoutePath, fixedParams, manifestView?.paramsSchema);
|
|
355
|
+
if (mappingError)
|
|
356
|
+
return { error: mappingError };
|
|
275
357
|
const title = renderable ? requiredRouteString(body, "title", "Display title") : { value: manifestView?.viewId ?? viewId.value };
|
|
276
358
|
if (title.error)
|
|
277
359
|
return { error: title.error };
|
|
@@ -280,12 +362,14 @@ function parseRouteCreateBody(body) {
|
|
|
280
362
|
path: path.value,
|
|
281
363
|
serviceId: serviceId.value,
|
|
282
364
|
viewId: viewId.value,
|
|
365
|
+
...(manifestView && servicePathVariant !== manifestView.path ? { servicePathVariant } : {}),
|
|
366
|
+
...(Object.keys(fixedParams).length > 0 ? { fixedParams } : {}),
|
|
283
367
|
title: title.value,
|
|
284
368
|
enabled: true,
|
|
285
369
|
methods: routeMethodsFromManifest(manifestView?.methods)
|
|
286
370
|
};
|
|
287
371
|
if (manifestView?.path)
|
|
288
|
-
route.targetPath =
|
|
372
|
+
route.targetPath = serviceRoutePath;
|
|
289
373
|
const query = trimmedString(body, "query");
|
|
290
374
|
if (query && renderable)
|
|
291
375
|
route.query = query.replace(/^\?+/, "");
|
|
@@ -1845,8 +1929,9 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1845
1929
|
const path = trimmedString(body, "path");
|
|
1846
1930
|
if (!path)
|
|
1847
1931
|
return validationError(event, "Mount path is required.");
|
|
1848
|
-
|
|
1849
|
-
|
|
1932
|
+
const pathError = validateCanonicalRoutePath(path, "Mount path");
|
|
1933
|
+
if (pathError)
|
|
1934
|
+
return validationError(event, pathError);
|
|
1850
1935
|
if (appDef.routes.some((candidate) => candidate.id !== route.id && appRoutePatternKey(candidate.path) === appRoutePatternKey(path))) {
|
|
1851
1936
|
return validationError(event, `A route already exists at ${path}.`);
|
|
1852
1937
|
}
|
|
@@ -1870,11 +1955,38 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1870
1955
|
const manifest = getManifestCache().get(route.serviceId);
|
|
1871
1956
|
const manifestView = manifest?.viewIndex[route.viewId];
|
|
1872
1957
|
if (manifestView) {
|
|
1958
|
+
const routeIdentityChanged = body.serviceId !== undefined || body.viewId !== undefined;
|
|
1959
|
+
const requestedServicePath = trimmedString(body, "servicePathVariant")
|
|
1960
|
+
?? (routeIdentityChanged ? undefined : route.servicePathVariant)
|
|
1961
|
+
?? manifestView.path;
|
|
1962
|
+
if (![manifestView.path, ...manifestView.pathVariants].includes(requestedServicePath)) {
|
|
1963
|
+
return validationError(event, "Selected service path is not available for this view.");
|
|
1964
|
+
}
|
|
1965
|
+
const servicePathError = validateCanonicalRoutePath(requestedServicePath, "Service path");
|
|
1966
|
+
if (servicePathError)
|
|
1967
|
+
return validationError(event, servicePathError);
|
|
1968
|
+
const submittedFixed = fixedParamsFromBody(body);
|
|
1969
|
+
const mappedAppPath = manifestView.renderable === false
|
|
1970
|
+
? apiRoutePath(manifest?.serviceId ?? route.serviceId, requestedServicePath)
|
|
1971
|
+
: route.path;
|
|
1972
|
+
const appParams = new Set(routeParamNames(mappedAppPath));
|
|
1973
|
+
const fixedParams = Object.fromEntries(Object.entries(submittedFixed).filter(([name]) => routeParamNames(requestedServicePath).includes(name) && !appParams.has(name)));
|
|
1974
|
+
const mappingError = validateRouteParamMapping(mappedAppPath, requestedServicePath, fixedParams, manifestView.paramsSchema);
|
|
1975
|
+
if (mappingError)
|
|
1976
|
+
return validationError(event, mappingError);
|
|
1977
|
+
if (requestedServicePath === manifestView.path)
|
|
1978
|
+
delete route.servicePathVariant;
|
|
1979
|
+
else
|
|
1980
|
+
route.servicePathVariant = requestedServicePath;
|
|
1981
|
+
if (Object.keys(fixedParams).length > 0)
|
|
1982
|
+
route.fixedParams = fixedParams;
|
|
1983
|
+
else
|
|
1984
|
+
delete route.fixedParams;
|
|
1873
1985
|
route.methods = routeMethodsFromManifest(manifestView.methods);
|
|
1874
|
-
route.targetPath =
|
|
1986
|
+
route.targetPath = requestedServicePath;
|
|
1875
1987
|
if (manifestView.renderable === false) {
|
|
1876
1988
|
route.kind = "api";
|
|
1877
|
-
route.path = apiRoutePath(manifest?.serviceId ?? route.serviceId,
|
|
1989
|
+
route.path = apiRoutePath(manifest?.serviceId ?? route.serviceId, requestedServicePath);
|
|
1878
1990
|
route.title = manifestView.viewId;
|
|
1879
1991
|
delete route.query;
|
|
1880
1992
|
}
|
|
@@ -2552,7 +2664,7 @@ function renderConfigControl(field, value, placeholder, disabled) {
|
|
|
2552
2664
|
const inputType = ["number", "color", "date", "time", "datetime-local", "url", "email", "password"].includes(control) ? control : "text";
|
|
2553
2665
|
return `<input class="form-control" type="${inputType}" name="${name}" value="${escapeHtml(value)}" placeholder="${placeholderAttr}" ${attrs}${disabledAttr} />`;
|
|
2554
2666
|
}
|
|
2555
|
-
function renderConfigClientShell(d) {
|
|
2667
|
+
export function renderConfigClientShell(d) {
|
|
2556
2668
|
const payload = JSON.stringify({
|
|
2557
2669
|
hostname: d.hostname,
|
|
2558
2670
|
tenantId: d.tenantId,
|
|
@@ -2631,6 +2743,26 @@ function renderConfigClientShell(d) {
|
|
|
2631
2743
|
if (!response.ok) throw new Error(data.error || data.message || "config HTTP " + response.status);
|
|
2632
2744
|
return data.values || {};
|
|
2633
2745
|
};
|
|
2746
|
+
const writeValues = async (token, appId, values, clearKeys = []) => {
|
|
2747
|
+
const headers = {
|
|
2748
|
+
"Content-Type": "application/json",
|
|
2749
|
+
Accept: "application/json",
|
|
2750
|
+
Authorization: "Bearer " + token,
|
|
2751
|
+
"x-bp-tenant-id": cfg.tenantId
|
|
2752
|
+
};
|
|
2753
|
+
if (appId) headers["x-bp-app-id"] = appId;
|
|
2754
|
+
const payload = { tenantId: cfg.tenantId, values };
|
|
2755
|
+
if (appId) payload.appId = appId;
|
|
2756
|
+
if (clearKeys.length > 0) payload.clearKeys = clearKeys;
|
|
2757
|
+
const response = await fetch(serviceBase() + "/.well-known/bp/config", {
|
|
2758
|
+
method: "POST",
|
|
2759
|
+
headers,
|
|
2760
|
+
body: JSON.stringify(payload)
|
|
2761
|
+
});
|
|
2762
|
+
const data = await readJson(response, "config write");
|
|
2763
|
+
if (!response.ok) throw new Error(data.error || data.message || "config HTTP " + response.status);
|
|
2764
|
+
return data;
|
|
2765
|
+
};
|
|
2634
2766
|
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
|
|
2635
2767
|
const compareOrder = (left, right) => {
|
|
2636
2768
|
const leftOrder = typeof left.order === "number" ? left.order : 0;
|
|
@@ -2730,13 +2862,16 @@ function renderConfigClientShell(d) {
|
|
|
2730
2862
|
const layout = schemaLayout(schema);
|
|
2731
2863
|
const fields = layout.fields;
|
|
2732
2864
|
const scope = selectedScope === "app" ? "app" : "tenant";
|
|
2733
|
-
const
|
|
2865
|
+
const apps = cfg.tenantApps || [];
|
|
2866
|
+
const selectedAppId = scope === "app"
|
|
2867
|
+
? (apps.some((app) => app.id === appId) ? appId : apps[0]?.id || "")
|
|
2868
|
+
: "";
|
|
2734
2869
|
const tenantValues = await loadValues(token, "");
|
|
2735
2870
|
const appValues = selectedAppId ? await loadValues(token, selectedAppId) : {};
|
|
2736
2871
|
const scopeSelector = '<div class="row g-2 mb-3"><div class="col-5"><label class="form-label">Scope</label>' +
|
|
2737
2872
|
'<select class="form-select" data-bp-config-scope><option value="tenant"' + (scope === "tenant" ? " selected" : "") + '>Tenant defaults</option><option value="app"' + (scope === "app" ? " selected" : "") + '>App override</option></select></div>' +
|
|
2738
2873
|
'<div class="col-7"><label class="form-label">App</label><select class="form-select" data-bp-config-app ' + (scope === "app" ? "" : "disabled") + '><option value="">Select app...</option>' +
|
|
2739
|
-
|
|
2874
|
+
apps.map((app) => '<option value="' + escapeHtml(app.id) + '"' + (app.id === selectedAppId ? " selected" : "") + '>' + escapeHtml(app.title) + '</option>').join("") +
|
|
2740
2875
|
'</select></div></div>';
|
|
2741
2876
|
if (scope === "app" && !selectedAppId) {
|
|
2742
2877
|
body.innerHTML = scopeSelector + '<div class="alert alert-info">Select an app to edit overrides.</div>';
|
|
@@ -2779,8 +2914,88 @@ function renderConfigClientShell(d) {
|
|
|
2779
2914
|
const content = sectionFields.map(renderField).join("");
|
|
2780
2915
|
return '<div class="border rounded p-3 mb-3" data-bp-config-section="' + escapeHtml(section.id) + '">' + header + content + '</div>';
|
|
2781
2916
|
}).join("");
|
|
2782
|
-
|
|
2917
|
+
const transferActions = '<div class="d-flex justify-content-end gap-2 mb-3">' +
|
|
2918
|
+
'<button type="button" class="btn btn-sm btn-link text-decoration-none p-0" data-bp-config-export>Export</button>' +
|
|
2919
|
+
'<button type="button" class="btn btn-sm btn-link text-decoration-none p-0" data-bp-config-import>Import</button>' +
|
|
2920
|
+
'<input type="file" accept="application/json,.json" hidden data-bp-config-import-file />' +
|
|
2921
|
+
'</div>';
|
|
2922
|
+
body.innerHTML = scopeSelector + transferActions + '<form data-bp-config-form>' + sectionsHtml + '<div data-bp-save-status></div><button type="submit" class="btn btn-primary w-100">Save Configuration</button></form>';
|
|
2783
2923
|
wireScopeControls(schema, token);
|
|
2924
|
+
const currentValues = scope === "app" ? appValues : tenantValues;
|
|
2925
|
+
const importFile = root.querySelector("[data-bp-config-import-file]");
|
|
2926
|
+
root.querySelector("[data-bp-config-export]")?.addEventListener("click", () => {
|
|
2927
|
+
const values = {};
|
|
2928
|
+
let omittedSecrets = 0;
|
|
2929
|
+
for (const field of fields) {
|
|
2930
|
+
if (!hasOwn(currentValues, field.key)) continue;
|
|
2931
|
+
if (field.visibility === "secret" && currentValues[field.key] === "__redacted__") {
|
|
2932
|
+
omittedSecrets += 1;
|
|
2933
|
+
continue;
|
|
2934
|
+
}
|
|
2935
|
+
values[field.key] = currentValues[field.key];
|
|
2936
|
+
}
|
|
2937
|
+
const exported = {
|
|
2938
|
+
format: "betterportal.service-config",
|
|
2939
|
+
version: 1,
|
|
2940
|
+
serviceId: cfg.serviceId,
|
|
2941
|
+
tenantId: cfg.tenantId,
|
|
2942
|
+
scope,
|
|
2943
|
+
...(selectedAppId ? { appId: selectedAppId } : {}),
|
|
2944
|
+
values
|
|
2945
|
+
};
|
|
2946
|
+
const blobUrl = URL.createObjectURL(new Blob([JSON.stringify(exported, null, 2) + "\\n"], { type: "application/json" }));
|
|
2947
|
+
const link = document.createElement("a");
|
|
2948
|
+
link.href = blobUrl;
|
|
2949
|
+
link.download = String(cfg.serviceId || "service").replace(/[^a-z0-9._-]+/gi, "-") + "-" + scope + ".json";
|
|
2950
|
+
document.body.appendChild(link);
|
|
2951
|
+
link.click();
|
|
2952
|
+
link.remove();
|
|
2953
|
+
URL.revokeObjectURL(blobUrl);
|
|
2954
|
+
setStatus("ok", "Configuration exported." + (omittedSecrets ? " Stored secrets were omitted." : ""));
|
|
2955
|
+
});
|
|
2956
|
+
root.querySelector("[data-bp-config-import]")?.addEventListener("click", () => importFile?.click());
|
|
2957
|
+
importFile?.addEventListener("change", async () => {
|
|
2958
|
+
const file = importFile.files?.[0];
|
|
2959
|
+
importFile.value = "";
|
|
2960
|
+
if (!file) return;
|
|
2961
|
+
setStatus("info", "Importing configuration...");
|
|
2962
|
+
try {
|
|
2963
|
+
const imported = JSON.parse(await file.text());
|
|
2964
|
+
if (!imported || typeof imported !== "object" || Array.isArray(imported)) throw new Error("Import must be a JSON object.");
|
|
2965
|
+
if (imported.format !== "betterportal.service-config" || imported.version !== 1) throw new Error("Unsupported config export format.");
|
|
2966
|
+
if (imported.serviceId !== cfg.serviceId) throw new Error("This export belongs to a different service.");
|
|
2967
|
+
if (!["tenant", "app"].includes(imported.scope)) throw new Error("Import scope must be tenant or app.");
|
|
2968
|
+
if (imported.scope !== scope) throw new Error("Select the " + imported.scope + " scope before importing this file.");
|
|
2969
|
+
if (!imported.values || typeof imported.values !== "object" || Array.isArray(imported.values)) throw new Error("Import values must be a JSON object.");
|
|
2970
|
+
const fieldsByKey = new Map(fields.map((field) => [field.key, field]));
|
|
2971
|
+
const unknownKeys = Object.keys(imported.values).filter((key) => !fieldsByKey.has(key));
|
|
2972
|
+
if (unknownKeys.length > 0) throw new Error("Unknown config fields: " + unknownKeys.join(", "));
|
|
2973
|
+
const values = {};
|
|
2974
|
+
const clearKeys = [];
|
|
2975
|
+
for (const field of fields) {
|
|
2976
|
+
if (hasOwn(imported.values, field.key)) {
|
|
2977
|
+
if (field.visibility !== "secret" || imported.values[field.key] !== "__redacted__") {
|
|
2978
|
+
values[field.key] = imported.values[field.key];
|
|
2979
|
+
}
|
|
2980
|
+
} else if (field.visibility !== "secret") {
|
|
2981
|
+
clearKeys.push(field.key);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const scopeLabel = selectedAppId
|
|
2985
|
+
? "the selected app override"
|
|
2986
|
+
: "the tenant defaults";
|
|
2987
|
+
if (!window.confirm("Replace " + scopeLabel + " with this import? Existing stored secrets are preserved unless the file supplies replacements.")) {
|
|
2988
|
+
setStatus("info", "Import cancelled.");
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
await writeValues(token, selectedAppId, values, clearKeys);
|
|
2992
|
+
document.body.dispatchEvent(new CustomEvent("bp:config-saved"));
|
|
2993
|
+
await renderForm(schema, token, scope, selectedAppId);
|
|
2994
|
+
setStatus("ok", "Configuration imported.");
|
|
2995
|
+
} catch (error) {
|
|
2996
|
+
setStatus("error", error instanceof Error ? error.message : String(error));
|
|
2997
|
+
}
|
|
2998
|
+
});
|
|
2784
2999
|
root.querySelectorAll("[data-bp-override-key]").forEach((checkbox) => {
|
|
2785
3000
|
checkbox.addEventListener("change", () => {
|
|
2786
3001
|
const field = checkbox.closest("[data-bp-field]");
|
|
@@ -2803,24 +3018,14 @@ function renderConfigClientShell(d) {
|
|
|
2803
3018
|
if (!key) return;
|
|
2804
3019
|
button.disabled = true;
|
|
2805
3020
|
setStatus("info", "Resetting " + key + "...");
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
"x-bp-tenant-id": cfg.tenantId
|
|
2813
|
-
},
|
|
2814
|
-
body: JSON.stringify({ tenantId: cfg.tenantId, values: {}, clearKeys: [key] })
|
|
2815
|
-
});
|
|
2816
|
-
if (!response.ok) {
|
|
2817
|
-
const error = await readJson(response, "reset").catch((err) => ({ error: err.message || "HTTP " + response.status }));
|
|
2818
|
-
setStatus("error", error.error || "Reset failed.");
|
|
3021
|
+
try {
|
|
3022
|
+
await writeValues(token, "", {}, [key]);
|
|
3023
|
+
document.body.dispatchEvent(new CustomEvent("bp:config-saved"));
|
|
3024
|
+
await renderForm(schema, token, "tenant", "");
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
setStatus("error", error instanceof Error ? error.message : String(error));
|
|
2819
3027
|
button.disabled = false;
|
|
2820
|
-
return;
|
|
2821
3028
|
}
|
|
2822
|
-
document.body.dispatchEvent(new CustomEvent("bp:config-saved"));
|
|
2823
|
-
await renderForm(schema, token, "tenant", "");
|
|
2824
3029
|
});
|
|
2825
3030
|
});
|
|
2826
3031
|
root.querySelector("[data-bp-config-form]")?.addEventListener("submit", async (event) => {
|
|
@@ -2849,31 +3054,17 @@ function renderConfigClientShell(d) {
|
|
|
2849
3054
|
if (value !== undefined && value !== "(unchanged)") valuesToSave[field.key] = value;
|
|
2850
3055
|
}
|
|
2851
3056
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
"
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
"
|
|
2860
|
-
|
|
2861
|
-
if (selectedAppId) headers["x-bp-app-id"] = selectedAppId;
|
|
2862
|
-
const response = await fetch(serviceBase() + "/.well-known/bp/config", {
|
|
2863
|
-
method: "POST",
|
|
2864
|
-
headers,
|
|
2865
|
-
body: JSON.stringify(payload)
|
|
2866
|
-
});
|
|
2867
|
-
if (!response.ok) {
|
|
2868
|
-
const error = await readJson(response, "save").catch((err) => ({ error: err.message || "HTTP " + response.status }));
|
|
2869
|
-
if (saveStatus) saveStatus.innerHTML = '<div class="alert alert-danger">' + escapeHtml(error.error || "Save failed") + '</div>';
|
|
2870
|
-
setStatus("error", error.error || "Save failed.");
|
|
3057
|
+
try {
|
|
3058
|
+
await writeValues(token, selectedAppId, valuesToSave, clearKeys);
|
|
3059
|
+
document.body.dispatchEvent(new CustomEvent("bp:config-saved"));
|
|
3060
|
+
await renderForm(schema, token, scope, selectedAppId);
|
|
3061
|
+
setStatus("ok", "Configuration saved.");
|
|
3062
|
+
} catch (error) {
|
|
3063
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3064
|
+
if (saveStatus) saveStatus.innerHTML = '<div class="alert alert-danger">' + escapeHtml(message) + '</div>';
|
|
3065
|
+
setStatus("error", message);
|
|
2871
3066
|
if (submitButton) submitButton.disabled = false;
|
|
2872
|
-
return;
|
|
2873
3067
|
}
|
|
2874
|
-
document.body.dispatchEvent(new CustomEvent("bp:config-saved"));
|
|
2875
|
-
await renderForm(schema, token, scope, selectedAppId);
|
|
2876
|
-
setStatus("ok", "Configuration saved.");
|
|
2877
3068
|
});
|
|
2878
3069
|
setStatus("ok", "Configuration loaded.");
|
|
2879
3070
|
};
|