@betterportal/config-manager 10.6.28 → 10.6.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts +2 -2
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.js +152 -148
- package/lib/plugins/service-betterportal-config-manager/adminApi.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 +1 -0
- package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/previewEnvironmentManagement.d.ts +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/appData.d.ts +230 -0
- package/lib/plugins/service-betterportal-config-manager/storage/appData.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/appData.js +193 -0
- package/lib/plugins/service-betterportal-config-manager/storage/appData.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts +5 -6
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +185 -73
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.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.6.
|
|
4
|
+
* @version 10.6.30
|
|
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.6.
|
|
4
|
+
* @version 10.6.30
|
|
5
5
|
*/
|
|
6
6
|
import { ServiceClient, importPortableSchema, createFireAndForgetEvent, createBroadcastEvent, createEventSchemas } from "@bsb/base";
|
|
7
7
|
const _webhookDeliveryAvailableSchema = importPortableSchema(JSON.parse("{\"anyvaliVersion\":\"1.0\",\"schemaVersion\":\"1.1\",\"root\":{\"kind\":\"object\",\"properties\":{},\"required\":[],\"unknownKeys\":\"strip\"},\"definitions\":{},\"extensions\":{}}"));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"pluginId": "org.betterportal.config-manager",
|
|
5
5
|
"title": "BetterPortal Config Manager",
|
|
6
6
|
"description": "Admin-facing BetterPortal service for discovering and managing service config surfaces.",
|
|
7
|
-
"version": "10.6.
|
|
7
|
+
"version": "10.6.30",
|
|
8
8
|
"category": "service",
|
|
9
9
|
"deploymentModes": [
|
|
10
10
|
"self-hosted"
|
|
@@ -31,7 +31,7 @@ declare function automationServiceCatalog(config: BetterPortalConfig, appDef: Be
|
|
|
31
31
|
scope: "tenant" | "app";
|
|
32
32
|
visibility: "public" | "protected" | "secret";
|
|
33
33
|
ownership: "bp" | "plugin" | "mixed";
|
|
34
|
-
sourceOfTruth: "
|
|
34
|
+
sourceOfTruth: "external" | "bp" | "plugin";
|
|
35
35
|
required: boolean;
|
|
36
36
|
groupId?: string | undefined;
|
|
37
37
|
order?: number | undefined;
|
|
@@ -168,7 +168,7 @@ declare function automationServiceCatalog(config: BetterPortalConfig, appDef: Be
|
|
|
168
168
|
scope: "tenant" | "app";
|
|
169
169
|
visibility: "public" | "protected" | "secret";
|
|
170
170
|
ownership: "bp" | "plugin" | "mixed";
|
|
171
|
-
sourceOfTruth: "
|
|
171
|
+
sourceOfTruth: "external" | "bp" | "plugin";
|
|
172
172
|
required: boolean;
|
|
173
173
|
groupId?: string | undefined;
|
|
174
174
|
order?: number | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EAEjB,mBAAmB,EACnB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAcjC,OAAO,KAAK,EAAuE,kBAAkB,EAAE,eAAe,EAAgE,MAAM,yBAAyB,CAAC;AACtN,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAgLzD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GACvC,MAAM,GAAG,SAAS,CAepB;AAwGD,iBAAS,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkEpF;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GAAG,MAAM,CA4CpG;AA8RD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAiDtH;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG;IACvG,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;CACzB,CA2FA;AAkPD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,IAAI,CAwiDN;AAwPD,wBAAgB,uBAAuB,CAAC,CAAC,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD,GAAG,MAAM,CAwaT"}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { isMenuRouteExcluded } from "./routeMounts.js";
|
|
2
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
3
2
|
import * as av from "anyvali";
|
|
4
3
|
import { AppAuthRoleSchema, eventHeaders, htmlResponse, jsonResponse, resolveEmbeddedRequestContext, signServiceConfigTicket, uuidv7, BetterPortalMenuItemSchema, BetterPortalThemeConfigSchema } from "@betterportal/framework";
|
|
5
4
|
import { generateApiKey, hashApiKey } from "./storage/index.js";
|
|
6
5
|
import { getManifestCache } from "./syncApi.js";
|
|
7
6
|
import { apiRoutePath, appRoutePatternKey, isApiRoute, pageRoutePath } from "./routeMounts.js";
|
|
8
7
|
import { M2MConnectionError, approveM2MConnections, buildM2MConnectionModel, revokeM2MConnection } from "./m2mConnections.js";
|
|
9
|
-
import { ConfigRevisionConflictError } from "./storage/core.js";
|
|
10
8
|
import { isPreviewApp } from "./previewEnvironments.js";
|
|
11
9
|
const API_BASE = "/.well-known/bp/admin";
|
|
12
10
|
const CONFIG_TICKET_TTL_SECONDS = 5 * 60;
|
|
@@ -72,28 +70,27 @@ function htmxError(message, status = 400) {
|
|
|
72
70
|
function validationError(event, message) {
|
|
73
71
|
return wantsHtmx(event) ? htmxError(message, 400) : jsonResponse({ error: message }, 400);
|
|
74
72
|
}
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
async function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
73
|
+
// PostgreSQL edits read current rows under an app-scoped transaction.
|
|
74
|
+
// File stores retain their existing single-process save semantics.
|
|
75
|
+
async function editApp(store, appId, action) {
|
|
76
|
+
const apply = async (config) => {
|
|
77
|
+
const response = await action(config);
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
throw response;
|
|
80
|
+
return response;
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
if (store.mutateApp)
|
|
84
|
+
return await store.mutateApp(appId, apply);
|
|
85
|
+
const config = await store.loadConfig();
|
|
86
|
+
const response = await apply(config);
|
|
87
|
+
await store.saveConfig(config);
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof Response)
|
|
92
|
+
return error;
|
|
93
|
+
throw error;
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
function validateRoleId(value) {
|
|
@@ -1672,10 +1669,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1672
1669
|
&& service.id === mountedServiceId
|
|
1673
1670
|
&& service.serviceId === identity.serviceId));
|
|
1674
1671
|
};
|
|
1675
|
-
const requireServiceIdentityForAuthoritativeService =
|
|
1676
|
-
const identity = await readServiceIdentity(event);
|
|
1677
|
-
if (identity instanceof Response)
|
|
1678
|
-
return identity;
|
|
1672
|
+
const requireServiceIdentityForAuthoritativeService = (identity, config, appDef, type) => {
|
|
1679
1673
|
const mountedServiceId = appMountedServiceId(appDef, type);
|
|
1680
1674
|
if (!serviceIdentityOwnsMountedService(config, appDef, identity, mountedServiceId)) {
|
|
1681
1675
|
return jsonResponse({
|
|
@@ -1687,7 +1681,6 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1687
1681
|
}
|
|
1688
1682
|
return undefined;
|
|
1689
1683
|
};
|
|
1690
|
-
const requireServiceIdentityForAppAuth = async (event, config, appDef) => requireServiceIdentityForAuthoritativeService(event, config, appDef, "auth");
|
|
1691
1684
|
const parseSyncedRoles = (appDef, roles) => {
|
|
1692
1685
|
const parsedRoles = [];
|
|
1693
1686
|
for (const rawRole of roles) {
|
|
@@ -1766,34 +1759,52 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1766
1759
|
const roles = Array.isArray(body.roles) ? body.roles : undefined;
|
|
1767
1760
|
if (!roles)
|
|
1768
1761
|
return jsonResponse({ error: "roles array required" }, 400);
|
|
1769
|
-
const
|
|
1770
|
-
if (
|
|
1771
|
-
return
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1762
|
+
const identity = await readServiceIdentity(event);
|
|
1763
|
+
if (identity instanceof Response)
|
|
1764
|
+
return identity;
|
|
1765
|
+
return editApp(store, appId, async (config) => {
|
|
1766
|
+
const appDef = config.apps.find(app => app.id === appId);
|
|
1767
|
+
if (!appDef)
|
|
1768
|
+
return jsonResponse({ error: "App not found" }, 404);
|
|
1769
|
+
const serviceError = requireServiceIdentityForAuthoritativeService(identity, config, appDef, "auth");
|
|
1770
|
+
if (serviceError)
|
|
1771
|
+
return serviceError;
|
|
1772
|
+
const authResult = requireAuthBlock(event, appDef);
|
|
1773
|
+
if (authResult.response)
|
|
1774
|
+
return authResult.response;
|
|
1775
|
+
const parsedRoles = parseSyncedRoles(appDef, roles);
|
|
1776
|
+
if (parsedRoles instanceof Response)
|
|
1777
|
+
return parsedRoles;
|
|
1778
|
+
authResult.auth.roles = parsedRoles;
|
|
1779
|
+
return jsonResponse({ ok: true, roles: parsedRoles.length });
|
|
1780
|
+
});
|
|
1784
1781
|
});
|
|
1785
1782
|
app.put(`${API_BASE}/services/self-mutation`, async (event) => {
|
|
1786
1783
|
const parsed = SelfMutationRequestSchema.safeParse(await readJsonBody(event));
|
|
1787
1784
|
if (!parsed.success)
|
|
1788
1785
|
return jsonResponse({ error: "Invalid self-mutation request", issues: parsed.issues }, 400);
|
|
1789
1786
|
const { tenantId, appId, type, mutation } = parsed.data;
|
|
1790
|
-
const
|
|
1791
|
-
if (
|
|
1792
|
-
return
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1787
|
+
const identity = await readServiceIdentity(event);
|
|
1788
|
+
if (identity instanceof Response)
|
|
1789
|
+
return identity;
|
|
1790
|
+
if (type === "theme") {
|
|
1791
|
+
const { config, appDef } = await getAppOr404(appId);
|
|
1792
|
+
if (!appDef || appDef.tenantId !== tenantId)
|
|
1793
|
+
return jsonResponse({ error: "App not found" }, 404);
|
|
1794
|
+
const serviceError = requireServiceIdentityForAuthoritativeService(identity, config, appDef, type);
|
|
1795
|
+
if (serviceError)
|
|
1796
|
+
return serviceError;
|
|
1797
|
+
appDef.themeConfig = mutation.themeConfig;
|
|
1798
|
+
await store.saveConfig(config);
|
|
1799
|
+
return jsonResponse({ ok: true, type });
|
|
1800
|
+
}
|
|
1801
|
+
return editApp(store, appId, async (config) => {
|
|
1802
|
+
const appDef = config.apps.find(app => app.id === appId);
|
|
1803
|
+
if (!appDef || appDef.tenantId !== tenantId)
|
|
1804
|
+
return jsonResponse({ error: "App not found" }, 404);
|
|
1805
|
+
const serviceError = requireServiceIdentityForAuthoritativeService(identity, config, appDef, type);
|
|
1806
|
+
if (serviceError)
|
|
1807
|
+
return serviceError;
|
|
1797
1808
|
const authResult = requireAuthBlock(event, appDef);
|
|
1798
1809
|
if (authResult.response)
|
|
1799
1810
|
return authResult.response;
|
|
@@ -1801,12 +1812,8 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1801
1812
|
if (parsedRoles instanceof Response)
|
|
1802
1813
|
return parsedRoles;
|
|
1803
1814
|
authResult.auth.roles = parsedRoles;
|
|
1804
|
-
await store.saveConfig(config);
|
|
1805
1815
|
return jsonResponse({ ok: true, type, roles: parsedRoles.length });
|
|
1806
|
-
}
|
|
1807
|
-
appDef.themeConfig = mutation.themeConfig;
|
|
1808
|
-
await store.saveConfig(config);
|
|
1809
|
-
return jsonResponse({ ok: true, type });
|
|
1816
|
+
});
|
|
1810
1817
|
});
|
|
1811
1818
|
app.post(`${API_BASE}/apps/:appId/auth/roles`, async (event) => {
|
|
1812
1819
|
const appId = getParam(event, "appId");
|
|
@@ -1821,27 +1828,28 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1821
1828
|
const message = "role id must start with a letter or number, may contain letters, numbers, dot, underscore, colon, or dash, and cannot be reserved";
|
|
1822
1829
|
return wantsHtmx(event) ? htmxError(message) : jsonResponse({ error: message }, 400);
|
|
1823
1830
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1831
|
+
return editApp(store, appId, async (config) => {
|
|
1832
|
+
const appDef = config.apps.find(app => app.id === appId);
|
|
1833
|
+
if (!appDef)
|
|
1834
|
+
return wantsHtmx(event) ? htmxError("App not found", 404) : jsonResponse({ error: "App not found" }, 404);
|
|
1835
|
+
const authResult = requireAuthBlock(event, appDef);
|
|
1836
|
+
if (authResult.response)
|
|
1837
|
+
return authResult.response;
|
|
1838
|
+
const auth = authResult.auth;
|
|
1839
|
+
const role = {
|
|
1840
|
+
id: roleId,
|
|
1841
|
+
title,
|
|
1842
|
+
description: body.description,
|
|
1843
|
+
permissions: Array.isArray(body.permissions) ? body.permissions : []
|
|
1844
|
+
};
|
|
1845
|
+
if (auth.roles.some((r) => r.id === role.id)) {
|
|
1846
|
+
return wantsHtmx(event) ? htmxError("role id already exists", 409) : jsonResponse({ error: "role id already exists" }, 409);
|
|
1847
|
+
}
|
|
1848
|
+
auth.roles.push(role);
|
|
1849
|
+
if (wantsHtmx(event))
|
|
1850
|
+
return htmxReload(`/auth?appId=${encodeURIComponent(appId)}`);
|
|
1851
|
+
return jsonResponse(role, 201);
|
|
1852
|
+
});
|
|
1845
1853
|
});
|
|
1846
1854
|
app.put(`${API_BASE}/apps/:appId/auth/roles/:roleId`, async (event) => {
|
|
1847
1855
|
const appId = getParam(event, "appId");
|
|
@@ -1849,62 +1857,64 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1849
1857
|
if (!appId || !roleId)
|
|
1850
1858
|
return jsonResponse({ error: "appId + roleId required" }, 400);
|
|
1851
1859
|
const body = await readFormOrJsonBody(event);
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
else if (typeof body.grant === "string" || Array.isArray(body.grant)) {
|
|
1870
|
-
const grants = Array.isArray(body.grant) ? body.grant : [body.grant];
|
|
1871
|
-
const byView = new Map();
|
|
1872
|
-
for (const grant of grants) {
|
|
1873
|
-
const [serviceId, viewId, action] = String(grant).split("|");
|
|
1874
|
-
if (!serviceId || !viewId || !["read", "create", "update", "delete"].includes(action))
|
|
1875
|
-
continue;
|
|
1876
|
-
const key = `${serviceId}::${viewId}`;
|
|
1877
|
-
if (!byView.has(key))
|
|
1878
|
-
byView.set(key, { serviceId, viewId, permissions: [] });
|
|
1879
|
-
byView.get(key).permissions.push(action);
|
|
1860
|
+
return editApp(store, appId, async (config) => {
|
|
1861
|
+
const appDef = config.apps.find(app => app.id === appId);
|
|
1862
|
+
if (!appDef)
|
|
1863
|
+
return wantsHtmx(event) ? htmxError("App not found", 404) : jsonResponse({ error: "App not found" }, 404);
|
|
1864
|
+
const authResult = requireAuthBlock(event, appDef);
|
|
1865
|
+
if (authResult.response)
|
|
1866
|
+
return authResult.response;
|
|
1867
|
+
const auth = authResult.auth;
|
|
1868
|
+
const role = auth.roles.find((r) => r.id === roleId);
|
|
1869
|
+
if (!role)
|
|
1870
|
+
return wantsHtmx(event) ? htmxError("Role not found", 404) : jsonResponse({ error: "Role not found" }, 404);
|
|
1871
|
+
if (typeof body.title === "string")
|
|
1872
|
+
role.title = body.title;
|
|
1873
|
+
if (typeof body.description === "string" || body.description === null)
|
|
1874
|
+
role.description = body.description ?? undefined;
|
|
1875
|
+
if (Array.isArray(body.permissions)) {
|
|
1876
|
+
role.permissions = body.permissions;
|
|
1880
1877
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1878
|
+
else if (typeof body.grant === "string" || Array.isArray(body.grant)) {
|
|
1879
|
+
const grants = Array.isArray(body.grant) ? body.grant : [body.grant];
|
|
1880
|
+
const byView = new Map();
|
|
1881
|
+
for (const grant of grants) {
|
|
1882
|
+
const [serviceId, viewId, action] = String(grant).split("|");
|
|
1883
|
+
if (!serviceId || !viewId || !["read", "create", "update", "delete"].includes(action))
|
|
1884
|
+
continue;
|
|
1885
|
+
const key = `${serviceId}::${viewId}`;
|
|
1886
|
+
if (!byView.has(key))
|
|
1887
|
+
byView.set(key, { serviceId, viewId, permissions: [] });
|
|
1888
|
+
byView.get(key).permissions.push(action);
|
|
1889
|
+
}
|
|
1890
|
+
role.permissions = Array.from(byView.values());
|
|
1891
|
+
}
|
|
1892
|
+
if (wantsHtmx(event))
|
|
1893
|
+
return htmxReload(`/auth?appId=${encodeURIComponent(appId)}`);
|
|
1894
|
+
return jsonResponse(role);
|
|
1895
|
+
});
|
|
1887
1896
|
});
|
|
1888
1897
|
app.delete(`${API_BASE}/apps/:appId/auth/roles/:roleId`, async (event) => {
|
|
1889
1898
|
const appId = getParam(event, "appId");
|
|
1890
1899
|
const roleId = getParam(event, "roleId");
|
|
1891
1900
|
if (!appId || !roleId)
|
|
1892
1901
|
return jsonResponse({ error: "appId + roleId required" }, 400);
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1902
|
+
return editApp(store, appId, async (config) => {
|
|
1903
|
+
const appDef = config.apps.find(app => app.id === appId);
|
|
1904
|
+
if (!appDef)
|
|
1905
|
+
return wantsHtmx(event) ? htmxError("App not found", 404) : jsonResponse({ error: "App not found" }, 404);
|
|
1906
|
+
const authResult = requireAuthBlock(event, appDef);
|
|
1907
|
+
if (authResult.response)
|
|
1908
|
+
return authResult.response;
|
|
1909
|
+
const auth = authResult.auth;
|
|
1910
|
+
const before = auth.roles.length;
|
|
1911
|
+
auth.roles = auth.roles.filter((r) => r.id !== roleId);
|
|
1912
|
+
if (auth.roles.length === before)
|
|
1913
|
+
return wantsHtmx(event) ? htmxError("Role not found", 404) : jsonResponse({ error: "Role not found" }, 404);
|
|
1914
|
+
if (wantsHtmx(event))
|
|
1915
|
+
return htmxReload(`/auth?appId=${encodeURIComponent(appId)}`);
|
|
1916
|
+
return jsonResponse({ ok: true });
|
|
1917
|
+
});
|
|
1908
1918
|
});
|
|
1909
1919
|
// Service connections (per app)
|
|
1910
1920
|
app.get(`${API_BASE}/apps/:appId/m2m/connections`, async (event) => {
|
|
@@ -1993,8 +2003,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
1993
2003
|
if (!appId)
|
|
1994
2004
|
return jsonResponse({ error: "appId required" }, 400);
|
|
1995
2005
|
const body = await readFormOrJsonBody(event);
|
|
1996
|
-
return
|
|
1997
|
-
const config = await store.loadConfig();
|
|
2006
|
+
return editApp(store, appId, async (config) => {
|
|
1998
2007
|
if (isPreviewApp(config, appId))
|
|
1999
2008
|
return jsonResponse({ error: "Preview resources are managed through Preview Environments" }, 404);
|
|
2000
2009
|
const appDef = config.apps.find((a) => a.id === appId);
|
|
@@ -2014,7 +2023,6 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
2014
2023
|
const route = { ...parsed.route, id: uuidv7() };
|
|
2015
2024
|
appDef.routes.push(route);
|
|
2016
2025
|
addRouteDependencies(appDef, route);
|
|
2017
|
-
await store.saveConfig(config);
|
|
2018
2026
|
if (wantsHtmx(event))
|
|
2019
2027
|
return htmxReload(routesReloadPath(appId));
|
|
2020
2028
|
return jsonResponse({ ok: true, id: route.id }, 201);
|
|
@@ -2026,8 +2034,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
2026
2034
|
if (!appId || !routeId)
|
|
2027
2035
|
return jsonResponse({ error: "appId and routeId required" }, 400);
|
|
2028
2036
|
const body = await readFormOrJsonBody(event);
|
|
2029
|
-
return
|
|
2030
|
-
const config = await store.loadConfig();
|
|
2037
|
+
return editApp(store, appId, async (config) => {
|
|
2031
2038
|
if (isPreviewApp(config, appId))
|
|
2032
2039
|
return jsonResponse({ error: "Preview resources are managed through Preview Environments" }, 404);
|
|
2033
2040
|
const appDef = config.apps.find((a) => a.id === appId);
|
|
@@ -2149,7 +2156,6 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
2149
2156
|
if (appDef.routes.some((candidate) => candidate.id !== route.id && appRoutePatternKey(candidate.path) === appRoutePatternKey(route.path))) {
|
|
2150
2157
|
return validationError(event, `A route already exists at ${route.path}.`);
|
|
2151
2158
|
}
|
|
2152
|
-
await store.saveConfig(config);
|
|
2153
2159
|
if (wantsHtmx(event))
|
|
2154
2160
|
return htmxReload(routesReloadPath(appId, route.kind === "api" ? route.serviceId : undefined));
|
|
2155
2161
|
return jsonResponse({ ok: true });
|
|
@@ -2160,8 +2166,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
2160
2166
|
const routeId = getParam(event, "routeId");
|
|
2161
2167
|
if (!appId || !routeId)
|
|
2162
2168
|
return jsonResponse({ error: "appId and routeId required" }, 400);
|
|
2163
|
-
return
|
|
2164
|
-
const config = await store.loadConfig();
|
|
2169
|
+
return editApp(store, appId, async (config) => {
|
|
2165
2170
|
if (isPreviewApp(config, appId))
|
|
2166
2171
|
return jsonResponse({ error: "Preview resources are managed through Preview Environments" }, 404);
|
|
2167
2172
|
const appDef = config.apps.find((a) => a.id === appId);
|
|
@@ -2185,7 +2190,6 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
2185
2190
|
return wantsHtmx(event) ? htmxAlert(message, "warning") : jsonResponse({ error: message }, 409);
|
|
2186
2191
|
}
|
|
2187
2192
|
appDef.routes = appDef.routes.filter((r) => r.id !== routeId);
|
|
2188
|
-
await store.saveConfig(config);
|
|
2189
2193
|
if (wantsHtmx(event))
|
|
2190
2194
|
return htmxReload(`/routes?appId=${encodeURIComponent(appId)}`);
|
|
2191
2195
|
return jsonResponse({ ok: true });
|
|
@@ -2208,20 +2212,20 @@ export function registerAdminApiRoutes(app, store, cpState) {
|
|
|
2208
2212
|
return jsonResponse({ error: "appId required" }, 400);
|
|
2209
2213
|
const body = await readJsonBody(event);
|
|
2210
2214
|
const items = Array.isArray(body.items) ? body.items : [];
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2215
|
+
return editApp(store, appId, async (config) => {
|
|
2216
|
+
const appDef = config.apps.find((a) => a.id === appId);
|
|
2217
|
+
if (!appDef)
|
|
2218
|
+
return jsonResponse({ error: "App not found" }, 404);
|
|
2219
|
+
const menu = items.map(normalizeMenuItem);
|
|
2220
|
+
const invalid = (entries) => entries.some(item => {
|
|
2221
|
+
const route = item.routeId ? appDef.routes.find(route => route.id === item.routeId) : undefined;
|
|
2222
|
+
return (Boolean(item.routeId) && (!route || isMenuRouteExcluded(route, getManifestCache()))) || invalid(item.children);
|
|
2223
|
+
});
|
|
2224
|
+
if (invalid(menu))
|
|
2225
|
+
return jsonResponse({ error: "Menu contains a missing or excluded route" }, 400);
|
|
2226
|
+
appDef.menu = menu;
|
|
2227
|
+
return jsonResponse({ ok: true });
|
|
2219
2228
|
});
|
|
2220
|
-
if (invalid(menu))
|
|
2221
|
-
return jsonResponse({ error: "Menu contains a missing or excluded route" }, 400);
|
|
2222
|
-
appDef.menu = menu;
|
|
2223
|
-
await store.saveConfig(config);
|
|
2224
|
-
return jsonResponse({ ok: true });
|
|
2225
2229
|
});
|
|
2226
2230
|
// Full config (read-only)
|
|
2227
2231
|
app.get(`${API_BASE}/config`, async () => {
|