create-ailk 0.5.0 → 0.6.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/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +142 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/[locale]/forgot-password` — request a password-reset link (#5963, wired by #5962).
|
|
3
|
+
*
|
|
4
|
+
* RSC. #5963 shipped this as a static `forgot` intent with no `onSubmit`;
|
|
5
|
+
* #5962 hands the available branch to a client island that calls Better Auth.
|
|
6
|
+
*
|
|
7
|
+
* Unavailable (the default) renders the `invitation` skeleton relabelled
|
|
8
|
+
* through `labels`: copy only, with the footer's "Back to sign in". Never a
|
|
9
|
+
* 404 — the segment exists as a file, so Next resolves it whatever the
|
|
10
|
+
* predicate says.
|
|
11
|
+
*
|
|
12
|
+
* The predicate is `resetSendConfigured()`, NOT the flag-only
|
|
13
|
+
* `isResetAvailable()`. The flag alone is the operator's INTENT; the
|
|
14
|
+
* conjunction is whether the flow can actually complete. Keying the page on
|
|
15
|
+
* intent would render a live form on a deployment whose server refuses to
|
|
16
|
+
* send — a form whose only possible outcome is the error state.
|
|
17
|
+
*
|
|
18
|
+
* WHAT SHARES THAT PREDICATE, AND WHAT DOES NOT (#6068). One predicate decides
|
|
19
|
+
* this page, `/reset-password`, and whether `sendResetPassword` is wired at
|
|
20
|
+
* all, so no visitor is offered a step the server would refuse. `auth.ts`'s
|
|
21
|
+
* `onPasswordReset` is DELIBERATELY OUTSIDE it: Better Auth registers the
|
|
22
|
+
* `/reset-password` POST on `emailAndPassword.enabled` rather than on the send
|
|
23
|
+
* conjunction, so a token minted before an operator removed a conjunct stays
|
|
24
|
+
* consumable for its whole hour. Gating that callback too would make exactly
|
|
25
|
+
* those resets the ones that skip sibling-token invalidation and the § D17
|
|
26
|
+
* notice — the degraded deployment getting the weaker guarantee. Offering a
|
|
27
|
+
* step is predicated; cleaning up after one that already happened is not.
|
|
28
|
+
*
|
|
29
|
+
* The `redirectTo` the island needs is built HERE, server-side, from
|
|
30
|
+
* `BETTER_AUTH_URL`. Resolving it in the browser from `window.location` would
|
|
31
|
+
* hand an attacker-influenced origin to the one call that mints a credential.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { localeHref } from "@working-theory/ui";
|
|
35
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
36
|
+
import type { Metadata } from "next";
|
|
37
|
+
import { getTranslations } from "next-intl/server";
|
|
38
|
+
import type { ReactElement } from "react";
|
|
39
|
+
|
|
40
|
+
import { resetSendConfigured } from "../../../../lib/auth-env";
|
|
41
|
+
|
|
42
|
+
import { ForgotPasswordCard } from "./forgot-password-card";
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
params: Promise<{ locale: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
49
|
+
const { locale } = await params;
|
|
50
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
51
|
+
return { title: t("forgotTitle"), description: t("forgotDescription") };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default async function ForgotPasswordPage({
|
|
55
|
+
params,
|
|
56
|
+
}: Props): Promise<ReactElement> {
|
|
57
|
+
const { locale } = await params;
|
|
58
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
59
|
+
const signInHref = localeHref(locale, "/sign-in");
|
|
60
|
+
|
|
61
|
+
const available = resetSendConfigured();
|
|
62
|
+
|
|
63
|
+
if (!available) {
|
|
64
|
+
const labels: AuthCardLabels = {
|
|
65
|
+
invitationHeading: t("forgotUnavailableHeading"),
|
|
66
|
+
invitationSubhead: t("forgotUnavailableSubhead"),
|
|
67
|
+
invitationBody: t("forgotUnavailableBody"),
|
|
68
|
+
backToSignIn: t("backToSignIn"),
|
|
69
|
+
};
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col gap-4" data-testid="forgot-password-page">
|
|
72
|
+
<AuthCard
|
|
73
|
+
intent="invitation"
|
|
74
|
+
signInHref={signInHref}
|
|
75
|
+
labels={labels}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const labels: AuthCardLabels = {
|
|
82
|
+
forgotHeading: t("forgotHeading"),
|
|
83
|
+
forgotSubhead: t("forgotSubhead"),
|
|
84
|
+
forgotSubmit: t("forgotSubmit"),
|
|
85
|
+
email: t("email"),
|
|
86
|
+
backToSignIn: t("backToSignIn"),
|
|
87
|
+
sentHeading: t("sentHeading"),
|
|
88
|
+
sentSubhead: t("sentSubhead"),
|
|
89
|
+
sentBody: t("sentBody"),
|
|
90
|
+
sentToPrefix: t("sentToPrefix"),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Absolute and same-origin: `originCheck` compares an ORIGIN, and the
|
|
94
|
+
// trusted allow-list has exactly one member. An empty or relative value
|
|
95
|
+
// would make Better Auth reject the emitted link's own callback.
|
|
96
|
+
const resetUrl = `${process.env.BETTER_AUTH_URL ?? ""}${localeHref(
|
|
97
|
+
locale,
|
|
98
|
+
"/reset-password",
|
|
99
|
+
)}`;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<ForgotPasswordCard
|
|
103
|
+
resetUrl={resetUrl}
|
|
104
|
+
signInHref={signInHref}
|
|
105
|
+
labels={labels}
|
|
106
|
+
errorLabels={{ requestFailed: t("resetRequestFailed") }}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/[locale]/(auth)` — the auth route group's layout (#5963).
|
|
3
|
+
*
|
|
4
|
+
* Mounts `AuthShell` for the five auth pages (`/sign-in`, `/sign-up`,
|
|
5
|
+
* `/forgot-password`, `/reset-password`, `/verify-email`). A route group
|
|
6
|
+
* contributes no URL segment, so every existing link to `/sign-in` keeps
|
|
7
|
+
* working; what changes is the frame.
|
|
8
|
+
*
|
|
9
|
+
* The marketing Nav + Footer are dropped ONE level up, in
|
|
10
|
+
* `app/[locale]/layout.tsx`: a route group's layout nests INSIDE its parent
|
|
11
|
+
* segment's layout, so this file cannot escape an ancestor's chrome on its
|
|
12
|
+
* own. It branches on `lib/auth-routes.ts`'s `isAuthPath()` — the same module
|
|
13
|
+
* that names the five segments below.
|
|
14
|
+
*
|
|
15
|
+
* Slots only: the shell owns the geometry, this layout supplies the site's own
|
|
16
|
+
* brand mark, footer hrefs and language switcher.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { AuthShell } from "@working-theory/templates/shells";
|
|
20
|
+
import { localeHref } from "@working-theory/ui";
|
|
21
|
+
import { LocaleSwitcherNav } from "@working-theory/ui/blocks";
|
|
22
|
+
import { readLocales } from "@working-theory/web-i18n";
|
|
23
|
+
import { headers } from "next/headers";
|
|
24
|
+
import { getTranslations } from "next-intl/server";
|
|
25
|
+
import type { ReactElement, ReactNode } from "react";
|
|
26
|
+
|
|
27
|
+
import { siteLogo } from "../../../lib/site-brand";
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
params: Promise<{ locale: string }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function AuthLayout({
|
|
35
|
+
children,
|
|
36
|
+
params,
|
|
37
|
+
}: Props): Promise<ReactElement> {
|
|
38
|
+
const { locale } = await params;
|
|
39
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
40
|
+
|
|
41
|
+
// `/faq` is the third always-scaffolded core route beside `/privacy` and
|
|
42
|
+
// `/terms` (the locale layout's own `legalLinks` comment): there is no
|
|
43
|
+
// `/help` segment in the tree, and inventing one is out of scope.
|
|
44
|
+
const footerLinks = [
|
|
45
|
+
{ label: t("footerPrivacy"), href: localeHref(locale, "/privacy") },
|
|
46
|
+
{ label: t("footerTerms"), href: localeHref(locale, "/terms") },
|
|
47
|
+
{ label: t("footerHelp"), href: localeHref(locale, "/faq") },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const pathname = (await headers()).get("x-pathname") ?? "/";
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<AuthShell
|
|
54
|
+
brand={
|
|
55
|
+
/*
|
|
56
|
+
* The mark is clamped to 28px the way `BrandLogo` clamps it inside
|
|
57
|
+
* Nav: `siteLogo()` emits an <img> with a height ATTRIBUTE and no
|
|
58
|
+
* width, and an SVG with no intrinsic size then stretches to the
|
|
59
|
+
* container — it rendered 48 x 350 here before the clamp. The shell
|
|
60
|
+
* owns the row's geometry; the site owns its mark's size.
|
|
61
|
+
*/
|
|
62
|
+
<span className="inline-flex h-7 items-center [&>img]:h-full [&>img]:w-auto [&>img]:object-contain">
|
|
63
|
+
{siteLogo()}
|
|
64
|
+
</span>
|
|
65
|
+
}
|
|
66
|
+
brandHref={localeHref(locale, "/")}
|
|
67
|
+
footerLinks={footerLinks}
|
|
68
|
+
languageSwitcher={
|
|
69
|
+
<LocaleSwitcherNav
|
|
70
|
+
current={locale}
|
|
71
|
+
locales={readLocales()}
|
|
72
|
+
pathname={pathname}
|
|
73
|
+
// AuthShell's footer link rows are already drawn at a 44px tap
|
|
74
|
+
// target (`max-md:py-3.5`) below `md` — this matches the
|
|
75
|
+
// switcher's DRAWN size to them so it doesn't read as a stray
|
|
76
|
+
// smaller control (#6018 A2). Not a new accessibility fix: the
|
|
77
|
+
// switcher's touch TARGET was already 44px via its own hit-area
|
|
78
|
+
// overlay (#5989) regardless of this prop.
|
|
79
|
+
size="touch"
|
|
80
|
+
/>
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
{/*
|
|
84
|
+
* ADR-0027 — exactly one <main> per page. AuthShell's content slot is a
|
|
85
|
+
* non-landmark <div> (the shell's own contract), and every page in this
|
|
86
|
+
* group is one card, so the group's layout owns the landmark rather than
|
|
87
|
+
* repeating it in five thin pages.
|
|
88
|
+
*/}
|
|
89
|
+
<main>{children}</main>
|
|
90
|
+
</AuthShell>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/[locale]/reset-password` — set a new password (#5963, wired by #5962).
|
|
3
|
+
*
|
|
4
|
+
* Three states, decided server-side:
|
|
5
|
+
* - reset not available → the relabelled `invitation` skeleton. A
|
|
6
|
+
* reset link cannot be used on a site that
|
|
7
|
+
* does not offer reset.
|
|
8
|
+
* - available, `?error=…` → the expired/invalid-token state. Better
|
|
9
|
+
* Auth redirects here with `?error=INVALID_TOKEN`
|
|
10
|
+
* when the token is missing, unknown or past
|
|
11
|
+
* its hour.
|
|
12
|
+
* - available, otherwise → the `reset` island, carrying `?token=`.
|
|
13
|
+
*
|
|
14
|
+
* Never a 404 in any of the three — the segment is a file.
|
|
15
|
+
*
|
|
16
|
+
* The availability predicate is `resetSendConfigured()`, NOT the flag-only
|
|
17
|
+
* `isResetAvailable()` (code review, AC-2). This is the CONSUME half of the
|
|
18
|
+
* flow, and Better Auth registers its `/reset-password` POST on
|
|
19
|
+
* `emailAndPassword.enabled` — which stays true when a send conjunct goes
|
|
20
|
+
* away. So for the token's whole hour after an operator removes, say,
|
|
21
|
+
* `BETTER_AUTH_URL`, an outstanding link would otherwise still set a password
|
|
22
|
+
* on a site that has stopped offering reset. Keying this page on the same
|
|
23
|
+
* predicate that decides `sendResetPassword` closes the visible half; the
|
|
24
|
+
* server-side half is `onPasswordReset` in `auth.ts`, which is now
|
|
25
|
+
* unconditional so a consumed token always invalidates its siblings and
|
|
26
|
+
* always attempts the § D17 notice, configured or not.
|
|
27
|
+
*
|
|
28
|
+
* The token is read from `searchParams` and handed to the island as a prop. It
|
|
29
|
+
* is the credential, so nothing here logs it and nothing re-appends it to a
|
|
30
|
+
* URL.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { localeHref } from "@working-theory/ui";
|
|
34
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
35
|
+
import type { Metadata } from "next";
|
|
36
|
+
import { getTranslations } from "next-intl/server";
|
|
37
|
+
import type { ReactElement } from "react";
|
|
38
|
+
|
|
39
|
+
import { resetSendConfigured } from "../../../../lib/auth-env";
|
|
40
|
+
|
|
41
|
+
import { ResetPasswordCard } from "./reset-password-card";
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
params: Promise<{ locale: string }>;
|
|
45
|
+
searchParams?: Promise<{ token?: string | string[]; error?: string | string[] }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** First value only — a repeated query param must not become an array here. */
|
|
49
|
+
function firstValue(raw: string | string[] | undefined): string {
|
|
50
|
+
if (Array.isArray(raw)) return raw[0] ?? "";
|
|
51
|
+
return raw ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
55
|
+
const { locale } = await params;
|
|
56
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
57
|
+
return { title: t("resetTitle"), description: t("resetDescription") };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default async function ResetPasswordPage({
|
|
61
|
+
params,
|
|
62
|
+
searchParams,
|
|
63
|
+
}: Props): Promise<ReactElement> {
|
|
64
|
+
const { locale } = await params;
|
|
65
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
66
|
+
const signInHref = localeHref(locale, "/sign-in");
|
|
67
|
+
|
|
68
|
+
const query = searchParams ? await searchParams : {};
|
|
69
|
+
const token = firstValue(query.token);
|
|
70
|
+
const error = firstValue(query.error);
|
|
71
|
+
|
|
72
|
+
const available = resetSendConfigured();
|
|
73
|
+
|
|
74
|
+
if (!available) {
|
|
75
|
+
const labels: AuthCardLabels = {
|
|
76
|
+
invitationHeading: t("resetUnavailableHeading"),
|
|
77
|
+
invitationSubhead: t("resetUnavailableSubhead"),
|
|
78
|
+
invitationBody: t("resetUnavailableBody"),
|
|
79
|
+
backToSignIn: t("backToSignIn"),
|
|
80
|
+
};
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col gap-4" data-testid="reset-password-page">
|
|
83
|
+
<AuthCard intent="invitation" signInHref={signInHref} labels={labels} />
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (error) {
|
|
89
|
+
// The `invitation` skeleton relabelled again — copy only, with the route
|
|
90
|
+
// back to sign-in. No new AuthCard intent and no new prop: `resolveLabel()`
|
|
91
|
+
// is the seam that already exists, so `packages/ui` stays out of this diff.
|
|
92
|
+
const labels: AuthCardLabels = {
|
|
93
|
+
invitationHeading: t("resetExpiredHeading"),
|
|
94
|
+
invitationSubhead: t("resetExpiredSubhead"),
|
|
95
|
+
invitationBody: t("resetExpiredBody"),
|
|
96
|
+
backToSignIn: t("backToSignIn"),
|
|
97
|
+
};
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
className="flex flex-col gap-4"
|
|
101
|
+
data-testid="reset-password-page"
|
|
102
|
+
>
|
|
103
|
+
<div data-testid="reset-token-error">
|
|
104
|
+
<AuthCard
|
|
105
|
+
intent="invitation"
|
|
106
|
+
signInHref={signInHref}
|
|
107
|
+
labels={labels}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const labels: AuthCardLabels = {
|
|
115
|
+
resetHeading: t("resetHeading"),
|
|
116
|
+
resetSubhead: t("resetSubhead"),
|
|
117
|
+
resetSubmit: t("resetSubmit"),
|
|
118
|
+
newPassword: t("newPassword"),
|
|
119
|
+
confirmPassword: t("confirmPassword"),
|
|
120
|
+
backToSignIn: t("backToSignIn"),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<ResetPasswordCard
|
|
125
|
+
token={token}
|
|
126
|
+
signInHref={signInHref}
|
|
127
|
+
labels={labels}
|
|
128
|
+
errorLabels={{
|
|
129
|
+
resetFailed: t("resetFailed"),
|
|
130
|
+
invalidToken: t("resetExpiredSubhead"),
|
|
131
|
+
mismatch: t("passwordMismatch"),
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client"; // Better Auth's client call (resetPassword) + the error state
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ResetPasswordCard — `/reset-password`'s client island (#5962).
|
|
5
|
+
*
|
|
6
|
+
* The token identifies the account, so the card renders no email field. It is
|
|
7
|
+
* passed to `authClient.resetPassword` in the request BODY and is never
|
|
8
|
+
* re-appended to a URL by this component — the one it arrived on is already in
|
|
9
|
+
* the browser's history and in every access log between here and the edge, and
|
|
10
|
+
* a second copy buys nothing but more places to leak from.
|
|
11
|
+
*
|
|
12
|
+
* On success it hard-navigates rather than routing client-side — see the
|
|
13
|
+
* comment at the call.
|
|
14
|
+
*
|
|
15
|
+
* An absent token is treated exactly like an expired one. That case is
|
|
16
|
+
* reachable — someone opening `/reset-password` directly — and a form that
|
|
17
|
+
* submits nothing and fails obscurely is worse than the honest message.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
21
|
+
import { createAuthClient } from "better-auth/react";
|
|
22
|
+
import type { ReactElement } from "react";
|
|
23
|
+
import { useState } from "react";
|
|
24
|
+
|
|
25
|
+
const authClient = createAuthClient();
|
|
26
|
+
|
|
27
|
+
export interface ResetPasswordCardProps {
|
|
28
|
+
/** The `?token=` Better Auth redirected back with. May be empty. */
|
|
29
|
+
token: string;
|
|
30
|
+
/** Where a completed reset lands. */
|
|
31
|
+
signInHref: string;
|
|
32
|
+
/** Translated AuthCard labels (resolved server-side by the page). */
|
|
33
|
+
labels: AuthCardLabels;
|
|
34
|
+
/** Translated error copy. */
|
|
35
|
+
errorLabels: { resetFailed: string; invalidToken: string; mismatch: string };
|
|
36
|
+
/**
|
|
37
|
+
* How a completed reset leaves the page. Defaults to a FULL navigation,
|
|
38
|
+
* deliberately: the reset just revoked every session this browser held, so
|
|
39
|
+
* every piece of client state behind it is stale and a hard load is the
|
|
40
|
+
* honest way to drop it.
|
|
41
|
+
*
|
|
42
|
+
* Injectable for the same reason `createAuth` takes an optional `prisma` —
|
|
43
|
+
* jsdom's `window.location` is read-only, so a test cannot observe the real
|
|
44
|
+
* one. No production caller passes this.
|
|
45
|
+
*/
|
|
46
|
+
navigate?: (href: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultNavigate = (href: string): void => {
|
|
50
|
+
window.location.assign(href);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function ResetPasswordCard({
|
|
54
|
+
token,
|
|
55
|
+
signInHref,
|
|
56
|
+
labels,
|
|
57
|
+
errorLabels,
|
|
58
|
+
navigate = defaultNavigate,
|
|
59
|
+
}: ResetPasswordCardProps): ReactElement {
|
|
60
|
+
const [errors, setErrors] = useState<{
|
|
61
|
+
password?: string;
|
|
62
|
+
confirmPassword?: string;
|
|
63
|
+
}>({});
|
|
64
|
+
|
|
65
|
+
const submit = async (
|
|
66
|
+
password: string,
|
|
67
|
+
confirmPassword: string,
|
|
68
|
+
): Promise<void> => {
|
|
69
|
+
setErrors({});
|
|
70
|
+
|
|
71
|
+
if (password !== confirmPassword) {
|
|
72
|
+
setErrors({ confirmPassword: errorLabels.mismatch });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!token) {
|
|
77
|
+
setErrors({ password: errorLabels.invalidToken });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const failed = await authClient
|
|
82
|
+
.resetPassword({ newPassword: password, token })
|
|
83
|
+
.then(
|
|
84
|
+
(res) => Boolean(res?.error),
|
|
85
|
+
() => true,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (failed) {
|
|
89
|
+
setErrors({ password: errorLabels.resetFailed });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
navigate(signInHref);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-col gap-4" data-testid="reset-password-page">
|
|
98
|
+
<AuthCard
|
|
99
|
+
intent="reset"
|
|
100
|
+
signInHref={signInHref}
|
|
101
|
+
labels={labels}
|
|
102
|
+
onSubmit={(data) =>
|
|
103
|
+
void submit(data.password ?? "", data.confirmPassword ?? "")
|
|
104
|
+
}
|
|
105
|
+
errors={errors}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* (e) a foreign / protocol-relative `?next=` falls back to /account.
|
|
14
14
|
* (f) password submit → `signIn.email` with the typed credentials; an
|
|
15
15
|
* error result renders the signInFailed message.
|
|
16
|
+
* (g) the "Forgot password?" link renders only when the reset predicate is
|
|
17
|
+
* true — never a dead end pointing at reset's not-available state
|
|
18
|
+
* (#6018 AC-1). (#6068 repointed that predicate from `isResetAvailable()`
|
|
19
|
+
* to `resetSendConfigured()`; the mock below binds both to one fn, so
|
|
20
|
+
* these cases read identically either way.)
|
|
16
21
|
*
|
|
17
22
|
* `next-intl/server` is mocked globally (apps/web/__tests__/setup.ts) against
|
|
18
23
|
* the real en.json, so `signIn` copy is asserted by its real strings.
|
|
@@ -21,10 +26,21 @@ import { render, screen } from "@testing-library/react";
|
|
|
21
26
|
import userEvent from "@testing-library/user-event";
|
|
22
27
|
|
|
23
28
|
const mockGetActiveProviderIds = jest.fn();
|
|
29
|
+
const mockIsResetAvailable = jest.fn();
|
|
24
30
|
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth-env.ts is a pure env-introspection module with no external boundary to mock instead.
|
|
25
|
-
jest.mock("
|
|
31
|
+
jest.mock("../../../../../lib/auth-env", () => ({
|
|
26
32
|
__esModule: true,
|
|
27
33
|
getActiveProviderIds: () => mockGetActiveProviderIds(),
|
|
34
|
+
isResetAvailable: () => mockIsResetAvailable(),
|
|
35
|
+
// #6068 repointed the forgot link from the flag-only `isResetAvailable()`
|
|
36
|
+
// at the send conjunction. This is a PARTIAL module mock, so an absent
|
|
37
|
+
// member is `undefined` rather than the real function — the page would
|
|
38
|
+
// throw on EVERY render here, not only the two (g) cases. Bound to the SAME
|
|
39
|
+
// jest.fn() the flag already drives, so every case below keeps the exact
|
|
40
|
+
// truth value and the exact expectation it was written with: no assertion
|
|
41
|
+
// in this file changes. Same remedy, same reason, as the sibling
|
|
42
|
+
// `auth-pages.test.tsx` mock.
|
|
43
|
+
resetSendConfigured: () => mockIsResetAvailable(),
|
|
28
44
|
}));
|
|
29
45
|
|
|
30
46
|
const mockSocial = jest.fn();
|
|
@@ -39,7 +55,7 @@ jest.mock("better-auth/react", () => ({
|
|
|
39
55
|
}),
|
|
40
56
|
}));
|
|
41
57
|
|
|
42
|
-
import en from "
|
|
58
|
+
import en from "../../../../../messages/en.json";
|
|
43
59
|
import Page, { generateMetadata } from "../page";
|
|
44
60
|
|
|
45
61
|
async function renderPage(next?: string): Promise<void> {
|
|
@@ -52,6 +68,7 @@ async function renderPage(next?: string): Promise<void> {
|
|
|
52
68
|
|
|
53
69
|
beforeEach(() => {
|
|
54
70
|
mockGetActiveProviderIds.mockReset().mockReturnValue(["google"]);
|
|
71
|
+
mockIsResetAvailable.mockReset().mockReturnValue(false);
|
|
55
72
|
mockSocial.mockReset().mockResolvedValue({ data: {}, error: null });
|
|
56
73
|
mockEmail.mockReset().mockResolvedValue({ data: {}, error: null });
|
|
57
74
|
});
|
|
@@ -149,4 +166,33 @@ describe("/sign-in page", () => {
|
|
|
149
166
|
});
|
|
150
167
|
expect(await screen.findByText(en.signIn.signInFailed)).toBeInTheDocument();
|
|
151
168
|
});
|
|
169
|
+
|
|
170
|
+
it("(f) a rejected signIn.email call surfaces signInFailed too (code review, #6018)", async () => {
|
|
171
|
+
const user = userEvent.setup();
|
|
172
|
+
mockGetActiveProviderIds.mockReturnValue(["google", "email"]);
|
|
173
|
+
mockEmail.mockRejectedValue(new Error("network"));
|
|
174
|
+
await renderPage("/account");
|
|
175
|
+
await user.type(screen.getByTestId("auth-email"), "ada@example.com");
|
|
176
|
+
await user.type(screen.getByTestId("auth-password"), "s3cr3t");
|
|
177
|
+
await user.click(screen.getByTestId("auth-submit"));
|
|
178
|
+
expect(await screen.findByText(en.signIn.signInFailed)).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("(g) renders the forgot link when password reset is available", async () => {
|
|
182
|
+
mockGetActiveProviderIds.mockReturnValue(["google", "email"]);
|
|
183
|
+
mockIsResetAvailable.mockReturnValue(true);
|
|
184
|
+
await renderPage("/account");
|
|
185
|
+
expect(screen.getByTestId("auth-forgot")).toHaveAttribute(
|
|
186
|
+
"href",
|
|
187
|
+
"/forgot-password",
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("(g) omits the forgot link when password reset is unavailable", async () => {
|
|
192
|
+
mockGetActiveProviderIds.mockReturnValue(["google", "email"]);
|
|
193
|
+
mockIsResetAvailable.mockReturnValue(false);
|
|
194
|
+
await renderPage("/account");
|
|
195
|
+
expect(screen.getByTestId("auth-password")).toBeInTheDocument();
|
|
196
|
+
expect(screen.queryByTestId("auth-forgot")).toBeNull();
|
|
197
|
+
});
|
|
152
198
|
});
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* RSC: resolves the active providers from auth-env, the sign-in method
|
|
5
5
|
* (password when the email provider is active, else sso), the sanitised
|
|
6
6
|
* `?next=` return path, and the translated `signIn` labels — then renders the
|
|
7
|
-
* `SignInCard` client island.
|
|
8
|
-
*
|
|
9
|
-
* (
|
|
7
|
+
* `SignInCard` client island.
|
|
8
|
+
*
|
|
9
|
+
* Since #5963 the page lives in the `(auth)` route group — the URL is
|
|
10
|
+
* unchanged (a group contributes no path segment) but the frame is
|
|
11
|
+
* `AuthShell`, not the marketing Nav + Footer. The shell owns the brand mark
|
|
12
|
+
* and the 400px column, so this page passes NO `logo` (it would render twice)
|
|
13
|
+
* and no framing of its own.
|
|
10
14
|
*
|
|
11
15
|
* Signed-in visitors are not redirected here — the Nav's session-aware action
|
|
12
16
|
* cluster (Account / Sign out) is the affordance for them.
|
|
@@ -18,8 +22,10 @@ import type { Metadata } from "next";
|
|
|
18
22
|
import { getTranslations } from "next-intl/server";
|
|
19
23
|
import type { ReactElement } from "react";
|
|
20
24
|
|
|
21
|
-
import {
|
|
22
|
-
|
|
25
|
+
import {
|
|
26
|
+
getActiveProviderIds,
|
|
27
|
+
resetSendConfigured,
|
|
28
|
+
} from "../../../../lib/auth-env";
|
|
23
29
|
|
|
24
30
|
import { resolveNextPath } from "./next-path";
|
|
25
31
|
import { SignInCard, type SignInProvider } from "./sign-in-card";
|
|
@@ -71,7 +77,15 @@ export default async function SignInPage({
|
|
|
71
77
|
providers={providers}
|
|
72
78
|
method={method}
|
|
73
79
|
next={next}
|
|
74
|
-
|
|
80
|
+
// The "Forgot password?" link now has somewhere real to go (#5963);
|
|
81
|
+
// AuthCard's default is a dead "#".
|
|
82
|
+
forgotHref={localeHref(locale, "/forgot-password")}
|
|
83
|
+
// Hide it entirely when reset isn't wired server-side, rather than
|
|
84
|
+
// pointing at a page that renders its own not-available state (#6018).
|
|
85
|
+
// `resetSendConfigured()`, not the flag: /forgot-password keys on the
|
|
86
|
+
// send conjunction, so the flag alone would offer a link to a page
|
|
87
|
+
// rendering its own not-available card (#6068 AC-2).
|
|
88
|
+
forgot={resetSendConfigured() ? "link" : "none"}
|
|
75
89
|
labels={labels}
|
|
76
90
|
notConfiguredSubhead={t("notConfiguredSubhead")}
|
|
77
91
|
errorLabels={{
|