@cosmicdrift/kumiko-bundled-features 0.35.0 → 0.38.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/auth-email-password/__tests__/auth-mailer.test.ts +138 -0
- package/src/auth-email-password/auth-mailer.ts +137 -0
- package/src/auth-email-password/email-templates.ts +7 -13
- package/src/auth-email-password/errors.ts +84 -0
- package/src/auth-email-password/handlers/change-password.write.ts +1 -10
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +3 -19
- package/src/auth-email-password/handlers/invite-accept.write.ts +15 -28
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -14
- package/src/auth-email-password/handlers/login.write.ts +7 -51
- package/src/auth-email-password/handlers/reset-password.write.ts +3 -10
- package/src/auth-email-password/handlers/signup-confirm.write.ts +2 -14
- package/src/auth-email-password/handlers/verify-email.write.ts +3 -10
- package/src/auth-email-password/index.ts +9 -0
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +24 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +1 -0
- package/src/auth-email-password/web/tenant-switcher.tsx +2 -1
- package/src/cap-counter/enforce-cap.ts +5 -0
- package/src/compliance-profiles/README.md +1 -1
- package/src/custom-fields/__tests__/feature.test.ts +1 -1
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +4 -4
- package/src/custom-fields/db/queries/retention.ts +1 -0
- package/src/custom-fields/lib/parse-serialized-field.ts +11 -0
- package/src/custom-fields/run-retention.ts +4 -22
- package/src/custom-fields/web/__tests__/custom-fields-form-section.test.tsx +148 -0
- package/src/custom-fields/web/custom-fields-form-section.tsx +26 -12
- package/src/custom-fields/wire-for-entity.ts +4 -12
- package/src/custom-fields/wire-user-data-rights.ts +3 -22
- package/src/data-retention/__tests__/data-retention.integration.test.ts +2 -2
- package/src/file-foundation/feature.ts +13 -3
- package/src/file-foundation/index.ts +1 -0
- package/src/file-provider-inmemory/__tests__/feature.test.ts +4 -7
- package/src/file-provider-s3/__tests__/feature.test.ts +4 -6
- package/src/files/README.md +1 -1
- package/src/legal-pages/markdown.ts +1 -13
- package/src/renderer-simple/simple-renderer.ts +1 -8
- package/src/subscription-stripe/feature.ts +5 -2
- package/src/template-resolver/feature.ts +1 -2
- package/src/template-resolver/handlers/list.query.ts +7 -14
- package/src/template-resolver/handlers/toggle-status.write.ts +37 -0
- package/src/tenant/command-schemas.ts +1 -1
- package/src/tenant/feature.ts +1 -2
- package/src/tenant/handlers/toggle-enabled.write.ts +23 -0
- package/src/user-data-rights/README.md +8 -8
- package/src/template-resolver/handlers/archive.write.ts +0 -39
- package/src/template-resolver/handlers/publish.write.ts +0 -42
- package/src/tenant/handlers/disable.write.ts +0 -18
- package/src/tenant/handlers/enable.write.ts +0 -20
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// T1.5c — user-data-rights wiring for custom-fields.
|
|
2
2
|
|
|
3
|
+
import { extractTableName } from "@cosmicdrift/kumiko-framework/db";
|
|
3
4
|
import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
5
|
import { EXT_USER_DATA, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
6
|
import {
|
|
@@ -7,16 +8,7 @@ import {
|
|
|
7
8
|
selectFieldDefinitionsForEntity,
|
|
8
9
|
stripSensitiveCustomFieldKeys,
|
|
9
10
|
} from "./db/queries/user-data-rights";
|
|
10
|
-
import { parseSerializedField } from "./lib/parse-serialized-field";
|
|
11
|
-
|
|
12
|
-
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
13
|
-
function getTableName(table: unknown): string {
|
|
14
|
-
if (typeof table === "object" && table !== null) {
|
|
15
|
-
const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
|
|
16
|
-
if (typeof sym === "string") return sym;
|
|
17
|
-
}
|
|
18
|
-
throw new Error("wire-user-data-rights: table missing kumiko:schema:Name symbol");
|
|
19
|
-
}
|
|
11
|
+
import { isFieldDefinitionRow, parseSerializedField } from "./lib/parse-serialized-field";
|
|
20
12
|
|
|
21
13
|
export interface WireCustomFieldsUserDataRightsOptions {
|
|
22
14
|
readonly entityName: string;
|
|
@@ -52,7 +44,7 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
|
|
|
52
44
|
r: TReg,
|
|
53
45
|
opts: WireCustomFieldsUserDataRightsOptions,
|
|
54
46
|
): void {
|
|
55
|
-
const tableName =
|
|
47
|
+
const tableName = extractTableName(opts.entityTable, "custom-fields/wire-user-data-rights");
|
|
56
48
|
|
|
57
49
|
const exportHook: UserDataExportHook = async (ctx) => {
|
|
58
50
|
const rows = await selectCustomFieldsHostRows(
|
|
@@ -100,17 +92,6 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
|
|
|
100
92
|
});
|
|
101
93
|
}
|
|
102
94
|
|
|
103
|
-
interface FieldDefinitionRow {
|
|
104
|
-
readonly field_key: string;
|
|
105
|
-
readonly serialized_field: unknown;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isFieldDefinitionRow(value: unknown): value is FieldDefinitionRow {
|
|
109
|
-
if (!value || typeof value !== "object") return false;
|
|
110
|
-
if (!("field_key" in value)) return false;
|
|
111
|
-
return typeof value.field_key === "string";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
95
|
async function loadSensitiveFieldKeys(
|
|
115
96
|
db: Parameters<UserDataExportHook>[0]["db"],
|
|
116
97
|
tenantId: string,
|
|
@@ -36,11 +36,11 @@ describe("data-retention :: feature-definition smoke", () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
test("tenantRetentionOverride-Entity ist registriert", () => {
|
|
39
|
-
expect(feature.entities["tenant-retention-override"]).toBeDefined();
|
|
39
|
+
expect(feature.entities?.["tenant-retention-override"]).toBeDefined();
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test("Entity-Definition hat UNIQUE(tenantId, entityName) als 1:1-Constraint", () => {
|
|
43
|
-
const entity = feature.entities["tenant-retention-override"];
|
|
43
|
+
const entity = feature.entities?.["tenant-retention-override"];
|
|
44
44
|
const indexes = entity?.indexes ?? [];
|
|
45
45
|
const uniqueIndex = indexes.find((i) => i.unique === true);
|
|
46
46
|
expect(uniqueIndex).toBeDefined();
|
|
@@ -85,6 +85,12 @@ export type FileProviderPlugin = {
|
|
|
85
85
|
readonly build: (ctx: FileProviderContext, tenantId: string) => Promise<FileStorageProvider>;
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
// extension-usage `options` is engine-payload (unknown) — structurally validate
|
|
89
|
+
// instead of casting blind.
|
|
90
|
+
export function isFileProviderPlugin(o: unknown): o is FileProviderPlugin {
|
|
91
|
+
return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
// =============================================================================
|
|
89
95
|
// Feature-definition
|
|
90
96
|
// =============================================================================
|
|
@@ -163,7 +169,11 @@ export async function createFileProviderForTenant(
|
|
|
163
169
|
);
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
if (!isFileProviderPlugin(usage.options)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`${FEATURE_NAME}: provider "${provider}" registered without a build() — ` +
|
|
175
|
+
`extension options must be a FileProviderPlugin.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return usage.options.build(ctx, tenantId);
|
|
169
179
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// feature.ts contract tests for file-provider-inmemory.
|
|
2
2
|
|
|
3
3
|
import { describe, expect, test } from "bun:test";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
type FileProviderPlugin,
|
|
6
|
+
isFileProviderPlugin,
|
|
7
|
+
} from "@cosmicdrift/kumiko-bundled-features/file-foundation";
|
|
5
8
|
import { clearStorage, fileProviderInMemoryFeature, listKeys } from "../feature";
|
|
6
9
|
|
|
7
10
|
describe("fileProviderInMemoryFeature — shape", () => {
|
|
@@ -35,12 +38,6 @@ describe("listKeys / clearStorage — per-tenant store helpers", () => {
|
|
|
35
38
|
});
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
// extension-usage `options` is engine-payload (unknown) — structurally validate
|
|
39
|
-
// instead of casting blind.
|
|
40
|
-
function isFileProviderPlugin(o: unknown): o is FileProviderPlugin {
|
|
41
|
-
return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
41
|
function inmemoryPlugin(): FileProviderPlugin {
|
|
45
42
|
const options = fileProviderInMemoryFeature.extensionUsages.find(
|
|
46
43
|
(u) => u.extensionName === "fileProvider" && u.entityName === "inmemory",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// feature.ts contract tests for file-provider-s3.
|
|
2
2
|
|
|
3
3
|
import { describe, expect, test } from "bun:test";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
type FileProviderPlugin,
|
|
6
|
+
isFileProviderPlugin,
|
|
7
|
+
} from "@cosmicdrift/kumiko-bundled-features/file-foundation";
|
|
5
8
|
import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../feature";
|
|
6
9
|
|
|
7
10
|
describe("fileProviderS3Feature — shape", () => {
|
|
@@ -54,11 +57,6 @@ describe("fileProviderS3Feature — plugin-registration", () => {
|
|
|
54
57
|
});
|
|
55
58
|
});
|
|
56
59
|
|
|
57
|
-
// extension-usage `options` is engine-payload (unknown) — structurally validate.
|
|
58
|
-
function isFileProviderPlugin(o: unknown): o is FileProviderPlugin {
|
|
59
|
-
return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
60
|
function s3Plugin(): FileProviderPlugin {
|
|
63
61
|
const options = fileProviderS3Feature.extensionUsages.find(
|
|
64
62
|
(u) => u.extensionName === "fileProvider" && u.entityName === "s3",
|
package/src/files/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
1
2
|
import { Marked } from "marked";
|
|
2
3
|
|
|
3
4
|
// Markdown→HTML mit eigener `marked`-Instance. GFM aus, breaks aus —
|
|
@@ -54,16 +55,3 @@ ${opts.bodyHtml}
|
|
|
54
55
|
</body>
|
|
55
56
|
</html>`;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
-
function escapeHtml(s: string): string {
|
|
59
|
-
return s
|
|
60
|
-
.replace(/&/g, "&")
|
|
61
|
-
.replace(/</g, "<")
|
|
62
|
-
.replace(/>/g, ">")
|
|
63
|
-
.replace(/"/g, """)
|
|
64
|
-
.replace(/'/g, "'");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function escapeHtmlAttr(s: string): string {
|
|
68
|
-
return escapeHtml(s);
|
|
69
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeHtml } from "@cosmicdrift/kumiko-headless";
|
|
1
2
|
import type { NotificationRenderer } from "../delivery";
|
|
2
3
|
|
|
3
4
|
type Section =
|
|
@@ -14,14 +15,6 @@ type EmailTemplateData = {
|
|
|
14
15
|
readonly body?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
function escapeHtml(str: string): string {
|
|
18
|
-
return str
|
|
19
|
-
.replace(/&/g, "&")
|
|
20
|
-
.replace(/</g, "<")
|
|
21
|
-
.replace(/>/g, ">")
|
|
22
|
-
.replace(/"/g, """);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
function renderSection(section: Section): string {
|
|
26
19
|
if ("text" in section) {
|
|
27
20
|
return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
|
|
@@ -65,8 +65,11 @@ export const subscriptionStripeEnvSchema = z.object({
|
|
|
65
65
|
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
66
66
|
STRIPE_API_KEY: z
|
|
67
67
|
.string()
|
|
68
|
-
.regex(
|
|
69
|
-
|
|
68
|
+
.regex(
|
|
69
|
+
/^(sk|rk)_(test|live)_/,
|
|
70
|
+
"STRIPE_API_KEY must start with 'sk_test_'/'sk_live_' or a restricted 'rk_test_'/'rk_live_' key",
|
|
71
|
+
)
|
|
72
|
+
.describe("Stripe API key (`sk_live_...` / `sk_test_...`, restricted `rk_...` keys allowed).")
|
|
70
73
|
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
71
74
|
});
|
|
72
75
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { archiveWrite } from "./handlers/archive.write";
|
|
3
2
|
import { findByIdQuery } from "./handlers/find-by-id.query";
|
|
4
3
|
import { listQuery } from "./handlers/list.query";
|
|
5
|
-
import { publishWrite } from "./handlers/
|
|
4
|
+
import { archiveWrite, publishWrite } from "./handlers/toggle-status.write";
|
|
6
5
|
import { upsertSystemWrite } from "./handlers/upsert-system.write";
|
|
7
6
|
import { upsertTenantWrite } from "./handlers/upsert-tenant.write";
|
|
8
7
|
import { templateResourceEntity } from "./table";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { defineQueryHandler
|
|
2
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { RENDER_KINDS, TEMPLATE_STATUSES } from "../constants";
|
|
5
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table";
|
|
@@ -17,14 +17,13 @@ export const listQuery = defineQueryHandler({
|
|
|
17
17
|
}),
|
|
18
18
|
access: { roles: ["TenantAdmin", "SystemAdmin", "User"] },
|
|
19
19
|
handler: async (query, ctx) => {
|
|
20
|
-
const isSystemAdmin = query.user.roles.includes("SystemAdmin");
|
|
21
20
|
const where: Record<string, unknown> = {};
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
22
|
+
// includeSystem=false narrows to own tenant AT THE DB — for non-SystemAdmin
|
|
23
|
+
// TenantDb permits narrowing within its enforced [own, SYSTEM] scope, for
|
|
24
|
+
// SystemAdmin (system-scoped db) the where applies verbatim. Filtering at
|
|
25
|
+
// the DB keeps the limit meaningful (no post-filter starvation).
|
|
26
|
+
if (!query.payload.includeSystem) {
|
|
28
27
|
where["tenantId"] = query.user.tenantId;
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -44,13 +43,7 @@ export const listQuery = defineQueryHandler({
|
|
|
44
43
|
limit: 500,
|
|
45
44
|
})) as TemplateResourceRow[];
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
// includeSystem=false drops them here since they can't be excluded at the DB.
|
|
49
|
-
const visible = query.payload.includeSystem
|
|
50
|
-
? rows
|
|
51
|
-
: rows.filter((row) => row.tenantId !== SYSTEM_TENANT_ID);
|
|
52
|
-
|
|
53
|
-
return visible.map((row) => ({
|
|
46
|
+
return rows.map((row) => ({
|
|
54
47
|
id: String(row.id),
|
|
55
48
|
tenantId: row.tenantId,
|
|
56
49
|
slug: row.slug,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { TemplateResourceRow } from "../table";
|
|
6
|
+
import { templateResourcesTable } from "../table";
|
|
7
|
+
import { executor } from "./shared";
|
|
8
|
+
|
|
9
|
+
type TemplateStatus = "active" | "archived";
|
|
10
|
+
|
|
11
|
+
function createStatusUpdateHandler(name: string, status: TemplateStatus) {
|
|
12
|
+
return defineWriteHandler({
|
|
13
|
+
name,
|
|
14
|
+
schema: z.object({ id: z.string().min(1) }),
|
|
15
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
16
|
+
handler: async (event, ctx) => {
|
|
17
|
+
// ctx.db is tenant-scoped: a foreign tenant's id reads as absent → NotFound.
|
|
18
|
+
// Cross-tenant toggling needs SystemAdmin with tenantIdOverride.
|
|
19
|
+
const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
20
|
+
id: event.payload.id,
|
|
21
|
+
});
|
|
22
|
+
if (!existing) {
|
|
23
|
+
return writeFailure(new NotFoundError("template-resource", event.payload.id));
|
|
24
|
+
}
|
|
25
|
+
const result = await executor.update(
|
|
26
|
+
{ id: existing.id, version: existing.version, changes: { status } },
|
|
27
|
+
event.user,
|
|
28
|
+
ctx.db,
|
|
29
|
+
);
|
|
30
|
+
if (!result.isSuccess) return result;
|
|
31
|
+
return { isSuccess: true as const, data: { id: String(existing.id), status } };
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const archiveWrite = createStatusUpdateHandler("archive", "archived");
|
|
37
|
+
export const publishWrite = createStatusUpdateHandler("publish", "active");
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import { addMemberWrite } from "./handlers/add-member.write";
|
|
24
24
|
import { createWrite } from "./handlers/create.write";
|
|
25
|
-
import { disableWrite } from "./handlers/disable.write";
|
|
26
25
|
import { removeMemberWrite } from "./handlers/remove-member.write";
|
|
26
|
+
import { disableWrite } from "./handlers/toggle-enabled.write";
|
|
27
27
|
import { updateWrite } from "./handlers/update.write";
|
|
28
28
|
import { updateMemberRolesWrite } from "./handlers/update-member-roles.write";
|
|
29
29
|
|
package/src/tenant/feature.ts
CHANGED
|
@@ -9,8 +9,6 @@ import { activeTenantIdsQuery } from "./handlers/active-tenant-ids.query";
|
|
|
9
9
|
import { addMemberWrite } from "./handlers/add-member.write";
|
|
10
10
|
import { cancelInvitationWrite } from "./handlers/cancel-invitation.write";
|
|
11
11
|
import { createWrite } from "./handlers/create.write";
|
|
12
|
-
import { disableWrite } from "./handlers/disable.write";
|
|
13
|
-
import { enableWrite } from "./handlers/enable.write";
|
|
14
12
|
import { invitationsQuery } from "./handlers/invitations.query";
|
|
15
13
|
import { listQuery } from "./handlers/list.query";
|
|
16
14
|
import { meQuery } from "./handlers/me.query";
|
|
@@ -18,6 +16,7 @@ import { membersQuery } from "./handlers/members.query";
|
|
|
18
16
|
import { membershipsQuery } from "./handlers/memberships.query";
|
|
19
17
|
import { removeMemberWrite } from "./handlers/remove-member.write";
|
|
20
18
|
import { resolveUserIdsQuery } from "./handlers/resolve-user-ids.query";
|
|
19
|
+
import { disableWrite, enableWrite } from "./handlers/toggle-enabled.write";
|
|
21
20
|
import { updateWrite } from "./handlers/update.write";
|
|
22
21
|
import { updateMemberRolesWrite } from "./handlers/update-member-roles.write";
|
|
23
22
|
import { tenantInvitationEntity } from "./invitation-table";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { tenantEntity, tenantTable } from "../schema/tenant";
|
|
5
|
+
|
|
6
|
+
const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
|
|
7
|
+
|
|
8
|
+
// Admin flip: last-writer-wins is fine. SystemAdmin is the only caller and
|
|
9
|
+
// there's no meaningful concurrent-edit race on this single boolean.
|
|
10
|
+
function createToggleTenantHandler(enable: boolean) {
|
|
11
|
+
return defineWriteHandler({
|
|
12
|
+
name: enable ? "enable" : "disable",
|
|
13
|
+
schema: z.object({ id: z.uuid() }),
|
|
14
|
+
access: { roles: ["SystemAdmin"] },
|
|
15
|
+
handler: async (event, ctx) =>
|
|
16
|
+
crud.update({ id: event.payload.id, changes: { isEnabled: enable } }, event.user, ctx.db, {
|
|
17
|
+
skipOptimisticLock: true,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const enableWrite = createToggleTenantHandler(true);
|
|
23
|
+
export const disableWrite = createToggleTenantHandler(false);
|
|
@@ -90,15 +90,15 @@ weglassen wenn er Custom-Hooks registrieren will.
|
|
|
90
90
|
|
|
91
91
|
| Datei | Pinst |
|
|
92
92
|
|-------|-------|
|
|
93
|
-
| `audit-log.integration.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
|
|
94
|
-
| `cross-data-matrix.integration.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
|
|
95
|
-
| `download.integration.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
|
|
96
|
-
| `request-export.integration.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
|
|
97
|
-
| `request-deletion-callback.integration.ts` + `request-cancel-deletion.integration.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
|
|
98
|
-
| `restriction-flow.integration.ts` | Status-Flip + Auth-Middleware-Block + Lift |
|
|
99
|
-
| `run-{export-jobs,forget-cleanup,user-export}.integration.ts` | Worker-Logic + Idempotency + Email-Callbacks |
|
|
93
|
+
| `audit-log.integration.test.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
|
|
94
|
+
| `cross-data-matrix.integration.test.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
|
|
95
|
+
| `download.integration.test.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
|
|
96
|
+
| `request-export.integration.test.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
|
|
97
|
+
| `request-deletion-callback.integration.test.ts` + `request-cancel-deletion.integration.test.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
|
|
98
|
+
| `restriction-flow.integration.test.ts` | Status-Flip + Auth-Middleware-Block + Lift |
|
|
99
|
+
| `run-{export-jobs,forget-cleanup,user-export}.integration.test.ts` | Worker-Logic + Idempotency + Email-Callbacks |
|
|
100
100
|
| `policy-to-strategy.test.ts` | Retention.strategy → UserDataDeleteStrategy mapping |
|
|
101
|
-
| `user-data-rights.integration.ts` | Boot-Smoke + Feature-Meta |
|
|
101
|
+
| `user-data-rights.integration.test.ts` | Boot-Smoke + Feature-Meta |
|
|
102
102
|
| `token-helpers.test.ts` + `zip-path.test.ts` | Token-Hashing + Path-Traversal-Schutz |
|
|
103
103
|
| `export-job-{idempotency,schema}.test.ts` | Active-job-uniqueness + Schema-Constraints |
|
|
104
104
|
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import type { TemplateResourceRow } from "../table";
|
|
6
|
-
import { templateResourcesTable } from "../table";
|
|
7
|
-
import { executor } from "./shared";
|
|
8
|
-
|
|
9
|
-
// Setzt einen Template-Eintrag auf status='archived'. Resolver liefert
|
|
10
|
-
// archivierte Templates nicht zurück — sie bleiben aber als Audit-Trail
|
|
11
|
-
// in der DB (kein physisches Delete). Reaktivierung via publish.
|
|
12
|
-
export const archiveWrite = defineWriteHandler({
|
|
13
|
-
name: "archive",
|
|
14
|
-
schema: z.object({ id: z.string().min(1) }),
|
|
15
|
-
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
16
|
-
handler: async (event, ctx) => {
|
|
17
|
-
const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
18
|
-
id: event.payload.id,
|
|
19
|
-
});
|
|
20
|
-
// ctx.db ist via createTenantDb tenant-scoped — existing ist null wenn
|
|
21
|
-
// das Template einem fremden Tenant gehört (SystemAdmin-Cross-Tenant
|
|
22
|
-
// braucht tenantIdOverride im Schema, M2-Erweiterung).
|
|
23
|
-
if (!existing) {
|
|
24
|
-
return writeFailure(new NotFoundError("template-resource", event.payload.id));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const result = await executor.update(
|
|
28
|
-
{
|
|
29
|
-
id: existing.id,
|
|
30
|
-
version: existing.version,
|
|
31
|
-
changes: { status: "archived" as const },
|
|
32
|
-
},
|
|
33
|
-
event.user,
|
|
34
|
-
ctx.db,
|
|
35
|
-
);
|
|
36
|
-
if (!result.isSuccess) return result;
|
|
37
|
-
return { isSuccess: true as const, data: { id: String(existing.id), status: "archived" } };
|
|
38
|
-
},
|
|
39
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import type { TemplateResourceRow } from "../table";
|
|
6
|
-
import { templateResourcesTable } from "../table";
|
|
7
|
-
import { executor } from "./shared";
|
|
8
|
-
|
|
9
|
-
// Setzt einen Template-Eintrag auf status='active'. Typischer Workflow:
|
|
10
|
-
// User editiert ein Draft, ist zufrieden, drückt Publish.
|
|
11
|
-
//
|
|
12
|
-
// Tenant-Isolation: Template muss zum event.user.tenantId gehören
|
|
13
|
-
// (oder zu SYSTEM_TENANT wenn SystemAdmin). Cross-Tenant-Publish-
|
|
14
|
-
// Versuche → NotFound (Pattern aus row-level-security).
|
|
15
|
-
export const publishWrite = defineWriteHandler({
|
|
16
|
-
name: "publish",
|
|
17
|
-
schema: z.object({ id: z.string().min(1) }),
|
|
18
|
-
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
19
|
-
handler: async (event, ctx) => {
|
|
20
|
-
const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
21
|
-
id: event.payload.id,
|
|
22
|
-
});
|
|
23
|
-
// ctx.db ist via createTenantDb tenant-scoped — existing ist null wenn
|
|
24
|
-
// das Template einem fremden Tenant gehört (SystemAdmin-Cross-Tenant
|
|
25
|
-
// braucht tenantIdOverride im Schema, M2-Erweiterung).
|
|
26
|
-
if (!existing) {
|
|
27
|
-
return writeFailure(new NotFoundError("template-resource", event.payload.id));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const result = await executor.update(
|
|
31
|
-
{
|
|
32
|
-
id: existing.id,
|
|
33
|
-
version: existing.version,
|
|
34
|
-
changes: { status: "active" as const },
|
|
35
|
-
},
|
|
36
|
-
event.user,
|
|
37
|
-
ctx.db,
|
|
38
|
-
);
|
|
39
|
-
if (!result.isSuccess) return result;
|
|
40
|
-
return { isSuccess: true as const, data: { id: String(existing.id), status: "active" } };
|
|
41
|
-
},
|
|
42
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { tenantEntity, tenantTable } from "../schema/tenant";
|
|
5
|
-
|
|
6
|
-
const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
|
|
7
|
-
|
|
8
|
-
export const disableWrite = defineWriteHandler({
|
|
9
|
-
name: "disable",
|
|
10
|
-
schema: z.object({ id: z.uuid() }),
|
|
11
|
-
access: { roles: ["SystemAdmin"] },
|
|
12
|
-
// Admin flip: last-writer-wins is fine. SystemAdmin is the only caller and
|
|
13
|
-
// there's no meaningful concurrent-edit race on this single boolean.
|
|
14
|
-
handler: async (event, ctx) =>
|
|
15
|
-
crud.update({ id: event.payload.id, changes: { isEnabled: false } }, event.user, ctx.db, {
|
|
16
|
-
skipOptimisticLock: true,
|
|
17
|
-
}),
|
|
18
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { tenantEntity, tenantTable } from "../schema/tenant";
|
|
5
|
-
|
|
6
|
-
const crud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
|
|
7
|
-
|
|
8
|
-
// Recovery-Gegenstück zu disable — ohne enable wäre ein Fehlklick des
|
|
9
|
-
// Operators nur per Event-Hack reversibel.
|
|
10
|
-
export const enableWrite = defineWriteHandler({
|
|
11
|
-
name: "enable",
|
|
12
|
-
schema: z.object({ id: z.uuid() }),
|
|
13
|
-
access: { roles: ["SystemAdmin"] },
|
|
14
|
-
// Admin flip: last-writer-wins is fine. SystemAdmin is the only caller and
|
|
15
|
-
// there's no meaningful concurrent-edit race on this single boolean.
|
|
16
|
-
handler: async (event, ctx) =>
|
|
17
|
-
crud.update({ id: event.payload.id, changes: { isEnabled: true } }, event.user, ctx.db, {
|
|
18
|
-
skipOptimisticLock: true,
|
|
19
|
-
}),
|
|
20
|
-
});
|