@cosmicdrift/kumiko-bundled-features 0.1.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 +90 -0
- package/src/audit/__tests__/audit.integration.ts +328 -0
- package/src/audit/constants.ts +7 -0
- package/src/audit/feature.ts +23 -0
- package/src/audit/handlers/list.query.ts +98 -0
- package/src/audit/index.ts +2 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
- package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
- package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
- package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
- package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
- package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
- package/src/auth-email-password/auth-user-row.ts +41 -0
- package/src/auth-email-password/constants.ts +101 -0
- package/src/auth-email-password/email-templates.ts +283 -0
- package/src/auth-email-password/feature.ts +140 -0
- package/src/auth-email-password/handlers/change-password.write.ts +58 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
- package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
- package/src/auth-email-password/handlers/login.write.ts +208 -0
- package/src/auth-email-password/handlers/logout.write.ts +12 -0
- package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
- package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
- package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
- package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
- package/src/auth-email-password/i18n.ts +211 -0
- package/src/auth-email-password/identity-v3-hash.ts +97 -0
- package/src/auth-email-password/index.ts +35 -0
- package/src/auth-email-password/invite-token-store.ts +92 -0
- package/src/auth-email-password/lockout-store.ts +118 -0
- package/src/auth-email-password/password-hashing.ts +43 -0
- package/src/auth-email-password/reset-token.ts +28 -0
- package/src/auth-email-password/seeding.ts +183 -0
- package/src/auth-email-password/signed-token.ts +85 -0
- package/src/auth-email-password/signup-token-store.ts +104 -0
- package/src/auth-email-password/stream-tenant.ts +31 -0
- package/src/auth-email-password/testing.ts +5 -0
- package/src/auth-email-password/token-burn-store.ts +57 -0
- package/src/auth-email-password/verification-token.ts +27 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
- package/src/auth-email-password/web/auth-client.ts +350 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
- package/src/auth-email-password/web/auth-gate.tsx +33 -0
- package/src/auth-email-password/web/client-plugin.ts +48 -0
- package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
- package/src/auth-email-password/web/index.ts +56 -0
- package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
- package/src/auth-email-password/web/login-screen.tsx +150 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
- package/src/auth-email-password/web/session.tsx +171 -0
- package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
- package/src/auth-email-password/web/signup-screen.tsx +130 -0
- package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
- package/src/auth-email-password/web/use-shell-user.ts +34 -0
- package/src/auth-email-password/web/user-menu.tsx +89 -0
- package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
- package/src/billing-foundation/__tests__/feature.test.ts +110 -0
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
- package/src/billing-foundation/aggregate-id.ts +21 -0
- package/src/billing-foundation/constants.ts +70 -0
- package/src/billing-foundation/entities.ts +50 -0
- package/src/billing-foundation/events.ts +71 -0
- package/src/billing-foundation/feature.ts +122 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
- package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
- package/src/billing-foundation/handlers/process-event.write.ts +160 -0
- package/src/billing-foundation/index.ts +42 -0
- package/src/billing-foundation/projection.ts +135 -0
- package/src/billing-foundation/types.ts +157 -0
- package/src/billing-foundation/webhook-handler.ts +184 -0
- package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
- package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
- package/src/cap-counter/aggregate-id.ts +61 -0
- package/src/cap-counter/constants.ts +32 -0
- package/src/cap-counter/enforce-cap.ts +404 -0
- package/src/cap-counter/entity.ts +48 -0
- package/src/cap-counter/feature.ts +90 -0
- package/src/cap-counter/handlers/get-counter.query.ts +43 -0
- package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
- package/src/cap-counter/handlers/increment.write.ts +92 -0
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
- package/src/cap-counter/index.ts +34 -0
- package/src/cap-counter/with-cap-enforcement.ts +179 -0
- package/src/channel-email/email-channel.ts +48 -0
- package/src/channel-email/feature.ts +15 -0
- package/src/channel-email/index.ts +4 -0
- package/src/channel-email/smtp-transport.ts +65 -0
- package/src/channel-email/types.ts +34 -0
- package/src/channel-in-app/constants.ts +11 -0
- package/src/channel-in-app/feature.ts +30 -0
- package/src/channel-in-app/handlers/inbox.query.ts +28 -0
- package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
- package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
- package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
- package/src/channel-in-app/in-app-channel.ts +44 -0
- package/src/channel-in-app/index.ts +4 -0
- package/src/channel-in-app/tables.ts +22 -0
- package/src/channel-push/feature.ts +15 -0
- package/src/channel-push/index.ts +3 -0
- package/src/channel-push/push-channel.ts +33 -0
- package/src/channel-push/types.ts +22 -0
- package/src/config/__tests__/app-overrides.test.ts +118 -0
- package/src/config/__tests__/config.integration.ts +1246 -0
- package/src/config/constants.ts +23 -0
- package/src/config/feature.ts +117 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
- package/src/config/handlers/reset.write.ts +45 -0
- package/src/config/handlers/schema.query.ts +22 -0
- package/src/config/handlers/set.write.ts +93 -0
- package/src/config/handlers/values.query.ts +43 -0
- package/src/config/index.ts +15 -0
- package/src/config/resolver.ts +283 -0
- package/src/config/table.ts +35 -0
- package/src/config/write-helpers.ts +268 -0
- package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
- package/src/delivery/__tests__/delivery.integration.ts +1405 -0
- package/src/delivery/constants.ts +33 -0
- package/src/delivery/delivery-service.ts +489 -0
- package/src/delivery/events.ts +18 -0
- package/src/delivery/feature.ts +70 -0
- package/src/delivery/handlers/log.query.ts +21 -0
- package/src/delivery/handlers/preferences.query.ts +18 -0
- package/src/delivery/handlers/set-preference.write.ts +28 -0
- package/src/delivery/index.ts +35 -0
- package/src/delivery/tables.ts +74 -0
- package/src/delivery/testing.ts +47 -0
- package/src/delivery/types.ts +71 -0
- package/src/delivery/unsubscribe.ts +99 -0
- package/src/delivery/upsert-preference.ts +145 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
- package/src/feature-toggles/constants.ts +20 -0
- package/src/feature-toggles/events.ts +18 -0
- package/src/feature-toggles/feature.ts +98 -0
- package/src/feature-toggles/global-feature-state-table.ts +28 -0
- package/src/feature-toggles/handlers/list.query.ts +26 -0
- package/src/feature-toggles/handlers/registered.query.ts +56 -0
- package/src/feature-toggles/handlers/set.write.ts +158 -0
- package/src/feature-toggles/index.ts +9 -0
- package/src/feature-toggles/toggle-runtime.ts +73 -0
- package/src/file-foundation/__tests__/feature.test.ts +35 -0
- package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
- package/src/file-foundation/feature.ts +123 -0
- package/src/file-foundation/index.ts +7 -0
- package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
- package/src/file-provider-inmemory/feature.ts +73 -0
- package/src/file-provider-inmemory/index.ts +3 -0
- package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
- package/src/file-provider-s3/feature.ts +169 -0
- package/src/file-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
- package/src/files-provider-s3/env-helper.ts +49 -0
- package/src/files-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/s3-provider.ts +114 -0
- package/src/foundation-shared/config-helpers.ts +67 -0
- package/src/foundation-shared/index.ts +4 -0
- package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
- package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
- package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
- package/src/jobs/constants.ts +21 -0
- package/src/jobs/events.ts +39 -0
- package/src/jobs/feature.ts +150 -0
- package/src/jobs/handlers/detail.query.ts +30 -0
- package/src/jobs/handlers/list.query.ts +36 -0
- package/src/jobs/handlers/retry.write.ts +69 -0
- package/src/jobs/handlers/trigger.write.ts +39 -0
- package/src/jobs/index.ts +5 -0
- package/src/jobs/job-run-logger.ts +213 -0
- package/src/jobs/job-run-table.ts +55 -0
- package/src/legal-pages/README.md +195 -0
- package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
- package/src/legal-pages/constants.ts +36 -0
- package/src/legal-pages/feature.ts +187 -0
- package/src/legal-pages/index.ts +13 -0
- package/src/legal-pages/markdown.ts +69 -0
- package/src/mail-foundation/__tests__/feature.test.ts +46 -0
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
- package/src/mail-foundation/feature.ts +160 -0
- package/src/mail-foundation/index.ts +14 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
- package/src/mail-transport-inmemory/feature.ts +90 -0
- package/src/mail-transport-inmemory/index.ts +3 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
- package/src/mail-transport-smtp/feature.ts +182 -0
- package/src/mail-transport-smtp/index.ts +3 -0
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
- package/src/rate-limiting/constants.ts +9 -0
- package/src/rate-limiting/feature.ts +16 -0
- package/src/rate-limiting/handlers/status.query.ts +52 -0
- package/src/rate-limiting/index.ts +2 -0
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
- package/src/renderer-simple/feature.ts +12 -0
- package/src/renderer-simple/index.ts +2 -0
- package/src/renderer-simple/simple-renderer.ts +72 -0
- package/src/secrets/__tests__/rotate.integration.ts +176 -0
- package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
- package/src/secrets/__tests__/secrets.integration.ts +118 -0
- package/src/secrets/feature.ts +84 -0
- package/src/secrets/handlers/delete.write.ts +20 -0
- package/src/secrets/handlers/list.query.ts +38 -0
- package/src/secrets/handlers/rotate.job.ts +193 -0
- package/src/secrets/handlers/set.write.ts +50 -0
- package/src/secrets/index.ts +16 -0
- package/src/secrets/secrets-context.ts +296 -0
- package/src/secrets/table.ts +68 -0
- package/src/sessions/__tests__/cleanup.integration.ts +175 -0
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
- package/src/sessions/__tests__/sessions.integration.ts +472 -0
- package/src/sessions/__tests__/test-helpers.ts +66 -0
- package/src/sessions/constants.ts +43 -0
- package/src/sessions/feature.ts +84 -0
- package/src/sessions/handlers/cleanup.job.ts +109 -0
- package/src/sessions/handlers/list.query.ts +35 -0
- package/src/sessions/handlers/mine.query.ts +37 -0
- package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
- package/src/sessions/handlers/revoke.write.ts +76 -0
- package/src/sessions/index.ts +17 -0
- package/src/sessions/schema/index.ts +5 -0
- package/src/sessions/schema/user-session.ts +67 -0
- package/src/sessions/session-callbacks.ts +110 -0
- package/src/sessions/testing.ts +42 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
- package/src/subscription-mollie/constants.ts +33 -0
- package/src/subscription-mollie/feature.ts +144 -0
- package/src/subscription-mollie/index.ts +13 -0
- package/src/subscription-mollie/plugin-methods.ts +79 -0
- package/src/subscription-mollie/verify-webhook.ts +244 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
- package/src/subscription-stripe/constants.ts +20 -0
- package/src/subscription-stripe/feature.ts +120 -0
- package/src/subscription-stripe/index.ts +14 -0
- package/src/subscription-stripe/plugin-methods.ts +91 -0
- package/src/subscription-stripe/verify-webhook.ts +235 -0
- package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
- package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
- package/src/tenant/__tests__/tenant.integration.ts +347 -0
- package/src/tenant/command-schemas.ts +37 -0
- package/src/tenant/constants.ts +37 -0
- package/src/tenant/feature.ts +109 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
- package/src/tenant/handlers/add-member.write.ts +53 -0
- package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
- package/src/tenant/handlers/create.write.ts +21 -0
- package/src/tenant/handlers/disable.write.ts +18 -0
- package/src/tenant/handlers/invitations.query.ts +31 -0
- package/src/tenant/handlers/list.query.ts +17 -0
- package/src/tenant/handlers/me.query.ts +17 -0
- package/src/tenant/handlers/members.query.ts +22 -0
- package/src/tenant/handlers/memberships.query.ts +24 -0
- package/src/tenant/handlers/remove-member.write.ts +40 -0
- package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
- package/src/tenant/handlers/update-member-roles.write.ts +54 -0
- package/src/tenant/handlers/update.write.ts +20 -0
- package/src/tenant/index.ts +12 -0
- package/src/tenant/invitation-table.ts +93 -0
- package/src/tenant/membership-table.ts +35 -0
- package/src/tenant/schema/index.ts +5 -0
- package/src/tenant/schema/tenant.ts +27 -0
- package/src/tenant/seeding.ts +155 -0
- package/src/tenant/testing.ts +8 -0
- package/src/text-content/README.md +190 -0
- package/src/text-content/__tests__/text-content.integration.ts +415 -0
- package/src/text-content/api.ts +92 -0
- package/src/text-content/constants.ts +19 -0
- package/src/text-content/feature.ts +29 -0
- package/src/text-content/handlers/by-slug.query.ts +55 -0
- package/src/text-content/handlers/set.write.ts +118 -0
- package/src/text-content/index.ts +14 -0
- package/src/text-content/seeding.ts +91 -0
- package/src/text-content/table.ts +45 -0
- package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
- package/src/tier-engine/__tests__/drift.test.ts +42 -0
- package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
- package/src/tier-engine/aggregate-id.ts +27 -0
- package/src/tier-engine/compose-app.ts +150 -0
- package/src/tier-engine/constants.ts +15 -0
- package/src/tier-engine/entity.ts +30 -0
- package/src/tier-engine/feature.ts +72 -0
- package/src/tier-engine/handlers/active-tier.query.ts +23 -0
- package/src/tier-engine/index.ts +22 -0
- package/src/user/__tests__/seed-testing.integration.ts +127 -0
- package/src/user/__tests__/user.integration.ts +198 -0
- package/src/user/command-schemas.ts +15 -0
- package/src/user/constants.ts +23 -0
- package/src/user/feature.ts +32 -0
- package/src/user/handlers/create.write.ts +54 -0
- package/src/user/handlers/detail.query.ts +9 -0
- package/src/user/handlers/find-for-auth.query.ts +38 -0
- package/src/user/handlers/list.query.ts +8 -0
- package/src/user/handlers/me.query.ts +15 -0
- package/src/user/handlers/update.write.ts +54 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/index.ts +5 -0
- package/src/user/schema/user.ts +69 -0
- package/src/user/seeding.ts +93 -0
- package/src/user/testing.ts +5 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// End-to-end: ein migrierter User mit ASP.NET-Identity-V3-passwordHash kann
|
|
2
|
+
// sich über den normalen `auth-email-password.login`-Handler einloggen, ohne
|
|
3
|
+
// vorher Password-Reset durchlaufen zu müssen.
|
|
4
|
+
//
|
|
5
|
+
// Das ist der Kern-Use-Case der BMC-Migration — Legacy-Hashes 1:1
|
|
6
|
+
// übernommen, Login funktioniert weiter.
|
|
7
|
+
|
|
8
|
+
import { pbkdf2Sync, randomBytes } from "node:crypto";
|
|
9
|
+
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
10
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
11
|
+
import {
|
|
12
|
+
createEntityTable,
|
|
13
|
+
pushTables,
|
|
14
|
+
setupTestStack,
|
|
15
|
+
type TestStack,
|
|
16
|
+
TestUsers,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
18
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
19
|
+
import { createConfigFeature } from "../../config";
|
|
20
|
+
import { createConfigResolver } from "../../config/resolver";
|
|
21
|
+
import { configValuesTable } from "../../config/table";
|
|
22
|
+
import { createTenantFeature } from "../../tenant";
|
|
23
|
+
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
24
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
25
|
+
import { seedTenantMembership } from "../../tenant/testing";
|
|
26
|
+
import { UserHandlers } from "../../user";
|
|
27
|
+
import { createUserFeature } from "../../user/feature";
|
|
28
|
+
import { userEntity, userTable } from "../../user/schema/user";
|
|
29
|
+
import { AuthErrors, AuthHandlers } from "../constants";
|
|
30
|
+
import { createAuthEmailPasswordFeature } from "../feature";
|
|
31
|
+
|
|
32
|
+
let stack: TestStack;
|
|
33
|
+
|
|
34
|
+
const systemAdmin = TestUsers.systemAdmin;
|
|
35
|
+
const encryptionKey = randomBytes(32).toString("base64");
|
|
36
|
+
|
|
37
|
+
// Build a V3-format hash with the BMC profile (HMACSHA256, 10000 iter,
|
|
38
|
+
// 16-byte salt). Mirrors the encoding in identity-v3-hash.test.ts — kept
|
|
39
|
+
// inline so this integration test stands alone.
|
|
40
|
+
function buildBmcStyleV3Hash(password: string, salt: Buffer): string {
|
|
41
|
+
const subkey = pbkdf2Sync(password, salt, 10_000, 32, "sha256");
|
|
42
|
+
const header = Buffer.alloc(13);
|
|
43
|
+
header.writeUInt8(0x01, 0); // V3 format marker
|
|
44
|
+
header.writeUInt32BE(1, 1); // PRF = HMACSHA256
|
|
45
|
+
header.writeUInt32BE(10_000, 5); // iterations
|
|
46
|
+
header.writeUInt32BE(salt.length, 9); // salt length
|
|
47
|
+
return Buffer.concat([header, salt, subkey]).toString("base64");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
const encryption = createEncryptionProvider(encryptionKey);
|
|
52
|
+
const resolver = createConfigResolver({ encryption });
|
|
53
|
+
|
|
54
|
+
stack = await setupTestStack({
|
|
55
|
+
features: [
|
|
56
|
+
createConfigFeature(),
|
|
57
|
+
createUserFeature(),
|
|
58
|
+
createTenantFeature(),
|
|
59
|
+
createAuthEmailPasswordFeature(),
|
|
60
|
+
],
|
|
61
|
+
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
62
|
+
authConfig: {
|
|
63
|
+
membershipQuery: "tenant:query:memberships",
|
|
64
|
+
loginHandler: AuthHandlers.login,
|
|
65
|
+
loginErrorStatusMap: {
|
|
66
|
+
[AuthErrors.invalidCredentials]: 401,
|
|
67
|
+
[AuthErrors.noMembership]: 403,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await createEntityTable(stack.db, userEntity);
|
|
73
|
+
await createEntityTable(stack.db, tenantEntity);
|
|
74
|
+
await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterAll(async () => {
|
|
78
|
+
await stack.cleanup();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
beforeEach(async () => {
|
|
82
|
+
await stack.db.delete(userTable);
|
|
83
|
+
await stack.db.delete(tenantMembershipsTable);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("Identity-V3 password-hash compatibility", () => {
|
|
87
|
+
test("legacy V3-hashed user can log in with the right password", async () => {
|
|
88
|
+
const password = "Migrated!Password-2025";
|
|
89
|
+
const salt = randomBytes(16);
|
|
90
|
+
const v3Hash = buildBmcStyleV3Hash(password, salt);
|
|
91
|
+
|
|
92
|
+
// Seed the migrated user with the legacy hash 1:1 — no rehash, no reset.
|
|
93
|
+
const tenantId = "00000000-0000-4000-8000-000000000099" as TenantId;
|
|
94
|
+
const created = await stack.http.writeOk<{ id: string }>(
|
|
95
|
+
UserHandlers.create,
|
|
96
|
+
{
|
|
97
|
+
email: "alice@legacy.example",
|
|
98
|
+
passwordHash: v3Hash,
|
|
99
|
+
displayName: "Alice Migrated",
|
|
100
|
+
},
|
|
101
|
+
systemAdmin,
|
|
102
|
+
);
|
|
103
|
+
await seedTenantMembership(stack.db, {
|
|
104
|
+
userId: created.id,
|
|
105
|
+
tenantId,
|
|
106
|
+
roles: ["User"],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Login: same /api/auth/login route the BMC frontend will hit
|
|
110
|
+
// post-migration. Public route — no JWT, no authenticated caller.
|
|
111
|
+
const res = await stack.http.raw("POST", "/api/auth/login", {
|
|
112
|
+
email: "alice@legacy.example",
|
|
113
|
+
password,
|
|
114
|
+
});
|
|
115
|
+
expect(res.status).toBe(200);
|
|
116
|
+
const body = await res.json();
|
|
117
|
+
expect(body.isSuccess).toBe(true);
|
|
118
|
+
expect(body.user).toMatchObject({ id: created.id, tenantId });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("legacy V3-hashed user is rejected with the wrong password", async () => {
|
|
122
|
+
const password = "right-password";
|
|
123
|
+
const salt = randomBytes(16);
|
|
124
|
+
const v3Hash = buildBmcStyleV3Hash(password, salt);
|
|
125
|
+
|
|
126
|
+
const tenantId = "00000000-0000-4000-8000-000000000098" as TenantId;
|
|
127
|
+
const created = await stack.http.writeOk<{ id: string }>(
|
|
128
|
+
UserHandlers.create,
|
|
129
|
+
{
|
|
130
|
+
email: "bob@legacy.example",
|
|
131
|
+
passwordHash: v3Hash,
|
|
132
|
+
displayName: "Bob Migrated",
|
|
133
|
+
},
|
|
134
|
+
systemAdmin,
|
|
135
|
+
);
|
|
136
|
+
await seedTenantMembership(stack.db, {
|
|
137
|
+
userId: created.id,
|
|
138
|
+
tenantId,
|
|
139
|
+
roles: ["User"],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const res = await stack.http.raw("POST", "/api/auth/login", {
|
|
143
|
+
email: "bob@legacy.example",
|
|
144
|
+
password: "wrong-password",
|
|
145
|
+
});
|
|
146
|
+
expect(res.status).toBe(401);
|
|
147
|
+
const body = await res.json();
|
|
148
|
+
expect(body.error?.details?.reason).toBe(AuthErrors.invalidCredentials);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// Tenant-Invite-Flow Full-Stack Integration-Test. Spec für die 3
|
|
2
|
+
// Accept-Branches via stack.http (echte HTTP-Routes durch).
|
|
3
|
+
//
|
|
4
|
+
// Setup:
|
|
5
|
+
// - Tenant-A mit Admin "alice@" als Admin-Member
|
|
6
|
+
// - Tenant-B mit User "bob@" als Member (für Branch 1: Bob ist
|
|
7
|
+
// eingeloggt in Tenant-B und akzeptiert ein Tenant-A-Invite)
|
|
8
|
+
// - "carol@" existiert NICHT (für Branch 3: neue Email)
|
|
9
|
+
//
|
|
10
|
+
// Flow pro Test:
|
|
11
|
+
// 1. Admin invitet email → invite-create (Admin-Auth)
|
|
12
|
+
// 2. Mail captured durch sendInviteEmail-callback
|
|
13
|
+
// 3. Token aus Activation-URL extrahieren
|
|
14
|
+
// 4. Branch-spezifischer Accept-Endpoint
|
|
15
|
+
// 5. DB-State + Membership + Cookies/JWT verifizieren
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createSystemUser,
|
|
19
|
+
type SessionUser,
|
|
20
|
+
SYSTEM_TENANT_ID,
|
|
21
|
+
type TenantId,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
23
|
+
import {
|
|
24
|
+
createEntityTable,
|
|
25
|
+
pushTables,
|
|
26
|
+
setupTestStack,
|
|
27
|
+
type TestStack,
|
|
28
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
29
|
+
import { eq } from "drizzle-orm";
|
|
30
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
31
|
+
import { createConfigFeature } from "../../config";
|
|
32
|
+
import { createConfigResolver } from "../../config/resolver";
|
|
33
|
+
import { configValuesTable } from "../../config/table";
|
|
34
|
+
import { createTenantFeature } from "../../tenant";
|
|
35
|
+
import { tenantInvitationEntity, tenantInvitationsTable } from "../../tenant/invitation-table";
|
|
36
|
+
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
37
|
+
import { tenantEntity, tenantTable } from "../../tenant/schema/tenant";
|
|
38
|
+
import { seedTenant, seedTenantMembership } from "../../tenant/seeding";
|
|
39
|
+
import { createUserFeature } from "../../user/feature";
|
|
40
|
+
import { userEntity, userTable } from "../../user/schema/user";
|
|
41
|
+
import { AuthErrors, AuthHandlers } from "../constants";
|
|
42
|
+
import { createAuthEmailPasswordFeature } from "../feature";
|
|
43
|
+
import { hashPassword } from "../password-hashing";
|
|
44
|
+
import { seedUser } from "../seeding";
|
|
45
|
+
|
|
46
|
+
const APP_ACCEPT_URL = "https://app.example.com/invite/accept";
|
|
47
|
+
const ALICE_EMAIL = "alice@example.com";
|
|
48
|
+
const BOB_EMAIL = "bob@example.com";
|
|
49
|
+
const CAROL_EMAIL = "carol@example.com";
|
|
50
|
+
const BOB_PASSWORD = "bob-existing-pw-1234";
|
|
51
|
+
const CAROL_PASSWORD = "carol-new-pw-1234";
|
|
52
|
+
|
|
53
|
+
const capturedInviteEmails: Array<{
|
|
54
|
+
email: string;
|
|
55
|
+
inviteUrl: string;
|
|
56
|
+
expiresAt: string;
|
|
57
|
+
role: string;
|
|
58
|
+
}> = [];
|
|
59
|
+
|
|
60
|
+
let stack: TestStack;
|
|
61
|
+
let aliceId: string;
|
|
62
|
+
let bobId: string;
|
|
63
|
+
// Pro Test frische Tenant-IDs damit der event-store-stream beim
|
|
64
|
+
// db.delete-cleanup nicht mit version_conflict beim Re-seed feuert.
|
|
65
|
+
let TENANT_A_ID: TenantId;
|
|
66
|
+
let TENANT_B_ID: TenantId;
|
|
67
|
+
|
|
68
|
+
function newTenantId(_suffix: string): TenantId {
|
|
69
|
+
// UUIDv4 + suffix für Lesbarkeit in Logs.
|
|
70
|
+
const rand = crypto.randomUUID();
|
|
71
|
+
return rand as TenantId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const GUEST: SessionUser = {
|
|
75
|
+
id: "00000000-0000-0000-0000-000000000000",
|
|
76
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
77
|
+
roles: ["all"],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
beforeAll(async () => {
|
|
81
|
+
stack = await setupTestStack({
|
|
82
|
+
features: [
|
|
83
|
+
createConfigFeature(),
|
|
84
|
+
createUserFeature(),
|
|
85
|
+
createTenantFeature(),
|
|
86
|
+
createAuthEmailPasswordFeature({
|
|
87
|
+
invite: { tokenTtlMinutes: 60 },
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
extraContext: { configResolver: createConfigResolver() },
|
|
91
|
+
authConfig: {
|
|
92
|
+
membershipQuery: "tenant:query:memberships",
|
|
93
|
+
loginHandler: AuthHandlers.login,
|
|
94
|
+
invite: {
|
|
95
|
+
acceptHandler: AuthHandlers.inviteAccept,
|
|
96
|
+
acceptWithLoginHandler: AuthHandlers.inviteAcceptWithLogin,
|
|
97
|
+
signupCompleteHandler: AuthHandlers.inviteSignupComplete,
|
|
98
|
+
appAcceptUrl: APP_ACCEPT_URL,
|
|
99
|
+
sendInviteEmail: async (args) => {
|
|
100
|
+
capturedInviteEmails.push(args);
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await createEntityTable(stack.db, userEntity);
|
|
107
|
+
await createEntityTable(stack.db, tenantEntity);
|
|
108
|
+
await createEntityTable(stack.db, tenantInvitationEntity);
|
|
109
|
+
await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterAll(async () => {
|
|
113
|
+
await stack.cleanup();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
beforeEach(async () => {
|
|
117
|
+
await stack.db.delete(userTable);
|
|
118
|
+
await stack.db.delete(tenantMembershipsTable);
|
|
119
|
+
await stack.db.delete(tenantInvitationsTable);
|
|
120
|
+
await stack.db.delete(tenantTable);
|
|
121
|
+
capturedInviteEmails.length = 0;
|
|
122
|
+
const allKeys = await stack.redis.redis.keys("invite:*");
|
|
123
|
+
if (allKeys.length > 0) await stack.redis.redis.del(...allKeys);
|
|
124
|
+
|
|
125
|
+
// Pro Test frische Tenant-IDs + tenant.key (sonst unique-violation
|
|
126
|
+
// auf read_tenants_key_unique beim 2. Run).
|
|
127
|
+
TENANT_A_ID = newTenantId("a");
|
|
128
|
+
TENANT_B_ID = newTenantId("b");
|
|
129
|
+
await seedTenant(stack.db, {
|
|
130
|
+
id: TENANT_A_ID,
|
|
131
|
+
key: `tenant-a-${TENANT_A_ID.slice(0, 8)}`,
|
|
132
|
+
name: "Tenant A",
|
|
133
|
+
});
|
|
134
|
+
await seedTenant(stack.db, {
|
|
135
|
+
id: TENANT_B_ID,
|
|
136
|
+
key: `tenant-b-${TENANT_B_ID.slice(0, 8)}`,
|
|
137
|
+
name: "Tenant B",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Alice = Admin von Tenant-A
|
|
141
|
+
aliceId = await seedUser(stack.db, {
|
|
142
|
+
email: ALICE_EMAIL,
|
|
143
|
+
displayName: "Alice",
|
|
144
|
+
passwordHash: await hashPassword("alice-pw-1234"),
|
|
145
|
+
emailVerified: true,
|
|
146
|
+
});
|
|
147
|
+
await seedTenantMembership(stack.db, {
|
|
148
|
+
userId: aliceId,
|
|
149
|
+
tenantId: TENANT_A_ID,
|
|
150
|
+
roles: ["Admin"],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Bob = Member von Tenant-B (für Branch 1 + 2 tests)
|
|
154
|
+
bobId = await seedUser(stack.db, {
|
|
155
|
+
email: BOB_EMAIL,
|
|
156
|
+
displayName: "Bob",
|
|
157
|
+
passwordHash: await hashPassword(BOB_PASSWORD),
|
|
158
|
+
emailVerified: true,
|
|
159
|
+
});
|
|
160
|
+
await seedTenantMembership(stack.db, {
|
|
161
|
+
userId: bobId,
|
|
162
|
+
tenantId: TENANT_B_ID,
|
|
163
|
+
roles: ["User"],
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function aliceSession(): SessionUser {
|
|
168
|
+
return { id: aliceId, tenantId: TENANT_A_ID, roles: ["Admin"] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function bobSession(): SessionUser {
|
|
172
|
+
return { id: bobId, tenantId: TENANT_B_ID, roles: ["User"] };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function authedRaw(
|
|
176
|
+
method: string,
|
|
177
|
+
path: string,
|
|
178
|
+
body: unknown,
|
|
179
|
+
user: SessionUser,
|
|
180
|
+
): Promise<Response> {
|
|
181
|
+
const token = await stack.jwt.sign(user);
|
|
182
|
+
return stack.http.raw(method, path, body, { Authorization: `Bearer ${token}` });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function inviteEmail(email: string, role: string): Promise<string> {
|
|
186
|
+
// invite-create geht via /api/write (Admin-Auth via JWT). Token kommt
|
|
187
|
+
// direkt aus dem handler-result; sendInviteEmail-callback ist die
|
|
188
|
+
// optionale Mail-Side, die in production-Setups separat von der
|
|
189
|
+
// Sample-App aufgerufen wird (NICHT framework-route — invite-create
|
|
190
|
+
// ist Admin-only und somit kein Magic-Link-Pattern wie signup-request).
|
|
191
|
+
const result = (await stack.http.writeOk(
|
|
192
|
+
AuthHandlers.inviteCreate,
|
|
193
|
+
{ email, role },
|
|
194
|
+
aliceSession(),
|
|
195
|
+
)) as { token: string };
|
|
196
|
+
return result.token;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
describe("invite-create", () => {
|
|
200
|
+
test("Admin invitet → invitation row + token in result", async () => {
|
|
201
|
+
const result = (await stack.http.writeOk(
|
|
202
|
+
AuthHandlers.inviteCreate,
|
|
203
|
+
{ email: BOB_EMAIL, role: "Admin" },
|
|
204
|
+
aliceSession(),
|
|
205
|
+
)) as { invitationId: string; email: string; role: string; token: string };
|
|
206
|
+
|
|
207
|
+
expect(result.email).toBe(BOB_EMAIL);
|
|
208
|
+
expect(result.role).toBe("Admin");
|
|
209
|
+
expect(result.token).toBeTruthy();
|
|
210
|
+
expect(result.token.length).toBeGreaterThanOrEqual(16);
|
|
211
|
+
|
|
212
|
+
const rows = await stack.db
|
|
213
|
+
.select()
|
|
214
|
+
.from(tenantInvitationsTable)
|
|
215
|
+
.where(eq(tenantInvitationsTable.email, BOB_EMAIL));
|
|
216
|
+
expect(rows).toHaveLength(1);
|
|
217
|
+
expect(rows[0]?.["status"]).toBe("pending");
|
|
218
|
+
expect(rows[0]?.["role"]).toBe("Admin");
|
|
219
|
+
expect(rows[0]?.["tenantId"]).toBe(TENANT_A_ID);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("Resend: zweiter invite für selbe email → existing row updated, gleicher token", async () => {
|
|
223
|
+
const firstToken = await inviteEmail(BOB_EMAIL, "Admin");
|
|
224
|
+
const secondToken = await inviteEmail(BOB_EMAIL, "Editor");
|
|
225
|
+
|
|
226
|
+
expect(secondToken).toBe(firstToken);
|
|
227
|
+
|
|
228
|
+
// Eine Row, role updated
|
|
229
|
+
const rows = await stack.db
|
|
230
|
+
.select()
|
|
231
|
+
.from(tenantInvitationsTable)
|
|
232
|
+
.where(eq(tenantInvitationsTable.email, BOB_EMAIL));
|
|
233
|
+
expect(rows).toHaveLength(1);
|
|
234
|
+
expect(rows[0]?.["role"]).toBe("Editor");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("invite-accept (Branch 1: logged-in)", () => {
|
|
239
|
+
test("Bob (logged-in in Tenant-B) accepts Tenant-A invite → membership added", async () => {
|
|
240
|
+
const token = await inviteEmail(BOB_EMAIL, "Admin");
|
|
241
|
+
|
|
242
|
+
const result = (await stack.http.writeOk(
|
|
243
|
+
AuthHandlers.inviteAccept,
|
|
244
|
+
{ token },
|
|
245
|
+
bobSession(),
|
|
246
|
+
)) as { tenantId: string; role: string; alreadyMember: boolean };
|
|
247
|
+
|
|
248
|
+
expect(result.tenantId).toBe(TENANT_A_ID);
|
|
249
|
+
expect(result.role).toBe("Admin");
|
|
250
|
+
expect(result.alreadyMember).toBe(false);
|
|
251
|
+
|
|
252
|
+
// Bob hat jetzt 2 Memberships
|
|
253
|
+
const memberships = await stack.db
|
|
254
|
+
.select()
|
|
255
|
+
.from(tenantMembershipsTable)
|
|
256
|
+
.where(eq(tenantMembershipsTable.userId, bobId));
|
|
257
|
+
expect(memberships).toHaveLength(2);
|
|
258
|
+
const tenantIds = memberships.map((m) => m["tenantId"]).sort();
|
|
259
|
+
expect(tenantIds).toEqual([TENANT_A_ID, TENANT_B_ID].sort());
|
|
260
|
+
|
|
261
|
+
// Invitation status = accepted
|
|
262
|
+
const inv = await stack.db
|
|
263
|
+
.select()
|
|
264
|
+
.from(tenantInvitationsTable)
|
|
265
|
+
.where(eq(tenantInvitationsTable.email, BOB_EMAIL));
|
|
266
|
+
expect(inv[0]?.["status"]).toBe("accepted");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("Email-Mismatch: Bob klickt Carol's Invite-Link → inviteEmailMismatch", async () => {
|
|
270
|
+
const token = await inviteEmail(CAROL_EMAIL, "Admin");
|
|
271
|
+
|
|
272
|
+
const res = await authedRaw("POST", "/api/auth/invite-accept", { token }, bobSession());
|
|
273
|
+
expect(res.status).toBe(422);
|
|
274
|
+
const body = (await res.json()) as { error?: { details?: { reason?: string } } };
|
|
275
|
+
expect(body.error?.details?.reason).toBe(AuthErrors.inviteEmailMismatch);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("Already-Member: Bob ist schon Member → idempotent no-op + alreadyMember=true", async () => {
|
|
279
|
+
// Bob direkt zu Tenant-A hinzufügen
|
|
280
|
+
await seedTenantMembership(stack.db, {
|
|
281
|
+
userId: bobId,
|
|
282
|
+
tenantId: TENANT_A_ID,
|
|
283
|
+
roles: ["User"],
|
|
284
|
+
by: createSystemUser(TENANT_A_ID),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const token = await inviteEmail(BOB_EMAIL, "Admin");
|
|
288
|
+
|
|
289
|
+
const result = (await stack.http.writeOk(
|
|
290
|
+
AuthHandlers.inviteAccept,
|
|
291
|
+
{ token },
|
|
292
|
+
bobSession(),
|
|
293
|
+
)) as { alreadyMember: boolean };
|
|
294
|
+
expect(result.alreadyMember).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("invite-accept-with-login (Branch 2: anon + existing email)", () => {
|
|
299
|
+
test("Bob (nicht eingeloggt) accepts mit email+password → JWT + membership", async () => {
|
|
300
|
+
const token = await inviteEmail(BOB_EMAIL, "Editor");
|
|
301
|
+
|
|
302
|
+
const res = await stack.http.raw("POST", "/api/auth/invite-accept-with-login", {
|
|
303
|
+
token,
|
|
304
|
+
email: BOB_EMAIL,
|
|
305
|
+
password: BOB_PASSWORD,
|
|
306
|
+
});
|
|
307
|
+
expect(res.status).toBe(200);
|
|
308
|
+
const body = (await res.json()) as {
|
|
309
|
+
isSuccess: boolean;
|
|
310
|
+
tenantId: string;
|
|
311
|
+
role: string;
|
|
312
|
+
token?: string;
|
|
313
|
+
};
|
|
314
|
+
expect(body.isSuccess).toBe(true);
|
|
315
|
+
expect(body.tenantId).toBe(TENANT_A_ID);
|
|
316
|
+
expect(body.role).toBe("Editor");
|
|
317
|
+
expect(body.token).toBeTruthy();
|
|
318
|
+
const setCookies = res.headers.get("set-cookie") ?? "";
|
|
319
|
+
expect(setCookies).toContain("kumiko_auth=");
|
|
320
|
+
|
|
321
|
+
// Membership added
|
|
322
|
+
const memberships = await stack.db
|
|
323
|
+
.select()
|
|
324
|
+
.from(tenantMembershipsTable)
|
|
325
|
+
.where(eq(tenantMembershipsTable.userId, bobId));
|
|
326
|
+
expect(memberships).toHaveLength(2);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("Wrong password → 422 invalid_invite_token (anti-enum)", async () => {
|
|
330
|
+
const token = await inviteEmail(BOB_EMAIL, "Editor");
|
|
331
|
+
const res = await stack.http.raw("POST", "/api/auth/invite-accept-with-login", {
|
|
332
|
+
token,
|
|
333
|
+
email: BOB_EMAIL,
|
|
334
|
+
password: "wrong-pw-1234",
|
|
335
|
+
});
|
|
336
|
+
expect(res.status).toBe(422);
|
|
337
|
+
const body = (await res.json()) as { error?: { details?: { reason?: string } } };
|
|
338
|
+
expect(body.error?.details?.reason).toBe(AuthErrors.invalidInviteToken);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("invite-signup-complete (Branch 3: anon + new email)", () => {
|
|
343
|
+
test("Carol (no account) accepts → user + membership entstehen, JWT", async () => {
|
|
344
|
+
const token = await inviteEmail(CAROL_EMAIL, "Admin");
|
|
345
|
+
|
|
346
|
+
const res = await stack.http.raw("POST", "/api/auth/invite-signup-complete", {
|
|
347
|
+
token,
|
|
348
|
+
password: CAROL_PASSWORD,
|
|
349
|
+
});
|
|
350
|
+
expect(res.status).toBe(200);
|
|
351
|
+
const body = (await res.json()) as {
|
|
352
|
+
isSuccess: boolean;
|
|
353
|
+
user: { id: string };
|
|
354
|
+
tenantId: string;
|
|
355
|
+
role: string;
|
|
356
|
+
};
|
|
357
|
+
expect(body.isSuccess).toBe(true);
|
|
358
|
+
expect(body.tenantId).toBe(TENANT_A_ID);
|
|
359
|
+
expect(body.role).toBe("Admin");
|
|
360
|
+
|
|
361
|
+
// Carol entstanden in users
|
|
362
|
+
const carolRows = await stack.db
|
|
363
|
+
.select()
|
|
364
|
+
.from(userTable)
|
|
365
|
+
.where(eq(userTable.email, CAROL_EMAIL));
|
|
366
|
+
expect(carolRows).toHaveLength(1);
|
|
367
|
+
expect(carolRows[0]?.["emailVerified"]).toBe(true);
|
|
368
|
+
expect(carolRows[0]?.["id"]).toBe(body.user.id);
|
|
369
|
+
|
|
370
|
+
// Login funktioniert
|
|
371
|
+
const loginRes = await stack.http.raw("POST", "/api/auth/login", {
|
|
372
|
+
email: CAROL_EMAIL,
|
|
373
|
+
password: CAROL_PASSWORD,
|
|
374
|
+
});
|
|
375
|
+
expect(loginRes.status).toBe(200);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("Existing email → invalid_invite_token (User soll Branch 2 nutzen)", async () => {
|
|
379
|
+
const token = await inviteEmail(BOB_EMAIL, "Admin");
|
|
380
|
+
|
|
381
|
+
const res = await stack.http.raw("POST", "/api/auth/invite-signup-complete", {
|
|
382
|
+
token,
|
|
383
|
+
password: "new-pw-1234",
|
|
384
|
+
});
|
|
385
|
+
expect(res.status).toBe(422);
|
|
386
|
+
const body = (await res.json()) as { error?: { details?: { reason?: string } } };
|
|
387
|
+
expect(body.error?.details?.reason).toBe(AuthErrors.invalidInviteToken);
|
|
388
|
+
|
|
389
|
+
// Bob hat keine zweite Membership erworben
|
|
390
|
+
const memberships = await stack.db
|
|
391
|
+
.select()
|
|
392
|
+
.from(tenantMembershipsTable)
|
|
393
|
+
.where(eq(tenantMembershipsTable.userId, bobId));
|
|
394
|
+
expect(memberships).toHaveLength(1);
|
|
395
|
+
void GUEST;
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("Single-Use-Burn (alle Branches)", () => {
|
|
400
|
+
test("Branch 1: zweiter accept mit gleichem Token → invalid", async () => {
|
|
401
|
+
const token = await inviteEmail(BOB_EMAIL, "Admin");
|
|
402
|
+
await stack.http.writeOk(AuthHandlers.inviteAccept, { token }, bobSession());
|
|
403
|
+
|
|
404
|
+
const res = await authedRaw("POST", "/api/auth/invite-accept", { token }, bobSession());
|
|
405
|
+
expect(res.status).toBe(422);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("cancel-invitation", () => {
|
|
410
|
+
test("Admin cancellt → status=cancelled + token weg, accept wird invalid", async () => {
|
|
411
|
+
const token = await inviteEmail(BOB_EMAIL, "Admin");
|
|
412
|
+
|
|
413
|
+
// Find invitationId
|
|
414
|
+
const rows = await stack.db
|
|
415
|
+
.select()
|
|
416
|
+
.from(tenantInvitationsTable)
|
|
417
|
+
.where(eq(tenantInvitationsTable.email, BOB_EMAIL));
|
|
418
|
+
const invitationId = rows[0]?.["id"] as string;
|
|
419
|
+
|
|
420
|
+
await stack.http.writeOk("tenant:write:cancel-invitation", { invitationId }, aliceSession());
|
|
421
|
+
|
|
422
|
+
const updated = await stack.db
|
|
423
|
+
.select()
|
|
424
|
+
.from(tenantInvitationsTable)
|
|
425
|
+
.where(eq(tenantInvitationsTable.id, invitationId));
|
|
426
|
+
expect(updated[0]?.["status"]).toBe("cancelled");
|
|
427
|
+
|
|
428
|
+
// Accept mit dem gecancelten Token → invalid
|
|
429
|
+
const res = await authedRaw("POST", "/api/auth/invite-accept", { token }, bobSession());
|
|
430
|
+
expect(res.status).toBe(422);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("invitations-query (pending list)", () => {
|
|
435
|
+
test("Admin sieht nur pending invitations", async () => {
|
|
436
|
+
await inviteEmail(BOB_EMAIL, "Admin");
|
|
437
|
+
await inviteEmail(CAROL_EMAIL, "Editor");
|
|
438
|
+
|
|
439
|
+
// Cancel das erste
|
|
440
|
+
const allRows = await stack.db.select().from(tenantInvitationsTable);
|
|
441
|
+
const bobInv = allRows.find((r) => r["email"] === BOB_EMAIL);
|
|
442
|
+
if (!bobInv) throw new Error("bob invitation missing");
|
|
443
|
+
await stack.http.writeOk(
|
|
444
|
+
"tenant:write:cancel-invitation",
|
|
445
|
+
{ invitationId: bobInv["id"] },
|
|
446
|
+
aliceSession(),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const list = (await stack.http.queryOk(
|
|
450
|
+
"tenant:query:invitations",
|
|
451
|
+
{},
|
|
452
|
+
aliceSession(),
|
|
453
|
+
)) as Array<{ email: string; status: string }>;
|
|
454
|
+
expect(list).toHaveLength(1);
|
|
455
|
+
expect(list[0]?.email).toBe(CAROL_EMAIL);
|
|
456
|
+
expect(list[0]?.status).toBe("pending");
|
|
457
|
+
});
|
|
458
|
+
});
|