@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.16.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 +2 -2
- package/src/__tests__/env-schemas.test.ts +1 -1
- package/src/__tests__/es-ops-e2e.integration.ts +10 -9
- package/src/audit/__tests__/audit.integration.ts +3 -3
- package/src/audit/handlers/list.query.ts +39 -51
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
- package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
- package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
- package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
- package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
- package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
- package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
- package/src/billing-foundation/__tests__/feature.test.ts +1 -1
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
- package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
- package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
- package/src/billing-foundation/projection.ts +32 -13
- package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
- package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +14 -20
- package/src/cap-counter/handlers/get-counter.query.ts +7 -13
- package/src/cap-counter/handlers/increment.write.ts +2 -2
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-in-app/handlers/inbox.query.ts +7 -13
- package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
- package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
- package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
- package/src/channel-in-app/in-app-channel.ts +10 -12
- package/src/channel-in-app/tables.ts +1 -1
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
- package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
- package/src/compliance-profiles/_internal/parse-override.ts +19 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +10 -32
- package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
- package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
- package/src/compliance-profiles/resolve-for-tenant.ts +11 -27
- package/src/compliance-profiles/schema/profile-selection.ts +2 -2
- package/src/compliance-profiles/seeding.ts +4 -7
- package/src/config/__tests__/app-overrides.test.ts +1 -1
- package/src/config/__tests__/cascade.integration.ts +1 -1
- package/src/config/__tests__/config.integration.ts +8 -27
- package/src/config/db/queries/resolver.ts +47 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
- package/src/config/resolver.ts +14 -62
- package/src/config/table.ts +4 -4
- package/src/config/write-helpers.ts +7 -11
- package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
- package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
- package/src/custom-fields/__tests__/feature.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
- package/src/custom-fields/__tests__/quota.integration.ts +6 -6
- package/src/custom-fields/__tests__/retention.integration.ts +12 -10
- package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
- package/src/custom-fields/db/queries/field-access.ts +16 -0
- package/src/custom-fields/db/queries/projection.ts +43 -0
- package/src/custom-fields/db/queries/quota.ts +14 -0
- package/src/custom-fields/db/queries/retention.ts +39 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
- package/src/custom-fields/lib/field-access.ts +2 -41
- package/src/custom-fields/lib/quota.ts +2 -25
- package/src/custom-fields/run-retention.ts +19 -21
- package/src/custom-fields/wire-for-entity.ts +30 -23
- package/src/custom-fields/wire-user-data-rights.ts +33 -85
- package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
- package/src/data-retention/__tests__/keep-for.test.ts +1 -1
- package/src/data-retention/__tests__/override-schema.test.ts +1 -1
- package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
- package/src/data-retention/__tests__/resolver.test.ts +1 -1
- package/src/data-retention/handlers/policy-for.query.ts +5 -8
- package/src/data-retention/resolve-for-tenant.ts +6 -8
- package/src/data-retention/schema/tenant-retention-override.ts +2 -2
- package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
- package/src/delivery/__tests__/delivery.integration.ts +100 -190
- package/src/delivery/db/queries/preferences.ts +30 -0
- package/src/delivery/delivery-service.ts +8 -36
- package/src/delivery/feature.ts +10 -2
- package/src/delivery/handlers/log.query.ts +5 -7
- package/src/delivery/handlers/preferences.query.ts +2 -5
- package/src/delivery/tables.ts +26 -1
- package/src/delivery/upsert-preference.ts +8 -14
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
- package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
- package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
- package/src/feature-toggles/feature.ts +16 -2
- package/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +9 -2
- package/src/feature-toggles/handlers/registered.query.ts +3 -7
- package/src/feature-toggles/handlers/set.write.ts +37 -25
- package/src/feature-toggles/toggle-runtime.ts +3 -6
- package/src/file-foundation/__tests__/feature.test.ts +1 -1
- package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
- package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
- package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
- package/src/files/__tests__/files.integration.ts +18 -7
- package/src/files/schema/file-ref.ts +1 -1
- package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
- package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
- package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
- package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
- package/src/jobs/feature.ts +26 -15
- package/src/jobs/handlers/detail.query.ts +10 -8
- package/src/jobs/handlers/list.query.ts +9 -21
- package/src/jobs/handlers/retry.write.ts +2 -7
- package/src/jobs/job-run-logger.ts +3 -9
- package/src/jobs/job-run-table.ts +49 -17
- package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
- package/src/mail-foundation/__tests__/feature.test.ts +1 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
- package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
- package/src/renderer-foundation/__tests__/api.test.ts +2 -2
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
- package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
- package/src/secrets/__tests__/rotate.integration.ts +6 -9
- package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
- package/src/secrets/__tests__/secrets.integration.ts +6 -11
- package/src/secrets/db/queries/read.ts +16 -0
- package/src/secrets/handlers/list.query.ts +16 -17
- package/src/secrets/handlers/rotate.job.ts +8 -12
- package/src/secrets/secrets-context.ts +9 -21
- package/src/secrets/table.ts +1 -1
- package/src/sessions/__tests__/cleanup.integration.ts +8 -6
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
- package/src/sessions/__tests__/sessions.integration.ts +23 -38
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/db/queries/cleanup.ts +21 -0
- package/src/sessions/handlers/cleanup.job.ts +6 -29
- package/src/sessions/handlers/list.query.ts +24 -24
- package/src/sessions/handlers/mine.query.ts +24 -23
- package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
- package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
- package/src/sessions/handlers/revoke.write.ts +11 -18
- package/src/sessions/schema/user-session.ts +2 -2
- package/src/sessions/session-callbacks.ts +19 -21
- package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
- package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
- package/src/subscription-stripe/verify-webhook.ts +1 -1
- package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
- package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
- package/src/template-resolver/api.ts +7 -13
- package/src/template-resolver/handlers/archive.write.ts +4 -7
- package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
- package/src/template-resolver/handlers/list.query.ts +13 -21
- package/src/template-resolver/handlers/publish.write.ts +4 -7
- package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
- package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
- package/src/template-resolver/table.ts +2 -5
- package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
- package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
- package/src/tenant/__tests__/tenant.integration.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
- package/src/tenant/handlers/add-member.write.ts +6 -8
- package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
- package/src/tenant/handlers/invitations.query.ts +5 -10
- package/src/tenant/handlers/me.query.ts +2 -3
- package/src/tenant/handlers/members.query.ts +4 -5
- package/src/tenant/handlers/memberships.query.ts +2 -5
- package/src/tenant/handlers/remove-member.write.ts +6 -8
- package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
- package/src/tenant/handlers/update-member-roles.write.ts +6 -8
- package/src/tenant/invitation-table.ts +2 -5
- package/src/tenant/membership-table.ts +3 -6
- package/src/tenant/schema/tenant.ts +2 -2
- package/src/tenant/seeding.ts +12 -18
- package/src/text-content/README.md +1 -1
- package/src/text-content/__tests__/text-content.integration.ts +2 -2
- package/src/text-content/api.ts +2 -9
- package/src/text-content/handlers/by-slug.query.ts +6 -9
- package/src/text-content/handlers/by-tenant.query.ts +2 -2
- package/src/text-content/handlers/set.write.ts +7 -9
- package/src/text-content/seeding.ts +6 -9
- package/src/text-content/table.ts +2 -2
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
- package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
- package/src/text-content/web/client-plugin.tsx +11 -23
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
- package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
- package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
- package/src/tier-engine/feature.ts +9 -16
- package/src/user/__tests__/seed-testing.integration.ts +10 -22
- package/src/user/__tests__/user-status.test.ts +1 -1
- package/src/user/__tests__/user.integration.ts +6 -5
- package/src/user/handlers/create.write.ts +5 -7
- package/src/user/handlers/find-for-auth.query.ts +5 -7
- package/src/user/schema/user.ts +2 -2
- package/src/user/seeding.ts +2 -3
- package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
- package/src/user-data-rights/__tests__/download.integration.ts +29 -46
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
- package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
- package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
- package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
- package/src/user-data-rights/audit-download.ts +3 -3
- package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
- package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
- package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
- package/src/user-data-rights/handlers/export-status.query.ts +19 -33
- package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
- package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
- package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
- package/src/user-data-rights/handlers/request-export.write.ts +7 -11
- package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
- package/src/user-data-rights/run-export-jobs.ts +20 -60
- package/src/user-data-rights/run-forget-cleanup.ts +19 -33
- package/src/user-data-rights/run-user-export.ts +4 -6
- package/src/user-data-rights/schema/download-attempt.ts +2 -2
- package/src/user-data-rights/schema/download-token.ts +2 -2
- package/src/user-data-rights/schema/export-job.ts +2 -3
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
- package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
- package/CHANGELOG.md +0 -689
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
// Unit-Test pinned (policy-to-strategy.test.ts), nicht hier. Hier nur
|
|
16
16
|
// der end-to-end-Default-Pfad (delete).
|
|
17
17
|
|
|
18
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
19
|
+
import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
18
20
|
import {
|
|
19
21
|
setupTestStack,
|
|
20
22
|
type TestStack,
|
|
21
23
|
unsafeCreateEntityTable,
|
|
22
24
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
25
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
24
|
-
import { sql } from "drizzle-orm";
|
|
25
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
26
26
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
27
27
|
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
|
|
28
28
|
import { createFilesFeature } from "../../files";
|
|
@@ -70,7 +70,7 @@ beforeAll(async () => {
|
|
|
70
70
|
await unsafeCreateEntityTable(stack.db, tenantRetentionOverrideEntity);
|
|
71
71
|
// tenant-membership-Tabelle (von tenant-feature) manuell anlegen weil
|
|
72
72
|
// wir ohne tenant-feature im stack arbeiten — minimaler Setup.
|
|
73
|
-
await stack.db.
|
|
73
|
+
await asRawClient(stack.db).unsafe(`
|
|
74
74
|
CREATE TABLE IF NOT EXISTS read_tenant_memberships (
|
|
75
75
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
76
76
|
tenant_id UUID NOT NULL,
|
|
@@ -87,7 +87,7 @@ beforeAll(async () => {
|
|
|
87
87
|
UNIQUE(user_id, tenant_id)
|
|
88
88
|
)
|
|
89
89
|
`);
|
|
90
|
-
await stack.db.
|
|
90
|
+
await asRawClient(stack.db).unsafe(`
|
|
91
91
|
CREATE TABLE IF NOT EXISTS file_refs (
|
|
92
92
|
id UUID PRIMARY KEY,
|
|
93
93
|
tenant_id UUID NOT NULL,
|
|
@@ -109,9 +109,9 @@ afterAll(async () => {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
beforeEach(async () => {
|
|
112
|
-
await stack.db.
|
|
113
|
-
await stack.db.
|
|
114
|
-
await stack.db.
|
|
112
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
113
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_tenant_memberships`);
|
|
114
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM file_refs`);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
@@ -131,7 +131,7 @@ async function seedUser(
|
|
|
131
131
|
displayName?: string;
|
|
132
132
|
} = {},
|
|
133
133
|
): Promise<void> {
|
|
134
|
-
await stack.db
|
|
134
|
+
await insertOne(stack.db, userTable, {
|
|
135
135
|
id,
|
|
136
136
|
tenantId: TENANT_SYSTEM,
|
|
137
137
|
email: overrides.email ?? `user-${id}@example.com`,
|
|
@@ -146,11 +146,14 @@ async function seedUser(
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
async function seedMembership(userId: string, tenantId: string): Promise<void> {
|
|
149
|
-
await stack.db.
|
|
149
|
+
await asRawClient(stack.db).unsafe(
|
|
150
|
+
`
|
|
150
151
|
INSERT INTO read_tenant_memberships (tenant_id, user_id, roles)
|
|
151
|
-
VALUES ($
|
|
152
|
+
VALUES ($1, $2, '["Member"]')
|
|
152
153
|
ON CONFLICT (user_id, tenant_id) DO NOTHING
|
|
153
|
-
|
|
154
|
+
`,
|
|
155
|
+
[tenantId, userId],
|
|
156
|
+
);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
async function seedFileRef(
|
|
@@ -159,11 +162,14 @@ async function seedFileRef(
|
|
|
159
162
|
insertedById: string | null,
|
|
160
163
|
fileName: string,
|
|
161
164
|
): Promise<void> {
|
|
162
|
-
await stack.db.
|
|
165
|
+
await asRawClient(stack.db).unsafe(
|
|
166
|
+
`
|
|
163
167
|
INSERT INTO file_refs (id, tenant_id, storage_key, file_name, mime_type, size, inserted_by_id)
|
|
164
|
-
VALUES ($
|
|
168
|
+
VALUES ($1, $2, $3, $4, 'application/pdf', 1024, $5)
|
|
165
169
|
ON CONFLICT (id) DO NOTHING
|
|
166
|
-
|
|
170
|
+
`,
|
|
171
|
+
[id, tenantId, `storage/${id}`, fileName, insertedById],
|
|
172
|
+
);
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
async function fetchUser(id: string): Promise<{
|
|
@@ -173,10 +179,13 @@ async function fetchUser(id: string): Promise<{
|
|
|
173
179
|
status: string;
|
|
174
180
|
deleted_at: string | null;
|
|
175
181
|
} | null> {
|
|
176
|
-
const result = await stack.db.
|
|
182
|
+
const result = await asRawClient(stack.db).unsafe(
|
|
183
|
+
`
|
|
177
184
|
SELECT email, display_name, password_hash, status, deleted_at
|
|
178
|
-
FROM read_users WHERE id = $
|
|
179
|
-
|
|
185
|
+
FROM read_users WHERE id = $1
|
|
186
|
+
`,
|
|
187
|
+
[id],
|
|
188
|
+
);
|
|
180
189
|
// biome-ignore lint/suspicious/noExplicitAny: drizzle execute typing
|
|
181
190
|
const rows = ((result as any).rows ?? result) as Array<{
|
|
182
191
|
email: string;
|
|
@@ -189,18 +198,24 @@ async function fetchUser(id: string): Promise<{
|
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
async function fetchFileRefsForUser(tenantId: string, userId: string): Promise<unknown[]> {
|
|
192
|
-
const result = await stack.db.
|
|
201
|
+
const result = await asRawClient(stack.db).unsafe(
|
|
202
|
+
`
|
|
193
203
|
SELECT id, file_name, inserted_by_id
|
|
194
|
-
FROM file_refs WHERE tenant_id = $
|
|
195
|
-
|
|
204
|
+
FROM file_refs WHERE tenant_id = $1 AND inserted_by_id = $2
|
|
205
|
+
`,
|
|
206
|
+
[tenantId, userId],
|
|
207
|
+
);
|
|
196
208
|
// biome-ignore lint/suspicious/noExplicitAny: drizzle execute typing
|
|
197
209
|
return ((result as any).rows ?? result) as unknown[];
|
|
198
210
|
}
|
|
199
211
|
|
|
200
212
|
async function fetchAllFileRefs(tenantId: string): Promise<unknown[]> {
|
|
201
|
-
const result = await stack.db.
|
|
202
|
-
|
|
203
|
-
|
|
213
|
+
const result = await asRawClient(stack.db).unsafe(
|
|
214
|
+
`
|
|
215
|
+
SELECT id, file_name, inserted_by_id FROM file_refs WHERE tenant_id = $1
|
|
216
|
+
`,
|
|
217
|
+
[tenantId],
|
|
218
|
+
);
|
|
204
219
|
// biome-ignore lint/suspicious/noExplicitAny: drizzle execute typing
|
|
205
220
|
return ((result as any).rows ?? result) as unknown[];
|
|
206
221
|
}
|
|
@@ -245,7 +260,7 @@ describe("runForgetCleanup :: happy path (Cross-Tenant Account-Deletion)", () =>
|
|
|
245
260
|
expect(aliceRow?.email).not.toContain("alice@example.com");
|
|
246
261
|
expect(aliceRow?.email).toContain("anonymized.invalid");
|
|
247
262
|
expect([USER_DELETED_DISPLAY_NAME, USER_ANONYMIZED_DISPLAY_NAME]).toContain(
|
|
248
|
-
aliceRow
|
|
263
|
+
aliceRow!.display_name,
|
|
249
264
|
);
|
|
250
265
|
expect(aliceRow?.password_hash).toBeNull();
|
|
251
266
|
|
|
@@ -352,9 +367,12 @@ describe("runForgetCleanup :: PII-Audit nach Cleanup", () => {
|
|
|
352
367
|
});
|
|
353
368
|
|
|
354
369
|
// Cross-Tabellen-PII-Check: koennen wir noch IRGENDWO Original-Werte finden?
|
|
355
|
-
const userRows = await stack.db.
|
|
356
|
-
|
|
357
|
-
|
|
370
|
+
const userRows = await asRawClient(stack.db).unsafe(
|
|
371
|
+
`
|
|
372
|
+
SELECT id FROM read_users WHERE email = $1 OR display_name = $2
|
|
373
|
+
`,
|
|
374
|
+
[ORIGINAL_EMAIL, ORIGINAL_NAME],
|
|
375
|
+
);
|
|
358
376
|
// biome-ignore lint/suspicious/noExplicitAny: drizzle execute typing
|
|
359
377
|
const userMatches = (userRows as any).rows ?? userRows;
|
|
360
378
|
expect(userMatches).toHaveLength(0);
|
|
@@ -577,7 +595,7 @@ describe("runForgetCleanup :: sendDeletionExecutedEmail callback (Atom 5b)", ()
|
|
|
577
595
|
// einem vorigen Run schon anonymisiert hat aber status haengen blieb,
|
|
578
596
|
// oder durch external Migration). Skip schuetzt vor crashing-callback
|
|
579
597
|
// mit invaliden Args.
|
|
580
|
-
await stack.db
|
|
598
|
+
await insertOne(stack.db, userTable, {
|
|
581
599
|
id: ALICE_ID,
|
|
582
600
|
tenantId: TENANT_SYSTEM,
|
|
583
601
|
email: "",
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
// - Orphan-User (0 Memberships): user-Profil-Hook laeuft trotzdem
|
|
15
15
|
// ueber Pseudo-Tenant.
|
|
16
16
|
|
|
17
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
19
|
import {
|
|
18
20
|
setupTestStack,
|
|
19
21
|
type TestStack,
|
|
20
22
|
unsafeCreateEntityTable,
|
|
21
23
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
22
24
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
23
|
-
import { sql } from "drizzle-orm";
|
|
24
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
25
25
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
26
26
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
27
27
|
import { createFilesFeature } from "../../files";
|
|
@@ -57,7 +57,7 @@ beforeAll(async () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
60
|
-
await stack.db.
|
|
60
|
+
await asRawClient(stack.db).unsafe(`
|
|
61
61
|
CREATE TABLE IF NOT EXISTS read_tenant_memberships (
|
|
62
62
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
63
63
|
tenant_id UUID NOT NULL,
|
|
@@ -74,7 +74,7 @@ beforeAll(async () => {
|
|
|
74
74
|
UNIQUE(user_id, tenant_id)
|
|
75
75
|
)
|
|
76
76
|
`);
|
|
77
|
-
await stack.db.
|
|
77
|
+
await asRawClient(stack.db).unsafe(`
|
|
78
78
|
CREATE TABLE IF NOT EXISTS file_refs (
|
|
79
79
|
id UUID PRIMARY KEY,
|
|
80
80
|
tenant_id UUID NOT NULL,
|
|
@@ -96,9 +96,9 @@ afterAll(async () => {
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
beforeEach(async () => {
|
|
99
|
-
await stack.db.
|
|
100
|
-
await stack.db.
|
|
101
|
-
await stack.db.
|
|
99
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
100
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_tenant_memberships`);
|
|
101
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM file_refs`);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
const NOW = () => getTemporal().Now.instant();
|
|
@@ -107,7 +107,7 @@ async function seedUser(
|
|
|
107
107
|
id: string,
|
|
108
108
|
overrides: { email?: string; displayName?: string; roles?: string } = {},
|
|
109
109
|
): Promise<void> {
|
|
110
|
-
await stack.db
|
|
110
|
+
await insertOne(stack.db, userTable, {
|
|
111
111
|
id,
|
|
112
112
|
tenantId: TENANT_SYSTEM,
|
|
113
113
|
email: overrides.email ?? `user-${id}@example.com`,
|
|
@@ -121,11 +121,14 @@ async function seedUser(
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
async function seedMembership(userId: string, tenantId: string): Promise<void> {
|
|
124
|
-
await stack.db.
|
|
124
|
+
await asRawClient(stack.db).unsafe(
|
|
125
|
+
`
|
|
125
126
|
INSERT INTO read_tenant_memberships (tenant_id, user_id, roles)
|
|
126
|
-
VALUES ($
|
|
127
|
+
VALUES ($1, $2, '["Member"]')
|
|
127
128
|
ON CONFLICT (user_id, tenant_id) DO NOTHING
|
|
128
|
-
|
|
129
|
+
`,
|
|
130
|
+
[tenantId, userId],
|
|
131
|
+
);
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
async function seedFileRef(
|
|
@@ -134,11 +137,14 @@ async function seedFileRef(
|
|
|
134
137
|
insertedById: string | null,
|
|
135
138
|
fileName: string,
|
|
136
139
|
): Promise<void> {
|
|
137
|
-
await stack.db.
|
|
140
|
+
await asRawClient(stack.db).unsafe(
|
|
141
|
+
`
|
|
138
142
|
INSERT INTO file_refs (id, tenant_id, storage_key, file_name, mime_type, size, inserted_by_id)
|
|
139
|
-
VALUES ($
|
|
143
|
+
VALUES ($1, $2, $3, $4, 'application/pdf', 1024, $5)
|
|
140
144
|
ON CONFLICT (id) DO NOTHING
|
|
141
|
-
|
|
145
|
+
`,
|
|
146
|
+
[id, tenantId, `storage/${id}`, fileName, insertedById],
|
|
147
|
+
);
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
describe("runUserExport :: alle Daten enthalten + Cross-Tenant", () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// - Web-Crypto-Universal: laeuft in vitest (= bun-runtime via vitest)
|
|
7
7
|
// ohne node:crypto-Import. Memory `feedback_universal_deps`.
|
|
8
8
|
|
|
9
|
-
import { describe, expect, test } from "
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
10
|
import { generateDownloadToken, hashDownloadToken } from "../token-helpers";
|
|
11
11
|
|
|
12
12
|
describe("generateDownloadToken", () => {
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// Tieferer Cross-Feature-Test (mit useExtension(EXT_USER_DATA, ...) +
|
|
7
7
|
// Sprint-2-H1/H2-Hooks) kommt in S2.T1 (Cross-Data-Matrix).
|
|
8
8
|
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
10
|
import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
10
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
11
11
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
12
12
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
13
13
|
import { createUserFeature } from "../../user";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// Filename mit "../" einen ZIP-Reader dazu bringen, ausserhalb des
|
|
6
6
|
// Extract-Roots zu schreiben.
|
|
7
7
|
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
8
9
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
-
import { describe, expect, test } from "vitest";
|
|
10
10
|
import { buildFileRefZipPath, sanitizeZipFilename } from "../zip-path";
|
|
11
11
|
|
|
12
12
|
const TENANT = "00000000-0000-0000-0000-000000000001" as TenantId;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// **ES via tokenCrud.update:** kein direct-UPDATE. Memory
|
|
16
16
|
// `feedback_no_fake_dispatcher` + `feedback_event_store_tenant_consistency`.
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
19
19
|
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
20
20
|
import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
21
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
@@ -29,7 +29,7 @@ const attemptCrud = createEventStoreExecutor(downloadAttemptsTable, downloadAtte
|
|
|
29
29
|
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
30
30
|
|
|
31
31
|
export interface RecordDownloadUseArgs {
|
|
32
|
-
readonly db:
|
|
32
|
+
readonly db: DbRunner;
|
|
33
33
|
readonly tokenId: string;
|
|
34
34
|
readonly tokenVersion: number;
|
|
35
35
|
readonly tokenUseCount: number;
|
|
@@ -85,7 +85,7 @@ export async function recordDownloadUse(args: RecordDownloadUseArgs): Promise<vo
|
|
|
85
85
|
export type DownloadAttemptResult = "notFound" | "expired" | "failed" | "signedUrlNotSupported";
|
|
86
86
|
|
|
87
87
|
export interface RecordInvalidAttemptArgs {
|
|
88
|
-
readonly db:
|
|
88
|
+
readonly db: DbRunner;
|
|
89
89
|
readonly tenantId: TenantId;
|
|
90
90
|
readonly now: Instant;
|
|
91
91
|
readonly result: DownloadAttemptResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
|
|
5
|
+
export type ExportJobCleanupCandidate = {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly version: number;
|
|
8
|
+
readonly status: string;
|
|
9
|
+
readonly requestedFromTenantId: TenantId;
|
|
10
|
+
readonly downloadStorageKey: string | null;
|
|
11
|
+
readonly expiresAt: Temporal.Instant | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function selectExportJobsForStorageCleanup(
|
|
15
|
+
db: DbConnection,
|
|
16
|
+
doneStatus: string,
|
|
17
|
+
failedStatus: string,
|
|
18
|
+
): Promise<readonly ExportJobCleanupCandidate[]> {
|
|
19
|
+
return asRawClient(db).unsafe<ExportJobCleanupCandidate>(
|
|
20
|
+
`SELECT id, version, status, requested_from_tenant_id AS "requestedFromTenantId", download_storage_key AS "downloadStorageKey", expires_at AS "expiresAt" FROM read_export_jobs WHERE status IN ($1, $2) AND download_storage_key IS NOT NULL`,
|
|
21
|
+
[doneStatus, failedStatus],
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
|
|
4
|
+
export async function selectUsersDueForForgetCleanup(
|
|
5
|
+
db: DbConnection,
|
|
6
|
+
status: string,
|
|
7
|
+
gracePeriodEnd: string,
|
|
8
|
+
): Promise<readonly { id: string }[]> {
|
|
9
|
+
return asRawClient(db).unsafe<{ id: string }>(
|
|
10
|
+
`SELECT id FROM read_users WHERE status = $1 AND grace_period_end <= $2`,
|
|
11
|
+
[status, gracePeriodEnd],
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { fetchOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import { eq, sql } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { USER_STATUS, userTable } from "../../user";
|
|
6
6
|
|
|
@@ -23,18 +23,13 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
23
23
|
// ctx.db.raw (kein TenantDb-Wrapper) weil User-Entity tenant-agnostisch
|
|
24
24
|
// ist — siehe request-deletion.write.ts fuer die Begruendung. Cancel
|
|
25
25
|
// muss aus jedem Tenant-Mode den User finden + zuruecksetzen koennen.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
inGrace: sql<boolean>`(${userTable["gracePeriodEnd"]} > now())`,
|
|
32
|
-
})
|
|
33
|
-
.from(userTable)
|
|
34
|
-
.where(eq(userTable["id"], event.user.id))
|
|
35
|
-
.limit(1);
|
|
26
|
+
const row = await fetchOne<{ status: string; grace_period_end: Date | null }>(
|
|
27
|
+
ctx.db.raw,
|
|
28
|
+
userTable,
|
|
29
|
+
{ id: event.user.id },
|
|
30
|
+
);
|
|
36
31
|
|
|
37
|
-
if (
|
|
32
|
+
if (!row) {
|
|
38
33
|
return writeFailure(
|
|
39
34
|
new UnprocessableError("user_not_found", {
|
|
40
35
|
details: { reason: "user_not_found", userId: event.user.id },
|
|
@@ -42,19 +37,28 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
42
37
|
);
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
if (!row || row.status !== USER_STATUS.DeletionRequested) {
|
|
40
|
+
if (row["status"] !== USER_STATUS.DeletionRequested) {
|
|
47
41
|
return writeFailure(
|
|
48
42
|
new UnprocessableError("no_pending_deletion", {
|
|
49
43
|
details: {
|
|
50
44
|
reason: "no_pending_deletion",
|
|
51
|
-
currentStatus: row
|
|
45
|
+
currentStatus: row["status"],
|
|
52
46
|
},
|
|
53
47
|
}),
|
|
54
48
|
);
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
|
|
51
|
+
// inGrace computed JS-side: compare grace_period_end (Temporal.Instant
|
|
52
|
+
// from bun-db boundary) against current server clock.
|
|
53
|
+
const gracePeriodEnd = row["grace_period_end"];
|
|
54
|
+
const inGrace =
|
|
55
|
+
gracePeriodEnd != null &&
|
|
56
|
+
Temporal.Instant.compare(
|
|
57
|
+
gracePeriodEnd as unknown as Temporal.Instant,
|
|
58
|
+
Temporal.Now.instant(),
|
|
59
|
+
) > 0;
|
|
60
|
+
|
|
61
|
+
if (!inGrace) {
|
|
58
62
|
return writeFailure(
|
|
59
63
|
new UnprocessableError("grace_period_expired", {
|
|
60
64
|
details: { reason: "grace_period_expired" },
|
|
@@ -62,13 +66,15 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
62
66
|
);
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
await
|
|
66
|
-
.
|
|
67
|
-
|
|
69
|
+
await updateMany(
|
|
70
|
+
ctx.db.raw,
|
|
71
|
+
userTable,
|
|
72
|
+
{
|
|
68
73
|
status: USER_STATUS.Active,
|
|
69
74
|
gracePeriodEnd: null,
|
|
70
|
-
}
|
|
71
|
-
|
|
75
|
+
},
|
|
76
|
+
{ id: event.user.id },
|
|
77
|
+
);
|
|
72
78
|
|
|
73
79
|
// gracePeriodEnd=null im Response symmetrisch zu request-deletion's
|
|
74
80
|
// ISO-Timestamp — Frontend kann beide Endpoints uniform behandeln.
|
|
@@ -77,7 +83,7 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
77
83
|
data: {
|
|
78
84
|
userId: event.user.id,
|
|
79
85
|
status: USER_STATUS.Active,
|
|
80
|
-
gracePeriodEnd: null as string | null,
|
|
86
|
+
gracePeriodEnd: null as string | null,
|
|
81
87
|
},
|
|
82
88
|
};
|
|
83
89
|
},
|
|
@@ -23,12 +23,10 @@
|
|
|
23
23
|
// 6. Audit-Update: useCount + 1, IP, UA, lastUsedAt (best-effort)
|
|
24
24
|
// 7. Return {url, expiresAt}
|
|
25
25
|
|
|
26
|
-
import
|
|
27
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
26
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
28
27
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
29
28
|
import { NotFoundError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
30
29
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
31
|
-
import { eq } from "drizzle-orm";
|
|
32
30
|
import { z } from "zod";
|
|
33
31
|
import { createFileProviderForTenant } from "../../file-foundation";
|
|
34
32
|
import { recordDownloadUse, recordInvalidAttempt } from "../audit-download";
|
|
@@ -76,15 +74,11 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
76
74
|
// Step 1-2: job-lookup + cross-user-isolation
|
|
77
75
|
// ctx.db.raw weil tenant-agnostisch — Alice in Tenant B sucht den
|
|
78
76
|
// aus Tenant A erstellten Job.
|
|
79
|
-
const jobRow =
|
|
80
|
-
ctx.db.raw,
|
|
81
|
-
exportJobsTable,
|
|
82
|
-
eq(exportJobsTable["id"], jobId),
|
|
83
|
-
)) as JobRow | null; // @cast-boundary db-row
|
|
77
|
+
const jobRow = await fetchOne<JobRow>(ctx.db.raw, exportJobsTable, { id: jobId });
|
|
84
78
|
|
|
85
79
|
if (!jobRow || jobRow.userId !== userId) {
|
|
86
80
|
await recordInvalidAttempt({
|
|
87
|
-
db: ctx.db.raw
|
|
81
|
+
db: ctx.db.raw,
|
|
88
82
|
tenantId,
|
|
89
83
|
now,
|
|
90
84
|
result: "notFound",
|
|
@@ -102,7 +96,7 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
102
96
|
|
|
103
97
|
if (jobRow.status !== EXPORT_JOB_STATUS.Done) {
|
|
104
98
|
await recordInvalidAttempt({
|
|
105
|
-
db: ctx.db.raw
|
|
99
|
+
db: ctx.db.raw,
|
|
106
100
|
tenantId,
|
|
107
101
|
now,
|
|
108
102
|
result: "failed",
|
|
@@ -119,7 +113,7 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
119
113
|
}
|
|
120
114
|
if (!jobRow.downloadStorageKey) {
|
|
121
115
|
await recordInvalidAttempt({
|
|
122
|
-
db: ctx.db.raw
|
|
116
|
+
db: ctx.db.raw,
|
|
123
117
|
tenantId,
|
|
124
118
|
now,
|
|
125
119
|
result: "expired",
|
|
@@ -142,7 +136,7 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
142
136
|
);
|
|
143
137
|
if (!provider.getSignedUrl) {
|
|
144
138
|
await recordInvalidAttempt({
|
|
145
|
-
db: ctx.db.raw
|
|
139
|
+
db: ctx.db.raw,
|
|
146
140
|
tenantId,
|
|
147
141
|
now,
|
|
148
142
|
result: "signedUrlNotSupported",
|
|
@@ -173,21 +167,17 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
173
167
|
// den plain-Token, aber wir wollen den useCount inkrementieren
|
|
174
168
|
// damit die Audit-Felder konsistent sind (UI-clicks zaehlen auch
|
|
175
169
|
// als Use). Lookup via jobId — UNIQUE-Index garantiert max 1 Row.
|
|
176
|
-
const tokenRow =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
eq(exportDownloadTokensTable["jobId"], jobId),
|
|
180
|
-
)) as TokenRow | null; // @cast-boundary db-row
|
|
170
|
+
const tokenRow = await fetchOne<TokenRow>(ctx.db.raw, exportDownloadTokensTable, {
|
|
171
|
+
jobId,
|
|
172
|
+
});
|
|
181
173
|
|
|
182
174
|
if (tokenRow) {
|
|
183
175
|
await recordDownloadUse({
|
|
184
|
-
db: ctx.db.raw
|
|
176
|
+
db: ctx.db.raw,
|
|
185
177
|
tokenId: tokenRow.id,
|
|
186
178
|
tokenVersion: tokenRow.version,
|
|
187
179
|
tokenUseCount: tokenRow.useCount ?? 0,
|
|
188
|
-
tenantId: jobRow.requestedFromTenantId
|
|
189
|
-
typeof recordDownloadUse
|
|
190
|
-
>[0]["tenantId"], // @cast-boundary engine-bridge
|
|
180
|
+
tenantId: jobRow.requestedFromTenantId,
|
|
191
181
|
now,
|
|
192
182
|
ip: query.payload.auditMeta?.ip ?? null,
|
|
193
183
|
userAgent: query.payload.auditMeta?.userAgent ?? null,
|
|
@@ -26,12 +26,10 @@
|
|
|
26
26
|
// signedUrl — User klickt 1× Email-Link, Browser folgt redirect, Download
|
|
27
27
|
// startet. Dieser query-handler liefert nur das JSON.
|
|
28
28
|
|
|
29
|
-
import
|
|
30
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
29
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
31
30
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
32
31
|
import { NotFoundError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
33
32
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
34
|
-
import { eq } from "drizzle-orm";
|
|
35
33
|
import { z } from "zod";
|
|
36
34
|
import { createFileProviderForTenant } from "../../file-foundation";
|
|
37
35
|
import { recordDownloadUse, recordInvalidAttempt } from "../audit-download";
|
|
@@ -87,11 +85,9 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
87
85
|
const hash = await hashDownloadToken(query.payload.token);
|
|
88
86
|
// ctx.db.raw weil Token+Job tenant-agnostisch — anonymous-pfad hat
|
|
89
87
|
// keinen tenant-context im query.user.
|
|
90
|
-
const tokenRow =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
eq(exportDownloadTokensTable["tokenHash"], hash),
|
|
94
|
-
)) as TokenRow | null; // @cast-boundary db-row
|
|
88
|
+
const tokenRow = await fetchOne<TokenRow>(ctx.db.raw, exportDownloadTokensTable, {
|
|
89
|
+
tokenHash: hash,
|
|
90
|
+
});
|
|
95
91
|
|
|
96
92
|
if (!tokenRow) {
|
|
97
93
|
// Invalid token — 404 ohne Existenz-Leak. Generic NotFoundError
|
|
@@ -116,18 +112,15 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
116
112
|
// Audit-Skip noch nicht moeglich — jobRow noch nicht geladen,
|
|
117
113
|
// tenantId unbekannt. Wir laden den Job hier noch fuer Audit-Context
|
|
118
114
|
// (best-effort — wenn Job auch fehlt, audit-skip ist akzeptabel).
|
|
119
|
-
const jobForAudit =
|
|
115
|
+
const jobForAudit = await fetchOne<{ requestedFromTenantId: string }>(
|
|
120
116
|
ctx.db.raw,
|
|
121
117
|
exportJobsTable,
|
|
122
|
-
|
|
123
|
-
)
|
|
118
|
+
{ id: tokenRow.jobId },
|
|
119
|
+
);
|
|
124
120
|
if (jobForAudit) {
|
|
125
|
-
const auditDb = ctx.db.raw as DbConnection; // @cast-boundary db-runner
|
|
126
121
|
await recordInvalidAttempt({
|
|
127
|
-
db:
|
|
128
|
-
tenantId: jobForAudit.requestedFromTenantId
|
|
129
|
-
typeof recordInvalidAttempt
|
|
130
|
-
>[0]["tenantId"], // @cast-boundary engine-bridge
|
|
122
|
+
db: ctx.db.raw,
|
|
123
|
+
tenantId: jobForAudit.requestedFromTenantId,
|
|
131
124
|
now,
|
|
132
125
|
result: "expired",
|
|
133
126
|
via: "token",
|
|
@@ -144,11 +137,9 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
144
137
|
}
|
|
145
138
|
|
|
146
139
|
// Step 3-4: job-checks
|
|
147
|
-
const jobRow =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
eq(exportJobsTable["id"], tokenRow.jobId),
|
|
151
|
-
)) as JobRow | null; // @cast-boundary db-row
|
|
140
|
+
const jobRow = await fetchOne<JobRow>(ctx.db.raw, exportJobsTable, {
|
|
141
|
+
id: tokenRow.jobId,
|
|
142
|
+
});
|
|
152
143
|
|
|
153
144
|
if (!jobRow) {
|
|
154
145
|
throw new NotFoundError("export-download", undefined, {
|
|
@@ -157,10 +148,8 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
157
148
|
}
|
|
158
149
|
if (jobRow.status !== EXPORT_JOB_STATUS.Done) {
|
|
159
150
|
await recordInvalidAttempt({
|
|
160
|
-
db: ctx.db.raw
|
|
161
|
-
tenantId: jobRow.requestedFromTenantId
|
|
162
|
-
typeof recordInvalidAttempt
|
|
163
|
-
>[0]["tenantId"], // @cast-boundary engine-bridge
|
|
151
|
+
db: ctx.db.raw,
|
|
152
|
+
tenantId: jobRow.requestedFromTenantId,
|
|
164
153
|
now,
|
|
165
154
|
result: "failed",
|
|
166
155
|
via: "token",
|
|
@@ -176,10 +165,8 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
176
165
|
}
|
|
177
166
|
if (!jobRow.downloadStorageKey) {
|
|
178
167
|
await recordInvalidAttempt({
|
|
179
|
-
db: ctx.db.raw
|
|
180
|
-
tenantId: jobRow.requestedFromTenantId
|
|
181
|
-
typeof recordInvalidAttempt
|
|
182
|
-
>[0]["tenantId"], // @cast-boundary engine-bridge
|
|
168
|
+
db: ctx.db.raw,
|
|
169
|
+
tenantId: jobRow.requestedFromTenantId,
|
|
183
170
|
now,
|
|
184
171
|
result: "expired",
|
|
185
172
|
via: "token",
|
|
@@ -203,10 +190,8 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
203
190
|
);
|
|
204
191
|
if (!provider.getSignedUrl) {
|
|
205
192
|
await recordInvalidAttempt({
|
|
206
|
-
db: ctx.db.raw
|
|
207
|
-
tenantId: jobRow.requestedFromTenantId
|
|
208
|
-
typeof recordInvalidAttempt
|
|
209
|
-
>[0]["tenantId"], // @cast-boundary engine-bridge
|
|
193
|
+
db: ctx.db.raw,
|
|
194
|
+
tenantId: jobRow.requestedFromTenantId,
|
|
210
195
|
now,
|
|
211
196
|
result: "signedUrlNotSupported",
|
|
212
197
|
via: "token",
|
|
@@ -236,11 +221,11 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
236
221
|
// Wrapper (trusted-source). Direct-API-caller koennen luegen, aber
|
|
237
222
|
// Audit ist nicht security-relevant.
|
|
238
223
|
await recordDownloadUse({
|
|
239
|
-
db: ctx.db.raw
|
|
224
|
+
db: ctx.db.raw,
|
|
240
225
|
tokenId: tokenRow.id,
|
|
241
226
|
tokenVersion: tokenRow.version,
|
|
242
227
|
tokenUseCount: tokenRow.useCount ?? 0,
|
|
243
|
-
tenantId: jobRow.requestedFromTenantId
|
|
228
|
+
tenantId: jobRow.requestedFromTenantId,
|
|
244
229
|
now,
|
|
245
230
|
ip: query.payload.auditMeta?.ip ?? null,
|
|
246
231
|
userAgent: query.payload.auditMeta?.userAgent ?? null,
|