@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,56 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Public exports für die Browser-Seite des auth-email-password Features.
|
|
3
|
+
// Wird über den Sub-Path-Export `@cosmicdrift/kumiko-bundled-features/auth-email-
|
|
4
|
+
// password/web` konsumiert — die Server-Seite (defineFeature) lebt in
|
|
5
|
+
// `@cosmicdrift/kumiko-bundled-features/auth-email-password` und hat keine
|
|
6
|
+
// React-/DOM-Deps. Trennung bleibt sauber so wie renderer vs renderer-web.
|
|
7
|
+
|
|
8
|
+
export { defaultTranslations } from "../i18n";
|
|
9
|
+
export type {
|
|
10
|
+
AuthTokenFailure,
|
|
11
|
+
CurrentUserProfile,
|
|
12
|
+
LoginFailure,
|
|
13
|
+
LoginRequest,
|
|
14
|
+
ResetPasswordFailure,
|
|
15
|
+
SignupConfirmSuccess,
|
|
16
|
+
TenantSummary,
|
|
17
|
+
VerifyEmailFailure,
|
|
18
|
+
} from "./auth-client";
|
|
19
|
+
export {
|
|
20
|
+
confirmSignup,
|
|
21
|
+
requestEmailVerification,
|
|
22
|
+
requestPasswordReset,
|
|
23
|
+
requestSignup,
|
|
24
|
+
resetPassword,
|
|
25
|
+
verifyEmail,
|
|
26
|
+
} from "./auth-client";
|
|
27
|
+
export { makeAuthGate } from "./auth-gate";
|
|
28
|
+
export type {
|
|
29
|
+
EmailPasswordClientFeature,
|
|
30
|
+
EmailPasswordClientOptions,
|
|
31
|
+
} from "./client-plugin";
|
|
32
|
+
export { emailPasswordClient } from "./client-plugin";
|
|
33
|
+
export type { DefaultTopbarActionsProps } from "./default-topbar-actions";
|
|
34
|
+
export { DefaultTopbarActions } from "./default-topbar-actions";
|
|
35
|
+
export type { ForgotPasswordScreenProps } from "./forgot-password-screen";
|
|
36
|
+
export { ForgotPasswordScreen } from "./forgot-password-screen";
|
|
37
|
+
export type { InviteAcceptScreenProps } from "./invite-accept-screen";
|
|
38
|
+
export { InviteAcceptScreen } from "./invite-accept-screen";
|
|
39
|
+
export type { LoginScreenProps } from "./login-screen";
|
|
40
|
+
export { LoginScreen } from "./login-screen";
|
|
41
|
+
export type { ResetPasswordScreenProps } from "./reset-password-screen";
|
|
42
|
+
export { ResetPasswordScreen } from "./reset-password-screen";
|
|
43
|
+
export type { SessionApi, SessionState, SessionStatus } from "./session";
|
|
44
|
+
export { SessionContext, SessionProvider, useSession } from "./session";
|
|
45
|
+
export type { SignupCompleteScreenProps } from "./signup-complete-screen";
|
|
46
|
+
export { SignupCompleteScreen } from "./signup-complete-screen";
|
|
47
|
+
export type { SignupScreenProps } from "./signup-screen";
|
|
48
|
+
export { SignupScreen } from "./signup-screen";
|
|
49
|
+
export type { TenantSwitcherProps } from "./tenant-switcher";
|
|
50
|
+
export { TenantSwitcher } from "./tenant-switcher";
|
|
51
|
+
export type { ShellUser } from "./use-shell-user";
|
|
52
|
+
export { useShellUser } from "./use-shell-user";
|
|
53
|
+
export type { UserMenuProps } from "./user-menu";
|
|
54
|
+
export { UserMenu } from "./user-menu";
|
|
55
|
+
export type { VerifyEmailScreenProps } from "./verify-email-screen";
|
|
56
|
+
export { VerifyEmailScreen } from "./verify-email-screen";
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// InviteAcceptScreen — Magic-Link-Accept für Tenant-Invitations.
|
|
3
|
+
//
|
|
4
|
+
// Liest `?token=...` aus der URL und branched intern je nach session-state:
|
|
5
|
+
// - Logged-in + Email-Match: 1-click accept (Branch 1, dispatcher
|
|
6
|
+
// invite-accept). Bei Email-Mismatch zeigen wir die fehlermeldung
|
|
7
|
+
// und einen "Mit anderem Account anmelden"-Link.
|
|
8
|
+
// - Anonymous: Email + Password-Form. Toggle "Schon einen Account?"
|
|
9
|
+
// entscheidet Branch 2 (existing user, login + accept) vs Branch 3
|
|
10
|
+
// (neuer user, signup + accept).
|
|
11
|
+
//
|
|
12
|
+
// Anti-enumeration: invalidInviteToken collapsed alle Token/User/
|
|
13
|
+
// Password-Failures auf einen Code (gleicher Trade-off wie reset).
|
|
14
|
+
//
|
|
15
|
+
// Auto-Login: Branch 2+3 setzen Cookies via Server, Frontend redirected
|
|
16
|
+
// zu loggedInHref. Branch 1 hat schon eine Session — Frontend redirected
|
|
17
|
+
// auch zu loggedInHref damit der invitee in seinem neuen Tenant landet.
|
|
18
|
+
|
|
19
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
20
|
+
import { type FormEvent, type ReactNode, useState } from "react";
|
|
21
|
+
import { csrfHeader } from "./auth-client";
|
|
22
|
+
import { AuthCard, authMutedLinkClass, parseUrlToken } from "./auth-form-primitives";
|
|
23
|
+
import { useSession } from "./session";
|
|
24
|
+
|
|
25
|
+
export type InviteAcceptScreenProps = {
|
|
26
|
+
readonly title?: string;
|
|
27
|
+
readonly token?: string;
|
|
28
|
+
/** Where to redirect on success. Default "/" — Apps mit Multi-Tenant-
|
|
29
|
+
* Routing können `(data) => "/${data.tenantId}/"` setzen. */
|
|
30
|
+
readonly loggedInHref?: string | ((args: { tenantId: string }) => string);
|
|
31
|
+
/** Login-Href für "Mit anderem Account anmelden". Default "/login". */
|
|
32
|
+
readonly loginHref?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type Mode = "loggedin" | "anon-existing" | "anon-new";
|
|
36
|
+
|
|
37
|
+
export function InviteAcceptScreen({
|
|
38
|
+
title,
|
|
39
|
+
token: tokenProp,
|
|
40
|
+
loggedInHref = "/",
|
|
41
|
+
loginHref = "/login",
|
|
42
|
+
}: InviteAcceptScreenProps): ReactNode {
|
|
43
|
+
const t = useTranslation();
|
|
44
|
+
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
45
|
+
const session = useSession();
|
|
46
|
+
const [token] = useState(() => tokenProp ?? parseUrlToken());
|
|
47
|
+
const [mode, setMode] = useState<Mode>(() =>
|
|
48
|
+
session.status === "authenticated" ? "loggedin" : "anon-existing",
|
|
49
|
+
);
|
|
50
|
+
const [email, setEmail] = useState("");
|
|
51
|
+
const [password, setPassword] = useState("");
|
|
52
|
+
const [submitting, setSubmitting] = useState(false);
|
|
53
|
+
const [error, setError] = useState<string | null>(null);
|
|
54
|
+
|
|
55
|
+
const effectiveTitle = title ?? t("auth.inviteAccept.title");
|
|
56
|
+
|
|
57
|
+
const acceptLoggedIn = async (): Promise<void> => {
|
|
58
|
+
setSubmitting(true);
|
|
59
|
+
setError(null);
|
|
60
|
+
const res = await fetch("/api/auth/invite-accept", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
credentials: "same-origin",
|
|
63
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
64
|
+
body: JSON.stringify({ token }),
|
|
65
|
+
});
|
|
66
|
+
setSubmitting(false);
|
|
67
|
+
if (res.ok) {
|
|
68
|
+
const data = (await res.json()) as { tenantId: string };
|
|
69
|
+
const target =
|
|
70
|
+
typeof loggedInHref === "function"
|
|
71
|
+
? loggedInHref({ tenantId: data.tenantId })
|
|
72
|
+
: loggedInHref;
|
|
73
|
+
window.location.assign(target);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setError(t("auth.errors.invalidInviteToken"));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const acceptAnonExisting = async (): Promise<void> => {
|
|
80
|
+
setSubmitting(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
const res = await fetch("/api/auth/invite-accept-with-login", {
|
|
83
|
+
method: "POST",
|
|
84
|
+
credentials: "same-origin",
|
|
85
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
86
|
+
body: JSON.stringify({ token, email, password }),
|
|
87
|
+
});
|
|
88
|
+
setSubmitting(false);
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
const data = (await res.json()) as { tenantId: string };
|
|
91
|
+
const target =
|
|
92
|
+
typeof loggedInHref === "function"
|
|
93
|
+
? loggedInHref({ tenantId: data.tenantId })
|
|
94
|
+
: loggedInHref;
|
|
95
|
+
window.location.assign(target);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setError(t("auth.errors.invalidInviteToken"));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const acceptAnonNew = async (): Promise<void> => {
|
|
102
|
+
setSubmitting(true);
|
|
103
|
+
setError(null);
|
|
104
|
+
const res = await fetch("/api/auth/invite-signup-complete", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
credentials: "same-origin",
|
|
107
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
108
|
+
body: JSON.stringify({ token, password }),
|
|
109
|
+
});
|
|
110
|
+
setSubmitting(false);
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
const data = (await res.json()) as { tenantId: string };
|
|
113
|
+
const target =
|
|
114
|
+
typeof loggedInHref === "function"
|
|
115
|
+
? loggedInHref({ tenantId: data.tenantId })
|
|
116
|
+
: loggedInHref;
|
|
117
|
+
window.location.assign(target);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setError(t("auth.errors.invalidInviteToken"));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const onSubmit = (e?: FormEvent): void => {
|
|
124
|
+
e?.preventDefault();
|
|
125
|
+
if (mode === "loggedin") {
|
|
126
|
+
void acceptLoggedIn();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (mode === "anon-existing") {
|
|
130
|
+
void acceptAnonExisting();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
void acceptAnonNew();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (token === "") {
|
|
137
|
+
return (
|
|
138
|
+
<AuthCard title={effectiveTitle}>
|
|
139
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
140
|
+
<p className="text-sm text-muted-foreground">{t("auth.inviteAccept.missingToken")}</p>
|
|
141
|
+
<a href={loginHref} className={authMutedLinkClass}>
|
|
142
|
+
{t("auth.inviteAccept.goToLogin")}
|
|
143
|
+
</a>
|
|
144
|
+
</div>
|
|
145
|
+
</AuthCard>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<AuthCard title={effectiveTitle}>
|
|
151
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
152
|
+
<p className="text-sm text-muted-foreground">{t("auth.inviteAccept.intro")}</p>
|
|
153
|
+
|
|
154
|
+
{mode === "loggedin" ? (
|
|
155
|
+
<Form onSubmit={onSubmit}>
|
|
156
|
+
<p className="text-sm">{t("auth.inviteAccept.loggedInAs")}</p>
|
|
157
|
+
{error !== null && <Banner variant="error">{error}</Banner>}
|
|
158
|
+
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
159
|
+
{submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.acceptButton")}
|
|
160
|
+
</Button>
|
|
161
|
+
<Button
|
|
162
|
+
type="button"
|
|
163
|
+
variant="secondary"
|
|
164
|
+
onClick={() => {
|
|
165
|
+
setMode("anon-existing");
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{t("auth.inviteAccept.useOtherAccount")}
|
|
169
|
+
</Button>
|
|
170
|
+
</Form>
|
|
171
|
+
) : (
|
|
172
|
+
<Form onSubmit={onSubmit}>
|
|
173
|
+
{mode === "anon-existing" && (
|
|
174
|
+
<Field id="invite-email" label={t("auth.inviteAccept.email")} required>
|
|
175
|
+
<Input
|
|
176
|
+
kind="email"
|
|
177
|
+
id="invite-email"
|
|
178
|
+
name="invite-email"
|
|
179
|
+
value={email}
|
|
180
|
+
onChange={setEmail}
|
|
181
|
+
disabled={submitting}
|
|
182
|
+
required
|
|
183
|
+
autoComplete="email"
|
|
184
|
+
/>
|
|
185
|
+
</Field>
|
|
186
|
+
)}
|
|
187
|
+
<Field id="invite-password" label={t("auth.inviteAccept.password")} required>
|
|
188
|
+
<Input
|
|
189
|
+
kind="password"
|
|
190
|
+
id="invite-password"
|
|
191
|
+
name="invite-password"
|
|
192
|
+
value={password}
|
|
193
|
+
onChange={setPassword}
|
|
194
|
+
disabled={submitting}
|
|
195
|
+
required
|
|
196
|
+
autoComplete={mode === "anon-existing" ? "current-password" : "new-password"}
|
|
197
|
+
/>
|
|
198
|
+
</Field>
|
|
199
|
+
{error !== null && <Banner variant="error">{error}</Banner>}
|
|
200
|
+
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
201
|
+
{submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.submit")}
|
|
202
|
+
</Button>
|
|
203
|
+
<Button
|
|
204
|
+
type="button"
|
|
205
|
+
variant="secondary"
|
|
206
|
+
onClick={() => {
|
|
207
|
+
setMode(mode === "anon-existing" ? "anon-new" : "anon-existing");
|
|
208
|
+
setError(null);
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{mode === "anon-existing"
|
|
212
|
+
? t("auth.inviteAccept.toggleNew")
|
|
213
|
+
: t("auth.inviteAccept.toggleExisting")}
|
|
214
|
+
</Button>
|
|
215
|
+
</Form>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</AuthCard>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Default-LoginScreen. Reine E-Mail-+-Passwort-Form, zentriert, Card-
|
|
3
|
+
// Layout. Texte kommen aus `useTranslation()` — die Default-Bundles
|
|
4
|
+
// (de+en) liefert das Feature selber mit (translations.ts); Apps die
|
|
5
|
+
// was anderes wollen, setzen entweder einen eigenen LocaleResolver auf
|
|
6
|
+
// App-Level oder reichen Key-Overrides an `emailPasswordClient()`.
|
|
7
|
+
//
|
|
8
|
+
// Die Reason-Codes aus dem Login-Handler werden 1:1 auf i18n-Keys
|
|
9
|
+
// gemappt (reasonToI18nKey) — neue Reason-Codes brauchen nur eine
|
|
10
|
+
// neue Zeile im Bundle + hier im Mapping.
|
|
11
|
+
|
|
12
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
13
|
+
import { type FormEvent, type ReactNode, useState } from "react";
|
|
14
|
+
import type { LoginFailure } from "./auth-client";
|
|
15
|
+
import { AuthCard, authMutedLinkClass } from "./auth-form-primitives";
|
|
16
|
+
import { useSession } from "./session";
|
|
17
|
+
|
|
18
|
+
export type LoginScreenProps = {
|
|
19
|
+
/** Overridet den `auth.login.title`-i18n-Key. Nur setzen wenn der
|
|
20
|
+
* Titel stark app-branded ist und keine Translation braucht. */
|
|
21
|
+
readonly title?: string;
|
|
22
|
+
readonly subtitle?: ReactNode;
|
|
23
|
+
readonly submitLabel?: string;
|
|
24
|
+
/** Optional href zum ForgotPasswordScreen. Wenn gesetzt rendert die
|
|
25
|
+
* LoginScreen unter dem Submit-Button einen "Passwort vergessen?"-
|
|
26
|
+
* Link. Apps die den Reset-Flow NICHT anbieten (z.B. nur Magic-Link),
|
|
27
|
+
* setzen das einfach nicht — Login bleibt minimalistisch. */
|
|
28
|
+
readonly forgotPasswordHref?: string;
|
|
29
|
+
/** Optional href zum SignupScreen. Wenn gesetzt rendert die LoginScreen
|
|
30
|
+
* einen "Account erstellen"-Link. Apps die kein Self-Signup wollen
|
|
31
|
+
* (closed-invite-only) setzen das einfach nicht. */
|
|
32
|
+
readonly signupHref?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Map vom Reason-Code des Login-Handlers auf einen i18n-Key plus
|
|
36
|
+
// optional extrahierte Interpolations-Parameter. Ungekannte Codes
|
|
37
|
+
// fallen auf `auth.errors.loginFailed` zurück.
|
|
38
|
+
function reasonToKey(failure: LoginFailure): {
|
|
39
|
+
readonly key: string;
|
|
40
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
41
|
+
} {
|
|
42
|
+
switch (failure.reason) {
|
|
43
|
+
case "invalid_credentials":
|
|
44
|
+
return { key: "auth.errors.invalidCredentials" };
|
|
45
|
+
case "no_membership":
|
|
46
|
+
return { key: "auth.errors.noMembership" };
|
|
47
|
+
case "account_locked":
|
|
48
|
+
if (failure.retryAfterSeconds !== undefined) {
|
|
49
|
+
return {
|
|
50
|
+
key: "auth.errors.accountLockedRetry",
|
|
51
|
+
params: { minutes: Math.ceil(failure.retryAfterSeconds / 60) },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { key: "auth.errors.accountLocked" };
|
|
55
|
+
case "email_not_verified":
|
|
56
|
+
return { key: "auth.errors.emailNotVerified" };
|
|
57
|
+
case "rate_limited":
|
|
58
|
+
return { key: "auth.errors.rateLimited" };
|
|
59
|
+
case "invalid_body":
|
|
60
|
+
return { key: "auth.errors.invalidBody" };
|
|
61
|
+
default:
|
|
62
|
+
return { key: "auth.errors.loginFailed" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function LoginScreen({
|
|
67
|
+
title,
|
|
68
|
+
subtitle,
|
|
69
|
+
submitLabel,
|
|
70
|
+
forgotPasswordHref,
|
|
71
|
+
signupHref,
|
|
72
|
+
}: LoginScreenProps): ReactNode {
|
|
73
|
+
const t = useTranslation();
|
|
74
|
+
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
75
|
+
const session = useSession();
|
|
76
|
+
const [email, setEmail] = useState("");
|
|
77
|
+
const [password, setPassword] = useState("");
|
|
78
|
+
const [submitting, setSubmitting] = useState(false);
|
|
79
|
+
const [error, setError] = useState<LoginFailure | null>(null);
|
|
80
|
+
|
|
81
|
+
const doSubmit = async (): Promise<void> => {
|
|
82
|
+
setSubmitting(true);
|
|
83
|
+
setError(null);
|
|
84
|
+
const res = await session.login({ email, password });
|
|
85
|
+
setSubmitting(false);
|
|
86
|
+
if (!res.ok) setError(res.error);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onSubmit = (e?: FormEvent): void => {
|
|
90
|
+
e?.preventDefault();
|
|
91
|
+
void doSubmit();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const effectiveTitle = title ?? t("auth.login.title");
|
|
95
|
+
const effectiveSubmit = submitLabel ?? t("auth.login.submit");
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<AuthCard title={effectiveTitle} subtitle={subtitle}>
|
|
99
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
100
|
+
<Form onSubmit={onSubmit}>
|
|
101
|
+
<Field id="login-email" label={t("auth.login.email")} required>
|
|
102
|
+
<Input
|
|
103
|
+
kind="email"
|
|
104
|
+
id="login-email"
|
|
105
|
+
name="login-email"
|
|
106
|
+
value={email}
|
|
107
|
+
onChange={setEmail}
|
|
108
|
+
disabled={submitting}
|
|
109
|
+
required
|
|
110
|
+
autoComplete="email"
|
|
111
|
+
/>
|
|
112
|
+
</Field>
|
|
113
|
+
<Field id="login-password" label={t("auth.login.password")} required>
|
|
114
|
+
<Input
|
|
115
|
+
kind="password"
|
|
116
|
+
id="login-password"
|
|
117
|
+
name="login-password"
|
|
118
|
+
value={password}
|
|
119
|
+
onChange={setPassword}
|
|
120
|
+
disabled={submitting}
|
|
121
|
+
required
|
|
122
|
+
autoComplete="current-password"
|
|
123
|
+
/>
|
|
124
|
+
</Field>
|
|
125
|
+
{error !== null && (
|
|
126
|
+
<Banner variant="error">
|
|
127
|
+
{(() => {
|
|
128
|
+
const { key, params } = reasonToKey(error);
|
|
129
|
+
return t(key, params);
|
|
130
|
+
})()}
|
|
131
|
+
</Banner>
|
|
132
|
+
)}
|
|
133
|
+
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
134
|
+
{submitting ? t("auth.login.submitting") : effectiveSubmit}
|
|
135
|
+
</Button>
|
|
136
|
+
</Form>
|
|
137
|
+
{forgotPasswordHref !== undefined && (
|
|
138
|
+
<a href={forgotPasswordHref} className={`${authMutedLinkClass} self-center`}>
|
|
139
|
+
{t("auth.login.forgotPassword")}
|
|
140
|
+
</a>
|
|
141
|
+
)}
|
|
142
|
+
{signupHref !== undefined && (
|
|
143
|
+
<a href={signupHref} className={`${authMutedLinkClass} self-center`}>
|
|
144
|
+
{t("auth.signup.title")}
|
|
145
|
+
</a>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</AuthCard>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// ResetPasswordScreen — liest `?token=...` aus der URL, zeigt Form mit
|
|
3
|
+
// new + confirm-password. Submit triggert /api/auth/reset-password mit
|
|
4
|
+
// dem Token. Server collapses alle Token-Verify-Failures auf einen
|
|
5
|
+
// Code (anti-enumeration); UI zeigt unified "Link ungültig oder
|
|
6
|
+
// abgelaufen"-message.
|
|
7
|
+
//
|
|
8
|
+
// Token-Quelle ist read-once: wir lesen via parseUrlToken() einmalig
|
|
9
|
+
// im useState-Initializer. Apps die das anders brauchen (server-
|
|
10
|
+
// injected Token-Prop, andere Parameter-Namen) reichen einen
|
|
11
|
+
// expliziten `token` als Prop durch.
|
|
12
|
+
|
|
13
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
14
|
+
import { type FormEvent, type ReactNode, useState } from "react";
|
|
15
|
+
import { resetPassword } from "./auth-client";
|
|
16
|
+
import {
|
|
17
|
+
AuthCard,
|
|
18
|
+
authButtonClass,
|
|
19
|
+
authMutedLinkClass,
|
|
20
|
+
parseUrlToken,
|
|
21
|
+
} from "./auth-form-primitives";
|
|
22
|
+
|
|
23
|
+
export type ResetPasswordScreenProps = {
|
|
24
|
+
readonly title?: string;
|
|
25
|
+
/** Override für den Token aus der URL — Apps die per server-side-
|
|
26
|
+
* Render einen Token reinreichen, brauchen das. Default: parsed aus
|
|
27
|
+
* `?token=...` in der URL. */
|
|
28
|
+
readonly token?: string;
|
|
29
|
+
/** href für "Zum Login"-Link nach Success. Default "/login". */
|
|
30
|
+
readonly loginHref?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function ResetPasswordScreen({
|
|
34
|
+
title,
|
|
35
|
+
token: tokenProp,
|
|
36
|
+
loginHref = "/login",
|
|
37
|
+
}: ResetPasswordScreenProps): ReactNode {
|
|
38
|
+
const t = useTranslation();
|
|
39
|
+
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
40
|
+
const [token] = useState(() => tokenProp ?? parseUrlToken());
|
|
41
|
+
const [newPassword, setNewPassword] = useState("");
|
|
42
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
43
|
+
const [submitting, setSubmitting] = useState(false);
|
|
44
|
+
const [done, setDone] = useState(false);
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
const doSubmit = async (): Promise<void> => {
|
|
48
|
+
setError(null);
|
|
49
|
+
if (newPassword.length < 8) {
|
|
50
|
+
setError(t("auth.resetPassword.tooShort"));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (newPassword !== confirmPassword) {
|
|
54
|
+
setError(t("auth.resetPassword.mismatch"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setSubmitting(true);
|
|
58
|
+
const res = await resetPassword(token, newPassword);
|
|
59
|
+
setSubmitting(false);
|
|
60
|
+
if (res.ok) {
|
|
61
|
+
setDone(true);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (res.error.reason === "invalid_reset_token") {
|
|
65
|
+
setError(t("auth.errors.invalidResetToken"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (res.error.reason === "rate_limited") {
|
|
69
|
+
setError(t("auth.errors.rateLimited"));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setError(t("auth.errors.unknownError"));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const onSubmit = (e?: FormEvent): void => {
|
|
76
|
+
e?.preventDefault();
|
|
77
|
+
void doSubmit();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const effectiveTitle = title ?? t("auth.resetPassword.title");
|
|
81
|
+
|
|
82
|
+
// Kein Token in der URL → User soll den Link aus seiner Mail nochmal
|
|
83
|
+
// klicken oder einen neuen Reset anfordern. Form ohne Token zu
|
|
84
|
+
// submitten würde nur den invalidResetToken-Error zeigen — das ist
|
|
85
|
+
// verwirrend. Lieber upfront eine klare Message.
|
|
86
|
+
if (token === "") {
|
|
87
|
+
return (
|
|
88
|
+
<AuthCard title={effectiveTitle}>
|
|
89
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
90
|
+
<p className="text-sm text-muted-foreground">{t("auth.resetPassword.missingToken")}</p>
|
|
91
|
+
<a href={loginHref} className={authMutedLinkClass}>
|
|
92
|
+
{t("auth.resetPassword.goToLogin")}
|
|
93
|
+
</a>
|
|
94
|
+
</div>
|
|
95
|
+
</AuthCard>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<AuthCard title={effectiveTitle}>
|
|
101
|
+
{done ? (
|
|
102
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
103
|
+
<Banner variant="info">
|
|
104
|
+
<p className="font-medium text-foreground">{t("auth.resetPassword.successTitle")}</p>
|
|
105
|
+
<p className="mt-1">{t("auth.resetPassword.successBody")}</p>
|
|
106
|
+
</Banner>
|
|
107
|
+
<a href={loginHref} className={authButtonClass}>
|
|
108
|
+
{t("auth.resetPassword.goToLogin")}
|
|
109
|
+
</a>
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
113
|
+
<p className="text-sm text-muted-foreground">{t("auth.resetPassword.intro")}</p>
|
|
114
|
+
<Form onSubmit={onSubmit}>
|
|
115
|
+
<Field id="reset-new-password" label={t("auth.resetPassword.newPassword")} required>
|
|
116
|
+
<Input
|
|
117
|
+
kind="password"
|
|
118
|
+
id="reset-new-password"
|
|
119
|
+
name="reset-new-password"
|
|
120
|
+
value={newPassword}
|
|
121
|
+
onChange={setNewPassword}
|
|
122
|
+
disabled={submitting}
|
|
123
|
+
required
|
|
124
|
+
autoComplete="new-password"
|
|
125
|
+
/>
|
|
126
|
+
</Field>
|
|
127
|
+
<Field
|
|
128
|
+
id="reset-confirm-password"
|
|
129
|
+
label={t("auth.resetPassword.confirmPassword")}
|
|
130
|
+
required
|
|
131
|
+
>
|
|
132
|
+
<Input
|
|
133
|
+
kind="password"
|
|
134
|
+
id="reset-confirm-password"
|
|
135
|
+
name="reset-confirm-password"
|
|
136
|
+
value={confirmPassword}
|
|
137
|
+
onChange={setConfirmPassword}
|
|
138
|
+
disabled={submitting}
|
|
139
|
+
required
|
|
140
|
+
autoComplete="new-password"
|
|
141
|
+
/>
|
|
142
|
+
</Field>
|
|
143
|
+
{error !== null && <Banner variant="error">{error}</Banner>}
|
|
144
|
+
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
145
|
+
{submitting ? t("auth.resetPassword.submitting") : t("auth.resetPassword.submit")}
|
|
146
|
+
</Button>
|
|
147
|
+
</Form>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</AuthCard>
|
|
151
|
+
);
|
|
152
|
+
}
|