anbaric-cloud-hosting 1.3.0 → 1.5.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/package.json +5 -5
- package/src/auditing/AuditRecordStore.ts +2 -1
- package/src/auditing/InMemoryAuditRecordStore.ts +1 -0
- package/src/data-store/PostgresAuditRecordStore.ts +11 -6
- package/src/data-store/Schema.ts +16 -2
- package/src/hosting/HostingServer.ts +1 -0
- package/src/hosting/Router.ts +307 -213
- package/src/hosting/pages/platform-ui.html +1 -1
package/src/hosting/Router.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {readFile} from "node:fs/promises";
|
|
2
2
|
import {IncomingMessage, ServerResponse} from "node:http";
|
|
3
|
-
import {JobPersistence, JsonStore, SecretStore, deserializeJob, serializeJob} from "anbaric-tsapi";
|
|
3
|
+
import {AuditChange, JobPersistence, JsonStore, SecretStore, deserializeJob, serializeJob} from "anbaric-tsapi";
|
|
4
4
|
import {BuildLayer} from "../app-management/BuildLayer";
|
|
5
5
|
import {AuditRecordStore} from "../auditing/AuditRecordStore";
|
|
6
6
|
import {CliAuthorizer} from "../auth/CliAuthorizer";
|
|
@@ -9,6 +9,8 @@ import {User} from "../auth/User";
|
|
|
9
9
|
import {ConfirmableQueue} from "../queuing/ConfirmableQueue";
|
|
10
10
|
import {ConsumerRegistry} from "../queuing/ConsumerRegistry";
|
|
11
11
|
|
|
12
|
+
const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
|
|
13
|
+
|
|
12
14
|
const readRawBody = (request : IncomingMessage) : Promise<Buffer> =>
|
|
13
15
|
new Promise((resolve, reject) => {
|
|
14
16
|
const chunks : Array<Buffer> = [];
|
|
@@ -22,6 +24,10 @@ const readBody = async (request : IncomingMessage) : Promise<any> => {
|
|
|
22
24
|
return raw.length === 0 ? undefined : JSON.parse(raw);
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
/* Routing is a tree: route() dispatches on the resource family (or the few
|
|
28
|
+
specific resources that fit no family), each family handler switches on
|
|
29
|
+
the specific resource shape, and the method switch sits underneath.
|
|
30
|
+
Unmatched requests fall through to the app proxy, then 404. */
|
|
25
31
|
class Router {
|
|
26
32
|
|
|
27
33
|
constructor(private persistence : JobPersistence, private queue : ConfirmableQueue,
|
|
@@ -38,64 +44,56 @@ class Router {
|
|
|
38
44
|
const [resource, id, subresource] = url.pathname.split("/").filter(Boolean);
|
|
39
45
|
const method = request.method ?? "GET";
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!user) return this.reply(response, 404, { error: "Not found" });
|
|
55
|
-
return this.reply(response, 200, { id: user.id, roles: user.roles.map(role => role.id) });
|
|
56
|
-
}
|
|
57
|
-
if (resource === "audits" && this.auditRecords && !id) {
|
|
58
|
-
if (method === "POST") {
|
|
59
|
-
const record = await readBody(request);
|
|
60
|
-
if (typeof record?.jobId !== "string" || typeof record?.description !== "string") {
|
|
61
|
-
return this.reply(response, 400, { error: "Expected a body of { jobId, description, ... }" });
|
|
47
|
+
switch (resource) {
|
|
48
|
+
case undefined:
|
|
49
|
+
if (method === "GET") return this.servePage(response);
|
|
50
|
+
break;
|
|
51
|
+
case "audit":
|
|
52
|
+
if (method === "GET" && !id) return this.servePage(response);
|
|
53
|
+
break;
|
|
54
|
+
case "manage-keys":
|
|
55
|
+
if (method === "GET" && !id && this.cliAuthorizer) return this.servePage(response);
|
|
56
|
+
break;
|
|
57
|
+
case "whoami":
|
|
58
|
+
if (method === "GET" && !id && user) {
|
|
59
|
+
return this.reply(response, 200, { id: user.id, roles: user.roles.map(role => role.id) });
|
|
62
60
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return this.handleSecrets(method, id, request, response);
|
|
61
|
+
break;
|
|
62
|
+
case "state-machines":
|
|
63
|
+
if (method === "GET" && !id) return this.reply(response, 200, this.registry.list());
|
|
64
|
+
break;
|
|
65
|
+
case "authorize-cli":
|
|
66
|
+
if (this.cliAuthorizer && id) {
|
|
67
|
+
return this.handleAuthorizeCli(method, id, subresource, request, response, user, sessionTenant);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case "keys":
|
|
71
|
+
if (this.cliAuthorizer) return this.handleKeys(method, id, response, user);
|
|
72
|
+
break;
|
|
73
|
+
case "audits":
|
|
74
|
+
if (this.auditRecords && !id) return this.handleAudits(method, url, request, response);
|
|
75
|
+
break;
|
|
76
|
+
case "jobs":
|
|
77
|
+
return this.handleJobs(method, id, subresource, url, request, response);
|
|
78
|
+
case "queue":
|
|
79
|
+
if (id && !subresource) return this.handleQueue(method, id, request, response);
|
|
80
|
+
break;
|
|
81
|
+
case "consumers":
|
|
82
|
+
if (!id) return this.handleConsumers(method, request, response);
|
|
83
|
+
break;
|
|
84
|
+
case "apps":
|
|
85
|
+
if (this.buildLayer) return this.handleApps(method, id, subresource, url, request, response);
|
|
86
|
+
break;
|
|
87
|
+
case "documents":
|
|
88
|
+
if (id && this.documentStoreFor) {
|
|
89
|
+
return this.handleDocuments(method, this.documentStoreFor(id), subresource, url, request, response);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case "secrets":
|
|
93
|
+
if (!subresource && this.secretStore) return this.handleSecrets(method, id, request, response);
|
|
94
|
+
break;
|
|
98
95
|
}
|
|
96
|
+
|
|
99
97
|
if (resource && this.buildLayer) {
|
|
100
98
|
const app = this.buildLayer.status(resource);
|
|
101
99
|
if (app && app.status === "running") {
|
|
@@ -103,216 +101,312 @@ class Router {
|
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
this.
|
|
104
|
+
this.notFound(response);
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
private async handleAuthorizeCli(method : string, requestId : string, subresource : string | undefined,
|
|
110
108
|
request : IncomingMessage, response : ServerResponse, user? : User,
|
|
111
109
|
sessionTenant? : Tenant) : Promise<void> {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
110
|
+
switch (subresource) {
|
|
111
|
+
case "poll":
|
|
112
|
+
switch (method) {
|
|
113
|
+
case "GET": {
|
|
114
|
+
const keyPair = this.cliAuthorizer!.collect(requestId);
|
|
115
|
+
if (!keyPair) return this.reply(response, 202, { status: "pending" });
|
|
116
|
+
return this.reply(response, 200, keyPair);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case undefined:
|
|
121
|
+
switch (method) {
|
|
122
|
+
case "GET":
|
|
123
|
+
return this.servePage(response);
|
|
124
|
+
case "POST": {
|
|
125
|
+
const { clientName } = await readBody(request);
|
|
126
|
+
if (typeof clientName !== "string" || clientName.trim().length === 0) {
|
|
127
|
+
return this.reply(response, 400, { error: "Expected a body of { clientName : string }" });
|
|
128
|
+
}
|
|
129
|
+
await this.cliAuthorizer!.approve(requestId, clientName.trim(), user ?? new User("local"),
|
|
130
|
+
sessionTenant?.id ?? this.tenant);
|
|
131
|
+
return this.reply(response, 204);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
this.
|
|
137
|
+
this.notFound(response);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
private async handleKeys(method : string, id : string | undefined,
|
|
136
141
|
response : ServerResponse, user? : User) : Promise<void> {
|
|
137
142
|
const owner = user ?? new User("local");
|
|
138
143
|
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
if (id) {
|
|
145
|
+
switch (method) {
|
|
146
|
+
case "DELETE":
|
|
147
|
+
await this.cliAuthorizer!.revoke(id, owner.id);
|
|
148
|
+
return this.reply(response, 204);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
switch (method) {
|
|
152
|
+
case "GET": {
|
|
153
|
+
const keys = await this.cliAuthorizer!.keysFor(owner.id);
|
|
154
|
+
return this.reply(response, 200, keys.map(key => ({
|
|
155
|
+
id: key.id,
|
|
156
|
+
clientName: key.clientName,
|
|
157
|
+
createdAt: key.createdAt.toISOString(),
|
|
158
|
+
})));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
this.
|
|
163
|
+
this.notFound(response);
|
|
154
164
|
}
|
|
155
165
|
|
|
156
|
-
private async
|
|
157
|
-
try {
|
|
158
|
-
const page = await readFile(new URL("./pages/platform-ui.html", import.meta.url));
|
|
159
|
-
response.writeHead(200, { "content-type": "text/html" });
|
|
160
|
-
response.end(page);
|
|
161
|
-
} catch {
|
|
162
|
-
this.reply(response, 501, { error: "The platform UI has not been built - run npm run build in anbaric-cloud-hosting/ui" });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private async forwardToApp(appHost : string, appPort : number, appName : string, method : string, url : URL,
|
|
166
|
+
private async handleAudits(method : string, url : URL,
|
|
167
167
|
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const payload = Buffer.from(await upstream.arrayBuffer());
|
|
178
|
-
response.writeHead(upstream.status, { "content-type": upstream.headers.get("content-type") ?? "application/octet-stream" });
|
|
179
|
-
response.end(payload);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private async handleSecrets(method : string, name : string | undefined,
|
|
183
|
-
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
184
|
-
if (!name && method === "GET") {
|
|
185
|
-
return this.reply(response, 200, await this.secretStore!.list());
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (name) {
|
|
189
|
-
if (method === "PUT") {
|
|
190
|
-
const { value } = await readBody(request);
|
|
191
|
-
if (typeof value !== "string") return this.reply(response, 400, { error: "Expected a body of { value : string }" });
|
|
192
|
-
await this.secretStore!.save(name, value);
|
|
168
|
+
switch (method) {
|
|
169
|
+
case "POST": {
|
|
170
|
+
const record = await readBody(request);
|
|
171
|
+
if (typeof record?.jobId !== "string" || typeof record?.description !== "string"
|
|
172
|
+
|| typeof record?.actorId !== "string" || typeof record?.actorType !== "string"
|
|
173
|
+
|| !AUDIT_CHANGES.has(record?.change)) {
|
|
174
|
+
return this.reply(response, 400, { error: "Expected a body of { jobId, actorId, actorType, change, description, ... }" });
|
|
175
|
+
}
|
|
176
|
+
await this.auditRecords!.save(record);
|
|
193
177
|
return this.reply(response, 204);
|
|
194
178
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
179
|
+
case "GET": {
|
|
180
|
+
const change = url.searchParams.get("change");
|
|
181
|
+
const records = await this.auditRecords!.list({
|
|
182
|
+
jobId: url.searchParams.get("jobId") ?? undefined,
|
|
183
|
+
actorId: url.searchParams.get("actorId") ?? undefined,
|
|
184
|
+
change: change && AUDIT_CHANGES.has(change) ? change as AuditChange : undefined,
|
|
185
|
+
search: url.searchParams.get("search") ?? undefined,
|
|
186
|
+
pageSize: url.searchParams.has("pageSize") ? Number(url.searchParams.get("pageSize")) : undefined,
|
|
187
|
+
page: url.searchParams.has("page") ? Number(url.searchParams.get("page")) : undefined,
|
|
188
|
+
});
|
|
189
|
+
return this.reply(response, 200, records);
|
|
201
190
|
}
|
|
202
191
|
}
|
|
203
192
|
|
|
204
|
-
this.
|
|
193
|
+
this.notFound(response);
|
|
205
194
|
}
|
|
206
195
|
|
|
207
|
-
private async
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
196
|
+
private async handleJobs(method : string, id : string | undefined, subresource : string | undefined,
|
|
197
|
+
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
198
|
+
switch (subresource) {
|
|
199
|
+
case "properties":
|
|
200
|
+
if (id) {
|
|
201
|
+
switch (method) {
|
|
202
|
+
case "PATCH": {
|
|
203
|
+
const properties = new Map<string, any>(Object.entries(await readBody(request)));
|
|
204
|
+
await this.persistence.updateProperties(id, properties);
|
|
205
|
+
return this.reply(response, 204);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
case undefined:
|
|
211
|
+
if (id) {
|
|
212
|
+
switch (method) {
|
|
213
|
+
case "PUT":
|
|
214
|
+
await this.persistence.save(deserializeJob(await readBody(request)));
|
|
215
|
+
return this.reply(response, 204);
|
|
216
|
+
case "GET":
|
|
217
|
+
return this.reply(response, 200, serializeJob(await this.persistence.retrieve(id)));
|
|
218
|
+
case "DELETE":
|
|
219
|
+
await this.persistence.delete(id);
|
|
220
|
+
return this.reply(response, 204);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
switch (method) {
|
|
224
|
+
case "GET": {
|
|
225
|
+
const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
|
|
226
|
+
const page = Number(url.searchParams.get("page") ?? 0);
|
|
227
|
+
const jobs = await this.persistence.list(pageSize, page);
|
|
228
|
+
return this.reply(response, 200, jobs.map(serializeJob));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
213
233
|
}
|
|
214
234
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
235
|
+
this.notFound(response);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async handleQueue(method : string, operation : string,
|
|
239
|
+
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
240
|
+
switch (operation) {
|
|
241
|
+
case "enqueue":
|
|
242
|
+
switch (method) {
|
|
243
|
+
case "POST": {
|
|
244
|
+
const { jobId, workflowId } = await readBody(request);
|
|
245
|
+
await this.queue.enqueue(jobId, workflowId);
|
|
246
|
+
return this.reply(response, 204);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
case "schedule":
|
|
251
|
+
switch (method) {
|
|
252
|
+
case "POST": {
|
|
253
|
+
const { jobId, workflowId, due } = await readBody(request);
|
|
254
|
+
await this.queue.schedule(jobId, workflowId, new Date(due));
|
|
255
|
+
return this.reply(response, 204);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case "dequeue":
|
|
260
|
+
switch (method) {
|
|
261
|
+
case "POST":
|
|
262
|
+
return this.reply(response, 200, { messages: await this.queue.dequeueSome() });
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
case "confirm":
|
|
266
|
+
switch (method) {
|
|
267
|
+
case "POST": {
|
|
268
|
+
const { jobId, workflowId } = await readBody(request);
|
|
269
|
+
await this.queue.confirm({ jobId, workflowId });
|
|
270
|
+
return this.reply(response, 204);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.notFound(response);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async handleConsumers(method : string, request : IncomingMessage,
|
|
280
|
+
response : ServerResponse) : Promise<void> {
|
|
281
|
+
switch (method) {
|
|
282
|
+
case "POST": {
|
|
283
|
+
const { workflowId, url: consumerUrl } = await readBody(request);
|
|
284
|
+
this.registry.register(workflowId, consumerUrl);
|
|
225
285
|
return this.reply(response, 204);
|
|
226
286
|
}
|
|
227
287
|
}
|
|
228
288
|
|
|
229
|
-
this.
|
|
289
|
+
this.notFound(response);
|
|
230
290
|
}
|
|
231
291
|
|
|
232
292
|
private async handleApps(method : string, appName : string | undefined, subresource : string | undefined,
|
|
233
293
|
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
234
|
-
|
|
235
|
-
|
|
294
|
+
switch (subresource) {
|
|
295
|
+
case "deploy":
|
|
296
|
+
if (appName) {
|
|
297
|
+
switch (method) {
|
|
298
|
+
case "POST": {
|
|
299
|
+
const appPort = Number(url.searchParams.get("port"));
|
|
300
|
+
if (!Number.isInteger(appPort) || appPort <= 0) {
|
|
301
|
+
return this.reply(response, 400, { error: "Expected a numeric port query parameter" });
|
|
302
|
+
}
|
|
303
|
+
const tarball = await readRawBody(request);
|
|
304
|
+
if (tarball.length === 0) return this.reply(response, 400, { error: "Expected a gzipped tarball body" });
|
|
305
|
+
return this.reply(response, 202, this.buildLayer!.deploy(appName, appPort, tarball));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
case undefined:
|
|
311
|
+
if (appName) {
|
|
312
|
+
switch (method) {
|
|
313
|
+
case "GET": {
|
|
314
|
+
const status = this.buildLayer!.status(appName);
|
|
315
|
+
if (!status) return this.reply(response, 404, { error: `No app named "${appName}"` });
|
|
316
|
+
return this.reply(response, 200, status);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
switch (method) {
|
|
321
|
+
case "GET":
|
|
322
|
+
return this.reply(response, 200, this.buildLayer!.list());
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
236
326
|
}
|
|
237
327
|
|
|
238
|
-
|
|
328
|
+
this.notFound(response);
|
|
329
|
+
}
|
|
239
330
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
331
|
+
private async handleDocuments(method : string, store : JsonStore, documentId : string | undefined,
|
|
332
|
+
url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
333
|
+
if (documentId) {
|
|
334
|
+
switch (method) {
|
|
335
|
+
case "PUT":
|
|
336
|
+
await store.save(documentId, await readBody(request));
|
|
337
|
+
return this.reply(response, 204);
|
|
338
|
+
case "GET":
|
|
339
|
+
return this.reply(response, 200, await store.retrieve(documentId));
|
|
340
|
+
case "DELETE":
|
|
341
|
+
await store.delete(documentId);
|
|
342
|
+
return this.reply(response, 204);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
switch (method) {
|
|
346
|
+
case "GET": {
|
|
347
|
+
const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
|
|
348
|
+
const page = Number(url.searchParams.get("page") ?? 0);
|
|
349
|
+
return this.reply(response, 200, await store.list(pageSize, page));
|
|
350
|
+
}
|
|
244
351
|
}
|
|
245
|
-
const tarball = await readRawBody(request);
|
|
246
|
-
if (tarball.length === 0) return this.reply(response, 400, { error: "Expected a gzipped tarball body" });
|
|
247
|
-
return this.reply(response, 202, this.buildLayer!.deploy(appName, appPort, tarball));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (method === "GET" && !subresource) {
|
|
251
|
-
const status = this.buildLayer!.status(appName);
|
|
252
|
-
if (!status) return this.reply(response, 404, { error: `No app named "${appName}"` });
|
|
253
|
-
return this.reply(response, 200, status);
|
|
254
352
|
}
|
|
255
353
|
|
|
256
|
-
this.
|
|
354
|
+
this.notFound(response);
|
|
257
355
|
}
|
|
258
356
|
|
|
259
|
-
private async
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
357
|
+
private async handleSecrets(method : string, name : string | undefined,
|
|
358
|
+
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
359
|
+
if (name) {
|
|
360
|
+
switch (method) {
|
|
361
|
+
case "PUT": {
|
|
362
|
+
const { value } = await readBody(request);
|
|
363
|
+
if (typeof value !== "string") return this.reply(response, 400, { error: "Expected a body of { value : string }" });
|
|
364
|
+
await this.secretStore!.save(name, value);
|
|
365
|
+
return this.reply(response, 204);
|
|
366
|
+
}
|
|
367
|
+
case "GET":
|
|
368
|
+
return this.reply(response, 200, { value: await this.secretStore!.retrieve(name) });
|
|
369
|
+
case "DELETE":
|
|
370
|
+
await this.secretStore!.delete(name);
|
|
371
|
+
return this.reply(response, 204);
|
|
272
372
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
373
|
+
} else {
|
|
374
|
+
switch (method) {
|
|
375
|
+
case "GET":
|
|
376
|
+
return this.reply(response, 200, await this.secretStore!.list());
|
|
276
377
|
}
|
|
277
|
-
if (method === "DELETE") {
|
|
278
|
-
await this.persistence.delete(id);
|
|
279
|
-
return this.reply(response, 204);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (id && subresource === "properties" && method === "PATCH") {
|
|
284
|
-
const properties = new Map<string, any>(Object.entries(await readBody(request)));
|
|
285
|
-
await this.persistence.updateProperties(id, properties);
|
|
286
|
-
return this.reply(response, 204);
|
|
287
378
|
}
|
|
288
379
|
|
|
289
|
-
this.
|
|
380
|
+
this.notFound(response);
|
|
290
381
|
}
|
|
291
382
|
|
|
292
|
-
private async
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
383
|
+
private async servePage(response : ServerResponse) : Promise<void> {
|
|
384
|
+
try {
|
|
385
|
+
const page = await readFile(new URL("./pages/platform-ui.html", import.meta.url));
|
|
386
|
+
response.writeHead(200, { "content-type": "text/html" });
|
|
387
|
+
response.end(page);
|
|
388
|
+
} catch {
|
|
389
|
+
this.reply(response, 501, { error: "The platform UI has not been built - run npm run build in anbaric-cloud-hosting/ui" });
|
|
298
390
|
}
|
|
391
|
+
}
|
|
299
392
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
393
|
+
private async forwardToApp(appHost : string, appPort : number, appName : string, method : string, url : URL,
|
|
394
|
+
request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
395
|
+
const appPath = url.pathname.slice(`/${appName}`.length) || "/";
|
|
396
|
+
const body = method === "GET" || method === "HEAD" ? undefined : await readRawBody(request);
|
|
305
397
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
398
|
+
const upstream = await fetch(`http://${appHost}:${appPort}${appPath}${url.search}`, {
|
|
399
|
+
method,
|
|
400
|
+
headers: { "content-type": String(request.headers["content-type"] ?? "application/json") },
|
|
401
|
+
body: body && body.length > 0 ? new Uint8Array(body) : undefined,
|
|
402
|
+
});
|
|
309
403
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
404
|
+
const payload = Buffer.from(await upstream.arrayBuffer());
|
|
405
|
+
response.writeHead(upstream.status, { "content-type": upstream.headers.get("content-type") ?? "application/octet-stream" });
|
|
406
|
+
response.end(payload);
|
|
407
|
+
}
|
|
315
408
|
|
|
409
|
+
private notFound(response : ServerResponse) : void {
|
|
316
410
|
this.reply(response, 404, { error: "Not found" });
|
|
317
411
|
}
|
|
318
412
|
|