@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,170 @@
|
|
|
1
|
+
// Magic-Link-Signup, Step 2 (confirm).
|
|
2
|
+
//
|
|
3
|
+
// Token aus URL + Password vom User → wir lösen den Token in Redis ein
|
|
4
|
+
// und legen Tenant + User + Admin-Membership atomar an. emailVerified
|
|
5
|
+
// wird sofort auf true gesetzt — der Magic-Link IST der Beweis.
|
|
6
|
+
//
|
|
7
|
+
// Pipeline:
|
|
8
|
+
// 1. Redis check: token → email lookup
|
|
9
|
+
// 2. Single-Use-Burn (SETNX) — gleichzeitiger Klick aus zwei Tabs
|
|
10
|
+
// gewinnt nur einer
|
|
11
|
+
// 3. Tenant-Key generieren (generateUniqueName mit DB-isAvailable-
|
|
12
|
+
// check gegen tenants.key)
|
|
13
|
+
// 4. provisionSignupAccount: Tenant + User + Membership in einem
|
|
14
|
+
// Rutsch (durch event-store-executor; events + projection +
|
|
15
|
+
// MSPs sehen das wie einen regulären create)
|
|
16
|
+
// 5. Token-Keys löschen (burn-key bleibt für TTL-Replay-Protection)
|
|
17
|
+
//
|
|
18
|
+
// Failure-Recovery: jeder Pfad nach dem burn checked `committed`-Flag;
|
|
19
|
+
// bei !committed wird der burn released damit ein legitimer Retry
|
|
20
|
+
// nicht durch einen stale Marker geblockt wird (wie reset/verify).
|
|
21
|
+
|
|
22
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
23
|
+
import {
|
|
24
|
+
defineWriteHandler,
|
|
25
|
+
type SessionUser,
|
|
26
|
+
type TenantId,
|
|
27
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
28
|
+
import {
|
|
29
|
+
InternalError,
|
|
30
|
+
UnprocessableError,
|
|
31
|
+
writeFailure,
|
|
32
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
33
|
+
import { generateUniqueName } from "@cosmicdrift/kumiko-framework/random";
|
|
34
|
+
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
35
|
+
import { eq } from "drizzle-orm";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
// kumiko-lint-ignore cross-feature-import signup-confirm reads tenants.key for slug-uniqueness check (TOCTOU + DB-unique-index zusammen)
|
|
38
|
+
import { tenantTable } from "../../tenant/schema/tenant";
|
|
39
|
+
import { AuthErrors } from "../constants";
|
|
40
|
+
// kumiko-lint-ignore cross-feature-import provisioning needs cross-feature seeding helpers
|
|
41
|
+
import { INITIAL_SIGNUP_ROLES, provisionSignupAccount } from "../seeding";
|
|
42
|
+
import {
|
|
43
|
+
burnSignupToken,
|
|
44
|
+
deleteSignupToken,
|
|
45
|
+
getEmailForSignupToken,
|
|
46
|
+
unburnSignupToken,
|
|
47
|
+
} from "../signup-token-store";
|
|
48
|
+
|
|
49
|
+
const SignupConfirmSchema = z.object({
|
|
50
|
+
token: z.string().min(8),
|
|
51
|
+
password: z.string().min(8).max(200),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Mirror der login-handler-Shape (kind: "auth-session", session: SessionUser)
|
|
55
|
+
// damit die Route-Layer den signup-confirm-success genauso behandeln kann
|
|
56
|
+
// wie einen erfolgreichen login: JWT-Mint, Cookies setzen, Session-Body
|
|
57
|
+
// returnen. Der zusätzliche tenantKey landet als sibling am data-objekt
|
|
58
|
+
// (NICHT in SessionUser — der ist generic, tenantKey ist signup-spezifisch
|
|
59
|
+
// für den Post-Signup-Redirect zu /<tenantKey>/).
|
|
60
|
+
export type SignupConfirmData = {
|
|
61
|
+
readonly kind: "auth-session";
|
|
62
|
+
readonly session: SessionUser;
|
|
63
|
+
readonly tenantKey: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function invalidSignupToken() {
|
|
67
|
+
return writeFailure(
|
|
68
|
+
new UnprocessableError(AuthErrors.invalidSignupToken, {
|
|
69
|
+
i18nKey: "auth.errors.invalidSignupToken",
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createSignupConfirmHandler() {
|
|
75
|
+
return defineWriteHandler<"signup-confirm", typeof SignupConfirmSchema, SignupConfirmData>({
|
|
76
|
+
name: "signup-confirm",
|
|
77
|
+
schema: SignupConfirmSchema,
|
|
78
|
+
access: { roles: ["all"] },
|
|
79
|
+
handler: async (event, ctx) => {
|
|
80
|
+
if (!ctx.redis) {
|
|
81
|
+
return writeFailure(
|
|
82
|
+
new InternalError({
|
|
83
|
+
message: "signup-confirm requires ctx.redis for token consumption",
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Token-Lookup: nicht-existent / abgelaufen / schon konsumiert →
|
|
89
|
+
// alle collapsen auf invalid_signup_token. Anti-enumeration.
|
|
90
|
+
const email = await getEmailForSignupToken(ctx.redis, event.payload.token);
|
|
91
|
+
if (!email) return invalidSignupToken();
|
|
92
|
+
|
|
93
|
+
// Single-Use-Burn: zwei parallele Confirms aus verschiedenen
|
|
94
|
+
// Tabs — einer wins, der andere kriegt invalid_signup_token.
|
|
95
|
+
const burn = await burnSignupToken(ctx.redis, event.payload.token);
|
|
96
|
+
if (burn === "already-used") return invalidSignupToken();
|
|
97
|
+
|
|
98
|
+
let committed = false;
|
|
99
|
+
try {
|
|
100
|
+
// Tenant-Key: 2-Wort-Slug aus framework/random, mit DB-Conflict-
|
|
101
|
+
// Check gegen tenants.key. 22.500 Default-Combos + Suffix-
|
|
102
|
+
// Fallback bei Kollision (siehe generateUniqueName).
|
|
103
|
+
// @cast-boundary db-runner — TenantDb.raw is DbRunner (Connection|Tx);
|
|
104
|
+
// provisioning helpers operate on plain drizzle-API that both shapes
|
|
105
|
+
// expose identically. Inside an event-store transaction the cast lands
|
|
106
|
+
// on the Tx flavor — same drizzle calls, same behavior.
|
|
107
|
+
const dbConn = ctx.db.raw as DbConnection;
|
|
108
|
+
|
|
109
|
+
const tenantKey = await generateUniqueName({
|
|
110
|
+
isAvailable: async (slug) => {
|
|
111
|
+
const existing = await dbConn
|
|
112
|
+
.select({ id: tenantTable.id })
|
|
113
|
+
.from(tenantTable)
|
|
114
|
+
.where(eq(tenantTable.key, slug))
|
|
115
|
+
.limit(1);
|
|
116
|
+
return existing.length === 0;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const tenantId = generateId() as TenantId;
|
|
121
|
+
// Display-Name aus email-prefix als sinnvolles Default; User kann
|
|
122
|
+
// den Tenant-Namen + sein eigenes displayName später ändern.
|
|
123
|
+
const displayName = email.split("@")[0] ?? email;
|
|
124
|
+
|
|
125
|
+
const provisioned = await provisionSignupAccount(dbConn, {
|
|
126
|
+
email,
|
|
127
|
+
password: event.payload.password,
|
|
128
|
+
displayName,
|
|
129
|
+
tenantId,
|
|
130
|
+
tenantKey,
|
|
131
|
+
// Tenant-Display-Name als Default = Email. User wechselt das im
|
|
132
|
+
// Settings-Screen. Konzept "Tenant" leakt nicht in die Signup-UI.
|
|
133
|
+
tenantName: email,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Cleanup beider Token-Lookup-Keys. Burn-Key bleibt für die
|
|
137
|
+
// restliche Burn-TTL als Replay-Schutz.
|
|
138
|
+
await deleteSignupToken(ctx.redis, { email, token: event.payload.token });
|
|
139
|
+
|
|
140
|
+
// SessionUser für JWT-Mint. Roles aus INITIAL_SIGNUP_ROLES
|
|
141
|
+
// damit DB-write (provisionSignupAccount) und Session-claim
|
|
142
|
+
// dieselbe Quelle teilen — sonst hätten zwei Stellen "Admin"
|
|
143
|
+
// hardcoded und ein Refactor würde role-mismatch zwischen DB
|
|
144
|
+
// und JWT erzeugen.
|
|
145
|
+
const session: SessionUser = {
|
|
146
|
+
id: provisioned.userId,
|
|
147
|
+
tenantId: provisioned.tenantId,
|
|
148
|
+
roles: [...INITIAL_SIGNUP_ROLES],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
committed = true;
|
|
152
|
+
return {
|
|
153
|
+
isSuccess: true,
|
|
154
|
+
data: {
|
|
155
|
+
kind: "auth-session",
|
|
156
|
+
session,
|
|
157
|
+
tenantKey,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
} finally {
|
|
161
|
+
if (!committed && ctx.redis) {
|
|
162
|
+
// Burn-release damit ein retry nach DB-Hiccup nicht blockt.
|
|
163
|
+
// Token-Lookup-Keys bleiben — der User kann seinen Mail-Link
|
|
164
|
+
// erneut klicken.
|
|
165
|
+
await unburnSignupToken(ctx.redis, event.payload.token);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Magic-Link-Signup, Step 1 (request).
|
|
2
|
+
//
|
|
3
|
+
// User gibt Email ein → wir minten einen opaken Random-Token, speichern
|
|
4
|
+
// ihn bidirektional in Redis (token↔email), und der Route-Layer schickt
|
|
5
|
+
// die Activation-Mail. Anders als reset/verify-Flows existiert der User
|
|
6
|
+
// HIER NOCH NICHT — daher kein userId-lookup, kein HMAC-signing (wofür
|
|
7
|
+
// gäbe es kein Subject), kein "skip if user already exists in DB"-pattern.
|
|
8
|
+
//
|
|
9
|
+
// Resend-Idempotenz: wenn für die Email bereits ein lebender Token in
|
|
10
|
+
// Redis liegt, geben wir denselben Token zurück (und refreshen TTL auf
|
|
11
|
+
// beiden Keys). Der User bekommt dann eine zweite Mail mit dem GLEICHEN
|
|
12
|
+
// Activation-Link. Erste Mail bleibt gültig — kein "old link broken"-
|
|
13
|
+
// annoyance.
|
|
14
|
+
//
|
|
15
|
+
// Always-200 (enumeration-safe): das Response sieht für jede Email
|
|
16
|
+
// gleich aus, egal ob sie schon registriert ist oder nicht. Anders als
|
|
17
|
+
// reset (das ein "no-op" zurückgibt wenn User nicht existiert) gibt's
|
|
18
|
+
// hier nichts zu enumerieren — eine Email kann nicht "schon registriert
|
|
19
|
+
// sein" weil bei Magic-Link der User-Row erst beim Confirm entsteht.
|
|
20
|
+
// Was es geben könnte: dieselbe Email versucht es zum N-ten Mal —
|
|
21
|
+
// Resend-Pfad ist by-design idempotent.
|
|
22
|
+
|
|
23
|
+
import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
24
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
|
+
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
26
|
+
import { Temporal } from "temporal-polyfill";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { AUTH_SIGNUP_DEFAULT_TTL_MINUTES } from "../constants";
|
|
29
|
+
import { getTokenForSignupEmail, normalizeEmail, storeSignupToken } from "../signup-token-store";
|
|
30
|
+
|
|
31
|
+
const SignupRequestSchema = z.object({
|
|
32
|
+
email: z.email(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type SignupRequestData =
|
|
36
|
+
| {
|
|
37
|
+
readonly kind: "signup-requested";
|
|
38
|
+
readonly email: string;
|
|
39
|
+
readonly token: string;
|
|
40
|
+
readonly expiresAt: string;
|
|
41
|
+
}
|
|
42
|
+
| { readonly kind: "no-op" };
|
|
43
|
+
|
|
44
|
+
export type SignupRequestOptions = {
|
|
45
|
+
/** TTL für den Activation-Token. Default 24 h — lang genug damit User
|
|
46
|
+
* "morgen aktivieren" können ohne Resend-Spam. */
|
|
47
|
+
readonly tokenTtlMinutes?: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function createSignupRequestHandler(opts: SignupRequestOptions = {}) {
|
|
51
|
+
const ttlMinutes = opts.tokenTtlMinutes ?? AUTH_SIGNUP_DEFAULT_TTL_MINUTES;
|
|
52
|
+
const ttlSeconds = ttlMinutes * 60;
|
|
53
|
+
|
|
54
|
+
return defineWriteHandler<"signup-request", typeof SignupRequestSchema, SignupRequestData>({
|
|
55
|
+
name: "signup-request",
|
|
56
|
+
schema: SignupRequestSchema,
|
|
57
|
+
access: { roles: ["all"] },
|
|
58
|
+
handler: async (event, ctx) => {
|
|
59
|
+
if (!ctx.redis) {
|
|
60
|
+
return writeFailure(
|
|
61
|
+
new InternalError({
|
|
62
|
+
message: "signup-request requires ctx.redis for the activation-token store",
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Email-Normalisierung lebt im Store (signup-token-store). Der
|
|
68
|
+
// Handler reicht die raw email durch — eine Quelle, kein Drift
|
|
69
|
+
// zwischen Lookup-Pfaden die unterschiedlich (oder gar nicht)
|
|
70
|
+
// lowercased haben.
|
|
71
|
+
const email = event.payload.email;
|
|
72
|
+
|
|
73
|
+
// Resend-Idempotenz: wenn ein Token für diese Email noch lebt,
|
|
74
|
+
// re-use ihn und refreshe beide Keys. Der User kriegt eine zweite
|
|
75
|
+
// Mail mit dem GLEICHEN Link.
|
|
76
|
+
const existingToken = await getTokenForSignupEmail(ctx.redis, email);
|
|
77
|
+
// 32 random bytes = 256 bits unguessable randomness, base64url
|
|
78
|
+
// encoded zu 43 chars. Math.random war früher ein Bug:
|
|
79
|
+
// xorshift128+ hat ~128 Bit State der nach ~5 beobachteten
|
|
80
|
+
// Outputs rekonstruierbar ist — Angreifer könnte eigene
|
|
81
|
+
// signup-requests triggern und die Tokens fremder User
|
|
82
|
+
// vorhersagen. generateToken nutzt randomBytes aus node:crypto,
|
|
83
|
+
// dieselbe Quelle wie CSRF/Session-Tokens.
|
|
84
|
+
const token = existingToken ?? generateToken();
|
|
85
|
+
|
|
86
|
+
const expiresAt = Temporal.Now.instant().add({ seconds: ttlSeconds });
|
|
87
|
+
|
|
88
|
+
await storeSignupToken(ctx.redis, { email, token, ttlSeconds });
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
isSuccess: true,
|
|
92
|
+
data: {
|
|
93
|
+
kind: "signup-requested",
|
|
94
|
+
// normalizeEmail aus dem Store — eine Quelle für die
|
|
95
|
+
// Normalisierungs-Verantwortung; Mail-Callback kriegt
|
|
96
|
+
// konsistent das gleiche Format wie der Lookup-Pfad.
|
|
97
|
+
email: normalizeEmail(email),
|
|
98
|
+
token,
|
|
99
|
+
expiresAt: expiresAt.toString(),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Shared factory for the request-side of out-of-band token flows
|
|
2
|
+
// (password-reset, email-verification). Both follow the same shape:
|
|
3
|
+
//
|
|
4
|
+
// POST email
|
|
5
|
+
// → resolve user (system-scoped query)
|
|
6
|
+
// → skip silently if user doesn't exist / is deleted / already done
|
|
7
|
+
// → mint an HMAC-signed token
|
|
8
|
+
// → return { kind: <successKind>, email, token, expiresAt }
|
|
9
|
+
//
|
|
10
|
+
// Differences between the flows are four parameters (successKind, sign fn,
|
|
11
|
+
// default TTL, extra skip condition) + two error codes — encoded on the
|
|
12
|
+
// spec rather than duplicated across two near-identical handler bodies.
|
|
13
|
+
|
|
14
|
+
import { createSystemUser, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
+
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
16
|
+
import type { Temporal } from "temporal-polyfill";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { UserQueries } from "../../user";
|
|
19
|
+
import { type AuthUserRow, parseAuthUserRow } from "../auth-user-row";
|
|
20
|
+
|
|
21
|
+
const RequestTokenSchema = z.object({
|
|
22
|
+
email: z.email(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// What the route layer reads off `result.data` after dispatching. Identical
|
|
26
|
+
// shape for both flows; only the `kind` discriminator differs so the route
|
|
27
|
+
// knows whether to forward to sendResetEmail or sendVerificationEmail.
|
|
28
|
+
export type TokenRequestSuccess<K extends string> = {
|
|
29
|
+
readonly kind: K;
|
|
30
|
+
readonly email: string;
|
|
31
|
+
readonly token: string;
|
|
32
|
+
readonly expiresAt: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TokenRequestNoOp = { readonly kind: "no-op" };
|
|
36
|
+
|
|
37
|
+
export type TokenRequestData<K extends string> = TokenRequestSuccess<K> | TokenRequestNoOp;
|
|
38
|
+
|
|
39
|
+
export type TokenRequestSpec<TName extends string, TSuccessKind extends string> = {
|
|
40
|
+
readonly handlerName: TName;
|
|
41
|
+
readonly successKind: TSuccessKind;
|
|
42
|
+
readonly defaultTtlMinutes: number;
|
|
43
|
+
// Feature-specific sign function. Signature matches both signResetToken
|
|
44
|
+
// and signVerificationToken (thin wrappers over signed-token.ts).
|
|
45
|
+
readonly sign: (
|
|
46
|
+
userId: string,
|
|
47
|
+
ttlMinutes: number,
|
|
48
|
+
secret: string,
|
|
49
|
+
) => { token: string; expiresAt: Temporal.Instant };
|
|
50
|
+
// Error code + i18nKey returned when the feature-factory wasn't given a
|
|
51
|
+
// working hmacSecret. Should never happen — feature-factory validates at
|
|
52
|
+
// boot — but defensive coverage for lazy secret-providers.
|
|
53
|
+
readonly notConfiguredError: string;
|
|
54
|
+
readonly notConfiguredI18nKey: string;
|
|
55
|
+
// Extra silent-skip predicate on top of "user doesn't exist or is
|
|
56
|
+
// soft-deleted". Verification skips when emailVerified is already true;
|
|
57
|
+
// password-reset has no extra condition (returns false).
|
|
58
|
+
readonly extraSilentSkip: (user: AuthUserRow) => boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type TokenRequestOptions = {
|
|
62
|
+
readonly hmacSecret: string;
|
|
63
|
+
readonly tokenTtlMinutes?: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function createTokenRequestHandler<TName extends string, TSuccessKind extends string>(
|
|
67
|
+
spec: TokenRequestSpec<TName, TSuccessKind>,
|
|
68
|
+
opts: TokenRequestOptions,
|
|
69
|
+
) {
|
|
70
|
+
const ttl = opts.tokenTtlMinutes ?? spec.defaultTtlMinutes;
|
|
71
|
+
|
|
72
|
+
return defineWriteHandler<TName, typeof RequestTokenSchema, TokenRequestData<TSuccessKind>>({
|
|
73
|
+
name: spec.handlerName,
|
|
74
|
+
schema: RequestTokenSchema,
|
|
75
|
+
access: { roles: ["all"] },
|
|
76
|
+
handler: async (event, ctx) => {
|
|
77
|
+
if (!opts.hmacSecret) {
|
|
78
|
+
// Feature-factory guards this at boot; defensive here for lazy-
|
|
79
|
+
// provided secrets that show up empty at runtime.
|
|
80
|
+
return writeFailure(
|
|
81
|
+
new UnprocessableError(spec.notConfiguredError, {
|
|
82
|
+
i18nKey: spec.notConfiguredI18nKey,
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const systemUser = createSystemUser(event.user.tenantId);
|
|
88
|
+
|
|
89
|
+
const user = parseAuthUserRow(
|
|
90
|
+
await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
91
|
+
email: event.payload.email,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Silent-success branches all return the SAME shape with kind="no-op".
|
|
96
|
+
// Response-level timing stays uniform (200 / isSuccess: true); the
|
|
97
|
+
// small difference in handler-internal work is accepted — no probing
|
|
98
|
+
// client can observe it through the HTTP surface.
|
|
99
|
+
if (!user || user.isDeleted || !user.email || spec.extraSilentSkip(user)) {
|
|
100
|
+
const data: TokenRequestData<TSuccessKind> = { kind: "no-op" };
|
|
101
|
+
return { isSuccess: true, data };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { token, expiresAt } = spec.sign(user.id, ttl, opts.hmacSecret);
|
|
105
|
+
const data: TokenRequestData<TSuccessKind> = {
|
|
106
|
+
kind: spec.successKind,
|
|
107
|
+
email: user.email,
|
|
108
|
+
token,
|
|
109
|
+
expiresAt: expiresAt.toString(),
|
|
110
|
+
};
|
|
111
|
+
return { isSuccess: true, data };
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { AuthErrors } from "../constants";
|
|
5
|
+
import { verifyVerificationToken } from "../verification-token";
|
|
6
|
+
import { runConfirmTokenFlow } from "./confirm-token-flow";
|
|
7
|
+
|
|
8
|
+
export type VerifyEmailOptions = {
|
|
9
|
+
readonly hmacSecret: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const VerifyEmailSchema = z.object({
|
|
13
|
+
token: z.string().min(1),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type VerifyEmailData = { readonly kind: "verified" } | { readonly kind: "already-verified" };
|
|
17
|
+
|
|
18
|
+
function invalidToken() {
|
|
19
|
+
return writeFailure(
|
|
20
|
+
new UnprocessableError(AuthErrors.invalidVerificationToken, {
|
|
21
|
+
i18nKey: "auth.errors.invalidVerificationToken",
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Sets user.emailVerified = true on a valid token. Idempotent via the
|
|
27
|
+
// `alreadyDone` short-circuit — when the row already reads verified
|
|
28
|
+
// (reached through another flow), we skip the write but keep the burn
|
|
29
|
+
// so replays still see invalid_verification_token on the burn check.
|
|
30
|
+
// Sessions are NOT revoked on verification — no security reason to
|
|
31
|
+
// nuke active logins when a user finally confirms their address.
|
|
32
|
+
export function createVerifyEmailHandler(opts: VerifyEmailOptions) {
|
|
33
|
+
return defineWriteHandler<"verify-email", typeof VerifyEmailSchema, VerifyEmailData>({
|
|
34
|
+
name: "verify-email",
|
|
35
|
+
schema: VerifyEmailSchema,
|
|
36
|
+
access: { roles: ["all"] },
|
|
37
|
+
handler: async (event, ctx) => {
|
|
38
|
+
if (!opts.hmacSecret) {
|
|
39
|
+
return writeFailure(
|
|
40
|
+
new UnprocessableError(AuthErrors.verificationNotConfigured, {
|
|
41
|
+
i18nKey: "auth.errors.verificationNotConfigured",
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const verify = verifyVerificationToken(event.payload.token, opts.hmacSecret);
|
|
47
|
+
if (!verify.ok) return invalidToken();
|
|
48
|
+
|
|
49
|
+
return runConfirmTokenFlow<VerifyEmailData>(ctx, verify.userId, verify.expiresAtMs, {
|
|
50
|
+
purpose: "verify",
|
|
51
|
+
redisRequiredMessage: "email-verification requires ctx.redis to enforce token single-use",
|
|
52
|
+
invalidToken,
|
|
53
|
+
buildChanges: async () => ({ emailVerified: true }),
|
|
54
|
+
successData: { kind: "verified" },
|
|
55
|
+
alreadyDone: {
|
|
56
|
+
check: (me) => me.emailVerified === true,
|
|
57
|
+
data: { kind: "already-verified" },
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Default-Bundles für die Feature-UI. Werden vom emailPasswordClient()
|
|
3
|
+
// als Fallback-Bundle in den LocaleProvider gehängt — Apps können
|
|
4
|
+
// einzelne Keys via `emailPasswordClient({ translations: { de: { ... } } })`
|
|
5
|
+
// überschreiben, ohne das ganze Bundle kopieren zu müssen.
|
|
6
|
+
//
|
|
7
|
+
// Keys folgen dem Schema `auth.<area>.<slug>` — `auth.login.*` für die
|
|
8
|
+
// Formular-UI, `auth.errors.*` für Reason-Codes aus dem Login-Handler
|
|
9
|
+
// (1:1 gespiegelt zu AuthErrors im server-side Feature).
|
|
10
|
+
|
|
11
|
+
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
12
|
+
|
|
13
|
+
export const defaultTranslations: TranslationsByLocale = {
|
|
14
|
+
de: {
|
|
15
|
+
"auth.login.title": "Anmelden",
|
|
16
|
+
"auth.login.email": "E-Mail",
|
|
17
|
+
"auth.login.password": "Passwort",
|
|
18
|
+
"auth.login.submit": "Einloggen",
|
|
19
|
+
"auth.login.submitting": "…",
|
|
20
|
+
"auth.login.forgotPassword": "Passwort vergessen?",
|
|
21
|
+
"auth.errors.invalidCredentials": "E-Mail oder Passwort falsch.",
|
|
22
|
+
"auth.errors.noMembership": "Dieses Konto hat keinen Tenant-Zugang.",
|
|
23
|
+
"auth.errors.accountLocked": "Konto vorübergehend gesperrt.",
|
|
24
|
+
"auth.errors.accountLockedRetry": "Konto gesperrt. Neuer Versuch in {minutes} Minuten.",
|
|
25
|
+
"auth.errors.emailNotVerified": "E-Mail-Adresse noch nicht bestätigt.",
|
|
26
|
+
"auth.errors.rateLimited": "Zu viele Login-Versuche. Bitte kurz warten.",
|
|
27
|
+
"auth.errors.invalidBody": "Ungültige Eingabe.",
|
|
28
|
+
"auth.errors.loginFailed": "Login fehlgeschlagen.",
|
|
29
|
+
"auth.errors.invalidResetToken":
|
|
30
|
+
"Der Link ist ungültig oder abgelaufen. Bitte fordere einen neuen an.",
|
|
31
|
+
"auth.errors.invalidVerificationToken": "Der Bestätigungs-Link ist ungültig oder abgelaufen.",
|
|
32
|
+
"auth.errors.invalidSignupToken":
|
|
33
|
+
"Der Aktivierungs-Link ist ungültig oder abgelaufen. Bitte fordere einen neuen an.",
|
|
34
|
+
"auth.errors.unknownError": "Etwas ist schief gegangen. Bitte erneut versuchen.",
|
|
35
|
+
"auth.forgotPassword.title": "Passwort zurücksetzen",
|
|
36
|
+
"auth.forgotPassword.intro":
|
|
37
|
+
"Gib deine E-Mail-Adresse ein. Falls ein Konto existiert, schicken wir dir einen Reset-Link.",
|
|
38
|
+
"auth.forgotPassword.email": "E-Mail",
|
|
39
|
+
"auth.forgotPassword.submit": "Link anfordern",
|
|
40
|
+
"auth.forgotPassword.submitting": "…",
|
|
41
|
+
"auth.forgotPassword.successTitle": "Mail gesendet",
|
|
42
|
+
"auth.forgotPassword.successBody":
|
|
43
|
+
"Falls die E-Mail in unserem System existiert, ist eine Nachricht mit einem Reset-Link unterwegs. Bitte schau in deinen Posteingang.",
|
|
44
|
+
"auth.forgotPassword.backToLogin": "Zurück zum Login",
|
|
45
|
+
"auth.resetPassword.title": "Neues Passwort setzen",
|
|
46
|
+
"auth.resetPassword.intro": "Wähle ein neues Passwort (mindestens 8 Zeichen).",
|
|
47
|
+
"auth.resetPassword.newPassword": "Neues Passwort",
|
|
48
|
+
"auth.resetPassword.confirmPassword": "Passwort bestätigen",
|
|
49
|
+
"auth.resetPassword.mismatch": "Die Passwörter stimmen nicht überein.",
|
|
50
|
+
"auth.resetPassword.tooShort": "Passwort muss mindestens 8 Zeichen lang sein.",
|
|
51
|
+
"auth.resetPassword.submit": "Passwort speichern",
|
|
52
|
+
"auth.resetPassword.submitting": "…",
|
|
53
|
+
"auth.resetPassword.successTitle": "Passwort gesetzt",
|
|
54
|
+
"auth.resetPassword.successBody": "Du kannst dich jetzt mit deinem neuen Passwort anmelden.",
|
|
55
|
+
"auth.resetPassword.goToLogin": "Zum Login",
|
|
56
|
+
"auth.resetPassword.missingToken":
|
|
57
|
+
"Der Reset-Link enthält keinen Token. Bitte fordere einen neuen an.",
|
|
58
|
+
"auth.verifyEmail.verifying": "E-Mail wird bestätigt …",
|
|
59
|
+
"auth.verifyEmail.successTitle": "E-Mail bestätigt",
|
|
60
|
+
"auth.verifyEmail.successBody": "Danke! Du kannst dich jetzt anmelden.",
|
|
61
|
+
"auth.verifyEmail.errorTitle": "Bestätigung fehlgeschlagen",
|
|
62
|
+
"auth.verifyEmail.errorBody":
|
|
63
|
+
"Der Link ist ungültig oder abgelaufen. Bitte fordere eine neue Bestätigungs-Mail an.",
|
|
64
|
+
"auth.verifyEmail.goToLogin": "Zum Login",
|
|
65
|
+
"auth.verifyEmail.missingToken": "Der Bestätigungs-Link enthält keinen Token.",
|
|
66
|
+
"auth.signup.title": "Account erstellen",
|
|
67
|
+
"auth.signup.intro":
|
|
68
|
+
"Gib deine E-Mail-Adresse ein. Wir schicken dir einen Aktivierungs-Link, mit dem du dein Passwort setzt.",
|
|
69
|
+
"auth.signup.email": "E-Mail",
|
|
70
|
+
"auth.signup.submit": "Aktivierungs-Link senden",
|
|
71
|
+
"auth.signup.submitting": "…",
|
|
72
|
+
"auth.signup.successTitle": "Mail gesendet",
|
|
73
|
+
"auth.signup.successBody":
|
|
74
|
+
"Wir haben dir einen Aktivierungs-Link an deine E-Mail-Adresse geschickt. Klicke ihn an, um dein Passwort zu setzen und dich einzuloggen.",
|
|
75
|
+
"auth.signup.resend": "Mail erneut senden",
|
|
76
|
+
"auth.signup.haveAccount": "Bereits einen Account? Anmelden",
|
|
77
|
+
"auth.signupComplete.title": "Passwort setzen",
|
|
78
|
+
"auth.signupComplete.intro":
|
|
79
|
+
"Wähle ein Passwort (mindestens 8 Zeichen) für deinen neuen Account.",
|
|
80
|
+
"auth.signupComplete.password": "Passwort",
|
|
81
|
+
"auth.signupComplete.confirmPassword": "Passwort bestätigen",
|
|
82
|
+
"auth.signupComplete.tooShort": "Passwort muss mindestens 8 Zeichen lang sein.",
|
|
83
|
+
"auth.signupComplete.mismatch": "Die Passwörter stimmen nicht überein.",
|
|
84
|
+
"auth.signupComplete.submit": "Account aktivieren",
|
|
85
|
+
"auth.signupComplete.submitting": "…",
|
|
86
|
+
"auth.signupComplete.missingToken":
|
|
87
|
+
"Der Aktivierungs-Link enthält keinen Token. Bitte fordere einen neuen an.",
|
|
88
|
+
"auth.inviteAccept.title": "Einladung annehmen",
|
|
89
|
+
"auth.inviteAccept.intro":
|
|
90
|
+
"Du wurdest zu einem Workspace eingeladen. Klicke auf 'Annehmen' um Mitglied zu werden.",
|
|
91
|
+
"auth.inviteAccept.loggedInAs": "Du bist eingeloggt — klicke 'Annehmen' um Mitglied zu werden.",
|
|
92
|
+
"auth.inviteAccept.email": "E-Mail",
|
|
93
|
+
"auth.inviteAccept.password": "Passwort",
|
|
94
|
+
"auth.inviteAccept.acceptButton": "Annehmen",
|
|
95
|
+
"auth.inviteAccept.submit": "Annehmen + Anmelden",
|
|
96
|
+
"auth.inviteAccept.submitting": "…",
|
|
97
|
+
"auth.inviteAccept.useOtherAccount": "Mit anderem Account anmelden",
|
|
98
|
+
"auth.inviteAccept.toggleNew": "Ich habe noch keinen Account",
|
|
99
|
+
"auth.inviteAccept.toggleExisting": "Ich habe schon einen Account",
|
|
100
|
+
"auth.inviteAccept.missingToken": "Der Einladungs-Link enthält keinen Token oder ist ungültig.",
|
|
101
|
+
"auth.inviteAccept.goToLogin": "Zum Login",
|
|
102
|
+
"auth.user.menu.label": "Konto",
|
|
103
|
+
"auth.user.menu.logout": "Abmelden",
|
|
104
|
+
"auth.tenant.switcher.label": "Tenant",
|
|
105
|
+
"auth.tenant.switcher.none": "Kein Tenant",
|
|
106
|
+
},
|
|
107
|
+
en: {
|
|
108
|
+
"auth.login.title": "Sign in",
|
|
109
|
+
"auth.login.email": "Email",
|
|
110
|
+
"auth.login.password": "Password",
|
|
111
|
+
"auth.login.submit": "Sign in",
|
|
112
|
+
"auth.login.submitting": "…",
|
|
113
|
+
"auth.login.forgotPassword": "Forgot password?",
|
|
114
|
+
"auth.errors.invalidCredentials": "Invalid email or password.",
|
|
115
|
+
"auth.errors.noMembership": "This account has no tenant access.",
|
|
116
|
+
"auth.errors.accountLocked": "Account temporarily locked.",
|
|
117
|
+
"auth.errors.accountLockedRetry": "Account locked. Try again in {minutes} minutes.",
|
|
118
|
+
"auth.errors.emailNotVerified": "Email address not yet verified.",
|
|
119
|
+
"auth.errors.rateLimited": "Too many login attempts. Please wait briefly.",
|
|
120
|
+
"auth.errors.invalidBody": "Invalid input.",
|
|
121
|
+
"auth.errors.loginFailed": "Login failed.",
|
|
122
|
+
"auth.errors.invalidResetToken": "Link is invalid or expired. Please request a new one.",
|
|
123
|
+
"auth.errors.invalidVerificationToken": "Verification link is invalid or expired.",
|
|
124
|
+
"auth.errors.invalidSignupToken":
|
|
125
|
+
"Activation link is invalid or expired. Please request a new one.",
|
|
126
|
+
"auth.errors.unknownError": "Something went wrong. Please try again.",
|
|
127
|
+
"auth.forgotPassword.title": "Reset password",
|
|
128
|
+
"auth.forgotPassword.intro":
|
|
129
|
+
"Enter your email. If an account exists, we'll send you a reset link.",
|
|
130
|
+
"auth.forgotPassword.email": "Email",
|
|
131
|
+
"auth.forgotPassword.submit": "Request link",
|
|
132
|
+
"auth.forgotPassword.submitting": "…",
|
|
133
|
+
"auth.forgotPassword.successTitle": "Email sent",
|
|
134
|
+
"auth.forgotPassword.successBody":
|
|
135
|
+
"If your email exists in our system, a reset link is on its way. Please check your inbox.",
|
|
136
|
+
"auth.forgotPassword.backToLogin": "Back to sign in",
|
|
137
|
+
"auth.resetPassword.title": "Set new password",
|
|
138
|
+
"auth.resetPassword.intro": "Choose a new password (at least 8 characters).",
|
|
139
|
+
"auth.resetPassword.newPassword": "New password",
|
|
140
|
+
"auth.resetPassword.confirmPassword": "Confirm password",
|
|
141
|
+
"auth.resetPassword.mismatch": "Passwords do not match.",
|
|
142
|
+
"auth.resetPassword.tooShort": "Password must be at least 8 characters.",
|
|
143
|
+
"auth.resetPassword.submit": "Save password",
|
|
144
|
+
"auth.resetPassword.submitting": "…",
|
|
145
|
+
"auth.resetPassword.successTitle": "Password set",
|
|
146
|
+
"auth.resetPassword.successBody": "You can now sign in with your new password.",
|
|
147
|
+
"auth.resetPassword.goToLogin": "Go to sign in",
|
|
148
|
+
"auth.resetPassword.missingToken": "Reset link is missing a token. Please request a new one.",
|
|
149
|
+
"auth.verifyEmail.verifying": "Verifying email …",
|
|
150
|
+
"auth.verifyEmail.successTitle": "Email verified",
|
|
151
|
+
"auth.verifyEmail.successBody": "Thanks! You can sign in now.",
|
|
152
|
+
"auth.verifyEmail.errorTitle": "Verification failed",
|
|
153
|
+
"auth.verifyEmail.errorBody":
|
|
154
|
+
"Link is invalid or expired. Please request a new verification email.",
|
|
155
|
+
"auth.verifyEmail.goToLogin": "Go to sign in",
|
|
156
|
+
"auth.verifyEmail.missingToken": "Verification link is missing a token.",
|
|
157
|
+
"auth.signup.title": "Create account",
|
|
158
|
+
"auth.signup.intro":
|
|
159
|
+
"Enter your email. We'll send you an activation link to set your password.",
|
|
160
|
+
"auth.signup.email": "Email",
|
|
161
|
+
"auth.signup.submit": "Send activation link",
|
|
162
|
+
"auth.signup.submitting": "…",
|
|
163
|
+
"auth.signup.successTitle": "Email sent",
|
|
164
|
+
"auth.signup.successBody":
|
|
165
|
+
"We've sent you an activation link. Click it to set your password and sign in.",
|
|
166
|
+
"auth.signup.resend": "Send email again",
|
|
167
|
+
"auth.signup.haveAccount": "Already have an account? Sign in",
|
|
168
|
+
"auth.signupComplete.title": "Set password",
|
|
169
|
+
"auth.signupComplete.intro": "Choose a password (at least 8 characters) for your new account.",
|
|
170
|
+
"auth.signupComplete.password": "Password",
|
|
171
|
+
"auth.signupComplete.confirmPassword": "Confirm password",
|
|
172
|
+
"auth.signupComplete.tooShort": "Password must be at least 8 characters.",
|
|
173
|
+
"auth.signupComplete.mismatch": "Passwords do not match.",
|
|
174
|
+
"auth.signupComplete.submit": "Activate account",
|
|
175
|
+
"auth.signupComplete.submitting": "…",
|
|
176
|
+
"auth.signupComplete.missingToken":
|
|
177
|
+
"Activation link is missing a token. Please request a new one.",
|
|
178
|
+
"auth.inviteAccept.title": "Accept invitation",
|
|
179
|
+
"auth.inviteAccept.intro": "You've been invited to a workspace. Click 'Accept' to join.",
|
|
180
|
+
"auth.inviteAccept.loggedInAs": "Signed in as {email}",
|
|
181
|
+
"auth.inviteAccept.email": "Email",
|
|
182
|
+
"auth.inviteAccept.password": "Password",
|
|
183
|
+
"auth.inviteAccept.acceptButton": "Accept",
|
|
184
|
+
"auth.inviteAccept.submit": "Accept + sign in",
|
|
185
|
+
"auth.inviteAccept.submitting": "…",
|
|
186
|
+
"auth.inviteAccept.useOtherAccount": "Sign in with a different account",
|
|
187
|
+
"auth.inviteAccept.toggleNew": "I don't have an account yet",
|
|
188
|
+
"auth.inviteAccept.toggleExisting": "I already have an account",
|
|
189
|
+
"auth.inviteAccept.missingToken": "The invitation link is missing or invalid.",
|
|
190
|
+
"auth.inviteAccept.goToLogin": "Go to sign in",
|
|
191
|
+
"auth.user.menu.label": "Account",
|
|
192
|
+
"auth.user.menu.logout": "Sign out",
|
|
193
|
+
"auth.tenant.switcher.label": "Tenant",
|
|
194
|
+
"auth.tenant.switcher.none": "No tenant",
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
|
|
199
|
+
* die Locales werden zusammengeführt. Wird von emailPasswordClient()
|
|
200
|
+
* benutzt, um App-Overrides über die Defaults zu legen. */
|
|
201
|
+
export function mergeTranslations(
|
|
202
|
+
base: TranslationsByLocale,
|
|
203
|
+
override: TranslationsByLocale,
|
|
204
|
+
): TranslationsByLocale {
|
|
205
|
+
const locales = new Set([...Object.keys(base), ...Object.keys(override)]);
|
|
206
|
+
const merged: Record<string, Record<string, string>> = {};
|
|
207
|
+
for (const locale of locales) {
|
|
208
|
+
merged[locale] = { ...(base[locale] ?? {}), ...(override[locale] ?? {}) };
|
|
209
|
+
}
|
|
210
|
+
return merged;
|
|
211
|
+
}
|