@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
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-versioning": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/database": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.30.0",
|
|
20
|
+
"@voltro/plugin-versioning": "0.30.0",
|
|
21
|
+
"@voltro/protocol": "0.30.0",
|
|
22
|
+
"@voltro/runtime": "0.30.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.30.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"vitest": "^4.1.10"
|
|
29
29
|
}
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-webhooks": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
23
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/database": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.30.0",
|
|
20
|
+
"@voltro/plugin-webhooks": "0.30.0",
|
|
21
|
+
"@voltro/protocol": "0.30.0",
|
|
22
|
+
"@voltro/runtime": "0.30.0",
|
|
23
|
+
"@voltro/sql-postgres": "0.30.0",
|
|
24
24
|
"effect": "^3.22.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.30.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
30
30
|
}
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/changelog": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/changelog": "0.30.0",
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/client": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/i18n": "0.30.0",
|
|
20
|
+
"@voltro/web": "0.30.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
@@ -22,7 +22,7 @@ export default {
|
|
|
22
22
|
// <I18nProvider> from these codes and loads the matching
|
|
23
23
|
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
24
24
|
// `useLocale()` work app-wide with no manual setup. The <LocaleSwitcher>
|
|
25
|
-
// in the layout writes the `voltro:
|
|
25
|
+
// in the layout writes the `voltro:locale` cookie + reloads. Grow the app to
|
|
26
26
|
// more languages by adding a code here + a `src/locales/<code>.ts`.
|
|
27
27
|
locales: ['en', 'de'] as const,
|
|
28
28
|
defaultLocale: 'en' as const,
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.30.0",
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/client": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/i18n": "0.30.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
21
|
+
"@voltro/web": "0.30.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.30.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
29
|
"typescript": "^6.0.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
2
|
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
-
// hands `meta` the active locale from the `voltro:
|
|
3
|
+
// hands `meta` the active locale from the `voltro:locale` cookie. Inside React,
|
|
4
4
|
// use `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
5
|
|
|
6
6
|
import en from './en'
|
|
@@ -27,7 +27,7 @@ export default {
|
|
|
27
27
|
// <I18nProvider> from these codes and loads the matching
|
|
28
28
|
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
29
29
|
// `useLocale()` work app-wide with no manual setup. The <LocaleSwitcher>
|
|
30
|
-
// in the layout writes the `voltro:
|
|
30
|
+
// in the layout writes the `voltro:locale` cookie + reloads. Grow the app to
|
|
31
31
|
// more languages by adding a code here + a `src/locales/<code>.ts`.
|
|
32
32
|
locales: ['en', 'de'] as const,
|
|
33
33
|
defaultLocale: 'en' as const,
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.30.0",
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/client": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/i18n": "0.30.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
21
|
+
"@voltro/web": "0.30.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.30.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
29
|
"typescript": "^6.0.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
2
|
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
-
// hands `meta` the active locale from the `voltro:
|
|
3
|
+
// hands `meta` the active locale from the `voltro:locale` cookie. Inside React,
|
|
4
4
|
// use `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
5
|
|
|
6
6
|
import en from './en'
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// `theme: 'dark' | 'light' | 'system'` in app.config.ts for the dark class.
|
|
5
5
|
//
|
|
6
6
|
// The app is bilingual (see `locales` in app.config.ts): nav labels come from
|
|
7
|
-
// the catalog via `<T>`, and <LocaleSwitcher> writes the `voltro:
|
|
7
|
+
// the catalog via `<T>`, and <LocaleSwitcher> writes the `voltro:locale` cookie
|
|
8
8
|
// + reloads so the framework re-renders in the chosen language.
|
|
9
9
|
|
|
10
10
|
import type { ReactNode } from 'react'
|
|
@@ -20,7 +20,7 @@ import { T, useTFn } from '@voltro/i18n'
|
|
|
20
20
|
import { getCatalog } from '../locales'
|
|
21
21
|
|
|
22
22
|
// Locale-aware tab title: @voltro/web resolves meta({ locale }) from the
|
|
23
|
-
// active locale (the `voltro:
|
|
23
|
+
// active locale (the `voltro:locale` cookie in this cookie-i18n app).
|
|
24
24
|
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
25
25
|
title: getCatalog(locale)['meta.notes.title'],
|
|
26
26
|
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — auth surface
|
|
2
|
+
|
|
3
|
+
A drop-in set of auth pages other apps in the project link to, so the login UI
|
|
4
|
+
lives in ONE place. Every page is a real form POSTing to the sibling api's
|
|
5
|
+
`/auth/*` routes (from [`@voltro/plugin-auth`](https://voltro.cloud/docs/plugins/auth))
|
|
6
|
+
with CSRF. Bilingual (en/de).
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
voltro init my-app --api=api-auth --web=frontend-auth
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Pages
|
|
13
|
+
|
|
14
|
+
| URL | Route it drives | Purpose |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `/login` | `POST /auth/sign-in` | email + password |
|
|
17
|
+
| `/signup` | `POST /auth/sign-up` | register + auto sign-in (with a strength meter) |
|
|
18
|
+
| `/forgot` | `POST /auth/password-reset` | request a reset link (neutral 202) |
|
|
19
|
+
| `/reset?token=` | `POST /auth/password-reset/confirm` | set a new password |
|
|
20
|
+
| `/magic` | `POST /auth/magic-link` | request a passwordless sign-in link |
|
|
21
|
+
| `/verify?token=` | `POST /auth/magic-link/callback` | consume the link / confirm email |
|
|
22
|
+
| `/logout` | `POST /auth/sign-out` | revoke the session, back to /login |
|
|
23
|
+
|
|
24
|
+
The emailed reset link points at `/reset?token=…`; the magic / verification link
|
|
25
|
+
points at `/verify?token=…`. Delivery needs `sendEmail` wired on the api (via
|
|
26
|
+
`@voltro/plugin-mail`) — without it those routes still mint tokens and return
|
|
27
|
+
`202`, they just don't send.
|
|
28
|
+
|
|
29
|
+
## Safety built in
|
|
30
|
+
|
|
31
|
+
- **Open-redirect guard** (`lib/redirect.ts`) — after sign-in we only honour a
|
|
32
|
+
same-origin, absolute-path `?next=`; a cross-origin or protocol-relative value
|
|
33
|
+
falls back to `AUTH_HOME`. Unit-tested.
|
|
34
|
+
- **CSRF** — `lib/auth.ts` fetches a token from `GET /auth/csrf` and echoes it on
|
|
35
|
+
every state-changing POST.
|
|
36
|
+
- **No account enumeration** — `/forgot` and `/magic` always show the same
|
|
37
|
+
neutral "check your email" message.
|
|
38
|
+
|
|
39
|
+
## Files
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
app.config.ts web app; apis.auth → @{{projectName}}/api
|
|
43
|
+
src/lib/
|
|
44
|
+
auth.ts csrfToken() + postAuth() over /auth/*
|
|
45
|
+
redirect.ts(.test) safeNext() open-redirect guard (unit-tested)
|
|
46
|
+
src/components/
|
|
47
|
+
AuthShell.tsx centered card + brand + locale switcher
|
|
48
|
+
PasswordStrength.tsx advisory 0–4 meter
|
|
49
|
+
src/pages/
|
|
50
|
+
page.tsx / → redirect to /login
|
|
51
|
+
login/ · signup/ · forgot/ · reset/ · magic/ · verify/ · logout/
|
|
52
|
+
src/locales/{en,de}.ts bilingual catalogs (parity-enforced)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Opt-in: standalone auth subdomain (shared-cookie SSO)
|
|
56
|
+
|
|
57
|
+
The default is a **path-based** auth surface mounted inside your app (`/login`,
|
|
58
|
+
`/signup`, …) — cookies are same-origin and nothing extra is needed. If instead
|
|
59
|
+
you want a dedicated `auth.acme.dev` that signs a cookie for the whole
|
|
60
|
+
`.acme.dev` family (so `app.acme.dev` and `cms.acme.dev` share one session),
|
|
61
|
+
deploy this app on its own subdomain and set the api's cookie domain
|
|
62
|
+
(`authRoutesPlugin({ cookieDomain: '.acme.dev' })`). The pages are identical; only
|
|
63
|
+
the deploy topology + cookie domain change.
|
|
64
|
+
|
|
65
|
+
## Opt-in: OAuth / SSO providers
|
|
66
|
+
|
|
67
|
+
The base `@voltro/plugin-auth` ships password + magic-link + reset. Third-party
|
|
68
|
+
sign-in (Google, GitHub, WorkOS, Auth0, Kinde, Supabase, generic OIDC) is a
|
|
69
|
+
SEPARATE provider plugin (`@voltro/plugin-auth-*`) with its own callback routes —
|
|
70
|
+
add the plugin on the api, then render its buttons here. We don't ship dead
|
|
71
|
+
OAuth buttons wired to routes that don't exist.
|
|
72
|
+
|
|
73
|
+
## Out of scope (V2)
|
|
74
|
+
|
|
75
|
+
- 2FA / passkeys — `@voltro/plugin-auth` has the primitives (`/auth/mfa/*`,
|
|
76
|
+
`/auth/passkey/*`); the pages follow.
|
|
77
|
+
- Bot defence (hCaptcha / Turnstile) — behind an env flag, render the widget only
|
|
78
|
+
when configured.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project — the AUTH surface. Every page is a
|
|
2
|
+
// static form that POSTs to the sibling api's `/auth/*` routes (contributed by
|
|
3
|
+
// @voltro/plugin-auth), which set the HttpOnly session cookie. Other apps in the
|
|
4
|
+
// project (dashboard, cms, portal) link here for sign-in so the auth UI lives in
|
|
5
|
+
// ONE place.
|
|
6
|
+
//
|
|
7
|
+
// The `apis` mapping names the api whose `/auth/*` routes the dev/serve pipeline
|
|
8
|
+
// proxies to and whose typed rpc surface codegen pulls (this template calls the
|
|
9
|
+
// HTTP routes directly, so it needs no rpc — the mapping is what wires the proxy
|
|
10
|
+
// + the shared origin the session cookie is set on).
|
|
11
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
12
|
+
|
|
13
|
+
export const env = defineEnv({
|
|
14
|
+
VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
15
|
+
// Where a successful sign-in lands when the request carries no safelisted
|
|
16
|
+
// `?next=`. Public (build-time) — never a secret in the browser bundle.
|
|
17
|
+
VOLTRO_PUBLIC_AUTH_HOME: envVar.string({ access: 'public', default: '/' }),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
type: 'web' as const,
|
|
22
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
23
|
+
port: {{port}},
|
|
24
|
+
theme: 'system' as const,
|
|
25
|
+
env,
|
|
26
|
+
// Bilingual by default (cookie-driven <I18nProvider>, SSR too).
|
|
27
|
+
locales: ['en', 'de'] as const,
|
|
28
|
+
defaultLocale: 'en' as const,
|
|
29
|
+
// The api whose /auth/* routes back these pages. Pair with the api-auth
|
|
30
|
+
// template: `voltro init my-app --api=api-auth --web=frontend-auth`.
|
|
31
|
+
apis: {
|
|
32
|
+
auth: { package: '@{{projectName}}/api' },
|
|
33
|
+
},
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
9
|
+
"build": "voltro build .",
|
|
10
|
+
"start": "voltro start .",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "voltro test"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@{{projectName}}/api": "0.30.0",
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/client": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/i18n": "0.30.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
21
|
+
"@voltro/web": "0.30.0",
|
|
22
|
+
"react": "^19.0.0",
|
|
23
|
+
"react-dom": "^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@voltro/devtools": "0.30.0",
|
|
27
|
+
"@types/react": "^19.0.0",
|
|
28
|
+
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vitest": "^4.1.10"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The centered auth card every page renders inside — brand, title, the form
|
|
2
|
+
// (children), an optional message, a footer link row, and the locale switcher.
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { T, useLocale, useT } from '@voltro/i18n'
|
|
5
|
+
import { LocaleSwitcher } from '@voltro/ui-shadcn'
|
|
6
|
+
import { APP_NAME } from '../config'
|
|
7
|
+
|
|
8
|
+
const LOCALES = [
|
|
9
|
+
{ code: 'en', label: 'English' },
|
|
10
|
+
{ code: 'de', label: 'Deutsch' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export interface AuthShellProps {
|
|
14
|
+
readonly titleId: string
|
|
15
|
+
readonly children: ReactNode
|
|
16
|
+
readonly footer?: ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AuthShell({ titleId, children, footer }: AuthShellProps): ReactNode {
|
|
20
|
+
const locale = useLocale()
|
|
21
|
+
const langLabel = useT('lang.label')
|
|
22
|
+
return (
|
|
23
|
+
<div className="auth-wrap">
|
|
24
|
+
<div className="auth-card">
|
|
25
|
+
<div className="auth-head">
|
|
26
|
+
<span className="brand">{APP_NAME}</span>
|
|
27
|
+
<LocaleSwitcher locales={LOCALES} current={locale} ariaLabel={langLabel} />
|
|
28
|
+
</div>
|
|
29
|
+
<h1><T id={titleId} /></h1>
|
|
30
|
+
{children}
|
|
31
|
+
{footer ? <div className="auth-footer">{footer}</div> : null}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// A tiny, dependency-free password strength meter. Purely advisory client-side
|
|
2
|
+
// UX — the api is the authority on password policy. Scores 0–4 on length +
|
|
3
|
+
// character-class variety and renders a labelled bar.
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { useT } from '@voltro/i18n'
|
|
6
|
+
|
|
7
|
+
const score = (pw: string): number => {
|
|
8
|
+
if (pw.length === 0) return 0
|
|
9
|
+
let s = 0
|
|
10
|
+
if (pw.length >= 8) s++
|
|
11
|
+
if (pw.length >= 12) s++
|
|
12
|
+
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++
|
|
13
|
+
if (/\d/.test(pw) && /[^A-Za-z0-9]/.test(pw)) s++
|
|
14
|
+
return Math.min(s, 4)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const LEVEL_KEYS = ['pw.weak', 'pw.weak', 'pw.fair', 'pw.good', 'pw.strong'] as const
|
|
18
|
+
|
|
19
|
+
export function PasswordStrength({ value }: { readonly value: string }): ReactNode {
|
|
20
|
+
const s = score(value)
|
|
21
|
+
const label = useT(LEVEL_KEYS[s] ?? 'pw.weak')
|
|
22
|
+
if (value.length === 0) return null
|
|
23
|
+
return (
|
|
24
|
+
<div className="pw-meter" aria-hidden={false}>
|
|
25
|
+
<div className="pw-bars">
|
|
26
|
+
{[0, 1, 2, 3].map((i) => (
|
|
27
|
+
<span key={i} className={i < s ? `bar on lvl${s}` : 'bar'} />
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
<span className="pw-label">{label}</span>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Brand + defaults shown across the auth pages.
|
|
2
|
+
//
|
|
3
|
+
// Plain constants — not `publicEnv` — so the template typechecks BEFORE
|
|
4
|
+
// `voltro dev` generates the env types. `VOLTRO_PUBLIC_APP_NAME` /
|
|
5
|
+
// `VOLTRO_PUBLIC_AUTH_HOME` are still declared in app.config.ts (validated + in
|
|
6
|
+
// the env manifest); to read them typed instead, after one `voltro dev` swap
|
|
7
|
+
// these for `publicEnv.VOLTRO_PUBLIC_*` from '@voltro/env/public'.
|
|
8
|
+
export const APP_NAME = '{{capProjectName}}'
|
|
9
|
+
|
|
10
|
+
// Where a successful sign-in lands when no safelisted `?next=` is present.
|
|
11
|
+
export const AUTH_HOME = '/'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* Plain CSS — no Tailwind, so there's no @source scanner to configure. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #ffffff;
|
|
4
|
+
--fg: #0a0a0a;
|
|
5
|
+
--muted: #6b7280;
|
|
6
|
+
--accent: #4f46e5;
|
|
7
|
+
--danger: #dc2626;
|
|
8
|
+
--border: #e5e7eb;
|
|
9
|
+
--panel: #f9fafb;
|
|
10
|
+
}
|
|
11
|
+
:root.dark {
|
|
12
|
+
--bg: #0a0a0a;
|
|
13
|
+
--fg: #fafafa;
|
|
14
|
+
--muted: #9ca3af;
|
|
15
|
+
--accent: #818cf8;
|
|
16
|
+
--danger: #f87171;
|
|
17
|
+
--border: #1f2937;
|
|
18
|
+
--panel: #111827;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
body {
|
|
23
|
+
margin: 0;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
color: var(--fg);
|
|
26
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
27
|
+
line-height: 1.55;
|
|
28
|
+
}
|
|
29
|
+
a { color: inherit; }
|
|
30
|
+
.muted { color: var(--muted); }
|
|
31
|
+
.spacer { flex: 1; }
|
|
32
|
+
|
|
33
|
+
/* Buttons + inputs */
|
|
34
|
+
.btn {
|
|
35
|
+
display: inline-block; cursor: pointer; border: 0; border-radius: 8px;
|
|
36
|
+
background: var(--accent); color: #fff; padding: 0.55rem 1rem;
|
|
37
|
+
font: inherit; font-weight: 600; text-decoration: none;
|
|
38
|
+
}
|
|
39
|
+
.btn:disabled { opacity: 0.6; cursor: default; }
|
|
40
|
+
.btn.ghost { background: transparent; color: var(--accent); border: 1px solid var(--border); }
|
|
41
|
+
.link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; padding: 0; }
|
|
42
|
+
input {
|
|
43
|
+
font: inherit; padding: 0.5rem 0.65rem; border: 1px solid var(--border);
|
|
44
|
+
border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0;
|
|
45
|
+
}
|
|
46
|
+
.error-text { color: var(--danger); }
|
|
47
|
+
|
|
48
|
+
/* Marketing */
|
|
49
|
+
.marketing header {
|
|
50
|
+
display: flex; align-items: center; gap: 1rem;
|
|
51
|
+
padding: 1rem 1.5rem; border-bottom: 1px solid var(--border);
|
|
52
|
+
}
|
|
53
|
+
.marketing .brand { font-weight: 700; text-decoration: none; }
|
|
54
|
+
.hero { max-width: 40rem; margin: 0 auto; padding: 3rem 1.5rem; }
|
|
55
|
+
.hero h1 { margin: 0 0 0.75rem; }
|
|
56
|
+
.auth-form { display: flex; flex-direction: column; gap: 0.85rem; max-width: 22rem; margin-top: 1.25rem; }
|
|
57
|
+
.auth-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.9rem; }
|
|
58
|
+
|
|
59
|
+
/* Dashboard shell */
|
|
60
|
+
.dash { display: grid; grid-template-columns: 15rem 1fr; min-height: 100vh; }
|
|
61
|
+
.dash aside { border-right: 1px solid var(--border); padding: 1.25rem; background: var(--panel); }
|
|
62
|
+
.dash aside .brand { font-weight: 700; margin-bottom: 1rem; }
|
|
63
|
+
.dash nav { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
64
|
+
.dash nav a { text-decoration: none; color: var(--muted); padding: 0.35rem 0; }
|
|
65
|
+
.dash nav a[aria-current="page"] { color: var(--fg); font-weight: 600; }
|
|
66
|
+
.dash main { padding: 1.5rem 2rem; }
|
|
67
|
+
.topbar { display: flex; align-items: center; margin-bottom: 1rem; }
|
|
68
|
+
@media (max-width: 40rem) { .dash { grid-template-columns: 1fr; } .dash aside { border-right: 0; border-bottom: 1px solid var(--border); } }
|
|
69
|
+
|
|
70
|
+
/* Bits */
|
|
71
|
+
.row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-top: 0.5rem; }
|
|
72
|
+
.empty { color: var(--muted); margin-top: 1.5rem; }
|
|
73
|
+
.cards { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 0.75rem; }
|
|
74
|
+
.card { border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem 1rem; background: var(--panel); }
|
|
75
|
+
.card .value { font-weight: 600; }
|
|
76
|
+
.card .label { color: var(--muted); font-size: 0.85rem; margin-top: 0.2rem; }
|
|
77
|
+
.paywall { border: 1px solid var(--accent); border-radius: 12px; padding: 1rem 1.25rem; margin-top: 1.25rem; background: color-mix(in oklab, var(--accent) 8%, transparent); }
|
|
78
|
+
.paywall p { margin: 0.35rem 0 0.75rem; }
|
|
79
|
+
.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.5rem; margin-top: 1rem; }
|
|
80
|
+
.facts dt { color: var(--muted); }
|
|
81
|
+
.facts dd { margin: 0; font-weight: 600; }
|
|
82
|
+
.rows { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
83
|
+
.rows li { display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.9rem; background: var(--panel); }
|
|
84
|
+
.tag { font-size: 0.8rem; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 0.05rem 0.55rem; }
|
|
85
|
+
code { background: color-mix(in oklab, var(--fg) 8%, transparent); padding: 0.1em 0.35em; border-radius: 4px; }
|
|
86
|
+
|
|
87
|
+
/* Auth card */
|
|
88
|
+
.auth-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 1.5rem; }
|
|
89
|
+
.auth-card { width: 100%; max-width: 24rem; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); padding: 1.5rem 1.5rem 1.25rem; }
|
|
90
|
+
.auth-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; }
|
|
91
|
+
.auth-head .brand { font-weight: 700; }
|
|
92
|
+
.auth-card h1 { margin: 0 0 1rem; font-size: 1.35rem; }
|
|
93
|
+
.auth-card .auth-form { max-width: none; margin-top: 0; }
|
|
94
|
+
.auth-footer { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; margin-top: 1.1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); font-size: 0.9rem; }
|
|
95
|
+
.auth-footer a { color: var(--accent); text-decoration: none; }
|
|
96
|
+
.ok { color: var(--accent); }
|
|
97
|
+
|
|
98
|
+
/* Password strength meter */
|
|
99
|
+
.pw-meter { display: flex; align-items: center; gap: 0.5rem; margin-top: -0.25rem; }
|
|
100
|
+
.pw-bars { display: flex; gap: 3px; flex: 1; }
|
|
101
|
+
.pw-bars .bar { height: 5px; flex: 1; border-radius: 3px; background: var(--border); }
|
|
102
|
+
.pw-bars .bar.on { background: var(--danger); }
|
|
103
|
+
.pw-bars .bar.on.lvl3 { background: #d97706; }
|
|
104
|
+
.pw-bars .bar.on.lvl4 { background: var(--accent); }
|
|
105
|
+
.pw-label { font-size: 0.8rem; color: var(--muted); min-width: 3.2rem; text-align: right; }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Ambient declarations.
|
|
2
|
+
//
|
|
3
|
+
// `declare module '*.css'`: TypeScript with `moduleResolution: Bundler` rejects
|
|
4
|
+
// a bare `import './globals.css'` unless the asset module is declared. Vite
|
|
5
|
+
// resolves it at build time; the declaration is type-only.
|
|
6
|
+
declare module '*.css'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Thin wrappers over the api's `/auth/*` HTTP routes (@voltro/plugin-auth).
|
|
2
|
+
//
|
|
3
|
+
// State-changing auth routes are CSRF-protected (double-submit): `GET /auth/csrf`
|
|
4
|
+
// sets a cookie AND returns a token that must be echoed in the `x-csrf-token`
|
|
5
|
+
// header on the following POST. `postAuth` does that round-trip so each page is
|
|
6
|
+
// a one-liner. These call the SAME-ORIGIN api (the dev/serve pipeline proxies
|
|
7
|
+
// `/auth/*` to the api named in app.config), so the HttpOnly session cookie the
|
|
8
|
+
// api sets lands on this origin.
|
|
9
|
+
|
|
10
|
+
export interface AuthResponse {
|
|
11
|
+
readonly ok: boolean
|
|
12
|
+
readonly status: number
|
|
13
|
+
readonly data: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fetch a CSRF token (also drops the csrf cookie into the jar). */
|
|
17
|
+
export const csrfToken = async (): Promise<string> => {
|
|
18
|
+
const res = await fetch('/auth/csrf')
|
|
19
|
+
const body = (await res.json()) as { csrfToken?: string }
|
|
20
|
+
return body.csrfToken ?? ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** POST a JSON body to an `/auth/*` route with the CSRF token attached. */
|
|
24
|
+
export const postAuth = async (path: string, body: Record<string, unknown>): Promise<AuthResponse> => {
|
|
25
|
+
const token = await csrfToken()
|
|
26
|
+
const res = await fetch(path, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'content-type': 'application/json', 'x-csrf-token': token },
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
})
|
|
31
|
+
let data: unknown = null
|
|
32
|
+
try { data = await res.json() } catch { /* some routes return 202 with no body */ }
|
|
33
|
+
return { ok: res.ok, status: res.status, data }
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Pure unit test of the open-redirect guard — no DOM needed. The safelist is
|
|
2
|
+
// the security-load-bearing part of the auth flow, so it gets a direct test.
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { safeNext } from './redirect'
|
|
5
|
+
|
|
6
|
+
describe('safeNext', () => {
|
|
7
|
+
test('passes a same-origin absolute path through', () => {
|
|
8
|
+
expect(safeNext('/dashboard', '/')).toBe('/dashboard')
|
|
9
|
+
expect(safeNext('/a/b?x=1', '/')).toBe('/a/b?x=1')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('falls back to home for a missing value', () => {
|
|
13
|
+
expect(safeNext(null, '/home')).toBe('/home')
|
|
14
|
+
expect(safeNext(undefined, '/home')).toBe('/home')
|
|
15
|
+
expect(safeNext('', '/home')).toBe('/home')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('rejects cross-origin + protocol-relative + absolute URLs', () => {
|
|
19
|
+
expect(safeNext('https://evil.example/login', '/')).toBe('/')
|
|
20
|
+
expect(safeNext('//evil.example', '/')).toBe('/')
|
|
21
|
+
expect(safeNext('/\\evil.example', '/')).toBe('/')
|
|
22
|
+
expect(safeNext('javascript:alert(1)', '/')).toBe('/')
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Open-redirect guard for the `?next=` parameter.
|
|
2
|
+
//
|
|
3
|
+
// After sign-in we send the browser to wherever it was headed — but a `next=`
|
|
4
|
+
// an attacker controls is a phishing vector (`?next=https://evil.example/login`
|
|
5
|
+
// that looks like us). So we accept ONLY same-origin, absolute-PATH targets:
|
|
6
|
+
// a value must start with a single `/` and NOT with `//` (a protocol-relative
|
|
7
|
+
// URL like `//evil.example` that browsers treat as cross-origin). Anything else
|
|
8
|
+
// falls back to `home`.
|
|
9
|
+
|
|
10
|
+
/** The safelisted redirect target, or `home` when `next` is missing/unsafe. */
|
|
11
|
+
export const safeNext = (next: string | null | undefined, home: string): string => {
|
|
12
|
+
if (!next) return home
|
|
13
|
+
// Must be an absolute path, and not protocol-relative (`//host`) or a
|
|
14
|
+
// backslash-smuggled variant (`/\evil`).
|
|
15
|
+
if (!next.startsWith('/') || next.startsWith('//') || next.startsWith('/\\')) return home
|
|
16
|
+
return next
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Read + safelist `?next=` from the current URL (browser only). */
|
|
20
|
+
export const nextFromLocation = (home: string): string => {
|
|
21
|
+
if (typeof window === 'undefined') return home
|
|
22
|
+
return safeNext(new URLSearchParams(window.location.search).get('next'), home)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read a `?token=` from the current URL (browser only). */
|
|
26
|
+
export const tokenFromLocation = (): string | null => {
|
|
27
|
+
if (typeof window === 'undefined') return null
|
|
28
|
+
return new URLSearchParams(window.location.search).get('token')
|
|
29
|
+
}
|