@voltro/cli 0.28.0 → 0.30.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/CHANGELOG.md +655 -0
- package/THIRD-PARTY-NOTICES.md +90 -4
- package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
- package/dist/apiBuild-CxvR1Dwt.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-BMr-X48w.js +2427 -0
- package/dist/checkCommand-BOk9y264.js +2 -0
- package/dist/cliRuntime-Oh517vCV.js +96 -0
- package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
- package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
- package/dist/dbCommand-D8nhyppR.js +2 -0
- package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
- package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
- package/dist/fileConventions-Cof68_BL.js +33 -0
- package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
- package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
- package/dist/inspect-mmBuRXmy.js +2 -0
- package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
- package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
- package/dist/manifestBuild-Dj8Jjoto.js +2 -0
- package/dist/sdkgen-CqpOq_fy.js +582 -0
- package/dist/serveCommand-Ciq_wIY8.js +1577 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +50 -0
- package/templates/AGENTS.md +54 -2
- package/templates/agent-docs/_index.md +4 -2
- package/templates/agent-docs/_manifest.json +26 -5
- package/templates/agent-docs/ai.md +191 -0
- package/templates/agent-docs/cli.md +143 -6
- package/templates/agent-docs/configuration.md +32 -0
- package/templates/agent-docs/data.md +282 -0
- package/templates/agent-docs/database/migrations.md +47 -0
- package/templates/agent-docs/database/misc.md +57 -0
- package/templates/agent-docs/database/schema.md +3 -1
- package/templates/agent-docs/internationalization.md +169 -8
- package/templates/agent-docs/local-first-mobile.md +426 -0
- package/templates/agent-docs/routing.md +94 -3
- package/templates/agent-docs/schema-driven-ui.md +12 -0
- package/templates/agent-docs/security.md +17 -0
- package/templates/agent-docs/templates/apibackends.md +210 -1
- package/templates/agent-docs/templates/appshells.md +144 -1
- package/templates/agent-docs/templates/mobile.md +60 -0
- package/templates/agent-docs/templates/overview.md +17 -7
- package/templates/agent-docs/whats-new.md +238 -158
- package/templates/agent-docs/workflows.md +376 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/.env.example +19 -0
- package/templates/apps/api-backend-sqlite/README.md +38 -0
- package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
- package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-sqlite/package.json +30 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-sqlite/template.json +6 -0
- package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/README.md +104 -0
- package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
- package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
- package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
- package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
- package/templates/apps/api-cms/actions/me.action.ts +16 -0
- package/templates/apps/api-cms/app.config.ts +61 -0
- package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
- package/templates/apps/api-cms/content/index.ts +18 -0
- package/templates/apps/api-cms/content/page.contentType.ts +24 -0
- package/templates/apps/api-cms/database/schema.ts +64 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
- package/templates/apps/api-cms/package.json +32 -0
- package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
- package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
- package/templates/apps/api-cms/template.json +6 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
- package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
- package/templates/apps/api-cms/tsconfig.json +5 -0
- package/templates/apps/api-collab/README.md +70 -0
- package/templates/apps/api-collab/app.config.ts +28 -0
- package/templates/apps/api-collab/database/schema.ts +63 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
- package/templates/apps/api-collab/package.json +30 -0
- package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
- package/templates/apps/api-collab/queries/documents.query.ts +23 -0
- package/templates/apps/api-collab/template.json +6 -0
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
- package/templates/apps/api-collab/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/README.md +103 -0
- package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
- package/templates/apps/api-saas-starter/app.config.ts +87 -0
- package/templates/apps/api-saas-starter/database/schema.ts +57 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/package.json +32 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas-starter/template.json +6 -0
- package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
- package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
- package/templates/apps/api-saas-starter/tsconfig.json +5 -0
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/README.md +88 -0
- package/templates/apps/api-status/app.config.ts +36 -0
- package/templates/apps/api-status/authz.ts +33 -0
- package/templates/apps/api-status/database/schema.ts +70 -0
- package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
- package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
- package/templates/apps/api-status/package.json +30 -0
- package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/components.list.query.ts +13 -0
- package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
- package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
- package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
- package/templates/apps/api-status/template.json +6 -0
- package/templates/apps/api-status/tests/status.test.ts +70 -0
- package/templates/apps/api-status/tsconfig.json +5 -0
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/app.config.ts +1 -1
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/app.config.ts +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-auth/README.md +78 -0
- package/templates/apps/frontend-auth/app.config.ts +34 -0
- package/templates/apps/frontend-auth/package.json +32 -0
- package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
- package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
- package/templates/apps/frontend-auth/src/config.ts +11 -0
- package/templates/apps/frontend-auth/src/globals.css +105 -0
- package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
- package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
- package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
- package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
- package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
- package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
- package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
- package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
- package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
- package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
- package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
- package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
- package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
- package/templates/apps/frontend-auth/template.json +6 -0
- package/templates/apps/frontend-auth/tsconfig.json +5 -0
- package/templates/apps/frontend-blank/app.config.ts +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-cms/README.md +47 -0
- package/templates/apps/frontend-cms/app.config.ts +37 -0
- package/templates/apps/frontend-cms/package.json +33 -0
- package/templates/apps/frontend-cms/src/config.ts +8 -0
- package/templates/apps/frontend-cms/src/globals.css +105 -0
- package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
- package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
- package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
- package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
- package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
- package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
- package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
- package/templates/apps/frontend-cms/template.json +6 -0
- package/templates/apps/frontend-cms/tsconfig.json +5 -0
- package/templates/apps/frontend-collab/README.md +80 -0
- package/templates/apps/frontend-collab/app.config.ts +38 -0
- package/templates/apps/frontend-collab/package.json +33 -0
- package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
- package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
- package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
- package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
- package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
- package/templates/apps/frontend-collab/template.json +6 -0
- package/templates/apps/frontend-collab/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/app.config.ts +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/README.md +71 -0
- package/templates/apps/frontend-portal/app.config.ts +37 -0
- package/templates/apps/frontend-portal/package.json +32 -0
- package/templates/apps/frontend-portal/src/config.ts +8 -0
- package/templates/apps/frontend-portal/src/globals.css +93 -0
- package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
- package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
- package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
- package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
- package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
- package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
- package/templates/apps/frontend-portal/template.json +6 -0
- package/templates/apps/frontend-portal/tsconfig.json +5 -0
- package/templates/apps/frontend-saas/README.md +73 -0
- package/templates/apps/frontend-saas/app.config.ts +46 -0
- package/templates/apps/frontend-saas/package.json +32 -0
- package/templates/apps/frontend-saas/src/config.ts +8 -0
- package/templates/apps/frontend-saas/src/globals.css +85 -0
- package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
- package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
- package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
- package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
- package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
- package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-saas/template.json +6 -0
- package/templates/apps/frontend-saas/tsconfig.json +5 -0
- package/templates/apps/frontend-spa/app.config.ts +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr/app.config.ts +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/README.md +51 -0
- package/templates/apps/frontend-status/app.config.ts +39 -0
- package/templates/apps/frontend-status/package.json +32 -0
- package/templates/apps/frontend-status/src/config.ts +7 -0
- package/templates/apps/frontend-status/src/globals.css +70 -0
- package/templates/apps/frontend-status/src/globals.d.ts +6 -0
- package/templates/apps/frontend-status/src/lib/status.ts +81 -0
- package/templates/apps/frontend-status/src/locales/de.ts +41 -0
- package/templates/apps/frontend-status/src/locales/en.ts +45 -0
- package/templates/apps/frontend-status/src/locales/index.ts +13 -0
- package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
- package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
- package/templates/apps/frontend-status/template.json +6 -0
- package/templates/apps/frontend-status/tsconfig.json +11 -0
- package/templates/apps/mobile-app/README.md +58 -0
- package/templates/apps/mobile-app/app.config.ts +33 -0
- package/templates/apps/mobile-app/babel.config.js +5 -0
- package/templates/apps/mobile-app/metro.config.js +20 -0
- package/templates/apps/mobile-app/package.json +33 -0
- package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
- package/templates/apps/mobile-app/src/app/index.tsx +62 -0
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
- package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
- package/templates/apps/mobile-app/src/client.ts +74 -0
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
- package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
- package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
- package/templates/apps/mobile-app/src/persistence.ts +38 -0
- package/templates/apps/mobile-app/template.json +6 -0
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
- package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
- package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
- package/templates/apps/mobile-app/tsconfig.json +10 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
- package/templates/baselines/compose/docker-compose.prod.yml +15 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
- package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
- package/dist/apiBuild-DgBS9ayv.js +0 -2
- package/dist/dbCommand-DvguqlzF.js +0 -2
- package/dist/fileConventions-3bffWssN.js +0 -30
- package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
- package/dist/inspect-gt8bq-Tz.js +0 -2
- package/dist/manifestBuild-ifczArzr.js +0 -2
- package/dist/serveCommand-ZTn-dPFa.js +0 -1425
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// German catalog. Mirrors every key in `en.ts` — `defineLocale<typeof en>()`
|
|
2
|
+
// fails the build if a key is missing. Translate the prose only.
|
|
3
|
+
import { defineLocale } from '@voltro/i18n'
|
|
4
|
+
import en from './en'
|
|
5
|
+
|
|
6
|
+
export default defineLocale<typeof en>()({
|
|
7
|
+
'meta.login.title': 'Anmelden',
|
|
8
|
+
'meta.signup.title': 'Konto erstellen',
|
|
9
|
+
'meta.forgot.title': 'Passwort zurücksetzen',
|
|
10
|
+
'meta.reset.title': 'Neues Passwort festlegen',
|
|
11
|
+
'meta.magic.title': 'Magischer Link',
|
|
12
|
+
'meta.verify.title': 'Wird geprüft',
|
|
13
|
+
'meta.logout.title': 'Abmeldung',
|
|
14
|
+
|
|
15
|
+
'lang.label': 'Sprache',
|
|
16
|
+
|
|
17
|
+
'auth.email': 'E-Mail',
|
|
18
|
+
'auth.password': 'Passwort',
|
|
19
|
+
'auth.newPassword': 'Neues Passwort',
|
|
20
|
+
'auth.pending': 'Bitte warten…',
|
|
21
|
+
'auth.error': 'Das hat nicht geklappt. Prüfe deine Angaben und versuche es erneut.',
|
|
22
|
+
|
|
23
|
+
'login.title': 'Anmelden',
|
|
24
|
+
'login.submit': 'Anmelden',
|
|
25
|
+
'login.forgot': 'Passwort vergessen?',
|
|
26
|
+
'login.magic': 'Link per E-Mail senden',
|
|
27
|
+
'login.toSignup': 'Konto erstellen',
|
|
28
|
+
|
|
29
|
+
'signup.title': 'Konto erstellen',
|
|
30
|
+
'signup.submit': 'Konto erstellen',
|
|
31
|
+
'signup.toLogin': 'Konto vorhanden? Anmelden',
|
|
32
|
+
|
|
33
|
+
'forgot.title': 'Passwort zurücksetzen',
|
|
34
|
+
'forgot.body': 'Gib deine E-Mail ein und wir senden dir einen Link zum Zurücksetzen.',
|
|
35
|
+
'forgot.submit': 'Link senden',
|
|
36
|
+
'forgot.sent': 'Falls zu dieser E-Mail ein Konto existiert, ist ein Link unterwegs.',
|
|
37
|
+
'forgot.toLogin': '← Zurück zur Anmeldung',
|
|
38
|
+
|
|
39
|
+
'reset.title': 'Neues Passwort festlegen',
|
|
40
|
+
'reset.body': 'Wähle ein neues Passwort für dein Konto.',
|
|
41
|
+
'reset.submit': 'Passwort aktualisieren',
|
|
42
|
+
'reset.done': 'Dein Passwort wurde aktualisiert. Du kannst dich jetzt anmelden.',
|
|
43
|
+
'reset.failed': 'Dieser Link ist ungültig oder abgelaufen. Fordere einen neuen an.',
|
|
44
|
+
'reset.toLogin': '← Zurück zur Anmeldung',
|
|
45
|
+
|
|
46
|
+
'magic.title': 'Mit einem Link anmelden',
|
|
47
|
+
'magic.body': 'Wir senden dir einen einmaligen Anmeldelink — kein Passwort nötig.',
|
|
48
|
+
'magic.submit': 'Link senden',
|
|
49
|
+
'magic.sent': 'Falls zu dieser E-Mail ein Konto existiert, ist ein Anmeldelink unterwegs.',
|
|
50
|
+
'magic.toLogin': '← Zurück zur Anmeldung',
|
|
51
|
+
|
|
52
|
+
'verify.title': 'Dein Link wird geprüft',
|
|
53
|
+
'verify.pending': 'Einen Moment, wir melden dich an…',
|
|
54
|
+
'verify.success': 'Geschafft — du wirst weitergeleitet…',
|
|
55
|
+
'verify.failed': 'Dieser Link ist ungültig oder abgelaufen. Fordere einen neuen an.',
|
|
56
|
+
'verify.noToken': 'Diesem Link fehlt sein Token. Fordere einen neuen an.',
|
|
57
|
+
'verify.toLogin': '← Zurück zur Anmeldung',
|
|
58
|
+
|
|
59
|
+
'logout.title': 'Abmeldung',
|
|
60
|
+
'logout.body': 'Du wirst abgemeldet…',
|
|
61
|
+
|
|
62
|
+
'pw.weak': 'Schwach',
|
|
63
|
+
'pw.fair': 'Mittel',
|
|
64
|
+
'pw.good': 'Gut',
|
|
65
|
+
'pw.strong': 'Stark',
|
|
66
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Base catalog — the source of truth for this app's UI strings. Every key here
|
|
2
|
+
// MUST be mirrored in `de.ts`; `defineLocale<typeof en>()` enforces that parity
|
|
3
|
+
// at build time.
|
|
4
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
5
|
+
|
|
6
|
+
export default defineCatalog({
|
|
7
|
+
// Browser tab titles — locale-aware via each page's `meta({ locale })`.
|
|
8
|
+
'meta.login.title': 'Sign in',
|
|
9
|
+
'meta.signup.title': 'Create account',
|
|
10
|
+
'meta.forgot.title': 'Reset password',
|
|
11
|
+
'meta.reset.title': 'Set a new password',
|
|
12
|
+
'meta.magic.title': 'Magic link',
|
|
13
|
+
'meta.verify.title': 'Verifying',
|
|
14
|
+
'meta.logout.title': 'Signing out',
|
|
15
|
+
|
|
16
|
+
'lang.label': 'Language',
|
|
17
|
+
|
|
18
|
+
// ---- Shared field labels ----
|
|
19
|
+
'auth.email': 'Email',
|
|
20
|
+
'auth.password': 'Password',
|
|
21
|
+
'auth.newPassword': 'New password',
|
|
22
|
+
'auth.pending': 'Please wait…',
|
|
23
|
+
'auth.error': 'That didn’t work. Check your details and try again.',
|
|
24
|
+
|
|
25
|
+
// ---- Sign in ----
|
|
26
|
+
'login.title': 'Sign in',
|
|
27
|
+
'login.submit': 'Sign in',
|
|
28
|
+
'login.forgot': 'Forgot password?',
|
|
29
|
+
'login.magic': 'Email me a link',
|
|
30
|
+
'login.toSignup': 'Create an account',
|
|
31
|
+
|
|
32
|
+
// ---- Sign up ----
|
|
33
|
+
'signup.title': 'Create your account',
|
|
34
|
+
'signup.submit': 'Create account',
|
|
35
|
+
'signup.toLogin': 'Have an account? Sign in',
|
|
36
|
+
|
|
37
|
+
// ---- Forgot password ----
|
|
38
|
+
'forgot.title': 'Reset your password',
|
|
39
|
+
'forgot.body': 'Enter your email and we’ll send you a reset link.',
|
|
40
|
+
'forgot.submit': 'Send reset link',
|
|
41
|
+
'forgot.sent': 'If that email has an account, a reset link is on its way.',
|
|
42
|
+
'forgot.toLogin': '← Back to sign in',
|
|
43
|
+
|
|
44
|
+
// ---- Reset password ----
|
|
45
|
+
'reset.title': 'Set a new password',
|
|
46
|
+
'reset.body': 'Choose a new password for your account.',
|
|
47
|
+
'reset.submit': 'Update password',
|
|
48
|
+
'reset.done': 'Your password has been updated. You can sign in now.',
|
|
49
|
+
'reset.failed': 'That reset link is invalid or has expired. Request a new one.',
|
|
50
|
+
'reset.toLogin': '← Back to sign in',
|
|
51
|
+
|
|
52
|
+
// ---- Magic link ----
|
|
53
|
+
'magic.title': 'Sign in with a link',
|
|
54
|
+
'magic.body': 'We’ll email you a one-time sign-in link — no password needed.',
|
|
55
|
+
'magic.submit': 'Send me a link',
|
|
56
|
+
'magic.sent': 'If that email has an account, a sign-in link is on its way.',
|
|
57
|
+
'magic.toLogin': '← Back to sign in',
|
|
58
|
+
|
|
59
|
+
// ---- Verify (link consumer) ----
|
|
60
|
+
'verify.title': 'Verifying your link',
|
|
61
|
+
'verify.pending': 'One moment while we sign you in…',
|
|
62
|
+
'verify.success': 'You’re in — redirecting…',
|
|
63
|
+
'verify.failed': 'That link is invalid or has expired. Request a new one.',
|
|
64
|
+
'verify.noToken': 'This link is missing its token. Request a new one.',
|
|
65
|
+
'verify.toLogin': '← Back to sign in',
|
|
66
|
+
|
|
67
|
+
// ---- Logout ----
|
|
68
|
+
'logout.title': 'Signing out',
|
|
69
|
+
'logout.body': 'Signing you out…',
|
|
70
|
+
|
|
71
|
+
// ---- Password strength meter ----
|
|
72
|
+
'pw.weak': 'Weak',
|
|
73
|
+
'pw.fair': 'Fair',
|
|
74
|
+
'pw.good': 'Good',
|
|
75
|
+
'pw.strong': 'Strong',
|
|
76
|
+
} as const)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
|
+
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
+
// hands `meta` the active locale from the `voltro:locale` cookie. Inside React use
|
|
4
|
+
// `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
|
+
import en from './en'
|
|
6
|
+
import de from './de'
|
|
7
|
+
|
|
8
|
+
// Both catalogs share `en`'s keys (defineLocale enforces the mirror), so a
|
|
9
|
+
// known-key lookup returns `string` — which is what `PageMeta.title` requires
|
|
10
|
+
// under exactOptionalPropertyTypes.
|
|
11
|
+
export type Messages = Record<keyof typeof en, string>
|
|
12
|
+
|
|
13
|
+
export const getCatalog = (locale?: string): Messages =>
|
|
14
|
+
(locale === 'de' ? de : en) as Messages
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Forgot password — URL `/forgot`. POST /auth/password-reset { email } always
|
|
2
|
+
// returns 202 (no account enumeration — the same "check your email" whether or
|
|
3
|
+
// not the address exists). The api emails a link to `/reset?token=…` (delivery
|
|
4
|
+
// needs `sendEmail` wired on the api; without it the route still 202s).
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import { useState, type FormEvent } from 'react'
|
|
7
|
+
import type { PageMeta } from '@voltro/web'
|
|
8
|
+
import { T, useT } from '@voltro/i18n'
|
|
9
|
+
import { AuthShell } from '../../components/AuthShell'
|
|
10
|
+
import { postAuth } from '../../lib/auth'
|
|
11
|
+
import { getCatalog } from '../../locales'
|
|
12
|
+
|
|
13
|
+
export const renderMode = 'static' as const
|
|
14
|
+
|
|
15
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
16
|
+
title: getCatalog(locale)['meta.forgot.title'],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default function Forgot(): ReactNode {
|
|
20
|
+
const [email, setEmail] = useState('')
|
|
21
|
+
const [pending, setPending] = useState(false)
|
|
22
|
+
const [sent, setSent] = useState(false)
|
|
23
|
+
const emailLabel = useT('auth.email')
|
|
24
|
+
|
|
25
|
+
const onSubmit = (e: FormEvent): void => {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
setPending(true)
|
|
28
|
+
void postAuth('/auth/password-reset', { email })
|
|
29
|
+
.then(() => { setSent(true); setPending(false) })
|
|
30
|
+
.catch(() => { setSent(true); setPending(false) }) // still show the neutral message
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<AuthShell titleId="forgot.title" footer={<a href="/login"><T id="forgot.toLogin" /></a>}>
|
|
35
|
+
{sent ? (
|
|
36
|
+
<p className="ok" role="status"><T id="forgot.sent" /></p>
|
|
37
|
+
) : (
|
|
38
|
+
<form className="auth-form" onSubmit={onSubmit}>
|
|
39
|
+
<p className="muted"><T id="forgot.body" /></p>
|
|
40
|
+
<label>
|
|
41
|
+
<span><T id="auth.email" /></span>
|
|
42
|
+
<input type="email" required aria-label={emailLabel} value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
43
|
+
</label>
|
|
44
|
+
<button type="submit" className="btn" disabled={pending}>
|
|
45
|
+
<T id={pending ? 'auth.pending' : 'forgot.submit'} />
|
|
46
|
+
</button>
|
|
47
|
+
</form>
|
|
48
|
+
)}
|
|
49
|
+
</AuthShell>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Root layout — wraps EVERY page (marketing + dashboard). Thin: it loads the
|
|
2
|
+
// stylesheet and lets each AREA layout ((marketing)/layout.tsx,
|
|
3
|
+
// dashboard/layout.tsx) supply its own chrome. Layouts nest, so a dashboard
|
|
4
|
+
// page is wrapped root → dashboard.
|
|
5
|
+
//
|
|
6
|
+
// Don't render <html>/<head>/<body> here — the framework shell owns them.
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import '../globals.css'
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({ children }: { readonly children: ReactNode }): ReactNode {
|
|
11
|
+
return <>{children}</>
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The sign-in page. Asserts config + that it renders the credential form and the
|
|
4
|
+
// footer navigation to the other flows. The network submit (CSRF + POST
|
|
5
|
+
// /auth/sign-in) is exercised against a running api by `voltro dev`, not here.
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
8
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
9
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
10
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
11
|
+
import enCatalog from '../../locales/en'
|
|
12
|
+
|
|
13
|
+
const { default: Login, meta, renderMode } = await import('./page')
|
|
14
|
+
|
|
15
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
16
|
+
|
|
17
|
+
let container: HTMLDivElement
|
|
18
|
+
let root: Root
|
|
19
|
+
|
|
20
|
+
const render = (node: ReactNode): void => {
|
|
21
|
+
container = document.createElement('div')
|
|
22
|
+
document.body.appendChild(container)
|
|
23
|
+
act(() => {
|
|
24
|
+
root = createRoot(container)
|
|
25
|
+
root.render(
|
|
26
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (root) act(() => root.unmount())
|
|
33
|
+
container?.remove()
|
|
34
|
+
document.body.innerHTML = ''
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('login — config', () => {
|
|
38
|
+
test('is a static page titled Sign in', () => {
|
|
39
|
+
expect(renderMode).toBe('static')
|
|
40
|
+
expect(meta({ locale: 'en' }).title).toBe('Sign in')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('login — render', () => {
|
|
45
|
+
test('renders email + password inputs and the flow links', () => {
|
|
46
|
+
render(createElement(Login))
|
|
47
|
+
expect(container.querySelector('input[type="email"]')).not.toBeNull()
|
|
48
|
+
expect(container.querySelector('input[type="password"]')).not.toBeNull()
|
|
49
|
+
expect(container.querySelector('a[href="/signup"]')).not.toBeNull()
|
|
50
|
+
expect(container.querySelector('a[href="/forgot"]')).not.toBeNull()
|
|
51
|
+
expect(container.querySelector('a[href="/magic"]')).not.toBeNull()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Sign in — URL `/login`. Email + password → POST /auth/sign-in (CSRF-guarded).
|
|
2
|
+
// On success we do a FULL navigation to the safelisted `?next=` (default
|
|
3
|
+
// AUTH_HOME) so the destination app's SSR gate re-runs with the fresh cookie.
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { useState, type FormEvent } from 'react'
|
|
6
|
+
import type { PageMeta } from '@voltro/web'
|
|
7
|
+
import { T, useT } from '@voltro/i18n'
|
|
8
|
+
import { AuthShell } from '../../components/AuthShell'
|
|
9
|
+
import { AUTH_HOME } from '../../config'
|
|
10
|
+
import { postAuth } from '../../lib/auth'
|
|
11
|
+
import { nextFromLocation } from '../../lib/redirect'
|
|
12
|
+
import { getCatalog } from '../../locales'
|
|
13
|
+
|
|
14
|
+
export const renderMode = 'static' as const
|
|
15
|
+
|
|
16
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
17
|
+
title: getCatalog(locale)['meta.login.title'],
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export default function Login(): ReactNode {
|
|
21
|
+
const [email, setEmail] = useState('')
|
|
22
|
+
const [password, setPassword] = useState('')
|
|
23
|
+
const [pending, setPending] = useState(false)
|
|
24
|
+
const [failed, setFailed] = useState(false)
|
|
25
|
+
const emailLabel = useT('auth.email')
|
|
26
|
+
const passwordLabel = useT('auth.password')
|
|
27
|
+
|
|
28
|
+
const onSubmit = (e: FormEvent): void => {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
setPending(true)
|
|
31
|
+
setFailed(false)
|
|
32
|
+
void postAuth('/auth/sign-in', { email, password })
|
|
33
|
+
.then((res) => {
|
|
34
|
+
if (res.ok) window.location.assign(nextFromLocation(AUTH_HOME))
|
|
35
|
+
else { setFailed(true); setPending(false) }
|
|
36
|
+
})
|
|
37
|
+
.catch(() => { setFailed(true); setPending(false) })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<AuthShell
|
|
42
|
+
titleId="login.title"
|
|
43
|
+
footer={
|
|
44
|
+
<>
|
|
45
|
+
<a href="/forgot"><T id="login.forgot" /></a>
|
|
46
|
+
<a href="/magic"><T id="login.magic" /></a>
|
|
47
|
+
<a href="/signup"><T id="login.toSignup" /></a>
|
|
48
|
+
</>
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
<form className="auth-form" onSubmit={onSubmit}>
|
|
52
|
+
<label>
|
|
53
|
+
<span><T id="auth.email" /></span>
|
|
54
|
+
<input type="email" required aria-label={emailLabel} value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
55
|
+
</label>
|
|
56
|
+
<label>
|
|
57
|
+
<span><T id="auth.password" /></span>
|
|
58
|
+
<input type="password" required aria-label={passwordLabel} value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
59
|
+
</label>
|
|
60
|
+
{failed ? <p className="error-text"><T id="auth.error" /></p> : null}
|
|
61
|
+
<button type="submit" className="btn" disabled={pending}>
|
|
62
|
+
<T id={pending ? 'auth.pending' : 'login.submit'} />
|
|
63
|
+
</button>
|
|
64
|
+
</form>
|
|
65
|
+
</AuthShell>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Logout — URL `/logout`. On mount it POSTs /auth/sign-out (revokes the session
|
|
2
|
+
// row + clears the cookie), then sends the browser to /login regardless of the
|
|
3
|
+
// result (a failed sign-out still shouldn't strand the user).
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { useEffect } from 'react'
|
|
6
|
+
import type { PageMeta } from '@voltro/web'
|
|
7
|
+
import { T } from '@voltro/i18n'
|
|
8
|
+
import { AuthShell } from '../../components/AuthShell'
|
|
9
|
+
import { postAuth } from '../../lib/auth'
|
|
10
|
+
import { getCatalog } from '../../locales'
|
|
11
|
+
|
|
12
|
+
export const renderMode = 'static' as const
|
|
13
|
+
|
|
14
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
15
|
+
title: getCatalog(locale)['meta.logout.title'],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export default function Logout(): ReactNode {
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
void postAuth('/auth/sign-out', {}).finally(() => window.location.assign('/login'))
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<AuthShell titleId="logout.title">
|
|
25
|
+
<p className="muted" role="status"><T id="logout.body" /></p>
|
|
26
|
+
</AuthShell>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Magic link — URL `/magic`. Passwordless sign-in: POST /auth/magic-link
|
|
2
|
+
// { email } mints a single-use, short-lived token and (when `sendEmail` is wired
|
|
3
|
+
// on the api) emails a link to `/verify?token=…`. Always shows the neutral
|
|
4
|
+
// "check your email" so it can't be used to probe which addresses exist.
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import { useState, type FormEvent } from 'react'
|
|
7
|
+
import type { PageMeta } from '@voltro/web'
|
|
8
|
+
import { T, useT } from '@voltro/i18n'
|
|
9
|
+
import { AuthShell } from '../../components/AuthShell'
|
|
10
|
+
import { postAuth } from '../../lib/auth'
|
|
11
|
+
import { getCatalog } from '../../locales'
|
|
12
|
+
|
|
13
|
+
export const renderMode = 'static' as const
|
|
14
|
+
|
|
15
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
16
|
+
title: getCatalog(locale)['meta.magic.title'],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default function Magic(): ReactNode {
|
|
20
|
+
const [email, setEmail] = useState('')
|
|
21
|
+
const [pending, setPending] = useState(false)
|
|
22
|
+
const [sent, setSent] = useState(false)
|
|
23
|
+
const emailLabel = useT('auth.email')
|
|
24
|
+
|
|
25
|
+
const onSubmit = (e: FormEvent): void => {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
setPending(true)
|
|
28
|
+
void postAuth('/auth/magic-link', { email })
|
|
29
|
+
.then(() => { setSent(true); setPending(false) })
|
|
30
|
+
.catch(() => { setSent(true); setPending(false) })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<AuthShell titleId="magic.title" footer={<a href="/login"><T id="magic.toLogin" /></a>}>
|
|
35
|
+
{sent ? (
|
|
36
|
+
<p className="ok" role="status"><T id="magic.sent" /></p>
|
|
37
|
+
) : (
|
|
38
|
+
<form className="auth-form" onSubmit={onSubmit}>
|
|
39
|
+
<p className="muted"><T id="magic.body" /></p>
|
|
40
|
+
<label>
|
|
41
|
+
<span><T id="auth.email" /></span>
|
|
42
|
+
<input type="email" required aria-label={emailLabel} value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
43
|
+
</label>
|
|
44
|
+
<button type="submit" className="btn" disabled={pending}>
|
|
45
|
+
<T id={pending ? 'auth.pending' : 'magic.submit'} />
|
|
46
|
+
</button>
|
|
47
|
+
</form>
|
|
48
|
+
)}
|
|
49
|
+
</AuthShell>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Root — URL `/`. The auth app has no landing of its own; send visitors to
|
|
2
|
+
// /login (preserving any `?next=`/`?token=` query so a deep link survives).
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import type { PageMeta } from '@voltro/web'
|
|
6
|
+
import { T } from '@voltro/i18n'
|
|
7
|
+
import { AuthShell } from '../components/AuthShell'
|
|
8
|
+
import { getCatalog } from '../locales'
|
|
9
|
+
|
|
10
|
+
export const renderMode = 'static' as const
|
|
11
|
+
|
|
12
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
13
|
+
title: getCatalog(locale)['meta.login.title'],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default function Index(): ReactNode {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const search = window.location.search
|
|
19
|
+
window.location.replace(`/login${search}`)
|
|
20
|
+
}, [])
|
|
21
|
+
return (
|
|
22
|
+
<AuthShell titleId="login.title">
|
|
23
|
+
<p className="muted"><a href="/login"><T id="login.submit" /></a></p>
|
|
24
|
+
</AuthShell>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The reset page. Asserts config + that submitting with NO `?token=` in the URL
|
|
4
|
+
// (jsdom's default location) surfaces the invalid-link message without a network
|
|
5
|
+
// call — the guard that a reset can't proceed tokenless.
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
8
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
9
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
10
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
11
|
+
import enCatalog from '../../locales/en'
|
|
12
|
+
|
|
13
|
+
const { default: Reset, meta, renderMode } = await import('./page')
|
|
14
|
+
|
|
15
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
16
|
+
|
|
17
|
+
let container: HTMLDivElement
|
|
18
|
+
let root: Root
|
|
19
|
+
|
|
20
|
+
const render = (node: ReactNode): void => {
|
|
21
|
+
container = document.createElement('div')
|
|
22
|
+
document.body.appendChild(container)
|
|
23
|
+
act(() => {
|
|
24
|
+
root = createRoot(container)
|
|
25
|
+
root.render(
|
|
26
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (root) act(() => root.unmount())
|
|
33
|
+
container?.remove()
|
|
34
|
+
document.body.innerHTML = ''
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('reset — config', () => {
|
|
38
|
+
test('is a static page titled Set a new password', () => {
|
|
39
|
+
expect(renderMode).toBe('static')
|
|
40
|
+
expect(meta({ locale: 'en' }).title).toBe('Set a new password')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('reset — tokenless guard', () => {
|
|
45
|
+
test('submitting without a ?token= shows the invalid-link message', () => {
|
|
46
|
+
render(createElement(Reset))
|
|
47
|
+
const form = container.querySelector('form')!
|
|
48
|
+
act(() => { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) })
|
|
49
|
+
expect(container.querySelector('.error-text')?.textContent).toContain('invalid or has expired')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Reset password — URL `/reset?token=…`. The link the reset email points at.
|
|
2
|
+
// Reads the single-use token from the URL and POSTs it with the new password to
|
|
3
|
+
// /auth/password-reset/confirm. On success the api invalidates the token and the
|
|
4
|
+
// user signs in with the new password.
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import { useState, type FormEvent } from 'react'
|
|
7
|
+
import type { PageMeta } from '@voltro/web'
|
|
8
|
+
import { T, useT } from '@voltro/i18n'
|
|
9
|
+
import { AuthShell } from '../../components/AuthShell'
|
|
10
|
+
import { PasswordStrength } from '../../components/PasswordStrength'
|
|
11
|
+
import { postAuth } from '../../lib/auth'
|
|
12
|
+
import { tokenFromLocation } from '../../lib/redirect'
|
|
13
|
+
import { getCatalog } from '../../locales'
|
|
14
|
+
|
|
15
|
+
export const renderMode = 'static' as const
|
|
16
|
+
|
|
17
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
18
|
+
title: getCatalog(locale)['meta.reset.title'],
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export default function Reset(): ReactNode {
|
|
22
|
+
const [password, setPassword] = useState('')
|
|
23
|
+
const [pending, setPending] = useState(false)
|
|
24
|
+
const [failed, setFailed] = useState(false)
|
|
25
|
+
const [done, setDone] = useState(false)
|
|
26
|
+
const passwordLabel = useT('auth.newPassword')
|
|
27
|
+
|
|
28
|
+
const onSubmit = (e: FormEvent): void => {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
const token = tokenFromLocation()
|
|
31
|
+
if (!token) { setFailed(true); return }
|
|
32
|
+
setPending(true)
|
|
33
|
+
setFailed(false)
|
|
34
|
+
void postAuth('/auth/password-reset/confirm', { token, password })
|
|
35
|
+
.then((res) => {
|
|
36
|
+
if (res.ok) setDone(true)
|
|
37
|
+
else setFailed(true)
|
|
38
|
+
setPending(false)
|
|
39
|
+
})
|
|
40
|
+
.catch(() => { setFailed(true); setPending(false) })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<AuthShell titleId="reset.title" footer={<a href="/login"><T id="reset.toLogin" /></a>}>
|
|
45
|
+
{done ? (
|
|
46
|
+
<p className="ok" role="status"><T id="reset.done" /></p>
|
|
47
|
+
) : (
|
|
48
|
+
<form className="auth-form" onSubmit={onSubmit}>
|
|
49
|
+
<p className="muted"><T id="reset.body" /></p>
|
|
50
|
+
<label>
|
|
51
|
+
<span><T id="auth.newPassword" /></span>
|
|
52
|
+
<input type="password" required minLength={8} aria-label={passwordLabel} value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
53
|
+
</label>
|
|
54
|
+
<PasswordStrength value={password} />
|
|
55
|
+
{failed ? <p className="error-text"><T id="reset.failed" /></p> : null}
|
|
56
|
+
<button type="submit" className="btn" disabled={pending}>
|
|
57
|
+
<T id={pending ? 'auth.pending' : 'reset.submit'} />
|
|
58
|
+
</button>
|
|
59
|
+
</form>
|
|
60
|
+
)}
|
|
61
|
+
</AuthShell>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Sign up — URL `/signup`. Email + password → POST /auth/sign-up (registers +
|
|
2
|
+
// auto signs in, setting the session cookie). A live password-strength meter is
|
|
3
|
+
// advisory; the api is the authority on policy.
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { useState, type FormEvent } from 'react'
|
|
6
|
+
import type { PageMeta } from '@voltro/web'
|
|
7
|
+
import { T, useT } from '@voltro/i18n'
|
|
8
|
+
import { AuthShell } from '../../components/AuthShell'
|
|
9
|
+
import { PasswordStrength } from '../../components/PasswordStrength'
|
|
10
|
+
import { AUTH_HOME } from '../../config'
|
|
11
|
+
import { postAuth } from '../../lib/auth'
|
|
12
|
+
import { nextFromLocation } from '../../lib/redirect'
|
|
13
|
+
import { getCatalog } from '../../locales'
|
|
14
|
+
|
|
15
|
+
export const renderMode = 'static' as const
|
|
16
|
+
|
|
17
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
18
|
+
title: getCatalog(locale)['meta.signup.title'],
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export default function Signup(): ReactNode {
|
|
22
|
+
const [email, setEmail] = useState('')
|
|
23
|
+
const [password, setPassword] = useState('')
|
|
24
|
+
const [pending, setPending] = useState(false)
|
|
25
|
+
const [failed, setFailed] = useState(false)
|
|
26
|
+
const emailLabel = useT('auth.email')
|
|
27
|
+
const passwordLabel = useT('auth.password')
|
|
28
|
+
|
|
29
|
+
const onSubmit = (e: FormEvent): void => {
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
setPending(true)
|
|
32
|
+
setFailed(false)
|
|
33
|
+
void postAuth('/auth/sign-up', { email, password })
|
|
34
|
+
.then((res) => {
|
|
35
|
+
if (res.ok) window.location.assign(nextFromLocation(AUTH_HOME))
|
|
36
|
+
else { setFailed(true); setPending(false) }
|
|
37
|
+
})
|
|
38
|
+
.catch(() => { setFailed(true); setPending(false) })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<AuthShell titleId="signup.title" footer={<a href="/login"><T id="signup.toLogin" /></a>}>
|
|
43
|
+
<form className="auth-form" onSubmit={onSubmit}>
|
|
44
|
+
<label>
|
|
45
|
+
<span><T id="auth.email" /></span>
|
|
46
|
+
<input type="email" required aria-label={emailLabel} value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
47
|
+
</label>
|
|
48
|
+
<label>
|
|
49
|
+
<span><T id="auth.password" /></span>
|
|
50
|
+
<input type="password" required minLength={8} aria-label={passwordLabel} value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
51
|
+
</label>
|
|
52
|
+
<PasswordStrength value={password} />
|
|
53
|
+
{failed ? <p className="error-text"><T id="auth.error" /></p> : null}
|
|
54
|
+
<button type="submit" className="btn" disabled={pending}>
|
|
55
|
+
<T id={pending ? 'auth.pending' : 'signup.submit'} />
|
|
56
|
+
</button>
|
|
57
|
+
</form>
|
|
58
|
+
</AuthShell>
|
|
59
|
+
)
|
|
60
|
+
}
|