@cosmicdrift/kumiko-bundled-features 0.165.0 → 0.165.2
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 +10 -6
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +279 -0
- package/src/__tests__/access-invalidation-stream.integration.test.ts +205 -0
- package/src/audit/__tests__/audit.integration.test.ts +52 -21
- package/src/audit/handlers/list.query.ts +1 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +6 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +34 -0
- package/src/auth-email-password/__tests__/login-gates.test.ts +39 -0
- package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +9 -14
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +16 -5
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +54 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -1
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -1
- package/src/auth-email-password/handlers/login.write.ts +196 -155
- package/src/auth-email-password/handlers/self-registration-status.query.ts +6 -3
- package/src/auth-email-password/handlers/signup-request.write.ts +12 -1
- package/src/auth-email-password/i18n.ts +2 -0
- package/src/auth-email-password/seeding.ts +41 -23
- package/src/auth-email-password/web/__tests__/auth-client.test.ts +13 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +55 -8
- package/src/auth-email-password/web/__tests__/client-plugin.test.tsx +34 -0
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +8 -1
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +3 -0
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +8 -2
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +5 -1
- package/src/auth-email-password/web/auth-client.ts +32 -10
- package/src/auth-email-password/web/auth-gate.tsx +17 -21
- package/src/auth-email-password/web/client-plugin.ts +15 -2
- package/src/auth-email-password/web/index.ts +5 -1
- package/src/auth-email-password/web/login-screen.tsx +7 -3
- package/src/auth-email-password/web/request-account-unlock-screen.tsx +1 -1
- package/src/auth-foundation/__tests__/session-store.test.ts +2 -4
- package/src/auth-foundation/__tests__/tenant-providers.test.ts +24 -0
- package/src/auth-foundation/__tests__/token-verifier.test.ts +36 -3
- package/src/auth-foundation/boot-checks.ts +74 -69
- package/src/auth-foundation/resolve-anonymous-access.ts +13 -5
- package/src/auth-foundation/types.ts +3 -3
- package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +61 -1
- package/src/auth-mfa/__tests__/no-redis.integration.test.ts +209 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +157 -4
- package/src/auth-mfa/handlers/disable.write.ts +16 -1
- package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +72 -42
- package/src/auth-mfa/handlers/enable-confirm.write.ts +23 -12
- package/src/auth-mfa/handlers/reencrypt.job.ts +26 -17
- package/src/auth-mfa/handlers/regenerate-recovery.write.ts +15 -1
- package/src/auth-mfa/handlers/verify.write.ts +28 -24
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +5 -1
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +10 -1
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +104 -1
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +5 -1
- package/src/auth-mfa/web/i18n.ts +4 -0
- package/src/auth-mfa/web/mfa-client.ts +13 -2
- package/src/auth-mfa/web/mfa-enable-screen.tsx +1 -1
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +25 -2
- package/src/billing-foundation/__tests__/subscription-provider-contract.ts +36 -12
- package/src/config/__tests__/backing-secrets.integration.test.ts +4 -0
- package/src/config/__tests__/cascade.integration.test.ts +13 -2
- package/src/config/__tests__/pii-encrypted.integration.test.ts +6 -6
- package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +78 -5
- package/src/config/db/queries/resolver.ts +3 -3
- package/src/config/handlers/reencrypt.job.ts +36 -24
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +14 -0
- package/src/document-ingest-foundation/__tests__/feature.test.ts +38 -0
- package/src/document-ingest-foundation/__tests__/pages.test.ts +37 -0
- package/src/document-ingest-foundation/entity.ts +10 -3
- package/src/document-ingest-foundation/feature.ts +26 -8
- package/src/document-ingest-foundation/index.ts +1 -0
- package/src/document-ingest-foundation/pages.ts +34 -0
- package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +114 -11
- package/src/feature-toggles/compose-tier-resolver.ts +77 -8
- package/src/feature-toggles/global-feature-state-table.ts +15 -12
- package/src/files-provider-s3/s3-provider.ts +12 -1
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +78 -1
- package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +58 -40
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +9 -1
- package/src/inbound-mail-foundation/entities.ts +3 -3
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +22 -12
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +1 -1
- package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +35 -8
- package/src/inbound-provider-inmemory/__tests__/feature.test.ts +37 -33
- package/src/jobs/__tests__/job-instant.test.ts +15 -0
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +19 -9
- package/src/jobs/feature.ts +6 -5
- package/src/jobs/job-instant.ts +9 -0
- package/src/mail-foundation/__tests__/mail-transport-contract.ts +27 -11
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +10 -6
- package/src/mail-transport-smtp/__tests__/feature.test.ts +10 -5
- package/src/page-render/__tests__/markdown.test.ts +29 -0
- package/src/page-render/markdown.ts +1 -9
- package/src/personal-access-tokens/feature.ts +3 -3
- package/src/personal-access-tokens/schema/api-token.ts +3 -3
- package/src/sessions/__tests__/rebuild-survival.integration.test.ts +3 -1
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +5 -10
- package/src/sessions/__tests__/sessions.integration.test.ts +88 -1
- package/src/sessions/feature.ts +21 -6
- package/src/sessions/handlers/revoke-all-for-user.write.ts +51 -12
- package/src/sessions/handlers/revoke-all-others.write.ts +19 -1
- package/src/sessions/handlers/revoke.write.ts +13 -0
- package/src/sessions/index.ts +7 -0
- package/src/sessions/schema/user-session.ts +6 -7
- package/src/sessions/session-callbacks.ts +58 -5
- package/src/sessions/session-revoked-event.ts +29 -0
- package/src/shared/__tests__/chunked-entity-migration.test.ts +4 -4
- package/src/shared/chunked-entity-migration.ts +2 -2
- package/src/shared/grace-period.test.ts +26 -0
- package/src/shared/grace-period.ts +20 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/map-with-concurrency.test.ts +104 -0
- package/src/shared/map-with-concurrency.ts +38 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +16 -6
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +21 -0
- package/src/subscription-mollie/verify-webhook.ts +1 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +14 -7
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +20 -0
- package/src/subscription-stripe/verify-webhook.ts +3 -2
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +2 -2
- package/src/tenant/__tests__/multi-tenant.integration.test.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +8 -2
- package/src/tenant/handlers/invitations.query.ts +13 -15
- package/src/tenant/handlers/members.query.ts +12 -12
- package/src/tenant/seeding.ts +110 -16
- package/src/tenant-lifecycle/boot-checks.ts +11 -4
- package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -6
- package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +94 -0
- package/src/tenant-settings/config.ts +44 -0
- package/src/tenant-settings/constants.ts +16 -0
- package/src/tenant-settings/feature.ts +49 -0
- package/src/tenant-settings/index.ts +8 -0
- package/src/tenant-settings/tenant-defaults.ts +72 -0
- package/src/text-content/__tests__/seed-legal-content.integration.test.ts +33 -6
- package/src/text-content/handlers/set.write.ts +2 -2
- package/src/text-content/seeding.ts +31 -29
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/seeding.ts +8 -0
- package/src/user-data-rights/COMPLIANCE.md +1 -1
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +65 -0
- package/src/user-data-rights/__tests__/boot-checks.test.ts +15 -17
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +9 -5
- package/src/user-data-rights/__tests__/download.integration.test.ts +25 -41
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +7 -10
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +15 -1
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +140 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +9 -4
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +74 -8
- package/src/user-data-rights/db/queries/__tests__/forget-cleanup.integration.test.ts +69 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +9 -2
- package/src/user-data-rights/feature.ts +45 -45
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +6 -11
- package/src/user-data-rights/handlers/download-by-job.query.ts +6 -6
- package/src/user-data-rights/handlers/download-by-token.query.ts +6 -6
- package/src/user-data-rights/handlers/lift-restriction.write.ts +33 -2
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -0
- package/src/user-data-rights/handlers/restrict-account.write.ts +33 -4
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +2 -0
- package/src/user-data-rights/lib/is-admin-actor.ts +19 -0
- package/src/user-data-rights/run-forget-cleanup.ts +27 -7
|
@@ -14,7 +14,10 @@ export const selfRegistrationStatusQuery = defineQueryHandler({
|
|
|
14
14
|
name: "signup-registration-status",
|
|
15
15
|
schema: z.object({}),
|
|
16
16
|
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
17
|
-
handler: async (_query, ctx) =>
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
handler: async (_query, ctx) => {
|
|
18
|
+
// Same fail-open reasoning as signup-request.write.ts (#1468): only
|
|
19
|
+
// report disabled when the toggle feature is actually composed.
|
|
20
|
+
const gated = ctx.registry.getFeature(AUTH_SELF_REGISTRATION_FEATURE) !== undefined;
|
|
21
|
+
return { enabled: gated ? await ctx.hasFeature(AUTH_SELF_REGISTRATION_FEATURE) : true };
|
|
22
|
+
},
|
|
20
23
|
});
|
|
@@ -76,7 +76,18 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
|
|
|
76
76
|
// email" either way. The client-visible signal is the `status` query
|
|
77
77
|
// on auth-self-registration, which the signup page uses to hide its
|
|
78
78
|
// own link/form instead of collecting input that silently no-ops.
|
|
79
|
-
|
|
79
|
+
// Fail-open when the companion toggle feature isn't composed at all —
|
|
80
|
+
// ctx.hasFeature() is fail-closed for *unregistered* names the moment
|
|
81
|
+
// feature-toggles/tier-engine is wired, which silently kills
|
|
82
|
+
// self-signup for every existing app that mounts auth-email-password
|
|
83
|
+
// without also composing createAuthSelfRegistrationToggleFeature()
|
|
84
|
+
// (#1468). Only gate when the toggle actually exists.
|
|
85
|
+
const gated = ctx.registry.getFeature(AUTH_SELF_REGISTRATION_FEATURE) !== undefined;
|
|
86
|
+
if (gated && !(await ctx.hasFeature(AUTH_SELF_REGISTRATION_FEATURE))) {
|
|
87
|
+
// No email address here — this fires on every no-op request, PII
|
|
88
|
+
// must not accumulate in logs just for the "was this toggled off"
|
|
89
|
+
// signal a support ticket needs.
|
|
90
|
+
ctx.log?.info("signup-request skipped: self-registration disabled");
|
|
80
91
|
return { isSuccess: true, data: { kind: "no-op" } };
|
|
81
92
|
}
|
|
82
93
|
if (!ctx.redis) {
|
|
@@ -89,6 +89,7 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
89
89
|
"auth.requestUnlock.successBody":
|
|
90
90
|
"Falls die E-Mail in unserem System existiert und gesperrt ist, ist eine Nachricht mit einem Entsperren-Link unterwegs. Bitte schau in deinen Posteingang.",
|
|
91
91
|
"auth.requestUnlock.backToLogin": "Zurück zum Login",
|
|
92
|
+
"auth.requestUnlock.rateLimited": "Zu viele Anfragen. Neuer Versuch in {minutes} Minuten.",
|
|
92
93
|
"auth.unlockAccount.verifying": "Konto wird entsperrt …",
|
|
93
94
|
"auth.unlockAccount.successTitle": "Konto entsperrt",
|
|
94
95
|
"auth.unlockAccount.successBody":
|
|
@@ -215,6 +216,7 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
215
216
|
"auth.requestUnlock.successBody":
|
|
216
217
|
"If that email exists in our system and is locked, a message with an unlock link is on its way. Please check your inbox.",
|
|
217
218
|
"auth.requestUnlock.backToLogin": "Back to sign in",
|
|
219
|
+
"auth.requestUnlock.rateLimited": "Too many requests. Try again in {minutes} minutes.",
|
|
218
220
|
"auth.unlockAccount.verifying": "Unlocking account …",
|
|
219
221
|
"auth.unlockAccount.successTitle": "Account unlocked",
|
|
220
222
|
"auth.unlockAccount.successBody": "Your account is unlocked again. You can sign in now.",
|
|
@@ -55,17 +55,25 @@ export type SeedUserWithPasswordOptions = {
|
|
|
55
55
|
export async function seedUserWithPassword(
|
|
56
56
|
db: DbConnection,
|
|
57
57
|
options: SeedUserWithPasswordOptions,
|
|
58
|
+
// Optional, append-only — same rationale as provisionSignupAccount's
|
|
59
|
+
// hooks param (#1478): the self-signup path passes it through so `user`'s
|
|
60
|
+
// postSave hooks fire on self-signup same as on the regular create path.
|
|
61
|
+
hooks?: SeedTenantHooks,
|
|
58
62
|
): Promise<{ id: string }> {
|
|
59
63
|
const passwordHash = await hashPassword(options.password);
|
|
60
|
-
return seedUser(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
return seedUser(
|
|
65
|
+
db,
|
|
66
|
+
{
|
|
67
|
+
email: options.email,
|
|
68
|
+
displayName: options.displayName,
|
|
69
|
+
passwordHash,
|
|
70
|
+
...(options.locale !== undefined && { locale: options.locale }),
|
|
71
|
+
...(options.roles !== undefined && { roles: options.roles }),
|
|
72
|
+
...(options.emailVerified !== undefined && { emailVerified: options.emailVerified }),
|
|
73
|
+
...(options.by !== undefined && { by: options.by }),
|
|
74
|
+
},
|
|
75
|
+
hooks,
|
|
76
|
+
);
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/** Provisioning-Helper für Self-Signup-Confirm. Legt einen frischen
|
|
@@ -117,9 +125,11 @@ export async function provisionSignupAccount(
|
|
|
117
125
|
options: ProvisionSignupAccountOptions,
|
|
118
126
|
// Optional, append-only — existing callers (tests, seed scripts) that
|
|
119
127
|
// don't pass hooks keep today's behavior. The real signup-confirm
|
|
120
|
-
// handler DOES pass it (#1463) so seedTenant
|
|
121
|
-
// (tier-engine's auto-default-tier,
|
|
122
|
-
//
|
|
128
|
+
// handler DOES pass it (#1463) so seedTenant/seedUserWithPassword/
|
|
129
|
+
// seedTenantMembership's postSave hooks (tier-engine's auto-default-tier,
|
|
130
|
+
// app auto-default-compliance, a Welcome-notification hook, ...) fire on
|
|
131
|
+
// self-signup for all three entities, same as they do on the regular
|
|
132
|
+
// create paths (#1478 — this was only wired for tenant before).
|
|
123
133
|
hooks?: SeedTenantHooks,
|
|
124
134
|
): Promise<{ readonly userId: string; readonly tenantId: TenantId }> {
|
|
125
135
|
// Create-only-Guard VOR seedTenant: bei bereits registrierter Email hart
|
|
@@ -139,17 +149,25 @@ export async function provisionSignupAccount(
|
|
|
139
149
|
},
|
|
140
150
|
hooks,
|
|
141
151
|
);
|
|
142
|
-
const { id: userId } = await seedUserWithPassword(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
const { id: userId } = await seedUserWithPassword(
|
|
153
|
+
db,
|
|
154
|
+
{
|
|
155
|
+
email: options.email,
|
|
156
|
+
password: options.password,
|
|
157
|
+
displayName: options.displayName,
|
|
158
|
+
emailVerified: true,
|
|
159
|
+
},
|
|
160
|
+
hooks,
|
|
161
|
+
);
|
|
162
|
+
await seedTenantMembership(
|
|
163
|
+
db,
|
|
164
|
+
{
|
|
165
|
+
userId,
|
|
166
|
+
tenantId: options.tenantId,
|
|
167
|
+
roles: options.memberRoles ?? INITIAL_SIGNUP_ROLES,
|
|
168
|
+
},
|
|
169
|
+
hooks,
|
|
170
|
+
);
|
|
153
171
|
return { userId, tenantId: options.tenantId };
|
|
154
172
|
}
|
|
155
173
|
|
|
@@ -33,6 +33,8 @@ function withoutCsrfDocument(): void {
|
|
|
33
33
|
delete (globalThis as { document?: { cookie: string } }).document;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const originalFetch = globalThis.fetch;
|
|
37
|
+
|
|
36
38
|
beforeEach(() => {
|
|
37
39
|
globalThis.fetch = mock(
|
|
38
40
|
async () => new Response(null, { status: 200 }),
|
|
@@ -41,6 +43,7 @@ beforeEach(() => {
|
|
|
41
43
|
|
|
42
44
|
afterEach(() => {
|
|
43
45
|
withoutCsrfDocument();
|
|
46
|
+
globalThis.fetch = originalFetch;
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
describe("csrfHeader", () => {
|
|
@@ -107,6 +110,16 @@ describe("login", () => {
|
|
|
107
110
|
expect(res).toEqual({ kind: "mfa-setup-required", preauthSetupToken: "setup-token-value" });
|
|
108
111
|
});
|
|
109
112
|
|
|
113
|
+
test("mfaSetupRequired without preauthSetupToken → explicit mfa_setup_required failure", async () => {
|
|
114
|
+
globalThis.fetch = mock(async () =>
|
|
115
|
+
jsonResponse({ isSuccess: true, mfaSetupRequired: true }),
|
|
116
|
+
) as unknown as typeof fetch;
|
|
117
|
+
|
|
118
|
+
const res = await login({ email: "a@b.c", password: "secret" });
|
|
119
|
+
|
|
120
|
+
expect(res).toEqual({ kind: "failure", error: { reason: "mfa_setup_required" } });
|
|
121
|
+
});
|
|
122
|
+
|
|
110
123
|
test("string error → kind:failure with reason from string", async () => {
|
|
111
124
|
globalThis.fetch = mock(async () =>
|
|
112
125
|
jsonResponse({ isSuccess: false, error: "invalid_credentials" }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import { fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { createLoginRoute, makeAuthGate } from "../auth-gate";
|
|
5
5
|
import { makeSessionApi, renderWithProviders } from "./test-utils";
|
|
@@ -10,7 +10,7 @@ describe("makeAuthGate", () => {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
test("loading → renders placeholder, not children, not login", () => {
|
|
13
|
-
const Gate = makeAuthGate(CustomLogin);
|
|
13
|
+
const Gate = makeAuthGate({ loginScreen: CustomLogin });
|
|
14
14
|
const session = makeSessionApi({ status: "loading", user: null });
|
|
15
15
|
const { container } = renderWithProviders(
|
|
16
16
|
<Gate>
|
|
@@ -25,7 +25,7 @@ describe("makeAuthGate", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
test("unauthenticated → renders LoginComponent, not children", () => {
|
|
28
|
-
const Gate = makeAuthGate(CustomLogin);
|
|
28
|
+
const Gate = makeAuthGate({ loginScreen: CustomLogin });
|
|
29
29
|
const session = makeSessionApi({ status: "unauthenticated", user: null });
|
|
30
30
|
renderWithProviders(
|
|
31
31
|
<Gate>
|
|
@@ -38,7 +38,7 @@ describe("makeAuthGate", () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
test("authenticated → renders children, not login", () => {
|
|
41
|
-
const Gate = makeAuthGate(CustomLogin);
|
|
41
|
+
const Gate = makeAuthGate({ loginScreen: CustomLogin });
|
|
42
42
|
renderWithProviders(
|
|
43
43
|
<Gate>
|
|
44
44
|
<div data-testid="protected">secret</div>
|
|
@@ -134,8 +134,30 @@ describe("createLoginRoute", () => {
|
|
|
134
134
|
expect(screen.getByTestId("mfa-verify").textContent).toBe("token-123");
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
test("no mfaVerifyScreen → onMfaChallenge passes through to loginScreenProps.onMfaChallenge (#266 footgun)", () => {
|
|
138
|
+
// The silent-no-MFA-fallback path: an app that wires its own
|
|
139
|
+
// onMfaChallenge via loginScreenProps but forgets mfaVerifyScreen still
|
|
140
|
+
// gets that callback invoked (createLoginRoute can't detect the
|
|
141
|
+
// omission) instead of the internal setChallengeToken/MfaVerifyComponent
|
|
142
|
+
// machinery kicking in. Covered here so a future refactor of that
|
|
143
|
+
// ternary can't silently swap the fallback direction.
|
|
144
|
+
const onMfaChallenge = mock<(challengeToken: string) => void>();
|
|
145
|
+
const LoginRoute = createLoginRoute({
|
|
146
|
+
loginScreen: LoginWithMfaTrigger,
|
|
147
|
+
loginScreenProps: { onMfaChallenge },
|
|
148
|
+
});
|
|
149
|
+
const session = makeSessionApi({ status: "unauthenticated" });
|
|
150
|
+
renderWithProviders(<LoginRoute />, { session });
|
|
151
|
+
fireEvent.click(screen.getByTestId("trigger-mfa"));
|
|
152
|
+
expect(onMfaChallenge).toHaveBeenCalledWith("token-123");
|
|
153
|
+
expect(screen.queryByTestId("mfa-verify")).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
137
156
|
test("makeAuthGate delegates mfaVerifyScreen wiring to createLoginRoute", () => {
|
|
138
|
-
const Gate = makeAuthGate(
|
|
157
|
+
const Gate = makeAuthGate({
|
|
158
|
+
loginScreen: LoginWithMfaTrigger,
|
|
159
|
+
mfaVerifyScreen: CustomMfaVerify,
|
|
160
|
+
});
|
|
139
161
|
const session = makeSessionApi({ status: "unauthenticated" });
|
|
140
162
|
renderWithProviders(
|
|
141
163
|
<Gate>
|
|
@@ -160,7 +182,7 @@ describe("createLoginRoute", () => {
|
|
|
160
182
|
);
|
|
161
183
|
});
|
|
162
184
|
|
|
163
|
-
test("MfaSetupComponent onSuccess → gate clears the request and refreshes the session", () => {
|
|
185
|
+
test("MfaSetupComponent onSuccess → gate clears the request and refreshes the session", async () => {
|
|
164
186
|
const LoginRoute = createLoginRoute({
|
|
165
187
|
loginScreen: LoginWithMfaSetupTrigger,
|
|
166
188
|
mfaSetupScreen: CustomMfaSetup,
|
|
@@ -171,11 +193,36 @@ describe("createLoginRoute", () => {
|
|
|
171
193
|
expect(screen.getByTestId("mfa-setup")).toBeTruthy();
|
|
172
194
|
fireEvent.click(screen.getByTestId("complete-mfa-setup"));
|
|
173
195
|
expect(session.refresh).toHaveBeenCalledTimes(1);
|
|
174
|
-
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(screen.queryByTestId("mfa-setup")).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("MfaSetupComponent onSuccess → keeps setup screen when refresh rejects", async () => {
|
|
202
|
+
const LoginRoute = createLoginRoute({
|
|
203
|
+
loginScreen: LoginWithMfaSetupTrigger,
|
|
204
|
+
mfaSetupScreen: CustomMfaSetup,
|
|
205
|
+
});
|
|
206
|
+
const session = makeSessionApi({
|
|
207
|
+
status: "unauthenticated",
|
|
208
|
+
refresh: mock(async () => {
|
|
209
|
+
throw new Error("refresh failed");
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
renderWithProviders(<LoginRoute />, { session });
|
|
213
|
+
fireEvent.click(screen.getByTestId("trigger-mfa-setup"));
|
|
214
|
+
fireEvent.click(screen.getByTestId("complete-mfa-setup"));
|
|
215
|
+
expect(session.refresh).toHaveBeenCalledTimes(1);
|
|
216
|
+
await Promise.resolve();
|
|
217
|
+
await Promise.resolve();
|
|
218
|
+
expect(screen.getByTestId("mfa-setup")).toBeTruthy();
|
|
175
219
|
});
|
|
176
220
|
|
|
177
221
|
test("makeAuthGate delegates mfaSetupScreen wiring to createLoginRoute", () => {
|
|
178
|
-
const Gate = makeAuthGate(
|
|
222
|
+
const Gate = makeAuthGate({
|
|
223
|
+
loginScreen: LoginWithMfaSetupTrigger,
|
|
224
|
+
mfaSetupScreen: CustomMfaSetup,
|
|
225
|
+
});
|
|
179
226
|
const session = makeSessionApi({ status: "unauthenticated" });
|
|
180
227
|
renderWithProviders(
|
|
181
228
|
<Gate>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// #1472: emailPasswordClient() built the session gate without threading
|
|
4
|
+
// options.mfaSetupScreen through to makeSessionAuthGate — the option was
|
|
5
|
+
// typed and documented but silently dropped on the standard mount path.
|
|
6
|
+
// makeSessionAuthGate wraps its own real SessionProvider (unlike
|
|
7
|
+
// makeAuthGate, which auth-gate.test.tsx exercises directly), so the
|
|
8
|
+
// cheapest live check is spying on the call args instead of rendering
|
|
9
|
+
// through a real session fetch.
|
|
10
|
+
const makeSessionAuthGateCalls: unknown[][] = [];
|
|
11
|
+
mock.module("../auth-gate", () => ({
|
|
12
|
+
makeSessionAuthGate: (...args: unknown[]) => {
|
|
13
|
+
makeSessionAuthGateCalls.push(args);
|
|
14
|
+
return () => null;
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const { emailPasswordClient } = await import("../client-plugin");
|
|
19
|
+
|
|
20
|
+
describe("emailPasswordClient — mfaSetupScreen wiring", () => {
|
|
21
|
+
test("passes options.mfaSetupScreen through to makeSessionAuthGate", () => {
|
|
22
|
+
makeSessionAuthGateCalls.length = 0;
|
|
23
|
+
function CustomMfaSetup() {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
emailPasswordClient({ mfaSetupScreen: CustomMfaSetup });
|
|
28
|
+
|
|
29
|
+
expect(makeSessionAuthGateCalls).toHaveLength(1);
|
|
30
|
+
expect((makeSessionAuthGateCalls[0]?.[0] as { mfaSetupScreen?: unknown })?.mfaSetupScreen).toBe(
|
|
31
|
+
CustomMfaSetup,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -3,12 +3,16 @@ import { screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { ConfirmAccountUnlockScreen } from "../confirm-account-unlock-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("ConfirmAccountUnlockScreen", () => {
|
|
14
18
|
test("ohne Token → missing-token page", () => {
|
|
@@ -28,6 +32,8 @@ describe("ConfirmAccountUnlockScreen", () => {
|
|
|
28
32
|
"/api/auth/confirm-account-unlock",
|
|
29
33
|
expect.objectContaining({
|
|
30
34
|
method: "POST",
|
|
35
|
+
headers: expect.objectContaining({ "Content-Type": "application/json" }),
|
|
36
|
+
credentials: "same-origin",
|
|
31
37
|
body: JSON.stringify({ token: "unlock-tok" }),
|
|
32
38
|
}),
|
|
33
39
|
);
|
|
@@ -53,5 +59,6 @@ describe("ConfirmAccountUnlockScreen", () => {
|
|
|
53
59
|
test("custom title on missing-token uses errorTitle slot only when set", () => {
|
|
54
60
|
renderWithProviders(<ConfirmAccountUnlockScreen title="Unlock kaputt" />);
|
|
55
61
|
expect(screen.getByText("Unlock kaputt")).toBeTruthy();
|
|
62
|
+
expect(screen.queryByText("Entsperren fehlgeschlagen")).toBeNull();
|
|
56
63
|
});
|
|
57
64
|
});
|
|
@@ -3,12 +3,16 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { ForgotPasswordScreen } from "../forgot-password-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("ForgotPasswordScreen", () => {
|
|
14
18
|
test("rendert title + email-input + submit-button (de)", () => {
|
|
@@ -27,6 +27,8 @@ function renderWithoutSessionProvider(token: string) {
|
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const originalFetch = globalThis.fetch;
|
|
31
|
+
|
|
30
32
|
beforeEach(() => {
|
|
31
33
|
globalThis.fetch = mock(
|
|
32
34
|
async () => new Response(JSON.stringify({ tenantId: "tenant-new" }), { status: 200 }),
|
|
@@ -43,6 +45,7 @@ afterEach(() => {
|
|
|
43
45
|
writable: true,
|
|
44
46
|
value: originalLocation,
|
|
45
47
|
});
|
|
48
|
+
globalThis.fetch = originalFetch;
|
|
46
49
|
});
|
|
47
50
|
|
|
48
51
|
describe("InviteAcceptScreen — no <SessionProvider> ancestor (632/1)", () => {
|
|
@@ -3,12 +3,16 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { RequestAccountUnlockScreen } from "../request-account-unlock-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("RequestAccountUnlockScreen", () => {
|
|
14
18
|
test("renders title + email input + submit (de)", () => {
|
|
@@ -33,6 +37,8 @@ describe("RequestAccountUnlockScreen", () => {
|
|
|
33
37
|
"/api/auth/request-account-unlock",
|
|
34
38
|
expect.objectContaining({
|
|
35
39
|
method: "POST",
|
|
40
|
+
headers: expect.objectContaining({ "Content-Type": "application/json" }),
|
|
41
|
+
credentials: "same-origin",
|
|
36
42
|
body: JSON.stringify({ email: "user@example.com" }),
|
|
37
43
|
}),
|
|
38
44
|
);
|
|
@@ -54,7 +60,7 @@ describe("RequestAccountUnlockScreen", () => {
|
|
|
54
60
|
);
|
|
55
61
|
});
|
|
56
62
|
|
|
57
|
-
test("429 with retryAfterSeconds →
|
|
63
|
+
test("429 with retryAfterSeconds → requestUnlock.rateLimited banner (#1333)", async () => {
|
|
58
64
|
globalThis.fetch = mock(
|
|
59
65
|
async () =>
|
|
60
66
|
new Response(JSON.stringify({ error: { details: { retryAfterSeconds: 180 } } }), {
|
|
@@ -3,12 +3,16 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { ResetPasswordScreen } from "../reset-password-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("ResetPasswordScreen", () => {
|
|
14
18
|
test("ohne Token in URL UND ohne token-Prop → missing-token-Page", () => {
|
|
@@ -3,12 +3,16 @@ import { screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { VerifyEmailScreen } from "../verify-email-screen";
|
|
4
4
|
import { renderWithProviders } from "./test-utils";
|
|
5
5
|
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
6
8
|
beforeEach(() => {
|
|
7
9
|
globalThis.fetch = mock(
|
|
8
10
|
async () => new Response(null, { status: 200 }),
|
|
9
11
|
) as unknown as typeof fetch;
|
|
10
12
|
});
|
|
11
|
-
afterEach(() => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
12
16
|
|
|
13
17
|
describe("VerifyEmailScreen", () => {
|
|
14
18
|
test("ohne Token → missing-token-Page", () => {
|
|
@@ -34,8 +34,24 @@ export type LoginResponse = {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
// Reason codes the login-handler is known to emit today (login-screen.tsx's
|
|
38
|
+
// reasonToKey maps every one of these to an i18n key). The server can also
|
|
39
|
+
// send a reason this client doesn't know about yet (rolling deploys, a new
|
|
40
|
+
// enforcement policy) — `string & {}` keeps that open while still giving
|
|
41
|
+
// autocomplete/typo-checking on the known set, instead of collapsing both
|
|
42
|
+
// to plain `string` (coding-standards: no magic strings without a union).
|
|
43
|
+
export type KnownLoginFailureReason =
|
|
44
|
+
| "invalid_credentials"
|
|
45
|
+
| "no_membership"
|
|
46
|
+
| "account_locked"
|
|
47
|
+
| "email_not_verified"
|
|
48
|
+
| "rate_limited"
|
|
49
|
+
| "invalid_body"
|
|
50
|
+
| "mfa_not_supported"
|
|
51
|
+
| "mfa_setup_required";
|
|
52
|
+
|
|
37
53
|
export type LoginFailure = {
|
|
38
|
-
readonly reason: string;
|
|
54
|
+
readonly reason: KnownLoginFailureReason | (string & {});
|
|
39
55
|
readonly message?: string;
|
|
40
56
|
readonly retryAfterSeconds?: number;
|
|
41
57
|
};
|
|
@@ -93,8 +109,14 @@ export async function login(req: LoginRequest): Promise<LoginResult> {
|
|
|
93
109
|
if (body.mfaRequired === true && typeof body.challengeToken === "string") {
|
|
94
110
|
return { kind: "mfa-challenge", challengeToken: body.challengeToken };
|
|
95
111
|
}
|
|
96
|
-
if (body.mfaSetupRequired === true
|
|
97
|
-
|
|
112
|
+
if (body.mfaSetupRequired === true) {
|
|
113
|
+
// Older servers / mounts may set the flag without minting a preauth token —
|
|
114
|
+
// surface an explicit failure (login-screen maps mfa_setup_required) instead
|
|
115
|
+
// of falling through to generic login_failed.
|
|
116
|
+
if (typeof body.preauthSetupToken === "string") {
|
|
117
|
+
return { kind: "mfa-setup-required", preauthSetupToken: body.preauthSetupToken };
|
|
118
|
+
}
|
|
119
|
+
return { kind: "failure", error: { reason: "mfa_setup_required" } };
|
|
98
120
|
}
|
|
99
121
|
if (body.token !== undefined && body.user !== undefined) {
|
|
100
122
|
return { kind: "success", data: { token: body.token, user: body.user } };
|
|
@@ -167,10 +189,10 @@ async function parseTokenFailure(res: Response): Promise<AuthTokenFailure> {
|
|
|
167
189
|
return { reason };
|
|
168
190
|
}
|
|
169
191
|
|
|
170
|
-
// POST /api/auth/request-password-reset. 200 silent-success:
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
192
|
+
// POST /api/auth/request-password-reset. 200 silent-success: even if the
|
|
193
|
+
// email doesn't exist, the caller sees `{ ok: true }` — no account
|
|
194
|
+
// enumeration. Server only triggers the mail internally when a user is
|
|
195
|
+
// found. 429 → typed rate-limit failure. 5xx → unknown-error.
|
|
174
196
|
export async function requestPasswordReset(
|
|
175
197
|
email: string,
|
|
176
198
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
@@ -204,7 +226,7 @@ export async function resetPassword(
|
|
|
204
226
|
}
|
|
205
227
|
|
|
206
228
|
// POST /api/auth/request-email-verification. Same silent-success
|
|
207
|
-
//
|
|
229
|
+
// semantics as request-password-reset. 429 → rate-limit failure.
|
|
208
230
|
export async function requestEmailVerification(
|
|
209
231
|
email: string,
|
|
210
232
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
@@ -234,8 +256,8 @@ export async function verifyEmail(
|
|
|
234
256
|
return { ok: false, error: await parseTokenFailure(res) };
|
|
235
257
|
}
|
|
236
258
|
|
|
237
|
-
// POST /api/auth/request-account-unlock. Same silent-success
|
|
238
|
-
// request-password-reset. 429 → rate-limit
|
|
259
|
+
// POST /api/auth/request-account-unlock. Same silent-success semantics as
|
|
260
|
+
// request-password-reset. 429 → rate-limit failure.
|
|
239
261
|
export async function requestAccountUnlock(
|
|
240
262
|
email: string,
|
|
241
263
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
@@ -109,10 +109,17 @@ export function createLoginRoute(
|
|
|
109
109
|
preauthSetupToken={setupRequest.preauthSetupToken}
|
|
110
110
|
accountLabel={setupRequest.accountLabel}
|
|
111
111
|
onSuccess={() => {
|
|
112
|
-
setSetupRequest(null);
|
|
113
112
|
// MfaSetupPreauthScreen has no session to refresh itself with
|
|
114
113
|
// (it runs pre-auth) — the gate owns the session, so it refreshes.
|
|
115
|
-
|
|
114
|
+
// Keep setupRequest until refresh settles: clearing first would
|
|
115
|
+
// dump the user back on the login form with no retry if refresh
|
|
116
|
+
// rejects (cookie+JWT already minted server-side).
|
|
117
|
+
void refresh().then(
|
|
118
|
+
() => setSetupRequest(null),
|
|
119
|
+
() => {
|
|
120
|
+
// leave setupRequest set so the user can cancel/retry
|
|
121
|
+
},
|
|
122
|
+
);
|
|
116
123
|
}}
|
|
117
124
|
onCancel={() => setSetupRequest(null)}
|
|
118
125
|
/>
|
|
@@ -136,18 +143,10 @@ export function createLoginRoute(
|
|
|
136
143
|
return LoginRoute;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
export function makeAuthGate(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
MfaSetupComponent?: ComponentType<MfaSetupComponentProps>,
|
|
144
|
-
): ComponentType<{ children: ReactNode }> {
|
|
145
|
-
const LoginRoute = createLoginRoute({
|
|
146
|
-
loginScreen: LoginComponent,
|
|
147
|
-
loginScreenProps: loginProps,
|
|
148
|
-
mfaVerifyScreen: MfaVerifyComponent,
|
|
149
|
-
mfaSetupScreen: MfaSetupComponent,
|
|
150
|
-
});
|
|
146
|
+
export function makeAuthGate(opts: LoginRouteOptions = {}): ComponentType<{
|
|
147
|
+
children: ReactNode;
|
|
148
|
+
}> {
|
|
149
|
+
const LoginRoute = createLoginRoute(opts);
|
|
151
150
|
function AuthGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
152
151
|
const { status } = useSession();
|
|
153
152
|
if (status === "authenticated") return <>{children}</>;
|
|
@@ -159,13 +158,10 @@ export function makeAuthGate(
|
|
|
159
158
|
// SessionProvider + AuthGate as one gate, so a public gate in front (e.g.
|
|
160
159
|
// /calculator) doesn't mount the session bootstrap — createKumikoApp stacks
|
|
161
160
|
// providers outside all gates, so SessionProvider can't be a provider anymore.
|
|
162
|
-
export function makeSessionAuthGate(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
MfaSetupComponent?: ComponentType<MfaSetupComponentProps>,
|
|
167
|
-
): ComponentType<{ children: ReactNode }> {
|
|
168
|
-
const AuthGate = makeAuthGate(LoginComponent, loginProps, MfaVerifyComponent, MfaSetupComponent);
|
|
161
|
+
export function makeSessionAuthGate(opts: LoginRouteOptions = {}): ComponentType<{
|
|
162
|
+
children: ReactNode;
|
|
163
|
+
}> {
|
|
164
|
+
const AuthGate = makeAuthGate(opts);
|
|
169
165
|
function SessionAuthGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
170
166
|
return (
|
|
171
167
|
<SessionProvider>
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
9
9
|
import type { ComponentType, ReactNode } from "react";
|
|
10
10
|
import { defaultTranslations, mergeTranslations } from "../i18n";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type MfaSetupComponentProps,
|
|
13
|
+
type MfaVerifyComponentProps,
|
|
14
|
+
makeSessionAuthGate,
|
|
15
|
+
} from "./auth-gate";
|
|
12
16
|
import type { LoginScreenProps } from "./login-screen";
|
|
13
17
|
|
|
14
18
|
export type EmailPasswordClientOptions = {
|
|
@@ -24,6 +28,10 @@ export type EmailPasswordClientOptions = {
|
|
|
24
28
|
* auth-mfa simply don't pass this — LoginScreen's built-in fallback
|
|
25
29
|
* error covers that case. */
|
|
26
30
|
readonly mfaVerifyScreen?: ComponentType<MfaVerifyComponentProps>;
|
|
31
|
+
/** auth-mfa's MfaSetupPreauthScreen, wired in from the app the same way
|
|
32
|
+
* as mfaVerifyScreen above. Apps not mounting auth-mfa simply don't
|
|
33
|
+
* pass this. */
|
|
34
|
+
readonly mfaSetupScreen?: ComponentType<MfaSetupComponentProps>;
|
|
27
35
|
/** Key-Overrides pro Locale. Wird mit den Default-Bundles (de/en)
|
|
28
36
|
* aus `translations.ts` gemerged — jeder hier gesetzte Key gewinnt.
|
|
29
37
|
* Für Branding ("Sign in" → "Login to Acme") oder weitere Sprachen
|
|
@@ -49,7 +57,12 @@ export function emailPasswordClient(
|
|
|
49
57
|
name: "auth-email-password",
|
|
50
58
|
providers: [],
|
|
51
59
|
gates: [
|
|
52
|
-
makeSessionAuthGate(
|
|
60
|
+
makeSessionAuthGate({
|
|
61
|
+
loginScreen: options.loginScreen,
|
|
62
|
+
loginScreenProps: options.loginScreenProps,
|
|
63
|
+
mfaVerifyScreen: options.mfaVerifyScreen,
|
|
64
|
+
mfaSetupScreen: options.mfaSetupScreen,
|
|
65
|
+
}),
|
|
53
66
|
],
|
|
54
67
|
translations,
|
|
55
68
|
};
|
|
@@ -29,7 +29,11 @@ export {
|
|
|
29
29
|
} from "./auth-client";
|
|
30
30
|
export type { AuthCardProps, AuthShellRenderer } from "./auth-form-primitives";
|
|
31
31
|
export { AuthCard, AuthShellProvider, useAuthShell } from "./auth-form-primitives";
|
|
32
|
-
export type {
|
|
32
|
+
export type {
|
|
33
|
+
LoginRouteOptions,
|
|
34
|
+
MfaSetupComponentProps,
|
|
35
|
+
MfaVerifyComponentProps,
|
|
36
|
+
} from "./auth-gate";
|
|
33
37
|
export { createLoginRoute, makeAuthGate, makeSessionAuthGate } from "./auth-gate";
|
|
34
38
|
export type {
|
|
35
39
|
EmailPasswordClientFeature,
|