@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.15.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/handlers/for-tenant.query.ts +4 -7
- 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 +5 -7
- 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 +2 -1
- 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 +22 -14
- 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
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// Gegen-Beweis: User OHNE globale Rollen verhält sich wie vorher
|
|
9
9
|
// (nur tenant-membership-roles in der Session).
|
|
10
10
|
|
|
11
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { asRawClient, insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
11
13
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
12
14
|
import {
|
|
13
15
|
setupTestStack,
|
|
@@ -17,7 +19,6 @@ import {
|
|
|
17
19
|
unsafeCreateEntityTable,
|
|
18
20
|
unsafePushTables,
|
|
19
21
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
20
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
21
22
|
import { createConfigFeature } from "../../config";
|
|
22
23
|
import { createConfigResolver } from "../../config/resolver";
|
|
23
24
|
import { configValuesTable } from "../../config/table";
|
|
@@ -71,8 +72,8 @@ afterAll(async () => {
|
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
beforeEach(async () => {
|
|
74
|
-
await stack.db.
|
|
75
|
-
await stack.db.
|
|
75
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
76
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
async function seedUser(
|
|
@@ -101,7 +102,7 @@ async function addMembership(
|
|
|
101
102
|
tenantId: TenantId,
|
|
102
103
|
roles: readonly string[],
|
|
103
104
|
): Promise<void> {
|
|
104
|
-
await stack.db
|
|
105
|
+
await insertOne(stack.db, tenantMembershipsTable, {
|
|
105
106
|
userId,
|
|
106
107
|
tenantId,
|
|
107
108
|
roles: JSON.stringify(roles),
|
|
@@ -129,11 +130,7 @@ describe("multi-roles: login mergt globale + membership-roles", () => {
|
|
|
129
130
|
// Pin write-path: roles MUSS in DB landen, sonst ist der session-merge
|
|
130
131
|
// nur Zufall (z.B. wenn login-handler hardcoded SystemAdmin reinpacken
|
|
131
132
|
// würde). Direct DB-read schließt das aus.
|
|
132
|
-
const
|
|
133
|
-
const dbRow = await stack.db
|
|
134
|
-
.select({ roles: userTable["roles"] })
|
|
135
|
-
.from(userTable)
|
|
136
|
-
.where(eq(userTable["id"], userId));
|
|
133
|
+
const dbRow = await selectMany(stack.db, userTable, { id: userId });
|
|
137
134
|
expect(dbRow[0]?.roles).toBe(JSON.stringify(["SystemAdmin"]));
|
|
138
135
|
|
|
139
136
|
const { user } = await login("syadmin@example.com", "pw-long-enough");
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
1
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
4
|
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
3
5
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
6
|
import {
|
|
@@ -9,7 +11,6 @@ import {
|
|
|
9
11
|
unsafePushTables,
|
|
10
12
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
11
13
|
import { Temporal } from "temporal-polyfill";
|
|
12
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
13
14
|
import { createConfigFeature } from "../../config";
|
|
14
15
|
import { createConfigResolver } from "../../config/resolver";
|
|
15
16
|
import { configValuesTable } from "../../config/table";
|
|
@@ -93,9 +94,9 @@ afterAll(async () => {
|
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
beforeEach(async () => {
|
|
96
|
-
await stack.db.
|
|
97
|
-
await stack.db.
|
|
98
|
-
await stack.db.
|
|
97
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
98
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
99
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userSessionTable.tableName}"`);
|
|
99
100
|
capturedEmails.length = 0;
|
|
100
101
|
autoRevokeCalls.length = 0;
|
|
101
102
|
});
|
|
@@ -182,7 +183,7 @@ describe("POST /auth/reset-password", () => {
|
|
|
182
183
|
|
|
183
184
|
// Proof: the new password actually hashes in. Read the row, verify the
|
|
184
185
|
// hash matches the new plaintext.
|
|
185
|
-
const row = (await stack.db
|
|
186
|
+
const row = (await selectMany(stack.db, userTable)).find((r) => r["id"] === seed.id);
|
|
186
187
|
if (!row?.["passwordHash"]) throw new Error("user row / hash missing");
|
|
187
188
|
expect(await verifyPassword(row["passwordHash"] as string, "brand-new-pw-9876")).toBe(true);
|
|
188
189
|
expect(await verifyPassword(row["passwordHash"] as string, "old-pw-1234")).toBe(false);
|
|
@@ -203,7 +204,7 @@ describe("POST /auth/reset-password", () => {
|
|
|
203
204
|
expect(body.error?.details?.reason).toBe(AuthErrors.invalidResetToken);
|
|
204
205
|
|
|
205
206
|
// Old password still wins.
|
|
206
|
-
const row = (await stack.db
|
|
207
|
+
const row = (await selectMany(stack.db, userTable)).find((r) => r["id"] === seed.id);
|
|
207
208
|
if (!row?.["passwordHash"]) throw new Error("user row / hash missing");
|
|
208
209
|
expect(await verifyPassword(row["passwordHash"] as string, "keep-me!")).toBe(true);
|
|
209
210
|
});
|
|
@@ -262,7 +263,7 @@ describe("POST /auth/reset-password", () => {
|
|
|
262
263
|
const seed = await seedUser({ email: "retry@example.com", password: "pw-retry-1234" });
|
|
263
264
|
const { token } = signResetToken(seed.id, 15, resetSecret);
|
|
264
265
|
|
|
265
|
-
await stack.db.
|
|
266
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
266
267
|
const firstAttempt = await post("/api/auth/reset-password", {
|
|
267
268
|
token,
|
|
268
269
|
newPassword: "never-lands-1234",
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// comment — a regression that silently moves the routes out of /api/auth/*
|
|
5
5
|
// or tightens loginRateLimit but forgets these would sail through.
|
|
6
6
|
|
|
7
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
7
8
|
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
8
10
|
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
9
11
|
import {
|
|
10
12
|
setupTestStack,
|
|
@@ -12,7 +14,6 @@ import {
|
|
|
12
14
|
unsafeCreateEntityTable,
|
|
13
15
|
unsafePushTables,
|
|
14
16
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
15
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
16
17
|
import { createConfigFeature } from "../../config";
|
|
17
18
|
import { createConfigResolver } from "../../config/resolver";
|
|
18
19
|
import { configValuesTable } from "../../config/table";
|
|
@@ -77,8 +78,8 @@ afterAll(async () => {
|
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
beforeEach(async () => {
|
|
80
|
-
await stack.db.
|
|
81
|
-
await stack.db.
|
|
81
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
82
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
// Unique IP per test so buckets don't cross-contaminate. L2 default bucket
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// 4. Rollen pro Tenant landen korrekt (unterschiedliche Rollen-Listen
|
|
11
11
|
// pro Membership).
|
|
12
12
|
|
|
13
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
13
15
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
16
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
15
17
|
import {
|
|
@@ -18,8 +20,6 @@ import {
|
|
|
18
20
|
unsafeCreateEntityTable,
|
|
19
21
|
unsafePushTables,
|
|
20
22
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
-
import { and, eq } from "drizzle-orm";
|
|
22
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
23
23
|
import { createConfigFeature } from "../../config/feature";
|
|
24
24
|
import { createConfigResolver } from "../../config/resolver";
|
|
25
25
|
import { configValuesTable } from "../../config/table";
|
|
@@ -53,10 +53,10 @@ afterAll(async () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
beforeEach(async () => {
|
|
56
|
-
await stack.db.
|
|
57
|
-
await stack.db.
|
|
58
|
-
await stack.db.
|
|
59
|
-
await stack.db.
|
|
56
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
57
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantTable.tableName}"`);
|
|
58
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
59
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${eventsTable.tableName}"`);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
describe("seedAdmin", () => {
|
|
@@ -82,14 +82,11 @@ describe("seedAdmin", () => {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
// Tenants angelegt
|
|
85
|
-
const tenants = await stack.db
|
|
85
|
+
const tenants = await selectMany(stack.db, tenantTable);
|
|
86
86
|
expect(tenants.map((t) => t["id"]).sort()).toEqual([TENANT_DEV, TENANT_BETA].sort());
|
|
87
87
|
|
|
88
88
|
// User angelegt mit Hash (NICHT plain-Password)
|
|
89
|
-
const [user] = await stack.db
|
|
90
|
-
.select()
|
|
91
|
-
.from(userTable)
|
|
92
|
-
.where(eq(userTable["email"], "admin@example.com"));
|
|
89
|
+
const [user] = await selectMany(stack.db, userTable, { email: "admin@example.com" });
|
|
93
90
|
expect(user?.["id"]).toBe(userId);
|
|
94
91
|
expect(user?.["passwordHash"]).not.toBe("secret-pw");
|
|
95
92
|
expect(user?.["passwordHash"]).toMatch(/^\$argon2/);
|
|
@@ -101,26 +98,16 @@ describe("seedAdmin", () => {
|
|
|
101
98
|
expect(invalid).toBe(false);
|
|
102
99
|
|
|
103
100
|
// Memberships pro Tenant mit unterschiedlichen Rollen
|
|
104
|
-
const devMembership = await stack.db
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
and(
|
|
109
|
-
eq(tenantMembershipsTable.userId, userId),
|
|
110
|
-
eq(tenantMembershipsTable.tenantId, TENANT_DEV),
|
|
111
|
-
),
|
|
112
|
-
);
|
|
101
|
+
const devMembership = await selectMany(stack.db, tenantMembershipsTable, {
|
|
102
|
+
userId: userId,
|
|
103
|
+
tenantId: TENANT_DEV,
|
|
104
|
+
});
|
|
113
105
|
expect(devMembership[0]?.["roles"]).toBe(JSON.stringify(["Admin"]));
|
|
114
106
|
|
|
115
|
-
const betaMembership = await stack.db
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
and(
|
|
120
|
-
eq(tenantMembershipsTable.userId, userId),
|
|
121
|
-
eq(tenantMembershipsTable.tenantId, TENANT_BETA),
|
|
122
|
-
),
|
|
123
|
-
);
|
|
107
|
+
const betaMembership = await selectMany(stack.db, tenantMembershipsTable, {
|
|
108
|
+
userId: userId,
|
|
109
|
+
tenantId: TENANT_BETA,
|
|
110
|
+
});
|
|
124
111
|
expect(betaMembership[0]?.["roles"]).toBe(JSON.stringify(["User"]));
|
|
125
112
|
});
|
|
126
113
|
|
|
@@ -148,7 +135,7 @@ describe("seedAdmin", () => {
|
|
|
148
135
|
expect(userId2).toBe(userId1);
|
|
149
136
|
|
|
150
137
|
// Genau ein User-Row, original-Hash (passt zu pw1, nicht pw2).
|
|
151
|
-
const users = await stack.db
|
|
138
|
+
const users = await selectMany(stack.db, userTable);
|
|
152
139
|
expect(users).toHaveLength(1);
|
|
153
140
|
const valid = await verifyPassword(users[0]?.["passwordHash"] as string, "pw1");
|
|
154
141
|
expect(valid).toBe(true);
|
|
@@ -156,11 +143,11 @@ describe("seedAdmin", () => {
|
|
|
156
143
|
expect(invalid).toBe(false);
|
|
157
144
|
|
|
158
145
|
// Genau ein Membership-Row.
|
|
159
|
-
const memberships = await stack.db
|
|
146
|
+
const memberships = await selectMany(stack.db, tenantMembershipsTable);
|
|
160
147
|
expect(memberships).toHaveLength(1);
|
|
161
148
|
|
|
162
149
|
// Genau ein .created-Event pro Aggregat-Typ.
|
|
163
|
-
const events = await stack.db
|
|
150
|
+
const events = await selectMany(stack.db, eventsTable);
|
|
164
151
|
const createdByType = events
|
|
165
152
|
.filter((e) => e.type.endsWith(".created"))
|
|
166
153
|
.reduce<Record<string, number>>((acc, e) => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
1
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
4
|
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
3
5
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
6
|
import {
|
|
@@ -9,7 +11,6 @@ import {
|
|
|
9
11
|
unsafePushTables,
|
|
10
12
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
11
13
|
import * as jose from "jose";
|
|
12
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
13
14
|
import { createConfigFeature } from "../../config";
|
|
14
15
|
import { createConfigResolver } from "../../config/resolver";
|
|
15
16
|
import { configValuesTable } from "../../config/table";
|
|
@@ -126,8 +127,8 @@ afterAll(async () => {
|
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
beforeEach(async () => {
|
|
129
|
-
await stack.db.
|
|
130
|
-
await stack.db.
|
|
130
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
131
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
131
132
|
// Reset the in-memory store but KEEP sidStream running — otherwise a test
|
|
132
133
|
// that leaks a sid into another test would produce confusing collisions.
|
|
133
134
|
store.live.clear();
|
|
@@ -235,7 +236,7 @@ describe("logout routes through sessionRevoker", () => {
|
|
|
235
236
|
expect(logoutRes.status).toBe(200);
|
|
236
237
|
|
|
237
238
|
// Revoker was called with exactly the sid from the caller's JWT
|
|
238
|
-
expect(store.revoked).toEqual([sidBefore]);
|
|
239
|
+
expect(store.revoked).toEqual([sidBefore!]);
|
|
239
240
|
expect(store.live.has(sidBefore ?? "")).toBe(false);
|
|
240
241
|
});
|
|
241
242
|
|
|
@@ -279,7 +280,7 @@ describe("switch-tenant rotates the session", () => {
|
|
|
279
280
|
expect(sidB).not.toBe(sidA);
|
|
280
281
|
|
|
281
282
|
// Old sid is revoked; new sid is live
|
|
282
|
-
expect(store.revoked).toContain(sidA);
|
|
283
|
+
expect(store.revoked).toContain(sidA!);
|
|
283
284
|
expect(store.live.has(sidA ?? "")).toBe(false);
|
|
284
285
|
expect(store.live.has(sidB ?? "")).toBe(true);
|
|
285
286
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// so legacy stateless tokens are expected to have expired. Default false
|
|
5
5
|
// keeps pre-upgrade tokens working; this suite flips it on and asserts.
|
|
6
6
|
|
|
7
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
7
8
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
9
|
import {
|
|
9
10
|
setupTestStack,
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
11
12
|
TestUsers,
|
|
12
13
|
testTenantId,
|
|
13
14
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
15
15
|
import { createConfigFeature } from "../../config";
|
|
16
16
|
import { createTenantFeature } from "../../tenant";
|
|
17
17
|
import { createUserFeature } from "../../user";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
1
2
|
import { Temporal } from "temporal-polyfill";
|
|
2
|
-
import { describe, expect, test } from "vitest";
|
|
3
3
|
import { signToken, TokenPurpose, verifyToken } from "../signed-token";
|
|
4
4
|
|
|
5
5
|
const SECRET = "test-hmac-secret-32-bytes-minimum!!";
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
// via DB-unique-index + generateUniqueName-isAvailable-check
|
|
25
25
|
// zusammen).
|
|
26
26
|
|
|
27
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
28
|
+
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
27
29
|
import {
|
|
28
30
|
setupTestStack,
|
|
29
31
|
type TestStack,
|
|
30
32
|
unsafeCreateEntityTable,
|
|
31
33
|
unsafePushTables,
|
|
32
34
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
33
|
-
import { eq } from "drizzle-orm";
|
|
34
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
35
35
|
import { createConfigFeature } from "../../config";
|
|
36
36
|
import { createConfigResolver } from "../../config/resolver";
|
|
37
37
|
import { configValuesTable } from "../../config/table";
|
|
@@ -80,7 +80,7 @@ beforeAll(async () => {
|
|
|
80
80
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
81
81
|
// tenant-entity hat den unique-constraint auf .key (siehe
|
|
82
82
|
// tenant.schema.indexes). unsafeCreateEntityTable baut das via
|
|
83
|
-
//
|
|
83
|
+
// buildEntityTable nach — pinst den TOCTOU-Schutz für signup-confirm.
|
|
84
84
|
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
85
85
|
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
86
86
|
});
|
|
@@ -90,9 +90,9 @@ afterAll(async () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
beforeEach(async () => {
|
|
93
|
-
await stack.db.
|
|
94
|
-
await stack.db.
|
|
95
|
-
await stack.db.
|
|
93
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
94
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
95
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantTable.tableName}"`);
|
|
96
96
|
capturedActivationEmails.length = 0;
|
|
97
97
|
// Redis-cleanup damit Resend-Tests keine state-leaks haben.
|
|
98
98
|
const allKeys = await stack.redis.redis.keys("signup:*");
|
|
@@ -185,22 +185,18 @@ describe("POST /api/auth/signup-confirm", () => {
|
|
|
185
185
|
expect(setCookies).toContain("kumiko_csrf=");
|
|
186
186
|
|
|
187
187
|
// DB-State pinst
|
|
188
|
-
const userRows = await stack.db
|
|
188
|
+
const userRows = await selectMany(stack.db, userTable, { email: email });
|
|
189
189
|
expect(userRows).toHaveLength(1);
|
|
190
190
|
expect(userRows[0]?.["emailVerified"]).toBe(true);
|
|
191
191
|
expect(userRows[0]?.["passwordHash"]).toBeTruthy();
|
|
192
192
|
|
|
193
|
-
const tenantRows = await stack.db
|
|
194
|
-
.select()
|
|
195
|
-
.from(tenantTable)
|
|
196
|
-
.where(eq(tenantTable.id, body.user?.tenantId ?? ""));
|
|
193
|
+
const tenantRows = await selectMany(stack.db, tenantTable, { id: body.user?.tenantId ?? "" });
|
|
197
194
|
expect(tenantRows).toHaveLength(1);
|
|
198
195
|
expect(tenantRows[0]?.["key"]).toBe(body.tenantKey);
|
|
199
196
|
|
|
200
|
-
const memberships = await stack.db
|
|
201
|
-
.
|
|
202
|
-
|
|
203
|
-
.where(eq(tenantMembershipsTable.userId, body.user?.id ?? ""));
|
|
197
|
+
const memberships = await selectMany(stack.db, tenantMembershipsTable, {
|
|
198
|
+
userId: body.user?.id ?? "",
|
|
199
|
+
});
|
|
204
200
|
expect(memberships).toHaveLength(1);
|
|
205
201
|
const rolesRaw = memberships[0]?.["roles"];
|
|
206
202
|
if (typeof rolesRaw === "string") {
|
|
@@ -17,12 +17,8 @@
|
|
|
17
17
|
// User entsteht — beide existieren bereits. Magic ist die kombinierte
|
|
18
18
|
// Login+Accept-Operation in einem Roundtrip.
|
|
19
19
|
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
createTenantDb,
|
|
23
|
-
type DbConnection,
|
|
24
|
-
fetchOne,
|
|
25
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
20
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
21
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
26
22
|
import {
|
|
27
23
|
createSystemUser,
|
|
28
24
|
defineWriteHandler,
|
|
@@ -34,7 +30,6 @@ import {
|
|
|
34
30
|
UnprocessableError,
|
|
35
31
|
writeFailure,
|
|
36
32
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
37
|
-
import { eq } from "drizzle-orm";
|
|
38
33
|
import { z } from "zod";
|
|
39
34
|
// kumiko-lint-ignore cross-feature-import invite-flow
|
|
40
35
|
import {
|
|
@@ -104,20 +99,27 @@ export function createInviteAcceptWithLoginHandler() {
|
|
|
104
99
|
const burn = await burnInviteToken(ctx.redis, event.payload.token);
|
|
105
100
|
if (burn === "already-used") return invalidInviteToken();
|
|
106
101
|
|
|
102
|
+
type InvitationRow = {
|
|
103
|
+
readonly status: string;
|
|
104
|
+
readonly tenantId: TenantId;
|
|
105
|
+
readonly email: string;
|
|
106
|
+
readonly role: string;
|
|
107
|
+
readonly version: number;
|
|
108
|
+
};
|
|
109
|
+
type UserAuthRow = { readonly id: string; readonly passwordHash: string | null };
|
|
110
|
+
|
|
107
111
|
let committed = false;
|
|
108
112
|
try {
|
|
109
|
-
const invitation = await fetchOne(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
|
|
113
|
+
const invitation = await fetchOne<InvitationRow>(ctx.db.raw, tenantInvitationsTable, {
|
|
114
|
+
id: invitationId,
|
|
115
|
+
});
|
|
116
|
+
if (!invitation || invitation.status !== INVITATION_STATUS.pending)
|
|
115
117
|
return invalidInviteToken();
|
|
116
118
|
|
|
117
|
-
const invitationTenantId = invitation
|
|
118
|
-
const invitationEmail = invitation
|
|
119
|
-
const invitationRole = invitation
|
|
120
|
-
const invitationVersion = invitation
|
|
119
|
+
const invitationTenantId = invitation.tenantId;
|
|
120
|
+
const invitationEmail = invitation.email;
|
|
121
|
+
const invitationRole = invitation.role;
|
|
122
|
+
const invitationVersion = invitation.version;
|
|
121
123
|
|
|
122
124
|
// Email-Match vom User-Input (nicht aus session — User ist anon)
|
|
123
125
|
if (event.payload.email.toLowerCase() !== invitationEmail) {
|
|
@@ -131,15 +133,14 @@ export function createInviteAcceptWithLoginHandler() {
|
|
|
131
133
|
// Password-Check gegen userTable. Anti-enumeration: bei
|
|
132
134
|
// user-not-found ODER wrong-password collapsed beides auf
|
|
133
135
|
// invalidInviteToken (gleicher anti-enum-Trade-off wie reset).
|
|
134
|
-
const userRow = await fetchOne(ctx.db.raw, userTable,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
136
|
+
const userRow = await fetchOne<UserAuthRow>(ctx.db.raw, userTable, {
|
|
137
|
+
email: invitationEmail,
|
|
138
|
+
});
|
|
139
|
+
if (!userRow?.passwordHash) return invalidInviteToken();
|
|
140
|
+
const passwordValid = await verifyPassword(userRow.passwordHash, event.payload.password);
|
|
140
141
|
if (!passwordValid) return invalidInviteToken();
|
|
141
142
|
|
|
142
|
-
const userId = userRow
|
|
143
|
+
const userId = userRow.id;
|
|
143
144
|
|
|
144
145
|
// Already-Member-Check (idempotent)
|
|
145
146
|
const memberships = (await ctx.queryAs(
|
|
@@ -149,8 +150,7 @@ export function createInviteAcceptWithLoginHandler() {
|
|
|
149
150
|
)) as Array<{ tenantId: string }>; // @cast-boundary db-row
|
|
150
151
|
const alreadyMember = memberships.some((m) => m.tenantId === invitationTenantId);
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
const dbConn = ctx.db.raw as DbConnection;
|
|
153
|
+
const dbConn = ctx.db.raw;
|
|
154
154
|
|
|
155
155
|
if (!alreadyMember) {
|
|
156
156
|
await seedTenantMembership(dbConn, {
|
|
@@ -15,12 +15,8 @@
|
|
|
15
15
|
// Session"-Flow. Branch 2 (anon + existing email) und Branch 3 (anon +
|
|
16
16
|
// new email) kommen als separate Handler.
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
createTenantDb,
|
|
21
|
-
type DbConnection,
|
|
22
|
-
fetchOne,
|
|
23
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
18
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
24
20
|
import {
|
|
25
21
|
createSystemUser,
|
|
26
22
|
defineWriteHandler,
|
|
@@ -31,7 +27,6 @@ import {
|
|
|
31
27
|
UnprocessableError,
|
|
32
28
|
writeFailure,
|
|
33
29
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
34
|
-
import { eq } from "drizzle-orm";
|
|
35
30
|
import { z } from "zod";
|
|
36
31
|
// kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link), DB-row-owner ist tenant-feature
|
|
37
32
|
import {
|
|
@@ -97,26 +92,35 @@ export function createInviteAcceptHandler() {
|
|
|
97
92
|
const burn = await burnInviteToken(ctx.redis, event.payload.token);
|
|
98
93
|
if (burn === "already-used") return invalidInviteToken();
|
|
99
94
|
|
|
95
|
+
type InvitationRow = {
|
|
96
|
+
readonly status: string;
|
|
97
|
+
readonly tenantId: TenantId;
|
|
98
|
+
readonly email: string;
|
|
99
|
+
readonly role: string;
|
|
100
|
+
readonly version: number;
|
|
101
|
+
};
|
|
102
|
+
type UserEmailRow = { readonly email: string };
|
|
103
|
+
|
|
100
104
|
let committed = false;
|
|
101
105
|
try {
|
|
102
|
-
const invitation = await fetchOne(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
|
|
106
|
+
const invitation = await fetchOne<InvitationRow>(ctx.db.raw, tenantInvitationsTable, {
|
|
107
|
+
id: invitationId,
|
|
108
|
+
});
|
|
109
|
+
if (!invitation || invitation.status !== INVITATION_STATUS.pending)
|
|
108
110
|
return invalidInviteToken();
|
|
109
111
|
|
|
110
|
-
const invitationTenantId = invitation
|
|
111
|
-
const invitationEmail = invitation
|
|
112
|
-
const invitationRole = invitation
|
|
113
|
-
const invitationVersion = invitation
|
|
112
|
+
const invitationTenantId = invitation.tenantId;
|
|
113
|
+
const invitationEmail = invitation.email;
|
|
114
|
+
const invitationRole = invitation.role;
|
|
115
|
+
const invitationVersion = invitation.version;
|
|
114
116
|
|
|
115
117
|
// Email-Match: User muss mit der eingeladenen Email matchen.
|
|
116
118
|
// Sonst kann ein Angreifer mit Zugriff zur invitee-Mail seinen
|
|
117
119
|
// eigenen Account dem Tenant zuschlagen.
|
|
118
|
-
const userRow = await fetchOne(ctx.db.raw, userTable,
|
|
119
|
-
|
|
120
|
+
const userRow = await fetchOne<UserEmailRow>(ctx.db.raw, userTable, {
|
|
121
|
+
id: event.user.id,
|
|
122
|
+
});
|
|
123
|
+
const userEmail = userRow?.email;
|
|
120
124
|
if (!userRow || !userEmail || userEmail.toLowerCase() !== invitationEmail) {
|
|
121
125
|
return writeFailure(
|
|
122
126
|
new UnprocessableError(AuthErrors.inviteEmailMismatch, {
|
|
@@ -135,8 +139,7 @@ export function createInviteAcceptHandler() {
|
|
|
135
139
|
)) as Array<{ tenantId: string }>; // @cast-boundary db-row
|
|
136
140
|
const alreadyMember = memberships.some((m) => m.tenantId === invitationTenantId);
|
|
137
141
|
|
|
138
|
-
|
|
139
|
-
const dbConn = ctx.db.raw as DbConnection;
|
|
142
|
+
const dbConn = ctx.db.raw;
|
|
140
143
|
|
|
141
144
|
if (!alreadyMember) {
|
|
142
145
|
// Membership-Add via seedTenantMembership-helper (event-store-
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
// invite-create.
|
|
16
16
|
|
|
17
17
|
import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
18
|
-
import {
|
|
18
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
19
20
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
21
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
21
|
-
import { eq } from "drizzle-orm";
|
|
22
22
|
import { Temporal } from "temporal-polyfill";
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
// kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
|
|
@@ -77,12 +77,7 @@ export function createInviteCreateHandler(opts: InviteCreateOptions = {}) {
|
|
|
77
77
|
// max. eine Row. Status egal (cancelled/accepted/expired/pending);
|
|
78
78
|
// wir setzen sie auf pending zurück und vergeben einen frischen
|
|
79
79
|
// Token wenn der bisherige nicht mehr lebt.
|
|
80
|
-
const existing = await fetchOne(
|
|
81
|
-
ctx.db.raw,
|
|
82
|
-
tenantInvitationsTable,
|
|
83
|
-
eq(tenantInvitationsTable.tenantId, tenantId),
|
|
84
|
-
eq(tenantInvitationsTable.email, email),
|
|
85
|
-
);
|
|
80
|
+
const existing = await fetchOne(ctx.db.raw, tenantInvitationsTable, { tenantId, email });
|
|
86
81
|
|
|
87
82
|
let invitationId: string;
|
|
88
83
|
let token: string;
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
// e. Invitation → accepted, Token gelöscht
|
|
18
18
|
// 5. Response: SessionUser + tenantId für Auto-Login
|
|
19
19
|
|
|
20
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
20
21
|
import {
|
|
21
22
|
createEventStoreExecutor,
|
|
22
23
|
createTenantDb,
|
|
23
24
|
type DbConnection,
|
|
24
|
-
fetchOne,
|
|
25
25
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
26
26
|
import {
|
|
27
27
|
createSystemUser,
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
UnprocessableError,
|
|
35
35
|
writeFailure,
|
|
36
36
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
37
|
-
import { eq } from "drizzle-orm";
|
|
38
37
|
import { z } from "zod";
|
|
39
38
|
// kumiko-lint-ignore cross-feature-import invite-flow
|
|
40
39
|
import {
|
|
@@ -104,30 +103,34 @@ export function createInviteSignupCompleteHandler() {
|
|
|
104
103
|
const burn = await burnInviteToken(ctx.redis, event.payload.token);
|
|
105
104
|
if (burn === "already-used") return invalidInviteToken();
|
|
106
105
|
|
|
106
|
+
type InvitationRow = {
|
|
107
|
+
readonly status: string;
|
|
108
|
+
readonly tenantId: TenantId;
|
|
109
|
+
readonly email: string;
|
|
110
|
+
readonly role: string;
|
|
111
|
+
readonly version: number;
|
|
112
|
+
};
|
|
113
|
+
|
|
107
114
|
let committed = false;
|
|
108
115
|
try {
|
|
109
|
-
const invitation = await fetchOne(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
|
|
116
|
+
const invitation = await fetchOne<InvitationRow>(ctx.db.raw, tenantInvitationsTable, {
|
|
117
|
+
id: invitationId,
|
|
118
|
+
});
|
|
119
|
+
if (!invitation || invitation.status !== INVITATION_STATUS.pending)
|
|
115
120
|
return invalidInviteToken();
|
|
116
121
|
|
|
117
|
-
const invitationTenantId = invitation
|
|
118
|
-
const invitationEmail = invitation
|
|
119
|
-
const invitationRole = invitation
|
|
120
|
-
const invitationVersion = invitation
|
|
122
|
+
const invitationTenantId = invitation.tenantId;
|
|
123
|
+
const invitationEmail = invitation.email;
|
|
124
|
+
const invitationRole = invitation.role;
|
|
125
|
+
const invitationVersion = invitation.version;
|
|
121
126
|
|
|
122
127
|
// User-Not-Exists-Check: wenn die Email schon registriert ist,
|
|
123
128
|
// muss der User Branch 2 (acceptWithLogin) nutzen. Hier ist
|
|
124
129
|
// explizit "neue Email" — sonst hätten wir zwei Wege ein
|
|
125
130
|
// Password zu setzen für denselben User.
|
|
126
|
-
const existingUser = await fetchOne(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
eq(userTable.email, invitationEmail),
|
|
130
|
-
);
|
|
131
|
+
const existingUser = await fetchOne(ctx.db.raw, userTable, {
|
|
132
|
+
email: invitationEmail,
|
|
133
|
+
});
|
|
131
134
|
if (existingUser) return invalidInviteToken();
|
|
132
135
|
|
|
133
136
|
// User anlegen via seedUserWithPassword (gleiches Pattern wie
|