anbaric-cloud-hosting 1.60.0 → 1.61.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 +7 -7
- package/src/app-management/BaseBuildLayer.ts +3 -0
- package/src/app-management/BuildLayer.ts +6 -0
- package/src/app-management/appAddress.ts +19 -0
- package/src/app-management/appHostname.ts +16 -23
- package/src/auth/HttpMembershipService.ts +2 -2
- package/src/auth/InMemoryMembershipService.ts +2 -1
- package/src/auth/MembershipService.ts +5 -1
- package/src/data-store/EntitlementStore.ts +23 -1
- package/src/data-store/InMemoryEntitlementStore.ts +25 -1
- package/src/data-store/PlatformMigrations.ts +19 -0
- package/src/data-store/PostgresEntitlementStore.ts +40 -1
- package/src/hosting/HostingServer.ts +26 -4
- package/src/hosting/handlers/AppsHandler.ts +65 -6
- package/src/hosting/handlers/MyAppsHandler.ts +48 -0
- package/src/hosting/handlers/auth/InvitationsHandler.ts +44 -4
- package/src/hosting/middleware/AuthenticationMiddleware.ts +20 -5
- package/src/hosting/pages/platform-ui.html +3 -3
- package/src/main.ts +34 -1
- package/src/subdomains/HttpSubdomains.ts +91 -0
- package/src/subdomains/Subdomains.ts +30 -0
- package/src/usage/UsageReporter.ts +63 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {MembershipService} from "../../../auth/MembershipService";
|
|
2
|
+
import {EntitlementOffer, EntitlementStore} from "../../../data-store/EntitlementStore";
|
|
2
3
|
import {canInvite, isTenantRole} from "../../../auth/TenantRole";
|
|
3
4
|
import {Request} from "../../Request";
|
|
4
5
|
import {RequestHandler} from "../../RequestHandler";
|
|
@@ -6,10 +7,17 @@ import {RequestHandler} from "../../RequestHandler";
|
|
|
6
7
|
const EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
7
8
|
|
|
8
9
|
/* The console's side of inviting people into this tenant: list what is
|
|
9
|
-
pending, invite by email (attributed to the signed-in user), revoke.
|
|
10
|
+
pending, invite by email (attributed to the signed-in user), revoke.
|
|
11
|
+
|
|
12
|
+
An invitation may also carry the app it should open on and the entitlements
|
|
13
|
+
the inviter picked out for whoever accepts it. The two are kept apart on
|
|
14
|
+
purpose: the landing app travels to the control plane, which owns the
|
|
15
|
+
invitation and the addresses, while the entitlements stay here, because
|
|
16
|
+
grants are the platform's and are held against the email address until
|
|
17
|
+
that person first signs in. */
|
|
10
18
|
class InvitationsHandler implements RequestHandler {
|
|
11
19
|
|
|
12
|
-
constructor(private memberships : MembershipService) {}
|
|
20
|
+
constructor(private memberships : MembershipService, private entitlements? : EntitlementStore) {}
|
|
13
21
|
|
|
14
22
|
async handle(request : Request) : Promise<void> {
|
|
15
23
|
// Only a tenant's owner or admin administers its membership. Central
|
|
@@ -34,7 +42,8 @@ class InvitationsHandler implements RequestHandler {
|
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
private async invite(request : Request) : Promise<void> {
|
|
37
|
-
const body = await request.body() as
|
|
45
|
+
const body = await request.body() as
|
|
46
|
+
{ email? : string, role? : string, landingApp? : string, entitlements? : Array<unknown> } | undefined;
|
|
38
47
|
const email = String(body?.email ?? "").trim();
|
|
39
48
|
if (! EMAIL_SHAPE.test(email)) return request.reply(400, { error: "A valid email address is required" });
|
|
40
49
|
|
|
@@ -43,7 +52,38 @@ class InvitationsHandler implements RequestHandler {
|
|
|
43
52
|
return request.reply(400, { error: "Invite someone as an ADMIN, BUILDER or USER" });
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
const landingApp = typeof body?.landingApp === "string" && body.landingApp.trim()
|
|
56
|
+
? body.landingApp.trim()
|
|
57
|
+
: undefined;
|
|
58
|
+
|
|
59
|
+
const invitation = await this.memberships.invite(email, role, this.asking(request), landingApp);
|
|
60
|
+
|
|
61
|
+
/* Put aside after the invitation exists, so nothing is held for an
|
|
62
|
+
email that was never actually invited. */
|
|
63
|
+
const chosen = this.chosenEntitlements(email, body?.entitlements);
|
|
64
|
+
if (chosen.length > 0 && this.entitlements) {
|
|
65
|
+
await this.entitlements.offer(chosen, request.user?.id ?? "system");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
request.reply(201, { ...invitation, entitlements: chosen.length });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Each is an app and an entitlement, with a null app meaning every app -
|
|
72
|
+
// the same scoping a grant already has.
|
|
73
|
+
private chosenEntitlements(email : string, chosen : Array<unknown> | undefined) : Array<EntitlementOffer> {
|
|
74
|
+
if (! Array.isArray(chosen)) return [];
|
|
75
|
+
|
|
76
|
+
return chosen.flatMap(each => {
|
|
77
|
+
const offer = each as { appId? : unknown, entitlementId? : unknown };
|
|
78
|
+
if (typeof offer?.entitlementId !== "string" || ! offer.entitlementId) return [];
|
|
79
|
+
|
|
80
|
+
return [{
|
|
81
|
+
email,
|
|
82
|
+
appId: typeof offer.appId === "string" && offer.appId ? offer.appId : null,
|
|
83
|
+
entitlementId: offer.entitlementId,
|
|
84
|
+
notes: "",
|
|
85
|
+
}];
|
|
86
|
+
});
|
|
47
87
|
}
|
|
48
88
|
|
|
49
89
|
}
|
|
@@ -23,6 +23,14 @@ class AuthenticationMiddleware implements Middleware {
|
|
|
23
23
|
|
|
24
24
|
private recorded = new Map<string, number>();
|
|
25
25
|
|
|
26
|
+
/* Anything owed to this person that could only be arranged against their
|
|
27
|
+
email address, because until now they had no user id - entitlements an
|
|
28
|
+
inviter chose for them before they ever arrived. Injected rather than
|
|
29
|
+
named here: the middleware's business is who someone is, not what they
|
|
30
|
+
are owed. Called where the directory first records them, which is that
|
|
31
|
+
moment, and safe to call again because claiming twice claims nothing. */
|
|
32
|
+
claimWhatIsOwed : (user : User) => Promise<void> = async () => {};
|
|
33
|
+
|
|
26
34
|
constructor(private authenticator? : Authenticator,
|
|
27
35
|
private tokenAuthenticator? : TokenAuthenticator,
|
|
28
36
|
private isOpen : OpenRequestPredicate = () => false,
|
|
@@ -96,18 +104,25 @@ class AuthenticationMiddleware implements Middleware {
|
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
private async remember(user : User) : Promise<void> {
|
|
99
|
-
if (! this.userDirectory) return;
|
|
100
|
-
|
|
101
107
|
const now = Date.now();
|
|
102
108
|
const last = this.recorded.get(user.id) ?? 0;
|
|
103
109
|
if (now - last < DIRECTORY_REFRESH_MS) return;
|
|
104
110
|
|
|
105
111
|
this.recorded.set(user.id, now);
|
|
112
|
+
|
|
113
|
+
if (this.userDirectory) {
|
|
114
|
+
try {
|
|
115
|
+
await this.userDirectory.record(user);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
this.recorded.delete(user.id);
|
|
118
|
+
console.error(`Could not record user "${user.id}" in the directory:`, error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
try {
|
|
107
|
-
await this.
|
|
123
|
+
await this.claimWhatIsOwed(user);
|
|
108
124
|
} catch (error) {
|
|
109
|
-
|
|
110
|
-
console.error(`Could not record user "${user.id}" in the directory:`, error);
|
|
125
|
+
console.error(`Could not claim what is owed to "${user.id}":`, error);
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
128
|
|