@cosmicdrift/kumiko-bundled-features 0.240.0 → 0.242.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 +9 -9
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +16 -7
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +30 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +31 -23
- package/src/admin-shell/changes.json +6 -0
- package/src/admin-shell/feature.ts +60 -6
- package/src/admin-shell/i18n.ts +0 -7
- package/src/admin-shell/web/client-plugin.tsx +1 -11
- package/src/agent-tools/__tests__/agent-doc-lint.test.ts +40 -0
- package/src/agent-tools/__tests__/agent-manifest.test.ts +36 -0
- package/src/agent-tools/__tests__/tool-catalog.test.ts +51 -0
- package/src/agent-tools/agent-doc-lint.ts +14 -3
- package/src/agent-tools/agent-manifest.ts +16 -3
- package/src/agent-tools/feature.ts +1 -1
- package/src/audit/__tests__/audit-screens.boot.test.ts +26 -6
- package/src/audit/changes.json +7 -0
- package/src/audit/feature.ts +57 -7
- package/src/audit/handlers/list.query.ts +50 -14
- package/src/audit/i18n.ts +3 -18
- package/src/audit/web/client-plugin.tsx +1 -7
- package/src/audit/web/index.ts +0 -2
- package/src/config/__tests__/config.integration.test.ts +31 -0
- package/src/config/handlers/readiness.query.ts +8 -1
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +7 -2
- package/src/jobs/__tests__/jobs-security.integration.test.ts +41 -2
- package/src/jobs/changes.json +7 -0
- package/src/jobs/feature.ts +101 -9
- package/src/jobs/handlers/list.query.ts +21 -4
- package/src/jobs/handlers/trigger.write.ts +16 -3
- package/src/jobs/i18n.ts +3 -25
- package/src/jobs/index.ts +1 -0
- package/src/jobs/web/client-plugin.tsx +1 -7
- package/src/jobs/web/index.ts +0 -2
- package/src/tenant/__tests__/tenant.integration.test.ts +32 -0
- package/src/tenant/handlers/list.query.ts +1 -0
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/tier-engine-screens.boot.test.ts +82 -0
- package/src/tier-engine/changes.json +9 -1
- package/src/tier-engine/constants.ts +5 -5
- package/src/tier-engine/feature.ts +36 -10
- package/src/tier-engine/i18n.ts +19 -22
- package/src/tier-engine/web/client-plugin.tsx +5 -11
- package/src/tier-engine/web/index.ts +0 -1
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +99 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/user-data-rights/constants.ts +20 -6
- package/src/user-data-rights/feature.ts +113 -14
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +29 -71
- package/src/user-data-rights/web/client-plugin.tsx +19 -21
- package/src/user-data-rights/web/i18n.ts +6 -11
- package/src/user-data-rights/web/index.ts +1 -1
- package/src/user-data-rights/web/privacy-center-screen.tsx +32 -245
- package/src/user-profile/__tests__/profile-screen.boot.test.ts +97 -0
- package/src/user-profile/__tests__/profile-screen.test.tsx +140 -104
- package/src/user-profile/changes.json +7 -0
- package/src/user-profile/constants.ts +15 -7
- package/src/user-profile/feature.ts +96 -9
- package/src/user-profile/i18n.ts +8 -12
- package/src/user-profile/web/client-plugin.ts +22 -5
- package/src/user-profile/web/index.ts +1 -1
- package/src/user-profile/web/profile-screen.tsx +122 -265
- package/src/admin-shell/web/overview-layout.tsx +0 -69
- package/src/admin-shell/web/overview-query.ts +0 -15
- package/src/admin-shell/web/platform-overview-screen.tsx +0 -91
- package/src/admin-shell/web/tenant-overview-screen.tsx +0 -91
- package/src/audit/__tests__/audit-log-screen.test.tsx +0 -302
- package/src/audit/web/audit-log-detail-screen.tsx +0 -169
- package/src/audit/web/audit-log-screen.tsx +0 -290
- package/src/jobs/web/__tests__/job-runs-screen.test.tsx +0 -225
- package/src/jobs/web/job-run-detail-screen.tsx +0 -181
- package/src/jobs/web/job-runs-screen.tsx +0 -290
- package/src/tier-engine/web/tier-admin-screen.tsx +0 -168
|
@@ -7,7 +7,13 @@ import {
|
|
|
7
7
|
SYSTEM_USER_ID,
|
|
8
8
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
EXPORT_SECTION_EXTENSION_NAME,
|
|
12
|
+
PRIVACY_CENTER_SCREEN_ID,
|
|
13
|
+
STATUS_OPTION_KEY_PREFIX,
|
|
14
|
+
USER_ME_QUERY,
|
|
15
|
+
UserDataRightsHandlers,
|
|
16
|
+
} from "./constants";
|
|
11
17
|
import { cancelDeletionWrite } from "./handlers/cancel-deletion.write";
|
|
12
18
|
import { createConfirmDeletionByTokenHandler } from "./handlers/confirm-deletion-by-token.write";
|
|
13
19
|
import { downloadAttemptListQuery } from "./handlers/download-attempt-list.query";
|
|
@@ -132,6 +138,17 @@ export type UserDataRightsOptions = {
|
|
|
132
138
|
* wenn der jeweilige send*Email-Opt NICHT gesetzt ist; Export-ready braucht
|
|
133
139
|
* zusaetzlich appExportDownloadUrl. */
|
|
134
140
|
readonly mailDefaults?: GdprMailDefaults;
|
|
141
|
+
/** Hides the Art. 17 deletion section + its request-deletion/cancel-deletion
|
|
142
|
+
* actions on the privacy-center screen — for apps that already offer
|
|
143
|
+
* account deletion elsewhere (e.g. a profile danger-zone), against
|
|
144
|
+
* duplication. Default `true` (deletion section shown).
|
|
145
|
+
* BREAKING (fw#2312): replaces the removed client-side
|
|
146
|
+
* `userDataRightsClient({ privacyCenter: { showDeletion } })` option —
|
|
147
|
+
* the screen is now declared once, server-side (`type: "projectionDetail"`),
|
|
148
|
+
* so per-app visibility can only be a boot-time option, not a client-side
|
|
149
|
+
* component swap. Consumers that passed `showDeletion: false` client-side
|
|
150
|
+
* must move it here. */
|
|
151
|
+
readonly privacyCenterShowDeletion?: boolean;
|
|
135
152
|
};
|
|
136
153
|
|
|
137
154
|
export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): FeatureDefinition {
|
|
@@ -139,6 +156,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
139
156
|
// process, not every run). Lives in the factory scope so the cron closure
|
|
140
157
|
// shares it across runs.
|
|
141
158
|
let warnedMissingExportUrl = false;
|
|
159
|
+
const showDeletion = opts.privacyCenterShowDeletion ?? true;
|
|
142
160
|
return defineFeature("user-data-rights", (r) => {
|
|
143
161
|
r.describe(
|
|
144
162
|
'Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` \u2192 ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })` \u2014 the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When `mail-foundation` and a `mail-transport-*` are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.',
|
|
@@ -283,24 +301,105 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
283
301
|
|
|
284
302
|
r.translations({ keys: USER_DATA_RIGHTS_I18N });
|
|
285
303
|
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
// its logged-in area
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
304
|
+
// Self-service screen (Art. 15/17/18/20): export, restriction, deletion
|
|
305
|
+
// in one projectionDetail screen bound to the user's own `me` row. No
|
|
306
|
+
// r.nav — the app places it in its logged-in area (createKumikoApp's
|
|
307
|
+
// missing-client-plugin boot diagnostic (#2025/#2034) only scans
|
|
308
|
+
// type: "custom" screens, so a non-navved projectionDetail never
|
|
309
|
+
// triggers it — no `dormant` flag needed, that field doesn't exist on
|
|
310
|
+
// this screen type). access is openToAll because no app role name is
|
|
311
|
+
// portable; the per-user handlers enforce auth server-side.
|
|
312
|
+
//
|
|
313
|
+
// Restriction/Deletion are declarative fields + actions; Export stays a
|
|
314
|
+
// custom extension section (web/client-plugin.tsx registers the
|
|
315
|
+
// component under EXPORT_SECTION_EXTENSION_NAME) since it needs
|
|
316
|
+
// async-job polling + a signed-URL download that a declarative action
|
|
317
|
+
// can't express. entityName: "export-job" names the domain entity the
|
|
318
|
+
// extension persists against (projectionDetail has no real entity of
|
|
319
|
+
// its own for the extension to default to).
|
|
296
320
|
r.screen({
|
|
297
321
|
id: PRIVACY_CENTER_SCREEN_ID,
|
|
298
|
-
type: "
|
|
299
|
-
|
|
322
|
+
type: "projectionDetail",
|
|
323
|
+
query: USER_ME_QUERY,
|
|
324
|
+
singleton: true,
|
|
300
325
|
access: { openToAll: true },
|
|
301
|
-
dormant: true,
|
|
302
326
|
description:
|
|
303
327
|
"Logged-in GDPR self-service page where a user requests and downloads a data export (Art. 20), restricts processing of their account (Art. 18) and requests its deletion (Art. 17).",
|
|
328
|
+
fieldLabels: {
|
|
329
|
+
status: "userDataRights.privacyCenter.field.status",
|
|
330
|
+
gracePeriodEnd: "userDataRights.privacyCenter.field.gracePeriodEnd",
|
|
331
|
+
},
|
|
332
|
+
layout: {
|
|
333
|
+
sections: [
|
|
334
|
+
{
|
|
335
|
+
kind: "extension",
|
|
336
|
+
title: "userDataRights.privacyCenter.export.title",
|
|
337
|
+
component: { react: { __component: EXPORT_SECTION_EXTENSION_NAME } },
|
|
338
|
+
entityName: "export-job",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
// `status` lives here (not in Deletion) because this is the one
|
|
342
|
+
// section always on the screen — Deletion is dropped entirely
|
|
343
|
+
// when `showDeletion` is false, and status covers restriction
|
|
344
|
+
// AND deletion state, so it can't hang off the optional section.
|
|
345
|
+
title: "userDataRights.privacyCenter.restriction.title",
|
|
346
|
+
description: "userDataRights.privacyCenter.restriction.explainer",
|
|
347
|
+
fields: [
|
|
348
|
+
{
|
|
349
|
+
field: "status",
|
|
350
|
+
renderer: { format: "enumOption", keyPrefix: STATUS_OPTION_KEY_PREFIX },
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
...(showDeletion
|
|
355
|
+
? [
|
|
356
|
+
{
|
|
357
|
+
title: "userDataRights.privacyCenter.deletion.title",
|
|
358
|
+
description: "userDataRights.privacyCenter.deletion.explainer",
|
|
359
|
+
// gracePeriodEnd is only meaningful once a deletion is
|
|
360
|
+
// actually pending — hide it instead of showing an empty
|
|
361
|
+
// date when status isn't deletionRequested.
|
|
362
|
+
fields: [
|
|
363
|
+
{
|
|
364
|
+
field: "gracePeriodEnd",
|
|
365
|
+
renderer: { format: "date" as const },
|
|
366
|
+
visible: { field: "status", eq: "deletionRequested" },
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
]
|
|
371
|
+
: []),
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
actions: [
|
|
375
|
+
{
|
|
376
|
+
id: "restrict",
|
|
377
|
+
label: "userDataRights.privacyCenter.restriction.restrict",
|
|
378
|
+
handler: UserDataRightsHandlers.restrictAccount,
|
|
379
|
+
confirm: "userDataRights.privacyCenter.restriction.dialogDescription",
|
|
380
|
+
visible: { field: "status", ne: "restricted" },
|
|
381
|
+
style: "danger",
|
|
382
|
+
},
|
|
383
|
+
...(showDeletion
|
|
384
|
+
? [
|
|
385
|
+
{
|
|
386
|
+
id: "request-deletion",
|
|
387
|
+
label: "userDataRights.privacyCenter.deletion.delete",
|
|
388
|
+
handler: UserDataRightsHandlers.requestDeletion,
|
|
389
|
+
confirm: "userDataRights.privacyCenter.deletion.dialogDescription",
|
|
390
|
+
visible: { field: "status", ne: "deletionRequested" },
|
|
391
|
+
style: "danger" as const,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: "cancel-deletion",
|
|
395
|
+
label: "userDataRights.privacyCenter.deletion.cancel",
|
|
396
|
+
handler: UserDataRightsHandlers.cancelDeletion,
|
|
397
|
+
visible: { field: "status", eq: "deletionRequested" },
|
|
398
|
+
style: "secondary" as const,
|
|
399
|
+
},
|
|
400
|
+
]
|
|
401
|
+
: []),
|
|
402
|
+
],
|
|
304
403
|
});
|
|
305
404
|
|
|
306
405
|
// Magic-link path (anonymous): the email link carries the token as a
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// Render
|
|
2
|
-
//
|
|
3
|
-
// (
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Render test against real i18n bundles (catches missing keys — the Section
|
|
2
|
+
// must never show raw "userDataRights.privacyCenter.*" keys) plus QN wiring
|
|
3
|
+
// (the dispatched query/handler names). Restriction/deletion (status-driven
|
|
4
|
+
// branches) moved to declarative fields/actions with fw#2312 (feature.ts,
|
|
5
|
+
// booted in inspector-screens.boot.test.ts) — only Export (ExportSection) is
|
|
6
|
+
// still a React component. Provider wrapper is local (the renderer-web →
|
|
7
|
+
// bundled-features dependency direction forbids importing test-utils).
|
|
6
8
|
|
|
7
9
|
import { describe, expect, spyOn, test } from "bun:test";
|
|
8
10
|
import { createStore, type Dispatcher, type DispatcherStatus } from "@cosmicdrift/kumiko-headless";
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
} from "../../constants";
|
|
29
31
|
import { EXPORT_JOB_STATUS as SCHEMA_EXPORT_JOB_STATUS } from "../../schema/export-job";
|
|
30
32
|
import { defaultTranslations } from "../i18n";
|
|
31
|
-
import {
|
|
33
|
+
import { ExportSection, formatDate } from "../privacy-center-screen";
|
|
32
34
|
|
|
33
35
|
const stubLiveEvents: LiveEventSubscriber = () => () => {};
|
|
34
36
|
const stubTokens = {
|
|
@@ -40,7 +42,6 @@ const stubTokens = {
|
|
|
40
42
|
const stubResolver = createStaticLocaleResolver();
|
|
41
43
|
|
|
42
44
|
type QueryResponses = {
|
|
43
|
-
readonly me: Record<string, unknown>;
|
|
44
45
|
readonly exportStatus?: unknown;
|
|
45
46
|
readonly auditLog?: unknown;
|
|
46
47
|
/** Signed URL returned by downloadByJob — drives postWithDownload navigation. */
|
|
@@ -55,7 +56,6 @@ function makeDispatcher(
|
|
|
55
56
|
const statusStore = createStore<DispatcherStatus>("online");
|
|
56
57
|
const query = (async (type: string, payload: unknown) => {
|
|
57
58
|
queries.push({ type, payload });
|
|
58
|
-
if (type === USER_ME_QUERY) return { isSuccess: true, data: responses.me };
|
|
59
59
|
if (type === UserDataRightsQueries.exportStatus) {
|
|
60
60
|
return { isSuccess: true, data: responses.exportStatus ?? { hasJob: false } };
|
|
61
61
|
}
|
|
@@ -85,10 +85,7 @@ function makeDispatcher(
|
|
|
85
85
|
} as unknown as Dispatcher; // @cast-boundary test-stub
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function
|
|
89
|
-
responses: QueryResponses,
|
|
90
|
-
opts: { readonly showDeletion?: boolean } = {},
|
|
91
|
-
): {
|
|
88
|
+
function renderExportSection(responses: QueryResponses): {
|
|
92
89
|
view: ReturnType<typeof render>;
|
|
93
90
|
writes: Array<{ type: string; payload: unknown }>;
|
|
94
91
|
queries: Array<{ type: string; payload: unknown }>;
|
|
@@ -111,22 +108,21 @@ function renderCenter(
|
|
|
111
108
|
</LocaleProvider>
|
|
112
109
|
</TokensProvider>
|
|
113
110
|
);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
// ExportSection renders no Section/title of its own in production (the
|
|
112
|
+
// renderer's ExtensionSectionMount supplies that) — a plain testId wrapper
|
|
113
|
+
// here is test-only scaffolding to detect "mounted".
|
|
114
|
+
const view = render(
|
|
115
|
+
<div data-testid="export-section-root">
|
|
116
|
+
<ExportSection />
|
|
117
|
+
</div>,
|
|
118
|
+
{ wrapper },
|
|
119
|
+
);
|
|
117
120
|
return { view, writes, queries };
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
const activeMe = {
|
|
121
|
-
id: "00000000-0000-4000-8000-000000000042",
|
|
122
|
-
email: "marc@example.com",
|
|
123
|
-
status: "active",
|
|
124
|
-
gracePeriodEnd: null,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
123
|
async function waitForMount(view: ReturnType<typeof render>): Promise<void> {
|
|
128
124
|
await waitFor(() => {
|
|
129
|
-
if (view.queryByTestId("
|
|
125
|
+
if (view.queryByTestId("export-section-root") === null) {
|
|
130
126
|
throw new Error("not mounted yet");
|
|
131
127
|
}
|
|
132
128
|
});
|
|
@@ -151,22 +147,16 @@ async function waitForDownloadReady(view: ReturnType<typeof render>): Promise<vo
|
|
|
151
147
|
// plus accumulated global DOM/event state across ~30 prior DOM test files corrupts
|
|
152
148
|
// these in-flight renders (#457-class). A fresh process has no such accumulation.
|
|
153
149
|
// The QN-Drift-Pins + formatDate describes below are pure-logic and CI-stable.
|
|
154
|
-
describe("
|
|
155
|
-
test("
|
|
156
|
-
const { view } =
|
|
150
|
+
describe("ExportSection", () => {
|
|
151
|
+
test("idle: Intro + Request-Button, Texte übersetzt (keine rohen Keys)", async () => {
|
|
152
|
+
const { view } = renderExportSection({});
|
|
157
153
|
await waitForMount(view);
|
|
158
|
-
expect(view.getByTestId("privacy-export")).toBeTruthy();
|
|
159
|
-
expect(view.getByTestId("privacy-restriction")).toBeTruthy();
|
|
160
|
-
expect(view.getByTestId("privacy-deletion")).toBeTruthy();
|
|
161
154
|
expect(view.getByTestId("privacy-export-request")).toBeTruthy();
|
|
162
|
-
expect(view.getByTestId("privacy-restriction-restrict")).toBeTruthy();
|
|
163
|
-
expect(view.getByTestId("privacy-deletion-delete")).toBeTruthy();
|
|
164
155
|
expect(view.container.textContent).not.toContain("userDataRights.privacyCenter");
|
|
165
156
|
});
|
|
166
157
|
|
|
167
158
|
test("export done: Download-Button + Verfügbar-bis-Datum", async () => {
|
|
168
|
-
const { view } =
|
|
169
|
-
me: activeMe,
|
|
159
|
+
const { view } = renderExportSection({
|
|
170
160
|
exportStatus: {
|
|
171
161
|
hasJob: true,
|
|
172
162
|
job: { id: "job-123", status: EXPORT_JOB_STATUS.Done, expiresAt: "2026-07-11T00:00:00Z" },
|
|
@@ -180,8 +170,7 @@ describe("PrivacyCenterScreen", () => {
|
|
|
180
170
|
});
|
|
181
171
|
|
|
182
172
|
test("export failed: Fehler-Banner + Re-Request möglich", async () => {
|
|
183
|
-
const { view } =
|
|
184
|
-
me: activeMe,
|
|
173
|
+
const { view } = renderExportSection({
|
|
185
174
|
exportStatus: { hasJob: true, job: { id: "job-9", status: EXPORT_JOB_STATUS.Failed } },
|
|
186
175
|
});
|
|
187
176
|
await waitForTestId(view, "privacy-export-failed");
|
|
@@ -190,8 +179,7 @@ describe("PrivacyCenterScreen", () => {
|
|
|
190
179
|
});
|
|
191
180
|
|
|
192
181
|
test("export pending: in-progress Banner, kein Request-Button", async () => {
|
|
193
|
-
const { view } =
|
|
194
|
-
me: activeMe,
|
|
182
|
+
const { view } = renderExportSection({
|
|
195
183
|
exportStatus: { hasJob: true, job: { id: "job-1", status: EXPORT_JOB_STATUS.Pending } },
|
|
196
184
|
});
|
|
197
185
|
await waitForTestId(view, "privacy-export-pending");
|
|
@@ -199,28 +187,8 @@ describe("PrivacyCenterScreen", () => {
|
|
|
199
187
|
expect(view.queryByTestId("privacy-export-request")).toBeNull();
|
|
200
188
|
});
|
|
201
189
|
|
|
202
|
-
test("deletionRequested: Frist-Banner + Abbrechen statt Lösch-Button", async () => {
|
|
203
|
-
const { view } = renderCenter({
|
|
204
|
-
me: { ...activeMe, status: "deletionRequested", gracePeriodEnd: "2026-07-11T00:00:00Z" },
|
|
205
|
-
});
|
|
206
|
-
await waitForMount(view);
|
|
207
|
-
const banner = view.getByTestId("privacy-deletion-requested");
|
|
208
|
-
expect(banner.textContent).toContain("2026-07-11");
|
|
209
|
-
expect(banner.textContent).not.toContain("{date}");
|
|
210
|
-
expect(banner.textContent).not.toContain("T00:00");
|
|
211
|
-
expect(view.queryByTestId("privacy-deletion-delete")).toBeNull();
|
|
212
|
-
expect(view.getByTestId("privacy-deletion-cancel")).toBeTruthy();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("restricted: Info-Banner statt Einschränken-Button", async () => {
|
|
216
|
-
const { view } = renderCenter({ me: { ...activeMe, status: "restricted" } });
|
|
217
|
-
await waitForMount(view);
|
|
218
|
-
expect(view.getByTestId("privacy-restriction-active")).toBeTruthy();
|
|
219
|
-
expect(view.queryByTestId("privacy-restriction-restrict")).toBeNull();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
190
|
test("Export-Request dispatcht den korrekten Handler-QN", async () => {
|
|
223
|
-
const { view, writes } =
|
|
191
|
+
const { view, writes } = renderExportSection({});
|
|
224
192
|
await waitForMount(view);
|
|
225
193
|
fireEvent.click(view.getByTestId("privacy-export-request"));
|
|
226
194
|
await waitFor(() => {
|
|
@@ -229,16 +197,8 @@ describe("PrivacyCenterScreen", () => {
|
|
|
229
197
|
expect(writes[0]?.type).toBe(UserDataRightsHandlers.requestExport);
|
|
230
198
|
});
|
|
231
199
|
|
|
232
|
-
test("showDeletion=false: keine Lösch-Sektion", async () => {
|
|
233
|
-
const { view } = renderCenter({ me: activeMe }, { showDeletion: false });
|
|
234
|
-
await waitForMount(view);
|
|
235
|
-
expect(view.queryByTestId("privacy-deletion")).toBeNull();
|
|
236
|
-
expect(view.queryByTestId("privacy-deletion-delete")).toBeNull();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
200
|
test("Download-Button dispatcht downloadByJob mit der korrekten jobId", async () => {
|
|
240
|
-
const { view, queries } =
|
|
241
|
-
me: activeMe,
|
|
201
|
+
const { view, queries } = renderExportSection({
|
|
242
202
|
exportStatus: {
|
|
243
203
|
hasJob: true,
|
|
244
204
|
job: { id: "job-123", status: EXPORT_JOB_STATUS.Done, expiresAt: "2026-07-11T00:00:00Z" },
|
|
@@ -259,8 +219,7 @@ describe("PrivacyCenterScreen", () => {
|
|
|
259
219
|
const signedUrl = "https://cdn.test/exports/job-123.zip?sig=abc";
|
|
260
220
|
const assign = spyOn(window.location, "assign").mockImplementation(() => {});
|
|
261
221
|
try {
|
|
262
|
-
const { view } =
|
|
263
|
-
me: activeMe,
|
|
222
|
+
const { view } = renderExportSection({
|
|
264
223
|
downloadUrl: signedUrl,
|
|
265
224
|
exportStatus: {
|
|
266
225
|
hasJob: true,
|
|
@@ -281,8 +240,7 @@ describe("PrivacyCenterScreen", () => {
|
|
|
281
240
|
test("Download-Button does not navigate when downloadByJob returns no url", async () => {
|
|
282
241
|
const assign = spyOn(window.location, "assign").mockImplementation(() => {});
|
|
283
242
|
try {
|
|
284
|
-
const { view } =
|
|
285
|
-
me: activeMe,
|
|
243
|
+
const { view } = renderExportSection({
|
|
286
244
|
downloadUrl: undefined,
|
|
287
245
|
exportStatus: {
|
|
288
246
|
hasJob: true,
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
// @runtime client
|
|
2
|
-
// Client-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// Client-feature factory for user-data-rights. Provides the ExportSection
|
|
3
|
+
// extension component for the privacy-center screen (registered server-side
|
|
4
|
+
// declaratively as `type: "projectionDetail"`, see feature.ts — restriction
|
|
5
|
+
// and deletion are fields/actions on the screen itself and need no client
|
|
6
|
+
// component) + default translations. Apps wire it in via
|
|
7
|
+
// createKumikoApp({ clientFeatures: [userDataRightsClient()] }); the screen
|
|
8
|
+
// has no own r.nav, the app places it via r.nav.
|
|
9
|
+
//
|
|
10
|
+
// fw#2312: the former `privacyCenter: { showDeletion }` option is gone — a
|
|
11
|
+
// declarative screen is now registered server-side once for all apps, so a
|
|
12
|
+
// client prop can no longer toggle its sections/actions per app. Its
|
|
13
|
+
// replacement is the server-side
|
|
14
|
+
// `UserDataRightsOptions.privacyCenterShowDeletion` (createUserDataRightsFeature,
|
|
15
|
+
// ../feature.ts) — BREAKING, consumers with `showDeletion: false` (e.g.
|
|
16
|
+
// money-horse) must carry the option over at feature setup.
|
|
8
17
|
|
|
9
18
|
import { mergeTranslations, type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
10
19
|
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
11
|
-
import {
|
|
20
|
+
import { EXPORT_SECTION_EXTENSION_NAME, USER_DATA_RIGHTS_FEATURE } from "../constants";
|
|
12
21
|
import { defaultTranslations } from "./i18n";
|
|
13
|
-
import {
|
|
22
|
+
import { ExportSection } from "./privacy-center-screen";
|
|
14
23
|
import { makePublicDeletionGate, type PublicDeletionRoutes } from "./public-deletion-gate";
|
|
15
24
|
|
|
16
25
|
export type UserDataRightsClientOptions = {
|
|
@@ -21,27 +30,16 @@ export type UserDataRightsClientOptions = {
|
|
|
21
30
|
* privacy-center-Screen. Den Client VOR dem Auth-Client listen, sonst
|
|
22
31
|
* landet der anonyme Besucher auf der Login-Maske. */
|
|
23
32
|
readonly publicDeletion?: PublicDeletionRoutes;
|
|
24
|
-
/** Konfiguration des eingeloggten privacy-center-Screens. */
|
|
25
|
-
readonly privacyCenter?: {
|
|
26
|
-
/** `false` blendet die Konto-Lösch-Sektion aus — für Apps, die die Löschung
|
|
27
|
-
* schon woanders anbieten (z.B. Profil-DangerZone), gegen Doppelung.
|
|
28
|
-
* Default `true`. */
|
|
29
|
-
readonly showDeletion?: boolean;
|
|
30
|
-
};
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
export function userDataRightsClient(
|
|
34
36
|
options?: UserDataRightsClientOptions,
|
|
35
37
|
): ClientFeatureDefinition {
|
|
36
|
-
const showDeletion = options?.privacyCenter?.showDeletion ?? true;
|
|
37
|
-
const privacyCenter = showDeletion
|
|
38
|
-
? PrivacyCenterScreen
|
|
39
|
-
: () => <PrivacyCenterScreen showDeletion={false} />;
|
|
40
38
|
const base: ClientFeatureDefinition = {
|
|
41
39
|
name: USER_DATA_RIGHTS_FEATURE,
|
|
42
40
|
translations: mergeTranslations(defaultTranslations, options?.translations ?? {}),
|
|
43
|
-
|
|
44
|
-
[
|
|
41
|
+
extensionSectionComponents: {
|
|
42
|
+
[EXPORT_SECTION_EXTENSION_NAME]: ExportSection,
|
|
45
43
|
},
|
|
46
44
|
};
|
|
47
45
|
if (options?.publicDeletion === undefined) return base;
|
|
@@ -37,12 +37,13 @@ const apexTranslations: TranslationsByLocale = {
|
|
|
37
37
|
"No token found in the link. Please open the link from the email again.",
|
|
38
38
|
"userDataRights.deletion.confirm.error": "Something went wrong. Please try again.",
|
|
39
39
|
|
|
40
|
-
"userDataRights.privacyCenter.title": "Privacy",
|
|
41
|
-
"userDataRights.privacyCenter.intro":
|
|
42
|
-
"Manage your GDPR rights: access, export, restrict, and delete your account.",
|
|
43
|
-
"userDataRights.privacyCenter.loading": "Loading …",
|
|
44
|
-
"userDataRights.privacyCenter.loadError": "Your data could not be loaded.",
|
|
45
40
|
"userDataRights.privacyCenter.errors.generic": "Something went wrong. Please try again.",
|
|
41
|
+
"userDataRights.privacyCenter.field.status": "Account status",
|
|
42
|
+
"userDataRights.privacyCenter.field.gracePeriodEnd": "Scheduled deletion date",
|
|
43
|
+
"userDataRights.privacyCenter.field.status.option.active": "Active",
|
|
44
|
+
"userDataRights.privacyCenter.field.status.option.restricted": "Restricted",
|
|
45
|
+
"userDataRights.privacyCenter.field.status.option.deletionRequested": "Deletion requested",
|
|
46
|
+
"userDataRights.privacyCenter.field.status.option.deleted": "Deleted",
|
|
46
47
|
|
|
47
48
|
"userDataRights.privacyCenter.export.title": "Export your data (Art. 20)",
|
|
48
49
|
"userDataRights.privacyCenter.export.intro":
|
|
@@ -62,20 +63,14 @@ const apexTranslations: TranslationsByLocale = {
|
|
|
62
63
|
"userDataRights.privacyCenter.restriction.explainer":
|
|
63
64
|
"Freeze your account: processing of your data is paused and you are signed out. Lifting the restriction afterwards is only possible via support.",
|
|
64
65
|
"userDataRights.privacyCenter.restriction.restrict": "Restrict account",
|
|
65
|
-
"userDataRights.privacyCenter.restriction.dialogTitle": "Restrict your account?",
|
|
66
66
|
"userDataRights.privacyCenter.restriction.dialogDescription":
|
|
67
67
|
"You will be signed out immediately and cannot sign in again until support lifts the restriction.",
|
|
68
|
-
"userDataRights.privacyCenter.restriction.restricted":
|
|
69
|
-
"Your account is restricted. Contact support to lift the restriction.",
|
|
70
68
|
|
|
71
69
|
"userDataRights.privacyCenter.deletion.title": "Delete account (Art. 17)",
|
|
72
70
|
"userDataRights.privacyCenter.deletion.explainer":
|
|
73
71
|
"Request deletion of your account. Until the grace period ends you can cancel the deletion.",
|
|
74
72
|
"userDataRights.privacyCenter.deletion.delete": "Delete account",
|
|
75
|
-
"userDataRights.privacyCenter.deletion.requested": "Your account will be deleted on {date}.",
|
|
76
73
|
"userDataRights.privacyCenter.deletion.cancel": "Cancel deletion",
|
|
77
|
-
"userDataRights.privacyCenter.deletion.cancelSuccess": "The deletion was cancelled.",
|
|
78
|
-
"userDataRights.privacyCenter.deletion.dialogTitle": "Delete your account?",
|
|
79
74
|
"userDataRights.privacyCenter.deletion.dialogDescription":
|
|
80
75
|
"Confirming starts the deletion grace period. You can cancel the deletion until it ends.",
|
|
81
76
|
|
|
@@ -9,7 +9,7 @@ export { type UserDataRightsClientOptions, userDataRightsClient } from "./client
|
|
|
9
9
|
export type { ConfirmAccountDeletionScreenProps } from "./confirm-deletion-screen";
|
|
10
10
|
export { ConfirmAccountDeletionScreen } from "./confirm-deletion-screen";
|
|
11
11
|
export { defaultTranslations } from "./i18n";
|
|
12
|
-
export { formatDate
|
|
12
|
+
export { formatDate } from "./privacy-center-screen";
|
|
13
13
|
export { makePublicDeletionGate, type PublicDeletionRoutes } from "./public-deletion-gate";
|
|
14
14
|
export type { RequestAccountDeletionScreenProps } from "./request-deletion-screen";
|
|
15
15
|
export { RequestAccountDeletionScreen } from "./request-deletion-screen";
|