@cosmicdrift/kumiko-samples 0.355.0 → 0.357.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 +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
- package/packages/bundled-features/src/auth-mfa/i18n.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/screens.ts +8 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +7 -1
- package/packages/bundled-features/src/cap-overview/screens.ts +3 -1
- package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +18 -24
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +16 -1
- package/packages/bundled-features/src/custom-fields/handlers/define-system-field.write.ts +10 -17
- package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +5 -2
- package/packages/bundled-features/src/custom-fields/lib/define-or-resurrect.ts +12 -3
- package/packages/bundled-features/src/delivery/changes.json +6 -0
- package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +1 -1
- package/packages/bundled-features/src/delivery/constants.ts +5 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +118 -50
- package/packages/bundled-features/src/delivery/feature.ts +7 -1
- package/packages/bundled-features/src/delivery/jobs.ts +27 -1
- package/packages/bundled-features/src/delivery/testing.ts +1 -0
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +8 -25
- package/packages/bundled-features/src/notes-history/entity.ts +7 -6
- package/packages/bundled-features/src/notes-history-user-data/hooks.ts +109 -62
- package/packages/bundled-features/src/notes-history-user-data/index.ts +3 -1
- package/packages/bundled-features/src/personal-access-tokens/screens.ts +2 -0
- package/packages/bundled-features/src/sessions/screens.ts +2 -0
- package/packages/bundled-features/src/shared/index.ts +1 -0
- package/packages/bundled-features/src/shared/require-for-tenant.ts +18 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +6 -4
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +7 -7
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +7 -6
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +7 -4
- package/packages/bundled-features/src/tenant/cancel-pending-invitation.ts +3 -1
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/constants.ts +6 -3
- package/packages/bundled-features/src/tenant/feature.ts +2 -0
- package/packages/bundled-features/src/tenant/handlers/member-role-availability.query.ts +25 -0
- package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +22 -8
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +19 -11
- package/packages/bundled-features/src/tenant/member-role-options.ts +6 -0
- package/packages/bundled-features/src/tenant/screens.ts +9 -6
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +9 -23
- package/packages/bundled-features/src/user/screens.ts +8 -1
- package/packages/bundled-features/src/user-data-rights/screens.ts +8 -1
- package/packages/bundled-features/src/waitlist/screens.ts +8 -1
- package/samples/apps/marketing-demo/src/features/assets/schema/asset.ts +4 -4
- package/samples/apps/marketing-demo/src/features/helpdesk/schema/ticket.ts +4 -4
- package/samples/apps/showcase/src/features/items/schema/item.ts +1 -1
- package/samples/apps/styleguide/src/features/demo/feature.ts +3 -3
- package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +1 -1
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +6 -2
- package/samples/apps/ui-walkthrough/src/features/vehicles/screens.ts +9 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import type { Redis } from "ioredis";
|
|
5
5
|
// kumiko-lint-ignore cross-feature-import cancel needs invite-token-store for Redis cleanup
|
|
@@ -23,6 +23,7 @@ export async function cancelPendingInvitation(
|
|
|
23
23
|
invitation: { readonly id: string; readonly version: number },
|
|
24
24
|
actor: SessionUser,
|
|
25
25
|
redis: Redis | undefined,
|
|
26
|
+
streamTenantId?: TenantId,
|
|
26
27
|
): Promise<WriteFailure | undefined> {
|
|
27
28
|
const updateResult = await invitationExecutor.update(
|
|
28
29
|
{
|
|
@@ -32,6 +33,7 @@ export async function cancelPendingInvitation(
|
|
|
32
33
|
},
|
|
33
34
|
actor,
|
|
34
35
|
db,
|
|
36
|
+
{ streamTenantId },
|
|
35
37
|
);
|
|
36
38
|
if (!updateResult.isSuccess) return updateResult;
|
|
37
39
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.357.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "The member roles form only offers roles the signed-in admin may assign",
|
|
6
|
+
"detail": "New query tenant:query:member-role-availability; TenantAdmin still sees every role."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.354.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -26,9 +26,10 @@ export const MEMBER_ROLES_CELL_COMPONENT = "MemberRolesCell" as const;
|
|
|
26
26
|
// Admin (rank 2) shares the invite-create screen with TenantAdmin — TenantAdmin
|
|
27
27
|
// must not appear here or Admin sees an unassignable option (fw#2414).
|
|
28
28
|
export const DEFAULT_INVITE_ROLE_OPTIONS = ["User", "Editor", "Admin"] as const;
|
|
29
|
-
// Full
|
|
30
|
-
//
|
|
31
|
-
//
|
|
29
|
+
// Full rank list for memberRolesEditScreen: it is prefilled with a member's
|
|
30
|
+
// *current* roles, so it must represent every rank a membership can hold,
|
|
31
|
+
// including TenantAdmin (fw#2452). The screen hides what the caller may not
|
|
32
|
+
// assign via TenantQueries.memberRoleAvailability.
|
|
32
33
|
export const OWNER_INVITE_ROLE_OPTIONS = [
|
|
33
34
|
"User",
|
|
34
35
|
"Member",
|
|
@@ -67,6 +68,8 @@ export const TenantQueries = {
|
|
|
67
68
|
invitations: "tenant:query:invitations",
|
|
68
69
|
// Combined active-members + pending-invitations list backing /members.
|
|
69
70
|
teamList: "tenant:query:team:list",
|
|
71
|
+
// Per-role assignability for the caller, mirrors the rank rule of updateMemberRoles.
|
|
72
|
+
memberRoleAvailability: "tenant:query:member-role-availability",
|
|
70
73
|
} as const;
|
|
71
74
|
|
|
72
75
|
// Error codes
|
|
@@ -20,6 +20,7 @@ import { invitationsQuery } from "./handlers/invitations.query.js";
|
|
|
20
20
|
import { listQuery } from "./handlers/list.query.js";
|
|
21
21
|
import { meQuery } from "./handlers/me.query.js";
|
|
22
22
|
import { memberDirectoryQuery } from "./handlers/member-directory.query.js";
|
|
23
|
+
import { memberRoleAvailabilityQuery } from "./handlers/member-role-availability.query.js";
|
|
23
24
|
import { membersQuery } from "./handlers/members.query.js";
|
|
24
25
|
import { membershipsQuery } from "./handlers/memberships.query.js";
|
|
25
26
|
import { removeMemberWrite } from "./handlers/remove-member.write.js";
|
|
@@ -166,6 +167,7 @@ export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefi
|
|
|
166
167
|
resolveUserIds: r.queryHandler(resolveUserIdsQuery),
|
|
167
168
|
invitations: r.queryHandler(invitationsQuery),
|
|
168
169
|
teamList: r.queryHandler(teamListQuery),
|
|
170
|
+
memberRoleAvailability: r.queryHandler(memberRoleAvailabilityQuery),
|
|
169
171
|
};
|
|
170
172
|
|
|
171
173
|
// Entity-convention handlers for the SystemAdmin entityList/entityEdit
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
access,
|
|
3
|
+
assignableAppRolesOf,
|
|
4
|
+
canActorAssignRole,
|
|
5
|
+
defineQueryHandler,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import { memberRoleOptions } from "../member-role-options.js";
|
|
9
|
+
|
|
10
|
+
export const memberRoleAvailabilityQuery = defineQueryHandler({
|
|
11
|
+
name: "member-role-availability",
|
|
12
|
+
schema: z.object({}),
|
|
13
|
+
access: { roles: access.admin },
|
|
14
|
+
description:
|
|
15
|
+
"Lists every role the member roles form offers and whether the caller's own rank may assign it; the form hides the roles marked disabled.",
|
|
16
|
+
handler: async (query, ctx) => {
|
|
17
|
+
const assignableAppRoles = assignableAppRolesOf(ctx.registry);
|
|
18
|
+
return {
|
|
19
|
+
rows: memberRoleOptions(assignableAppRoles).map((value) => ({
|
|
20
|
+
value,
|
|
21
|
+
disabled: !canActorAssignRole(query.user.roles, value, assignableAppRoles),
|
|
22
|
+
})),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createSystemUser,
|
|
9
9
|
defineWriteHandler,
|
|
10
|
+
parseTenantId,
|
|
10
11
|
type SessionUser,
|
|
12
|
+
type TenantId,
|
|
11
13
|
withResponseData,
|
|
12
14
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
15
|
import {
|
|
@@ -34,6 +36,7 @@ type PendingInvitationCancel = {
|
|
|
34
36
|
readonly userId: string;
|
|
35
37
|
readonly tenantId: string;
|
|
36
38
|
readonly actor: SessionUser;
|
|
39
|
+
readonly streamTenantId: TenantId;
|
|
37
40
|
readonly redis: Redis | undefined;
|
|
38
41
|
};
|
|
39
42
|
|
|
@@ -51,7 +54,13 @@ async function cancelPendingInvitationsOfUser(
|
|
|
51
54
|
{ tenantId: options.tenantId, email, status: INVITATION_STATUS.pending },
|
|
52
55
|
);
|
|
53
56
|
for (const invitation of pendingInvitations) {
|
|
54
|
-
const failure = await cancelPendingInvitation(
|
|
57
|
+
const failure = await cancelPendingInvitation(
|
|
58
|
+
db,
|
|
59
|
+
invitation,
|
|
60
|
+
options.actor,
|
|
61
|
+
options.redis,
|
|
62
|
+
options.streamTenantId,
|
|
63
|
+
);
|
|
55
64
|
if (failure) return failure;
|
|
56
65
|
}
|
|
57
66
|
return undefined;
|
|
@@ -79,11 +88,15 @@ export const removeMemberWrite = defineWriteHandler({
|
|
|
79
88
|
const db = ctx.systemDb.acknowledgeCrossTenant(
|
|
80
89
|
"SystemAdmin manages memberships across tenants",
|
|
81
90
|
);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
const streamTenantId = parseTenantId(event.payload.tenantId);
|
|
92
|
+
const existing =
|
|
93
|
+
streamTenantId === null
|
|
94
|
+
? undefined
|
|
95
|
+
: await fetchOne(db, tenantMembershipsTable, {
|
|
96
|
+
userId: event.payload.userId,
|
|
97
|
+
tenantId: streamTenantId,
|
|
98
|
+
});
|
|
99
|
+
if (!existing || streamTenantId === null) {
|
|
87
100
|
return writeFailure(
|
|
88
101
|
new NotFoundError("membership", undefined, {
|
|
89
102
|
i18nKey: "tenant.errors.membershipNotFound",
|
|
@@ -125,14 +138,15 @@ export const removeMemberWrite = defineWriteHandler({
|
|
|
125
138
|
{ id: (existing as DbRow)["id"] as string }, // @cast-boundary db-row
|
|
126
139
|
event.user,
|
|
127
140
|
db,
|
|
141
|
+
{ streamTenantId },
|
|
128
142
|
);
|
|
129
143
|
if (!result.isSuccess) return result;
|
|
130
144
|
|
|
131
|
-
// Actor tenant = invitation tenant so the update hits the invitation's stream.
|
|
132
145
|
const cancelFailure = await cancelPendingInvitationsOfUser(db, {
|
|
133
146
|
userId: event.payload.userId,
|
|
134
147
|
tenantId: event.payload.tenantId,
|
|
135
|
-
actor:
|
|
148
|
+
actor: event.user,
|
|
149
|
+
streamTenantId,
|
|
136
150
|
redis: ctx.redis,
|
|
137
151
|
});
|
|
138
152
|
if (cancelFailure !== undefined) return cancelFailure;
|
|
@@ -6,8 +6,12 @@ import {
|
|
|
6
6
|
createSystemUser,
|
|
7
7
|
defineWriteHandler,
|
|
8
8
|
findForbiddenRoleAssignment,
|
|
9
|
+
mayOverrideTenant,
|
|
9
10
|
mergeAssignedRoles,
|
|
11
|
+
parseTenantId,
|
|
12
|
+
type SessionUser,
|
|
10
13
|
SYSTEM_ROLE,
|
|
14
|
+
type TenantId,
|
|
11
15
|
withResponseData,
|
|
12
16
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
17
|
import {
|
|
@@ -40,9 +44,14 @@ const REVOKE_ALL_SESSIONS_QN = "sessions:write:user-session:revoke-all-for-user"
|
|
|
40
44
|
function resolveTargetTenantId(
|
|
41
45
|
isSystem: boolean,
|
|
42
46
|
payloadTenantId: string | undefined,
|
|
43
|
-
sessionTenantId:
|
|
44
|
-
):
|
|
45
|
-
|
|
47
|
+
sessionTenantId: TenantId,
|
|
48
|
+
): TenantId | undefined {
|
|
49
|
+
if (!isSystem || payloadTenantId === undefined) return sessionTenantId;
|
|
50
|
+
return parseTenantId(payloadTenantId) ?? undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function foreignStreamTenant(user: SessionUser, targetTenantId: TenantId): TenantId | undefined {
|
|
54
|
+
return targetTenantId === user.tenantId ? undefined : targetTenantId;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
// Editing your OWN roles keeps the current session: roles are re-derived per
|
|
@@ -50,7 +59,7 @@ function resolveTargetTenantId(
|
|
|
50
59
|
// of that user and any foreign edit still revoke all.
|
|
51
60
|
function revokeSessionsPayload(
|
|
52
61
|
targetUserId: string,
|
|
53
|
-
targetTenantId:
|
|
62
|
+
targetTenantId: TenantId,
|
|
54
63
|
actor: { readonly id: string; readonly sid?: string | undefined },
|
|
55
64
|
): { userId: string; tenantId: string; exceptSessionId?: string } {
|
|
56
65
|
const payload = { userId: targetUserId, tenantId: targetTenantId };
|
|
@@ -82,7 +91,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
const isSystemActor = event.user.roles.includes(SYSTEM_ROLE);
|
|
85
|
-
const isSystem = event.user
|
|
94
|
+
const isSystem = mayOverrideTenant(event.user) || isSystemActor;
|
|
86
95
|
const assignableAppRoles = assignableAppRolesOf(ctx.registry);
|
|
87
96
|
const targetTenantId = resolveTargetTenantId(
|
|
88
97
|
isSystem,
|
|
@@ -185,19 +194,18 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
185
194
|
);
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// Keep the real actor for audit metadata; only override tenantId so
|
|
192
|
-
// streamTenantFor writes into the membership tenant (#2401).
|
|
197
|
+
// The membership stream lives on the membership's tenant (where seedTenantMembership/
|
|
198
|
+
// invite-accept wrote it); a cross-tenant operator addresses it without becoming a member.
|
|
199
|
+
// Same-tenant writes run on a tenant-mode db, which refuses a stream override.
|
|
193
200
|
const result = await executor.update(
|
|
194
201
|
{
|
|
195
202
|
id: row["id"] as string, // @cast-boundary db-row
|
|
196
203
|
version: row["version"] as number, // @cast-boundary db-row
|
|
197
204
|
changes: { roles: JSON.stringify(nextRoles) },
|
|
198
205
|
},
|
|
199
|
-
|
|
206
|
+
event.user,
|
|
200
207
|
db,
|
|
208
|
+
{ streamTenantId: foreignStreamTenant(event.user, targetTenantId) },
|
|
201
209
|
);
|
|
202
210
|
|
|
203
211
|
return withResponseData(result, {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AssignableAppRoles } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { OWNER_INVITE_ROLE_OPTIONS } from "./constants.js";
|
|
3
|
+
|
|
4
|
+
export function memberRoleOptions(assignableAppRoles: AssignableAppRoles): readonly string[] {
|
|
5
|
+
return [...OWNER_INVITE_ROLE_OPTIONS, ...assignableAppRoles.keys()];
|
|
6
|
+
}
|
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
MEMBER_ROLES_EDIT_SCREEN_ID,
|
|
16
16
|
MEMBER_STATUS_CELL_COMPONENT,
|
|
17
17
|
MEMBERS_SCREEN_ID,
|
|
18
|
-
OWNER_INVITE_ROLE_OPTIONS,
|
|
19
18
|
TenantHandlers,
|
|
20
19
|
TenantQueries,
|
|
21
20
|
} from "./constants.js";
|
|
21
|
+
import { memberRoleOptions } from "./member-role-options.js";
|
|
22
22
|
|
|
23
23
|
// Cross-tenant SystemAdmin platform view of the tenants themselves. The tenant
|
|
24
24
|
// feature runs with `r.systemScope()`, so the entityList returns every tenant.
|
|
@@ -107,6 +107,7 @@ export function createMembersScreen(options?: {
|
|
|
107
107
|
field: "lastSeenAt",
|
|
108
108
|
label: i18nKey("tenant.members.col.lastActivity"),
|
|
109
109
|
renderer: { format: "timestamp" },
|
|
110
|
+
hideOnNarrow: true,
|
|
110
111
|
},
|
|
111
112
|
],
|
|
112
113
|
defaultSort: { field: "createdAt", dir: "desc" },
|
|
@@ -199,13 +200,15 @@ export function createMemberRolesEditScreen(assignableAppRoles: AssignableAppRol
|
|
|
199
200
|
cancelTarget: MEMBERS_SCREEN_ID,
|
|
200
201
|
fields: {
|
|
201
202
|
userId: { type: "text", required: true },
|
|
202
|
-
// Prefilled with the member's current roles, so the
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
203
|
+
// Prefilled with the member's current roles, so the static options must
|
|
204
|
+
// represent every rank (DEFAULT_INVITE_ROLE_OPTIONS omits TenantAdmin and
|
|
205
|
+
// would silently strip it on submit); the availability query hides what the
|
|
206
|
+
// caller may not assign. Enforcement stays server-side via
|
|
207
|
+
// findForbiddenRoleAssignment (update-member-roles.write.ts).
|
|
206
208
|
roles: {
|
|
207
209
|
type: "multiSelect",
|
|
208
|
-
options:
|
|
210
|
+
options: memberRoleOptions(assignableAppRoles),
|
|
211
|
+
optionsAvailabilityQuery: TenantQueries.memberRoleAvailability,
|
|
209
212
|
required: true,
|
|
210
213
|
},
|
|
211
214
|
},
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import {
|
|
3
|
-
buildEntityTable,
|
|
4
|
-
createEventStoreExecutor,
|
|
5
|
-
createTenantDb,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
7
3
|
import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
4
|
import * as z from "zod";
|
|
5
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
9
6
|
import { tierAssignmentAggregateId } from "../aggregate-id.js";
|
|
10
7
|
import { TierAssignmentSources } from "../constants.js";
|
|
11
8
|
import { type TierAssignmentRow, tierAssignmentEntity } from "../entity.js";
|
|
@@ -14,13 +11,8 @@ import { type TierAssignmentRow, tierAssignmentEntity } from "../entity.js";
|
|
|
14
11
|
// Billing. Cross-tenant, daher SystemAdmin-only (kein TenantAdmin: sonst
|
|
15
12
|
// Gratis-Self-Upgrade).
|
|
16
13
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// der executor-user wird ebenfalls auf den Ziel-Tenant gestellt, sonst landet das
|
|
20
|
-
// Event im Stream des Admins (Memory feedback_event_store_tenant_consistency).
|
|
21
|
-
// Das set.write-"override-user"-Muster trägt NICHT für beliebige Tenants — es
|
|
22
|
-
// funktioniert nur für SYSTEM_TENANT_ID (immer im IN-Filter). Dies ist das
|
|
23
|
-
// auto-default-Hook-Muster (feature.ts), generalisiert auf einen Request-Handler.
|
|
14
|
+
// Cross-tenant mechanics: ctx.forTenant(tenantId) yields a tenant-mode db bound to the target plus
|
|
15
|
+
// the streamTenantId, so the event lands in the target's stream with the operator as actor.
|
|
24
16
|
//
|
|
25
17
|
// `source: TierAssignmentSources.manual` marks the grant so the billing sync skips it.
|
|
26
18
|
// Upsert: one aggregate per tenant (deterministic aggregate id).
|
|
@@ -38,8 +30,6 @@ const executor = createEventStoreExecutor(tierAssignmentTable, tierAssignmentEnt
|
|
|
38
30
|
entityName: "tier-assignment",
|
|
39
31
|
});
|
|
40
32
|
|
|
41
|
-
const SET_TENANT_TIER_REASON = "SystemAdmin assigns the tier of the tenant named in the payload";
|
|
42
|
-
|
|
43
33
|
export type SetTenantTierOptions = {
|
|
44
34
|
/** Runs after the write commits so feature.ts can update the resolver cache
|
|
45
35
|
* and notify the other processes. Without a tierMap there is no resolver
|
|
@@ -68,10 +58,6 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
|
|
|
68
58
|
}),
|
|
69
59
|
access: { roles: ["SystemAdmin"] },
|
|
70
60
|
agent: { risk: "high" },
|
|
71
|
-
escapeHatch: {
|
|
72
|
-
grants: ["unsafeRaw"],
|
|
73
|
-
reason: SET_TENANT_TIER_REASON,
|
|
74
|
-
},
|
|
75
61
|
handler: async (event, ctx) => {
|
|
76
62
|
// Without a commit sink (unit harness) there is no transaction to wait for.
|
|
77
63
|
const afterCommit = (tenantId: TenantId, tier: string): void => {
|
|
@@ -80,9 +66,7 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
|
|
|
80
66
|
else opts.onAssigned?.(tenantId, tier);
|
|
81
67
|
};
|
|
82
68
|
const tenantId = event.payload.tenantId as TenantId; // @cast-boundary engine-bridge
|
|
83
|
-
const
|
|
84
|
-
const tdb = createTenantDb(rawDb, tenantId, "system");
|
|
85
|
-
const systemUser = { ...event.user, tenantId };
|
|
69
|
+
const { db: tdb, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
86
70
|
const tier = event.payload.tier;
|
|
87
71
|
|
|
88
72
|
const existing = await fetchOne<TierAssignmentRow>(tdb, tierAssignmentTable, { tenantId });
|
|
@@ -94,8 +78,9 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
|
|
|
94
78
|
version: existing.version,
|
|
95
79
|
changes: { tier, source: TierAssignmentSources.manual },
|
|
96
80
|
},
|
|
97
|
-
|
|
81
|
+
event.user,
|
|
98
82
|
tdb,
|
|
83
|
+
{ streamTenantId },
|
|
99
84
|
);
|
|
100
85
|
if (!result.isSuccess) return result;
|
|
101
86
|
afterCommit(tenantId, tier);
|
|
@@ -109,8 +94,9 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
|
|
|
109
94
|
source: TierAssignmentSources.manual,
|
|
110
95
|
tenantId,
|
|
111
96
|
},
|
|
112
|
-
|
|
97
|
+
event.user,
|
|
113
98
|
tdb,
|
|
99
|
+
{ streamTenantId },
|
|
114
100
|
);
|
|
115
101
|
if (!result.isSuccess) return result;
|
|
116
102
|
afterCommit(tenantId, tier);
|
|
@@ -17,7 +17,14 @@ export const userListScreen: EntityListScreenDefinition = {
|
|
|
17
17
|
id: "user-list",
|
|
18
18
|
type: "entityList",
|
|
19
19
|
entity: "user",
|
|
20
|
-
columns: [
|
|
20
|
+
columns: [
|
|
21
|
+
"email",
|
|
22
|
+
"displayName",
|
|
23
|
+
"roles",
|
|
24
|
+
"tenants",
|
|
25
|
+
"status",
|
|
26
|
+
{ field: "emailVerified", hideOnNarrow: true },
|
|
27
|
+
],
|
|
21
28
|
rowActions: [
|
|
22
29
|
{
|
|
23
30
|
kind: "navigate",
|
|
@@ -68,7 +68,14 @@ export const downloadAttemptListScreen: EntityListScreenDefinition = {
|
|
|
68
68
|
id: "download-attempt-list",
|
|
69
69
|
type: "entityList",
|
|
70
70
|
entity: "download-attempt",
|
|
71
|
-
columns: [
|
|
71
|
+
columns: [
|
|
72
|
+
"attemptedAt",
|
|
73
|
+
"result",
|
|
74
|
+
"via",
|
|
75
|
+
"ip",
|
|
76
|
+
{ field: "attemptedByUserId", hideOnNarrow: true },
|
|
77
|
+
{ field: "jobId", hideOnNarrow: true },
|
|
78
|
+
],
|
|
72
79
|
searchable: false,
|
|
73
80
|
access: { roles: access.systemAdmin },
|
|
74
81
|
// Inert until an app navs it (see file header) — no nav area to resolve
|
|
@@ -6,7 +6,14 @@ export const waitlistListScreen: EntityListScreenDefinition = {
|
|
|
6
6
|
id: "waitlist-list",
|
|
7
7
|
type: "entityList",
|
|
8
8
|
entity: "waitlistEntry",
|
|
9
|
-
columns: [
|
|
9
|
+
columns: [
|
|
10
|
+
"name",
|
|
11
|
+
"email",
|
|
12
|
+
"company",
|
|
13
|
+
"status",
|
|
14
|
+
"locale",
|
|
15
|
+
{ field: "submittedAt", hideOnNarrow: true },
|
|
16
|
+
],
|
|
10
17
|
defaultSort: { field: "submittedAt", dir: "desc" },
|
|
11
18
|
access: adminAccess,
|
|
12
19
|
searchable: true,
|
|
@@ -116,10 +116,10 @@ export const assetListScreen: EntityListScreenDefinition = {
|
|
|
116
116
|
"status",
|
|
117
117
|
"department",
|
|
118
118
|
"owner",
|
|
119
|
-
"location",
|
|
120
|
-
"vendor",
|
|
121
|
-
"purchaseDate",
|
|
122
|
-
"warrantyUntil",
|
|
119
|
+
{ field: "location", hideOnNarrow: true },
|
|
120
|
+
{ field: "vendor", hideOnNarrow: true },
|
|
121
|
+
{ field: "purchaseDate", hideOnNarrow: true },
|
|
122
|
+
{ field: "warrantyUntil", hideOnNarrow: true },
|
|
123
123
|
],
|
|
124
124
|
pageSize: 25,
|
|
125
125
|
defaultSort: { field: "name", dir: "asc" },
|
|
@@ -123,10 +123,10 @@ export const ticketListScreen: EntityListScreenDefinition = {
|
|
|
123
123
|
"severity",
|
|
124
124
|
"status",
|
|
125
125
|
"department",
|
|
126
|
-
"assignee",
|
|
127
|
-
"reporter",
|
|
128
|
-
"dueDate",
|
|
129
|
-
"spentMinutes",
|
|
126
|
+
{ field: "assignee", hideOnNarrow: true },
|
|
127
|
+
{ field: "reporter", hideOnNarrow: true },
|
|
128
|
+
{ field: "dueDate", hideOnNarrow: true },
|
|
129
|
+
{ field: "spentMinutes", hideOnNarrow: true },
|
|
130
130
|
],
|
|
131
131
|
pageSize: 25,
|
|
132
132
|
defaultSort: { field: "severity", dir: "desc" },
|
|
@@ -102,7 +102,7 @@ export const itemListScreen: EntityListScreenDefinition = {
|
|
|
102
102
|
"dueDate",
|
|
103
103
|
// Tier 2.7e-4 Demo — Reference-Spalte. Renderer macht Bulk-Lookup
|
|
104
104
|
// auf item:list (limit:200), zeigt parentId als parent.title.
|
|
105
|
-
"parentId",
|
|
105
|
+
{ field: "parentId", hideOnNarrow: true },
|
|
106
106
|
],
|
|
107
107
|
// Server-side Pagination Demo — Showcase seedet ~200 items, der
|
|
108
108
|
// Pager hat 4 Seiten zum Durchklicken bei pageSize: 50.
|
|
@@ -228,9 +228,9 @@ export const demoFeature = defineFeature("styleguide", (r) => {
|
|
|
228
228
|
"status",
|
|
229
229
|
"isActive",
|
|
230
230
|
"quantity",
|
|
231
|
-
"rating",
|
|
232
|
-
"publishedAt",
|
|
233
|
-
"price",
|
|
231
|
+
{ field: "rating", hideOnNarrow: true },
|
|
232
|
+
{ field: "publishedAt", hideOnNarrow: true },
|
|
233
|
+
{ field: "price", hideOnNarrow: true },
|
|
234
234
|
],
|
|
235
235
|
pagination: "pages",
|
|
236
236
|
pageSize: 25,
|
|
@@ -331,7 +331,7 @@ export function Gallery(): ReactNode {
|
|
|
331
331
|
{/* Standalone <Section> (outside Form) = the calculator-result-card
|
|
332
332
|
standard. title-only = existing consumer (publicstatus/tier-admin),
|
|
333
333
|
must NOT regress from the border-b removal; subtitle +
|
|
334
|
-
actions
|
|
334
|
+
actions (title row, top right) = new. No divider under the title (shadcn pattern). */}
|
|
335
335
|
<div className="flex max-w-md flex-col gap-6">
|
|
336
336
|
<Section title="Affordable rate / month">
|
|
337
337
|
<div className="text-2xl font-semibold tabular-nums">€2,500.00</div>
|
|
@@ -27,7 +27,7 @@ const OPEN_FILL_COLUMNS = [
|
|
|
27
27
|
"einheit",
|
|
28
28
|
"liegenschaft",
|
|
29
29
|
"beginn",
|
|
30
|
-
"ende",
|
|
30
|
+
{ field: "ende", hideOnNarrow: true },
|
|
31
31
|
"status",
|
|
32
32
|
] as const;
|
|
33
33
|
|
|
@@ -145,7 +145,11 @@ const POSITION_COLUMNS = [
|
|
|
145
145
|
label: "rental:entity:leasePosition:field:gueltigVon",
|
|
146
146
|
sortable: true,
|
|
147
147
|
},
|
|
148
|
-
{
|
|
148
|
+
{
|
|
149
|
+
field: "gueltigBis",
|
|
150
|
+
label: "rental:entity:leasePosition:field:gueltigBis",
|
|
151
|
+
hideOnNarrow: true,
|
|
152
|
+
},
|
|
149
153
|
] as const;
|
|
150
154
|
|
|
151
155
|
export const leaseDetailScreen: ProjectionDetailScreenDefinition = {
|
|
@@ -31,7 +31,14 @@ export const vehicleListScreen: EntityListScreenDefinition = {
|
|
|
31
31
|
id: "vehicle-list",
|
|
32
32
|
type: "entityList",
|
|
33
33
|
entity: "vehicle",
|
|
34
|
-
columns: [
|
|
34
|
+
columns: [
|
|
35
|
+
"marke",
|
|
36
|
+
"modell",
|
|
37
|
+
"baujahr",
|
|
38
|
+
"preis",
|
|
39
|
+
"zustand",
|
|
40
|
+
{ field: "scheckheftGepflegt", hideOnNarrow: true },
|
|
41
|
+
],
|
|
35
42
|
searchPlaceholder: "vehicles.search.placeholder",
|
|
36
43
|
defaultSort: { field: "marke", dir: "asc" },
|
|
37
44
|
rowActions: [
|
|
@@ -129,7 +136,7 @@ export const campaignListScreen: EntityListScreenDefinition = {
|
|
|
129
136
|
"aktionStartet",
|
|
130
137
|
"aktionEndet",
|
|
131
138
|
"meta",
|
|
132
|
-
"gepostet",
|
|
139
|
+
{ field: "gepostet", hideOnNarrow: true },
|
|
133
140
|
{ field: "gestartetAm", hideOnNarrow: true },
|
|
134
141
|
],
|
|
135
142
|
searchPlaceholder: "vehicles.search.placeholder",
|
|
@@ -1954,7 +1954,7 @@
|
|
|
1954
1954
|
},
|
|
1955
1955
|
{
|
|
1956
1956
|
"name": "notes-history-user-data",
|
|
1957
|
-
"description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `notes-history` feature's `note-entry` and `note-mention` entities. Mounts the export hook so a user's authored notes are included in the user-data export bundle; the note-entry delete hook
|
|
1957
|
+
"description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `notes-history` feature's `note-entry` and `note-mention` entities. Mounts the export hook so a user's authored notes are included in the user-data export bundle; the note-entry delete hook crypto-shreds the row-subject key of each note that structurally @-mentions the forgotten user (`note-mention`) or hangs on the user as host entity (after consulting that note's host entity's retention strategy — blockDelete/anonymize win over erasure), keeping the append-only history intact for all other notes. The manual `crypto-shredding` forget-subject command reaches the same notes for user and record subjects through the exported `eraseNoteBodiesReachingSubject`, and the export lists them under `note-mention`. `note-mention`'s own hooks are a no-op — it is a plain FK pointer, not separately exportable content. Kept separate from `notes-history` so notes consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `notes-history`.",
|
|
1958
1958
|
"toggleableDefault": null,
|
|
1959
1959
|
"requires": [
|
|
1960
1960
|
"user-data-rights"
|