@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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 +1 -1
- package/packages/bundled-features/package.json +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
- package/packages/bundled-features/src/cap-counter/changes.json +30 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +18 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
- package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +42 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +12 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +15 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
- package/packages/bundled-features/src/sessions/changes.json +7 -0
- package/packages/bundled-features/src/sessions/feature.ts +8 -2
- package/packages/bundled-features/src/sessions/index.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
- package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +84 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/feature.ts +1 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
- package/packages/bundled-features/src/waitlist/changes.json +36 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
- package/packages/bundled-features/src/waitlist/import.ts +28 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +5 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +4 -1
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/golden-path/src/feature.ts +2 -1
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/samples/recipes/state-machine/src/events.ts +3 -2
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -12,6 +12,8 @@ import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
|
12
12
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
13
13
|
import { requestPasswordReset } from "./auth-client.ts";
|
|
14
14
|
import { AuthCard } from "./auth-form-primitives.tsx";
|
|
15
|
+
import { readReturnToFromWindow } from "./auth-redirect.ts";
|
|
16
|
+
import { isSafeReturnPath } from "./return-path.ts";
|
|
15
17
|
|
|
16
18
|
export type ForgotPasswordScreenProps = {
|
|
17
19
|
readonly title?: string;
|
|
@@ -19,12 +21,20 @@ export type ForgotPasswordScreenProps = {
|
|
|
19
21
|
/** href for the "Back to login" link in the success and form states. App-
|
|
20
22
|
* specific — defaults to "/login". */
|
|
21
23
|
readonly loginHref?: string;
|
|
24
|
+
/** Page the reset mail should lead back to after the new password is set; defaults to the
|
|
25
|
+
* `returnTo` query parameter of this page. Unsafe values are dropped. */
|
|
26
|
+
readonly returnTo?: string;
|
|
22
27
|
};
|
|
23
28
|
|
|
29
|
+
function resolveReturnTo(prop: string | undefined): string | undefined {
|
|
30
|
+
return (isSafeReturnPath(prop) ? prop : readReturnToFromWindow()) ?? undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
export function ForgotPasswordScreen({
|
|
25
34
|
title,
|
|
26
35
|
subtitle,
|
|
27
36
|
loginHref = "/login",
|
|
37
|
+
returnTo,
|
|
28
38
|
}: ForgotPasswordScreenProps): ReactNode {
|
|
29
39
|
const t = useTranslation();
|
|
30
40
|
const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
|
|
@@ -38,7 +48,7 @@ export function ForgotPasswordScreen({
|
|
|
38
48
|
setSubmitting(true);
|
|
39
49
|
setError(null);
|
|
40
50
|
try {
|
|
41
|
-
const res = await requestPasswordReset(email);
|
|
51
|
+
const res = await requestPasswordReset(email, resolveReturnTo(returnTo));
|
|
42
52
|
if (res.ok) {
|
|
43
53
|
setDone(true);
|
|
44
54
|
} else if (res.error.reason === "rate_limited") {
|
|
@@ -63,6 +63,11 @@ export type { RequestAccountUnlockScreenProps } from "./request-account-unlock-s
|
|
|
63
63
|
export { RequestAccountUnlockScreen } from "./request-account-unlock-screen.tsx";
|
|
64
64
|
export type { ResetPasswordScreenProps } from "./reset-password-screen.tsx";
|
|
65
65
|
export { ResetPasswordScreen } from "./reset-password-screen.tsx";
|
|
66
|
+
export {
|
|
67
|
+
isSafeReturnPath,
|
|
68
|
+
RETURN_TO_QUERY_PARAM,
|
|
69
|
+
readReturnToFromSearch,
|
|
70
|
+
} from "./return-path.ts";
|
|
66
71
|
export type {
|
|
67
72
|
SessionApi,
|
|
68
73
|
SessionBootstrapFailure,
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// reason code needs a bundle entry plus a line in that mapping.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
prependBasePath,
|
|
6
|
+
useBasePath,
|
|
5
7
|
useOptionalDispatcher,
|
|
6
8
|
usePrimitives,
|
|
7
9
|
useQuery,
|
|
@@ -15,7 +17,11 @@ import {
|
|
|
15
17
|
} from "./auth-client.ts";
|
|
16
18
|
import { AuthCard } from "./auth-form-primitives.tsx";
|
|
17
19
|
import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.ts";
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
appendReturnTo,
|
|
22
|
+
followNextOrLandingAfterLogin,
|
|
23
|
+
readNextFromWindow,
|
|
24
|
+
} from "./auth-redirect.ts";
|
|
19
25
|
import { AuthQueries } from "./constants.ts";
|
|
20
26
|
import { useSession } from "./session.tsx";
|
|
21
27
|
|
|
@@ -128,16 +134,21 @@ function LoginScreenBody({
|
|
|
128
134
|
// Hides the resend button once the email input changes; otherwise resend
|
|
129
135
|
// would silently go to the edited, possibly mistyped address.
|
|
130
136
|
const [failedLoginEmail, setFailedLoginEmail] = useState<string | null>(null);
|
|
137
|
+
const basePath = useBasePath();
|
|
138
|
+
// The page the user was sent away from rides along so the reset or signup mail leads back to it.
|
|
139
|
+
const returnTo = readNextFromWindow();
|
|
131
140
|
const effectiveForgotHref =
|
|
132
141
|
forgotPasswordHref === undefined
|
|
133
142
|
? loginOptions?.passwordReset === true
|
|
134
|
-
? DEFAULT_FORGOT_PASSWORD_PATH
|
|
143
|
+
? appendReturnTo(prependBasePath(DEFAULT_FORGOT_PASSWORD_PATH, basePath), returnTo)
|
|
135
144
|
: null
|
|
136
|
-
: forgotPasswordHref
|
|
145
|
+
: forgotPasswordHref === null
|
|
146
|
+
? null
|
|
147
|
+
: appendReturnTo(forgotPasswordHref, returnTo);
|
|
137
148
|
const effectiveUnlockHref =
|
|
138
149
|
unlockAccountHref === undefined
|
|
139
150
|
? loginOptions?.accountUnlock === true
|
|
140
|
-
? DEFAULT_AUTH_PATHS.unlockAccount
|
|
151
|
+
? prependBasePath(DEFAULT_AUTH_PATHS.unlockAccount, basePath)
|
|
141
152
|
: null
|
|
142
153
|
: unlockAccountHref;
|
|
143
154
|
|
|
@@ -281,7 +292,7 @@ function LoginScreenBody({
|
|
|
281
292
|
</Link>
|
|
282
293
|
)}
|
|
283
294
|
{signupHref !== undefined && (
|
|
284
|
-
<Link href={signupHref} variant="muted" className="self-center">
|
|
295
|
+
<Link href={appendReturnTo(signupHref, returnTo)} variant="muted" className="self-center">
|
|
285
296
|
{t("auth.signup.title")}
|
|
286
297
|
</Link>
|
|
287
298
|
)}
|
|
@@ -13,6 +13,7 @@ import { type FormEvent, type ReactNode, useState } from "react";
|
|
|
13
13
|
import { resetPassword } from "./auth-client.ts";
|
|
14
14
|
import { passwordPairIssue } from "./auth-form-logic.ts";
|
|
15
15
|
import { AuthCard, useUrlToken } from "./auth-form-primitives.tsx";
|
|
16
|
+
import { buildLoginRedirectUrl, readReturnToFromWindow } from "./auth-redirect.ts";
|
|
16
17
|
|
|
17
18
|
export type ResetPasswordScreenProps = {
|
|
18
19
|
readonly title?: string;
|
|
@@ -37,6 +38,7 @@ export function ResetPasswordScreen({
|
|
|
37
38
|
const [submitting, setSubmitting] = useState(false);
|
|
38
39
|
const [done, setDone] = useState(false);
|
|
39
40
|
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
const [afterResetHref, setAfterResetHref] = useState(loginHref);
|
|
40
42
|
|
|
41
43
|
const doSubmit = async (): Promise<void> => {
|
|
42
44
|
setError(null);
|
|
@@ -53,6 +55,10 @@ export function ResetPasswordScreen({
|
|
|
53
55
|
const res = await resetPassword(token, newPassword);
|
|
54
56
|
setSubmitting(false);
|
|
55
57
|
if (res.ok) {
|
|
58
|
+
const returnTo = readReturnToFromWindow();
|
|
59
|
+
if (returnTo !== null) {
|
|
60
|
+
setAfterResetHref(buildLoginRedirectUrl(loginHref, returnTo, window.location.origin));
|
|
61
|
+
}
|
|
56
62
|
setDone(true);
|
|
57
63
|
return;
|
|
58
64
|
}
|
|
@@ -97,7 +103,7 @@ export function ResetPasswordScreen({
|
|
|
97
103
|
<p className="font-medium text-foreground">{t("auth.resetPassword.successTitle")}</p>
|
|
98
104
|
<p className="mt-1">{t("auth.resetPassword.successBody")}</p>
|
|
99
105
|
</Banner>
|
|
100
|
-
<Link href={
|
|
106
|
+
<Link href={afterResetHref} variant="button">
|
|
101
107
|
{t("auth.resetPassword.goToLogin")}
|
|
102
108
|
</Link>
|
|
103
109
|
</>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Shared by the server (mail link) and the web pages (landing page), so it is plain string logic:
|
|
2
|
+
// no DOM, no window, no `new URL` (React Native's URL getters throw).
|
|
3
|
+
|
|
4
|
+
export const RETURN_TO_QUERY_PARAM = "returnTo";
|
|
5
|
+
export const MAX_RETURN_PATH_LENGTH = 2048;
|
|
6
|
+
|
|
7
|
+
const DOT_SEGMENTS = new Set([".", "..", ".%2e", "%2e.", "%2e%2e", "%2e"]);
|
|
8
|
+
|
|
9
|
+
function hasForbiddenCharacter(candidate: string): boolean {
|
|
10
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
11
|
+
const code = candidate.charCodeAt(i);
|
|
12
|
+
if (code <= 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) return true;
|
|
13
|
+
if (code === 0x5c) return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A dot segment is resolved by URL normalization, so "/.//evil" would become "//evil".
|
|
19
|
+
function hasDotSegment(candidate: string): boolean {
|
|
20
|
+
const pathEnd = candidate.search(/[?#]/);
|
|
21
|
+
const path = pathEnd === -1 ? candidate : candidate.slice(0, pathEnd);
|
|
22
|
+
return path.split("/").some((segment) => DOT_SEGMENTS.has(segment.toLowerCase()));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isSafeReturnPath(candidate: unknown): candidate is string {
|
|
26
|
+
if (typeof candidate !== "string") return false;
|
|
27
|
+
if (candidate.length < 1 || candidate.length > MAX_RETURN_PATH_LENGTH) return false;
|
|
28
|
+
if (!candidate.startsWith("/") || candidate[1] === "/") return false;
|
|
29
|
+
return !hasForbiddenCharacter(candidate) && !hasDotSegment(candidate);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readReturnToFromSearch(search: string): string | null {
|
|
33
|
+
const candidate = new URLSearchParams(search).get(RETURN_TO_QUERY_PARAM);
|
|
34
|
+
return isSafeReturnPath(candidate) ? candidate : null;
|
|
35
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useSessionEndedSignal,
|
|
15
15
|
} from "@cosmicdrift/kumiko-renderer";
|
|
16
16
|
import { readCsrfToken } from "@cosmicdrift/kumiko-renderer/live";
|
|
17
|
+
import { clearRememberedListState } from "@cosmicdrift/kumiko-renderer/web";
|
|
17
18
|
import {
|
|
18
19
|
createContext,
|
|
19
20
|
type ReactNode,
|
|
@@ -216,6 +217,8 @@ export function SessionProvider({
|
|
|
216
217
|
|
|
217
218
|
const logout = useCallback<SessionApi["logout"]>(async () => {
|
|
218
219
|
await logoutApi();
|
|
220
|
+
// The reload below would hide the identity change from the list-state boot.
|
|
221
|
+
clearRememberedListState();
|
|
219
222
|
if (postLogoutUrl !== undefined && typeof window !== "undefined") {
|
|
220
223
|
// State stays untouched: flipping to unauthenticated would let an AuthGate
|
|
221
224
|
// with loginUrl race this navigation to its own redirect.
|
|
@@ -251,6 +254,7 @@ export function SessionProvider({
|
|
|
251
254
|
const userEmail = profile?.email;
|
|
252
255
|
const userDisplayName = profile?.displayName;
|
|
253
256
|
const userTimeZone = profile?.effectiveTimeZone;
|
|
257
|
+
const activeTenantId = state.activeTenantId ?? undefined;
|
|
254
258
|
const currentUser = useMemo<CurrentUser | undefined>(
|
|
255
259
|
() =>
|
|
256
260
|
userId === undefined || userEmail === undefined || userDisplayName === undefined
|
|
@@ -260,8 +264,9 @@ export function SessionProvider({
|
|
|
260
264
|
email: userEmail,
|
|
261
265
|
displayName: userDisplayName,
|
|
262
266
|
...(userTimeZone !== undefined && { timeZone: userTimeZone }),
|
|
267
|
+
...(activeTenantId !== undefined && { tenantId: activeTenantId }),
|
|
263
268
|
},
|
|
264
|
-
[userId, userEmail, userDisplayName, userTimeZone],
|
|
269
|
+
[userId, userEmail, userDisplayName, userTimeZone, activeTenantId],
|
|
265
270
|
);
|
|
266
271
|
return (
|
|
267
272
|
<SessionContext.Provider value={api}>
|
|
@@ -21,7 +21,7 @@ import { type FormEvent, type ReactNode, useState } from "react";
|
|
|
21
21
|
import { confirmSignup, type SignupConfirmSuccess } from "./auth-client.ts";
|
|
22
22
|
import { passwordPairIssue, resolvePostAuthHref } from "./auth-form-logic.ts";
|
|
23
23
|
import { AuthCard, useUrlToken } from "./auth-form-primitives.tsx";
|
|
24
|
-
import { buildLoginRedirectUrl } from "./auth-redirect.ts";
|
|
24
|
+
import { buildLoginRedirectUrl, readReturnToFromWindow } from "./auth-redirect.ts";
|
|
25
25
|
|
|
26
26
|
export type SignupCompleteScreenProps = {
|
|
27
27
|
readonly title?: string;
|
|
@@ -74,15 +74,23 @@ export function SignupCompleteScreen({
|
|
|
74
74
|
const res = await confirmSignup(token, password);
|
|
75
75
|
setSubmitting(false);
|
|
76
76
|
if (res.ok) {
|
|
77
|
+
// The page the signup started from beats the server's landing path.
|
|
78
|
+
const returnTo = readReturnToFromWindow();
|
|
77
79
|
if (res.data.kind === "mfa-pending") {
|
|
78
80
|
setMfaPending(true);
|
|
81
|
+
const next = returnTo ?? res.data.landingPath;
|
|
79
82
|
setContinueHref(
|
|
80
|
-
|
|
83
|
+
next === undefined
|
|
81
84
|
? loginHref
|
|
82
|
-
: buildLoginRedirectUrl(loginHref,
|
|
85
|
+
: buildLoginRedirectUrl(loginHref, next, window.location.origin),
|
|
83
86
|
);
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
89
|
+
// A claimed handover keeps its app-defined target.
|
|
90
|
+
if (returnTo !== null && res.data.handover === undefined) {
|
|
91
|
+
setContinueHref(returnTo);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
86
94
|
// Cookies are already set (auto-login). Show a confirmation with an
|
|
87
95
|
// explicit continue button instead of navigating away silently —
|
|
88
96
|
// the user otherwise gets no signal that activation worked.
|
|
@@ -16,6 +16,8 @@ import { type FormEvent, type ReactNode, useState } from "react";
|
|
|
16
16
|
import { requestSignup } from "./auth-client.ts";
|
|
17
17
|
import { retryAfterMinutes } from "./auth-form-logic.ts";
|
|
18
18
|
import { AuthCard } from "./auth-form-primitives.tsx";
|
|
19
|
+
import { readReturnToFromWindow } from "./auth-redirect.ts";
|
|
20
|
+
import { isSafeReturnPath } from "./return-path.ts";
|
|
19
21
|
|
|
20
22
|
export type SignupScreenProps = {
|
|
21
23
|
readonly title?: string;
|
|
@@ -23,12 +25,20 @@ export type SignupScreenProps = {
|
|
|
23
25
|
/** href for the "Already have an account?" link. App-specific; defaults to
|
|
24
26
|
* "/login". */
|
|
25
27
|
readonly loginHref?: string;
|
|
28
|
+
/** Page the activation mail should lead to after signup; defaults to the `returnTo` query
|
|
29
|
+
* parameter of this page. Unsafe values are dropped. */
|
|
30
|
+
readonly returnTo?: string;
|
|
26
31
|
};
|
|
27
32
|
|
|
33
|
+
function resolveReturnTo(prop: string | undefined): string | undefined {
|
|
34
|
+
return (isSafeReturnPath(prop) ? prop : readReturnToFromWindow()) ?? undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
export function SignupScreen({
|
|
29
38
|
title,
|
|
30
39
|
subtitle,
|
|
31
40
|
loginHref = "/login",
|
|
41
|
+
returnTo,
|
|
32
42
|
}: SignupScreenProps): ReactNode {
|
|
33
43
|
const t = useTranslation();
|
|
34
44
|
const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
|
|
@@ -41,7 +51,7 @@ export function SignupScreen({
|
|
|
41
51
|
setSubmitting(true);
|
|
42
52
|
setError(null);
|
|
43
53
|
try {
|
|
44
|
-
const res = await requestSignup(email);
|
|
54
|
+
const res = await requestSignup(email, undefined, resolveReturnTo(returnTo));
|
|
45
55
|
if (res.ok) {
|
|
46
56
|
setDone(true);
|
|
47
57
|
} else if (res.error.reason === "rate_limited") {
|
|
@@ -73,7 +83,7 @@ export function SignupScreen({
|
|
|
73
83
|
setSubmitting(true);
|
|
74
84
|
setError(null);
|
|
75
85
|
try {
|
|
76
|
-
await requestSignup(email);
|
|
86
|
+
await requestSignup(email, undefined, resolveReturnTo(returnTo));
|
|
77
87
|
} finally {
|
|
78
88
|
setSubmitting(false);
|
|
79
89
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "requiredPolicy \"systemAdmins\"",
|
|
6
|
+
"detail": "The new auth-mfa:config:required value systemAdmins (between optional and admins) forces MFA enrollment only for users whose merged global and tenant roles contain SystemAdmin. TenantAdmins and other roles log in as before; enrolled users are always challenged regardless of policy. No locale labels are involved, the option list has none."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Six-cell code input, autofocus, auto-submit and recovery switch",
|
|
12
|
+
"detail": "The MFA login, setup, disable and regenerate screens use the six-cell code input with autofocus. Login and setup submit automatically on the sixth digit; disable and regenerate keep the explicit confirm button. Verify, disable and regenerate can switch to a recovery-code field. New keys auth.mfa.verify.useRecoveryCode, useAuthenticatorCode, recoveryCode and recoverySubtitle in all locale packages; auth.mfa.disable.code now names the authenticator code only."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.379.0",
|
|
4
16
|
"type": "fix",
|
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
// they haven't opted in themselves? Default "optional" means the feature
|
|
3
3
|
// is fully opt-in (existing tenants/users unaffected — no migration step).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
// mfa-status-checker.ts
|
|
7
|
-
// for a blocked user to complete enrollment yet (that flow ships in PR3's
|
|
8
|
-
// UI). Flipping this away from "optional" before PR3 locks out every
|
|
9
|
-
// unenrolled matching user with no recovery path. Fine for a tenant that
|
|
10
|
-
// enrolls its admins out-of-band first; a footgun otherwise.
|
|
5
|
+
// Every non-optional policy blocks login for an unenrolled matching user until
|
|
6
|
+
// they finish the pre-auth enrollment step (mfa-status-checker.ts setupRequired).
|
|
11
7
|
import {
|
|
12
8
|
access,
|
|
13
9
|
type ConfigKeyHandle,
|
|
14
10
|
createTenantConfig,
|
|
15
11
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
16
12
|
|
|
17
|
-
export const MFA_REQUIRED_POLICIES = ["optional", "admins", "all"] as const;
|
|
13
|
+
export const MFA_REQUIRED_POLICIES = ["optional", "systemAdmins", "admins", "all"] as const;
|
|
18
14
|
export type MfaRequiredPolicy = (typeof MFA_REQUIRED_POLICIES)[number];
|
|
19
15
|
|
|
20
16
|
export const mfaRequiredConfigHandle: ConfigKeyHandle<"select"> = {
|
|
@@ -8,7 +8,7 @@ import type { MfaTokenSecretsEnv } from "./token-secrets.ts";
|
|
|
8
8
|
|
|
9
9
|
export type BindMfaRevokeAllOtherSessions = (
|
|
10
10
|
revoker: (userId: string, currentSid: string | undefined) => Promise<number>,
|
|
11
|
-
) => void;
|
|
11
|
+
) => () => void;
|
|
12
12
|
|
|
13
13
|
// Reads the late-bind setter off a mounted auth-mfa feature's exports —
|
|
14
14
|
// mirrors sessions' own bindAutoRevokeFromFeature. run{Prod,Dev}App call
|
|
@@ -79,7 +79,7 @@ export function bindMfaTokenSecretsFromFeature(
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => void;
|
|
82
|
+
export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => () => void;
|
|
83
83
|
|
|
84
84
|
// Reads the late-bind setter off a mounted auth-mfa feature's exports —
|
|
85
85
|
// run{Prod,Dev}App call this once the personal-access-tokens feature (if
|
|
@@ -220,10 +220,16 @@ export function createAuthMfaFeature(
|
|
|
220
220
|
|
|
221
221
|
const bindRevokeAllOtherSessions: BindMfaRevokeAllOtherSessions = (revoker) => {
|
|
222
222
|
revokeAllOtherSessions = revoker;
|
|
223
|
+
return () => {
|
|
224
|
+
if (revokeAllOtherSessions === revoker) revokeAllOtherSessions = undefined;
|
|
225
|
+
};
|
|
223
226
|
};
|
|
224
227
|
|
|
225
228
|
const bindRevokeAllPatTokens: BindRevokeAllPatTokens = (revoker) => {
|
|
226
229
|
revokeAllPatTokens = revoker;
|
|
230
|
+
return () => {
|
|
231
|
+
if (revokeAllPatTokens === revoker) revokeAllPatTokens = undefined;
|
|
232
|
+
};
|
|
227
233
|
};
|
|
228
234
|
|
|
229
235
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
1
2
|
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import {
|
|
3
4
|
access,
|
|
@@ -31,8 +32,8 @@ export type MfaStatusChecker = (
|
|
|
31
32
|
ctx: HandlerContext,
|
|
32
33
|
userId: string,
|
|
33
34
|
tenantId: TenantId,
|
|
34
|
-
// Merged global+tenant roles — needed to evaluate the "admins"
|
|
35
|
-
//
|
|
35
|
+
// Merged global+tenant roles — needed to evaluate the "admins" and "systemAdmins"
|
|
36
|
+
// policies. Must be the MERGED set (see login.write.ts caller): a
|
|
36
37
|
// SystemAdmin whose admin-ness lives only in global roles would be
|
|
37
38
|
// missed if the caller passed tenant-membership roles alone.
|
|
38
39
|
roles: readonly string[],
|
|
@@ -84,9 +85,11 @@ export function createMfaStatusChecker(opts: {
|
|
|
84
85
|
opts.challengeTokenSecret,
|
|
85
86
|
).token;
|
|
86
87
|
if (policy === "all") return { setupRequired: true, preauthSetupToken: buildSetupToken() };
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const isEnforced =
|
|
89
|
+
policy === "systemAdmins"
|
|
90
|
+
? roles.includes(ROLES.SystemAdmin)
|
|
91
|
+
: roles.some((role) => access.admin.includes(role));
|
|
92
|
+
return isEnforced
|
|
90
93
|
? { setupRequired: true, preauthSetupToken: buildSetupToken() }
|
|
91
94
|
: { required: false };
|
|
92
95
|
};
|
|
@@ -13,6 +13,11 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
13
13
|
"auth.mfa.verify.submit": "Verify",
|
|
14
14
|
"auth.mfa.verify.submitting": "…",
|
|
15
15
|
"auth.mfa.verify.backToLogin": "Back to login",
|
|
16
|
+
"auth.mfa.verify.recoveryCode": "Recovery code",
|
|
17
|
+
"auth.mfa.verify.recoverySubtitle":
|
|
18
|
+
"Enter one of the recovery codes you saved when you set up two-factor authentication.",
|
|
19
|
+
"auth.mfa.verify.useAuthenticatorCode": "Use the code from your app",
|
|
20
|
+
"auth.mfa.verify.useRecoveryCode": "Use a recovery code instead",
|
|
16
21
|
"auth.mfa.errors.invalidCode": "Invalid code. Please try again.",
|
|
17
22
|
"auth.mfa.errors.invalidTotpCode": "Invalid code. Please try again.",
|
|
18
23
|
"auth.mfa.errors.invalidChallengeToken": "Your sign-in has expired. Please sign in again.",
|
|
@@ -43,7 +48,7 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
43
48
|
"auth.mfa.disable.title": "Disable two-factor authentication",
|
|
44
49
|
"auth.mfa.disable.description":
|
|
45
50
|
"Confirm with a code from your authenticator app or a recovery code. Your account will then be protected by your password alone.",
|
|
46
|
-
"auth.mfa.disable.code": "Code from your authenticator app
|
|
51
|
+
"auth.mfa.disable.code": "Code from your authenticator app",
|
|
47
52
|
"auth.mfa.disable.confirm": "Disable",
|
|
48
53
|
"auth.mfa.disable.cancel": "Cancel",
|
|
49
54
|
"auth.mfa.disable.trigger": "Disable two-factor authentication",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type MfaCodeOrRecoveryFieldProps = {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly codeLabel: string;
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly onCodeChange: (code: string) => void;
|
|
9
|
+
readonly useRecovery: boolean;
|
|
10
|
+
readonly onUseRecoveryChange: (useRecovery: boolean) => void;
|
|
11
|
+
readonly disabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function MfaCodeOrRecoveryField({
|
|
15
|
+
id,
|
|
16
|
+
codeLabel,
|
|
17
|
+
code,
|
|
18
|
+
onCodeChange,
|
|
19
|
+
useRecovery,
|
|
20
|
+
onUseRecoveryChange,
|
|
21
|
+
disabled,
|
|
22
|
+
}: MfaCodeOrRecoveryFieldProps): ReactNode {
|
|
23
|
+
const t = useTranslation();
|
|
24
|
+
const { Button, Field, Input } = usePrimitives();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<Field id={id} label={useRecovery ? t("auth.mfa.verify.recoveryCode") : codeLabel} required>
|
|
29
|
+
{useRecovery ? (
|
|
30
|
+
<Input
|
|
31
|
+
kind="text"
|
|
32
|
+
id={id}
|
|
33
|
+
name={id}
|
|
34
|
+
value={code}
|
|
35
|
+
onChange={onCodeChange}
|
|
36
|
+
disabled={disabled}
|
|
37
|
+
autoFocus
|
|
38
|
+
autoComplete="off"
|
|
39
|
+
/>
|
|
40
|
+
) : (
|
|
41
|
+
<Input
|
|
42
|
+
kind="code"
|
|
43
|
+
length={6}
|
|
44
|
+
id={id}
|
|
45
|
+
name={id}
|
|
46
|
+
value={code}
|
|
47
|
+
onChange={onCodeChange}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
autoFocus
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</Field>
|
|
53
|
+
<Button
|
|
54
|
+
type="button"
|
|
55
|
+
variant="link"
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
onClick={() => {
|
|
58
|
+
onUseRecoveryChange(!useRecovery);
|
|
59
|
+
onCodeChange("");
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{useRecovery
|
|
63
|
+
? t("auth.mfa.verify.useAuthenticatorCode")
|
|
64
|
+
: t("auth.mfa.verify.useRecoveryCode")}
|
|
65
|
+
</Button>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
13
13
|
import { type ReactNode, useState } from "react";
|
|
14
14
|
import { AuthMfaHandlers } from "./constants.ts";
|
|
15
|
+
import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.tsx";
|
|
15
16
|
|
|
16
17
|
export type MfaDisableDialogProps = {
|
|
17
18
|
readonly open: boolean;
|
|
@@ -28,13 +29,24 @@ export function MfaDisableDialog({
|
|
|
28
29
|
onError,
|
|
29
30
|
}: MfaDisableDialogProps): ReactNode {
|
|
30
31
|
const t = useTranslation();
|
|
31
|
-
const { Dialog
|
|
32
|
+
const { Dialog } = usePrimitives();
|
|
32
33
|
const dispatcher = useDispatcher();
|
|
33
34
|
const [code, setCode] = useState("");
|
|
35
|
+
const [useRecovery, setUseRecovery] = useState(false);
|
|
36
|
+
|
|
37
|
+
const reset = (): void => {
|
|
38
|
+
setCode("");
|
|
39
|
+
setUseRecovery(false);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleOpenChange = (nextOpen: boolean): void => {
|
|
43
|
+
if (!nextOpen) reset();
|
|
44
|
+
onOpenChange(nextOpen);
|
|
45
|
+
};
|
|
34
46
|
|
|
35
47
|
const confirm = async (): Promise<void> => {
|
|
36
48
|
const res = await dispatcher.write<{ disabled: boolean }>(AuthMfaHandlers.disable, { code });
|
|
37
|
-
|
|
49
|
+
reset();
|
|
38
50
|
if (!res.isSuccess) {
|
|
39
51
|
onError(res.error.i18nKey);
|
|
40
52
|
return;
|
|
@@ -45,7 +57,7 @@ export function MfaDisableDialog({
|
|
|
45
57
|
return (
|
|
46
58
|
<Dialog
|
|
47
59
|
open={open}
|
|
48
|
-
onOpenChange={
|
|
60
|
+
onOpenChange={handleOpenChange}
|
|
49
61
|
variant="danger"
|
|
50
62
|
title={t("auth.mfa.disable.title")}
|
|
51
63
|
description={t("auth.mfa.disable.description")}
|
|
@@ -54,16 +66,14 @@ export function MfaDisableDialog({
|
|
|
54
66
|
onConfirm={confirm}
|
|
55
67
|
testId="mfa-disable-dialog"
|
|
56
68
|
>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/>
|
|
66
|
-
</Field>
|
|
69
|
+
<MfaCodeOrRecoveryField
|
|
70
|
+
id="mfa-disable-code"
|
|
71
|
+
codeLabel={t("auth.mfa.disable.code")}
|
|
72
|
+
code={code}
|
|
73
|
+
onCodeChange={setCode}
|
|
74
|
+
useRecovery={useRecovery}
|
|
75
|
+
onUseRecoveryChange={setUseRecovery}
|
|
76
|
+
/>
|
|
67
77
|
</Dialog>
|
|
68
78
|
);
|
|
69
79
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
10
10
|
import { type ReactNode, useState } from "react";
|
|
11
11
|
import { AuthMfaHandlers } from "./constants.ts";
|
|
12
|
+
import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.tsx";
|
|
12
13
|
|
|
13
14
|
export type MfaRegenerateRecoveryDialogProps = {
|
|
14
15
|
readonly open: boolean;
|
|
@@ -25,16 +26,27 @@ export function MfaRegenerateRecoveryDialog({
|
|
|
25
26
|
onError,
|
|
26
27
|
}: MfaRegenerateRecoveryDialogProps): ReactNode {
|
|
27
28
|
const t = useTranslation();
|
|
28
|
-
const { Dialog
|
|
29
|
+
const { Dialog } = usePrimitives();
|
|
29
30
|
const dispatcher = useDispatcher();
|
|
30
31
|
const [code, setCode] = useState("");
|
|
32
|
+
const [useRecovery, setUseRecovery] = useState(false);
|
|
33
|
+
|
|
34
|
+
const reset = (): void => {
|
|
35
|
+
setCode("");
|
|
36
|
+
setUseRecovery(false);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleOpenChange = (nextOpen: boolean): void => {
|
|
40
|
+
if (!nextOpen) reset();
|
|
41
|
+
onOpenChange(nextOpen);
|
|
42
|
+
};
|
|
31
43
|
|
|
32
44
|
const confirm = async (): Promise<void> => {
|
|
33
45
|
const res = await dispatcher.write<{ recoveryCodes: readonly string[] }>(
|
|
34
46
|
AuthMfaHandlers.regenerateRecovery,
|
|
35
47
|
{ code },
|
|
36
48
|
);
|
|
37
|
-
|
|
49
|
+
reset();
|
|
38
50
|
if (!res.isSuccess) {
|
|
39
51
|
onError(res.error.i18nKey);
|
|
40
52
|
return;
|
|
@@ -45,7 +57,7 @@ export function MfaRegenerateRecoveryDialog({
|
|
|
45
57
|
return (
|
|
46
58
|
<Dialog
|
|
47
59
|
open={open}
|
|
48
|
-
onOpenChange={
|
|
60
|
+
onOpenChange={handleOpenChange}
|
|
49
61
|
title={t("auth.mfa.regenerate.title")}
|
|
50
62
|
description={t("auth.mfa.regenerate.description")}
|
|
51
63
|
confirmLabel={t("auth.mfa.regenerate.confirm")}
|
|
@@ -53,16 +65,14 @@ export function MfaRegenerateRecoveryDialog({
|
|
|
53
65
|
onConfirm={confirm}
|
|
54
66
|
testId="mfa-regenerate-recovery-dialog"
|
|
55
67
|
>
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/>
|
|
65
|
-
</Field>
|
|
68
|
+
<MfaCodeOrRecoveryField
|
|
69
|
+
id="mfa-regenerate-code"
|
|
70
|
+
codeLabel={t("auth.mfa.regenerate.code")}
|
|
71
|
+
code={code}
|
|
72
|
+
onCodeChange={setCode}
|
|
73
|
+
useRecovery={useRecovery}
|
|
74
|
+
onUseRecoveryChange={setUseRecovery}
|
|
75
|
+
/>
|
|
66
76
|
</Dialog>
|
|
67
77
|
);
|
|
68
78
|
}
|