@cosmicdrift/kumiko-bundled-features 0.125.2 → 0.127.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 -6
- package/src/audit/__tests__/audit-security.integration.test.ts +50 -0
- package/src/audit/i18n.ts +9 -0
- package/src/audit/web/audit-log-screen.tsx +112 -2
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +206 -2
- package/src/billing-foundation/entities.ts +28 -4
- package/src/billing-foundation/events.ts +6 -2
- package/src/billing-foundation/feature.ts +9 -1
- package/src/billing-foundation/get-subscription-for-tenant.ts +16 -5
- package/src/billing-foundation/handlers/create-portal-session.write.ts +17 -1
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +15 -1
- package/src/billing-foundation/handlers/process-event.write.ts +39 -3
- package/src/billing-foundation/tenant-destroy-hook.ts +47 -0
- package/src/cap-counter/entity.ts +1 -1
- package/src/cap-counter/feature.ts +10 -2
- package/src/cap-counter/i18n.ts +12 -0
- package/src/compliance-profiles/feature.ts +3 -0
- package/src/compliance-profiles/i18n.ts +6 -0
- package/src/config/feature.ts +3 -0
- package/src/config/i18n.ts +9 -0
- package/src/delivery/feature.ts +3 -0
- package/src/delivery/i18n.ts +6 -0
- package/src/feature-toggles/feature.ts +3 -0
- package/src/feature-toggles/i18n.ts +6 -0
- package/src/jobs/i18n.ts +4 -0
- package/src/jobs/web/job-runs-screen.tsx +9 -9
- package/src/managed-pages/i18n.ts +5 -0
- package/src/managed-pages/screens/page-screens.ts +5 -1
- package/src/personal-access-tokens/feature.ts +2 -0
- package/src/personal-access-tokens/i18n.ts +5 -0
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +7 -3
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.test.ts +12 -1
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +9 -0
- package/src/subscription-stripe/feature.ts +2 -0
- package/src/tags/feature.ts +2 -0
- package/src/tags/i18n.ts +5 -0
- package/src/tenant/__tests__/members-screens.boot.test.ts +7 -0
- package/src/tenant/__tests__/tenant-security.integration.test.ts +20 -5
- package/src/tenant/feature.ts +4 -1
- package/src/tenant/handlers/members.query.ts +19 -4
- package/src/tenant/i18n.ts +28 -0
- package/src/tenant/index.ts +7 -2
- package/src/tenant/schema/tenant.ts +27 -2
- package/src/tenant/screens.ts +3 -2
- package/src/tenant/web/members-screen.tsx +8 -3
- package/src/tenant-lifecycle/__tests__/stages.test.ts +22 -0
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +469 -0
- package/src/tenant-lifecycle/constants.ts +56 -0
- package/src/tenant-lifecycle/events.ts +46 -0
- package/src/tenant-lifecycle/feature.ts +105 -0
- package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +81 -0
- package/src/tenant-lifecycle/handlers/request-destruction.write.ts +83 -0
- package/src/tenant-lifecycle/index.ts +7 -0
- package/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +16 -0
- package/src/tenant-lifecycle/lifecycle-gate.ts +58 -0
- package/src/tenant-lifecycle/run-tenant-destroy.ts +368 -0
- package/src/tenant-lifecycle/stages.ts +206 -0
- package/src/tier-engine/feature.ts +2 -0
- package/src/tier-engine/i18n.ts +8 -0
- package/src/tier-engine/web/tier-admin-screen.tsx +20 -14
- package/src/user/feature.ts +3 -0
- package/src/user/i18n.ts +20 -0
- package/src/user/schema/user.ts +1 -0
- package/src/user/screens.ts +3 -15
- package/src/user-data-rights/feature.ts +17 -0
- package/src/user-data-rights/i18n.ts +32 -33
- package/src/user-data-rights/schema/export-job.ts +2 -0
- package/src/user-data-rights/screens.ts +2 -1
- package/src/user-data-rights/web/i18n.ts +9 -0
package/src/tenant/feature.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { resolveUserIdsQuery } from "./handlers/resolve-user-ids.query";
|
|
|
23
23
|
import { disableWrite, enableWrite } from "./handlers/toggle-enabled.write";
|
|
24
24
|
import { updateWrite } from "./handlers/update.write";
|
|
25
25
|
import { updateMemberRolesWrite } from "./handlers/update-member-roles.write";
|
|
26
|
+
import { TENANT_I18N } from "./i18n";
|
|
26
27
|
import { tenantInvitationEntity } from "./invitation-table";
|
|
27
28
|
import { tenantMembershipEntity } from "./membership-table";
|
|
28
29
|
import { tenantEntity } from "./schema/tenant";
|
|
@@ -145,12 +146,14 @@ export function createTenantFeature(): FeatureDefinition {
|
|
|
145
146
|
});
|
|
146
147
|
r.nav({
|
|
147
148
|
id: "members",
|
|
148
|
-
label: "tenant
|
|
149
|
+
label: "tenant.nav.members",
|
|
149
150
|
icon: "users",
|
|
150
151
|
screen: "tenant:screen:members",
|
|
151
152
|
order: 20,
|
|
152
153
|
});
|
|
153
154
|
|
|
155
|
+
r.translations({ keys: TENANT_I18N });
|
|
156
|
+
|
|
154
157
|
return { handlers, queries };
|
|
155
158
|
});
|
|
156
159
|
}
|
|
@@ -2,8 +2,11 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { userTable } from "../../user";
|
|
5
6
|
import { tenantMembershipsTable } from "../membership-table";
|
|
6
7
|
|
|
8
|
+
type UserRow = { readonly id: unknown; readonly email?: unknown; readonly displayName?: unknown };
|
|
9
|
+
|
|
7
10
|
export const membersQuery = defineQueryHandler({
|
|
8
11
|
name: "members",
|
|
9
12
|
schema: z.object({}),
|
|
@@ -13,9 +16,21 @@ export const membersQuery = defineQueryHandler({
|
|
|
13
16
|
tenantId: query.user.tenantId,
|
|
14
17
|
});
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const userIds = [...new Set(rows.map((row) => row["userId"]))];
|
|
20
|
+
const users =
|
|
21
|
+
userIds.length > 0 ? await selectMany<UserRow>(ctx.db, userTable, { id: userIds }) : [];
|
|
22
|
+
const userById = new Map(users.map((u) => [String(u.id), u]));
|
|
23
|
+
|
|
24
|
+
return rows.map((row) => {
|
|
25
|
+
const user = userById.get(String(row["userId"]));
|
|
26
|
+
const email = typeof user?.email === "string" ? user.email : null;
|
|
27
|
+
const displayName = typeof user?.displayName === "string" ? user.displayName : null;
|
|
28
|
+
return {
|
|
29
|
+
...row,
|
|
30
|
+
email,
|
|
31
|
+
displayName,
|
|
32
|
+
roles: parseRoles(row["roles"]),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
20
35
|
},
|
|
21
36
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type LocalizedString = { readonly de: string; readonly en: string };
|
|
2
|
+
|
|
3
|
+
export const TENANT_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
4
|
+
"screen:tenant-list.title": { de: "Mandanten", en: "Tenants" },
|
|
5
|
+
"screen:tenant-edit.title": { de: "Mandant bearbeiten", en: "Edit tenant" },
|
|
6
|
+
"screen:members.title": { de: "Team", en: "Team" },
|
|
7
|
+
"tenant.nav.members": { de: "Team", en: "Team" },
|
|
8
|
+
"tenant:entity:tenant:field:key": { de: "Schlüssel", en: "Key" },
|
|
9
|
+
"tenant:entity:tenant:field:name": { de: "Name", en: "Name" },
|
|
10
|
+
"tenant:entity:tenant:field:isEnabled": { de: "Aktiv", en: "Enabled" },
|
|
11
|
+
"tenant:entity:tenant:field:status": { de: "Status", en: "Status" },
|
|
12
|
+
"tenant:entity:tenant:field:status:option:active": { de: "Aktiv", en: "Active" },
|
|
13
|
+
"tenant:entity:tenant:field:status:option:destroyRequested": {
|
|
14
|
+
de: "Löschung angefordert",
|
|
15
|
+
en: "Destroy requested",
|
|
16
|
+
},
|
|
17
|
+
"tenant:entity:tenant:field:status:option:destroying": {
|
|
18
|
+
de: "Wird gelöscht",
|
|
19
|
+
en: "Destroying",
|
|
20
|
+
},
|
|
21
|
+
"tenant:entity:tenant:field:status:option:destroyFailed": {
|
|
22
|
+
de: "Löschung fehlgeschlagen",
|
|
23
|
+
en: "Destroy failed",
|
|
24
|
+
},
|
|
25
|
+
"tenant:entity:tenant:field:status:option:destroyed": { de: "Gelöscht", en: "Destroyed" },
|
|
26
|
+
"tenant:entity:tenant:field:isEnabled:option:true": { de: "Ja", en: "Yes" },
|
|
27
|
+
"tenant:entity:tenant:field:isEnabled:option:false": { de: "Nein", en: "No" },
|
|
28
|
+
};
|
package/src/tenant/index.ts
CHANGED
|
@@ -8,5 +8,10 @@ export {
|
|
|
8
8
|
tenantInvitationEntity,
|
|
9
9
|
tenantInvitationsTable,
|
|
10
10
|
} from "./invitation-table";
|
|
11
|
-
export { tenantMembershipsTable } from "./membership-table";
|
|
12
|
-
export {
|
|
11
|
+
export { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table";
|
|
12
|
+
export {
|
|
13
|
+
TENANT_LIFECYCLE_STATUSES,
|
|
14
|
+
type TenantLifecycleStatus,
|
|
15
|
+
tenantEntity,
|
|
16
|
+
tenantTable,
|
|
17
|
+
} from "./schema/tenant";
|
|
@@ -2,9 +2,21 @@ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import {
|
|
3
3
|
createBooleanField,
|
|
4
4
|
createEntity,
|
|
5
|
+
createSelectField,
|
|
5
6
|
createTextField,
|
|
7
|
+
createTimestampField,
|
|
6
8
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
9
|
|
|
10
|
+
export const TENANT_LIFECYCLE_STATUSES = [
|
|
11
|
+
"active",
|
|
12
|
+
"destroyRequested",
|
|
13
|
+
"destroying",
|
|
14
|
+
"destroyFailed",
|
|
15
|
+
"destroyed",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type TenantLifecycleStatus = (typeof TENANT_LIFECYCLE_STATUSES)[number];
|
|
19
|
+
|
|
8
20
|
export const tenantEntity = createEntity({
|
|
9
21
|
table: "read_tenants",
|
|
10
22
|
// tenant.id IS the tenantId-value that every other table references as FK.
|
|
@@ -12,8 +24,21 @@ export const tenantEntity = createEntity({
|
|
|
12
24
|
// UUID sein, sonst findet der tenants-Lookup nie. Default gen_random_uuid().
|
|
13
25
|
fields: {
|
|
14
26
|
key: createTextField({ required: true, maxLength: 50 }),
|
|
15
|
-
name: createTextField({ required: true, maxLength: 200, searchable: true }),
|
|
16
|
-
isEnabled: createBooleanField({ default: true }),
|
|
27
|
+
name: createTextField({ required: true, maxLength: 200, searchable: true, sortable: true }),
|
|
28
|
+
isEnabled: createBooleanField({ default: true, filterable: true }),
|
|
29
|
+
// Tenant-destroy lifecycle (tenant-lifecycle feature). Defaults keep
|
|
30
|
+
// existing tenants valid when the feature is not mounted.
|
|
31
|
+
status: createSelectField({
|
|
32
|
+
options: TENANT_LIFECYCLE_STATUSES,
|
|
33
|
+
default: "active",
|
|
34
|
+
required: true,
|
|
35
|
+
filterable: true,
|
|
36
|
+
}),
|
|
37
|
+
destroyRequestedAt: createTimestampField({}),
|
|
38
|
+
destroyRequestedBy: createTextField({ maxLength: 36 }),
|
|
39
|
+
gracePeriodEnd: createTimestampField({}),
|
|
40
|
+
destroyStartedAt: createTimestampField({}),
|
|
41
|
+
destroyedAt: createTimestampField({}),
|
|
17
42
|
},
|
|
18
43
|
// tenant.key wird in Admin-URLs verwendet (`admin.<host>/<key>/...`) und
|
|
19
44
|
// muss eindeutig sein. Ohne unique-constraint hätte ein konkurrenter
|
package/src/tenant/screens.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const tenantListScreen: EntityListScreenDefinition = {
|
|
|
16
16
|
id: "tenant-list",
|
|
17
17
|
type: "entityList",
|
|
18
18
|
entity: "tenant",
|
|
19
|
-
columns: ["key", "name", "isEnabled"],
|
|
19
|
+
columns: ["key", "name", "status", "isEnabled"],
|
|
20
20
|
rowActions: [
|
|
21
21
|
{
|
|
22
22
|
kind: "navigate",
|
|
@@ -26,7 +26,8 @@ export const tenantListScreen: EntityListScreenDefinition = {
|
|
|
26
26
|
entityId: "id",
|
|
27
27
|
},
|
|
28
28
|
],
|
|
29
|
-
|
|
29
|
+
defaultSort: { field: "name", dir: "asc" },
|
|
30
|
+
searchable: true,
|
|
30
31
|
access: { roles: ["SystemAdmin"] },
|
|
31
32
|
};
|
|
32
33
|
|
|
@@ -12,6 +12,8 @@ type MemberRow = {
|
|
|
12
12
|
readonly id: string;
|
|
13
13
|
readonly userId: string;
|
|
14
14
|
readonly tenantId: string;
|
|
15
|
+
readonly email: string | null;
|
|
16
|
+
readonly displayName: string | null;
|
|
15
17
|
readonly roles: readonly string[];
|
|
16
18
|
};
|
|
17
19
|
|
|
@@ -131,8 +133,8 @@ export function MembersScreen({
|
|
|
131
133
|
testId="members-active-table"
|
|
132
134
|
columns={[
|
|
133
135
|
{
|
|
134
|
-
field: "
|
|
135
|
-
label: t("tenant.members.col.
|
|
136
|
+
field: "email",
|
|
137
|
+
label: t("tenant.members.col.email"),
|
|
136
138
|
type: "string",
|
|
137
139
|
sortable: false,
|
|
138
140
|
},
|
|
@@ -145,7 +147,10 @@ export function MembersScreen({
|
|
|
145
147
|
]}
|
|
146
148
|
rows={state.members.map((m) => ({
|
|
147
149
|
id: m.id,
|
|
148
|
-
values: {
|
|
150
|
+
values: {
|
|
151
|
+
email: m.email ?? `${m.userId.slice(0, 8)}…`,
|
|
152
|
+
roles: m.roles.join(", "),
|
|
153
|
+
},
|
|
149
154
|
}))}
|
|
150
155
|
/>
|
|
151
156
|
</Card>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { DESTRUCTION_STAGES, isDestructionPipelineComplete, pickNextStage } from "../stages";
|
|
3
|
+
|
|
4
|
+
describe("tenant-lifecycle stages", () => {
|
|
5
|
+
test("pickNextStage halts when any stage was abandoned", () => {
|
|
6
|
+
const completed = new Set(["external-resources", "search-indices"]);
|
|
7
|
+
const abandoned = new Set(["app-data"]);
|
|
8
|
+
expect(pickNextStage(completed, abandoned)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("pickNextStage returns first incomplete stage when healthy", () => {
|
|
12
|
+
const completed = new Set(["external-resources"]);
|
|
13
|
+
expect(pickNextStage(completed, new Set())?.name).toBe("search-indices");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("isDestructionPipelineComplete requires every stage", () => {
|
|
17
|
+
const partial = new Set(DESTRUCTION_STAGES.slice(0, 3).map((s) => s.name));
|
|
18
|
+
expect(isDestructionPipelineComplete(partial)).toBe(false);
|
|
19
|
+
const all = new Set(DESTRUCTION_STAGES.map((s) => s.name));
|
|
20
|
+
expect(isDestructionPipelineComplete(all)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|