vibecarbon 0.1.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.
Files changed (495) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +188 -0
  3. package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
  4. package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
  5. package/carbon/.claude/settings.json +29 -0
  6. package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
  7. package/carbon/.dockerignore +57 -0
  8. package/carbon/.env.example +219 -0
  9. package/carbon/.github/copilot-instructions.md +3 -0
  10. package/carbon/.github/workflows/deploy.yml +346 -0
  11. package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
  12. package/carbon/.windsurfrules +74 -0
  13. package/carbon/AGENTS.md +422 -0
  14. package/carbon/CLAUDE.md +3 -0
  15. package/carbon/DEVELOPMENT.md +187 -0
  16. package/carbon/Dockerfile +98 -0
  17. package/carbon/LICENSE +21 -0
  18. package/carbon/PRODUCTION.md +364 -0
  19. package/carbon/README.md +193 -0
  20. package/carbon/TESTING.md +138 -0
  21. package/carbon/backup/Dockerfile +75 -0
  22. package/carbon/backup/backup.sh +95 -0
  23. package/carbon/backup/compose-backup.sh +140 -0
  24. package/carbon/biome.json +83 -0
  25. package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
  26. package/carbon/cloud-init/k3s/master-init.sh +215 -0
  27. package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
  28. package/carbon/cloud-init/k3s/worker-init.sh +147 -0
  29. package/carbon/components.json +24 -0
  30. package/carbon/content/blog/authentication-guide.mdx +34 -0
  31. package/carbon/content/blog/getting-started.mdx +41 -0
  32. package/carbon/content/changelog/v0-1-0.mdx +40 -0
  33. package/carbon/content/docs/analytics.mdx +90 -0
  34. package/carbon/content/docs/authentication.mdx +231 -0
  35. package/carbon/content/docs/background-jobs.mdx +116 -0
  36. package/carbon/content/docs/cli.mdx +630 -0
  37. package/carbon/content/docs/database.mdx +236 -0
  38. package/carbon/content/docs/deployment.mdx +227 -0
  39. package/carbon/content/docs/development.mdx +238 -0
  40. package/carbon/content/docs/environments.mdx +84 -0
  41. package/carbon/content/docs/getting-started.mdx +112 -0
  42. package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
  43. package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
  44. package/carbon/content/docs/optional-services.mdx +160 -0
  45. package/carbon/db/Dockerfile +23 -0
  46. package/carbon/docker-compose.dev-init.yml +5 -0
  47. package/carbon/docker-compose.metabase.override.yml +14 -0
  48. package/carbon/docker-compose.metabase.prod.yml +28 -0
  49. package/carbon/docker-compose.metabase.yml +84 -0
  50. package/carbon/docker-compose.n8n.override.yml +14 -0
  51. package/carbon/docker-compose.n8n.prod.yml +31 -0
  52. package/carbon/docker-compose.n8n.yml +97 -0
  53. package/carbon/docker-compose.observability.override.yml +18 -0
  54. package/carbon/docker-compose.observability.prod.yml +28 -0
  55. package/carbon/docker-compose.observability.yml +125 -0
  56. package/carbon/docker-compose.override.yml +28 -0
  57. package/carbon/docker-compose.prod.yml +294 -0
  58. package/carbon/docker-compose.yml +508 -0
  59. package/carbon/docker-entrypoint.sh +12 -0
  60. package/carbon/ha/activate-standby.sh +52 -0
  61. package/carbon/ha/primary-init.sql +36 -0
  62. package/carbon/ha/standby-init.sh +74 -0
  63. package/carbon/k8s/LICENSE +99 -0
  64. package/carbon/k8s/README.md +272 -0
  65. package/carbon/k8s/base/app/deployment.yaml +130 -0
  66. package/carbon/k8s/base/app/hpa.yaml +44 -0
  67. package/carbon/k8s/base/app/kustomization.yaml +10 -0
  68. package/carbon/k8s/base/app/network-policy.yaml +124 -0
  69. package/carbon/k8s/base/app/pdb.yaml +14 -0
  70. package/carbon/k8s/base/app/rbac.yaml +36 -0
  71. package/carbon/k8s/base/app/service.yaml +16 -0
  72. package/carbon/k8s/base/backup/cronjob.yaml +120 -0
  73. package/carbon/k8s/base/backup/kustomization.yaml +7 -0
  74. package/carbon/k8s/base/backup/network-policy.yaml +31 -0
  75. package/carbon/k8s/base/backup/rbac.yaml +7 -0
  76. package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
  77. package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
  78. package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
  79. package/carbon/k8s/base/config/configmap.yaml +48 -0
  80. package/carbon/k8s/base/config/kustomization.yaml +8 -0
  81. package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
  82. package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
  83. package/carbon/k8s/base/kustomization.yaml +43 -0
  84. package/carbon/k8s/base/namespace.yaml +7 -0
  85. package/carbon/k8s/base/network-policies.yaml +95 -0
  86. package/carbon/k8s/base/registry/kustomization.yaml +5 -0
  87. package/carbon/k8s/base/registry/local-registry.yaml +193 -0
  88. package/carbon/k8s/base/traefik/certificate.yaml +21 -0
  89. package/carbon/k8s/base/traefik/configmap.yaml +13 -0
  90. package/carbon/k8s/base/traefik/deployment.yaml +165 -0
  91. package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
  92. package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
  93. package/carbon/k8s/base/traefik/middleware.yaml +109 -0
  94. package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
  95. package/carbon/k8s/base/traefik/service.yaml +38 -0
  96. package/carbon/k8s/flux/README.md +49 -0
  97. package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
  98. package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
  99. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
  100. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
  101. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
  102. package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
  103. package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
  104. package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
  105. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
  106. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
  107. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
  108. package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
  109. package/carbon/k8s/infra/kustomization.yaml +21 -0
  110. package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
  111. package/carbon/k8s/overlays/local/configmap.yaml +23 -0
  112. package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
  113. package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
  114. package/carbon/k8s/overlays/local/namespace.yaml +8 -0
  115. package/carbon/k8s/overlays/local/secrets.yaml +32 -0
  116. package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
  117. package/carbon/k8s/test-local.sh +318 -0
  118. package/carbon/k8s/values/supabase.values.yaml +133 -0
  119. package/carbon/package.json +154 -0
  120. package/carbon/runtime.Dockerfile +17 -0
  121. package/carbon/scripts/_dev-jwt.mjs +45 -0
  122. package/carbon/scripts/check-rls.ts +53 -0
  123. package/carbon/scripts/dev-init.js +191 -0
  124. package/carbon/scripts/dev.js +191 -0
  125. package/carbon/scripts/docker-down.js +63 -0
  126. package/carbon/scripts/docker-logs.js +59 -0
  127. package/carbon/scripts/docker-up.js +222 -0
  128. package/carbon/scripts/generate-dev-configs.sh +131 -0
  129. package/carbon/scripts/generate-rss.ts +116 -0
  130. package/carbon/scripts/generate-sitemap.ts +102 -0
  131. package/carbon/scripts/k8s-apply.js +71 -0
  132. package/carbon/scripts/k8s-delete.js +75 -0
  133. package/carbon/scripts/lib/manifest.js +176 -0
  134. package/carbon/scripts/secret-scan.mjs +278 -0
  135. package/carbon/scripts/validate-dev-configs.sh +101 -0
  136. package/carbon/src/client/App.tsx +202 -0
  137. package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
  138. package/carbon/src/client/assets/hyperformant-light.svg +29 -0
  139. package/carbon/src/client/assets/logos/aider.svg +1 -0
  140. package/carbon/src/client/assets/logos/antigravity.svg +1 -0
  141. package/carbon/src/client/assets/logos/bolt.svg +1 -0
  142. package/carbon/src/client/assets/logos/claude-code.svg +1 -0
  143. package/carbon/src/client/assets/logos/copilot.svg +1 -0
  144. package/carbon/src/client/assets/logos/cursor.svg +1 -0
  145. package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
  146. package/carbon/src/client/assets/logos/lovable.svg +1 -0
  147. package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
  148. package/carbon/src/client/assets/logos/v0.svg +1 -0
  149. package/carbon/src/client/assets/logos/windsurf.svg +1 -0
  150. package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
  151. package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
  152. package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
  153. package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
  154. package/carbon/src/client/components/AppSidebar.tsx +760 -0
  155. package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
  156. package/carbon/src/client/components/CTAFooter.tsx +59 -0
  157. package/carbon/src/client/components/ComparisonSection.tsx +132 -0
  158. package/carbon/src/client/components/ContentPanel.tsx +66 -0
  159. package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
  160. package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
  161. package/carbon/src/client/components/FAQSection.tsx +76 -0
  162. package/carbon/src/client/components/FileUpload.tsx +210 -0
  163. package/carbon/src/client/components/HeaderActions.tsx +17 -0
  164. package/carbon/src/client/components/Hero.tsx +608 -0
  165. package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
  166. package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
  167. package/carbon/src/client/components/Logo.tsx +87 -0
  168. package/carbon/src/client/components/LogoStrip.tsx +76 -0
  169. package/carbon/src/client/components/MetricsStrip.tsx +48 -0
  170. package/carbon/src/client/components/Nav.tsx +195 -0
  171. package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
  172. package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
  173. package/carbon/src/client/components/PageHeader.tsx +24 -0
  174. package/carbon/src/client/components/PlanGate.tsx +36 -0
  175. package/carbon/src/client/components/PricingSection.tsx +371 -0
  176. package/carbon/src/client/components/SEO.tsx +34 -0
  177. package/carbon/src/client/components/SmoothScroll.tsx +45 -0
  178. package/carbon/src/client/components/TechStackSection.tsx +165 -0
  179. package/carbon/src/client/components/WorkflowSection.tsx +604 -0
  180. package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
  181. package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
  182. package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
  183. package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
  184. package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
  185. package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
  186. package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
  187. package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
  188. package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
  189. package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
  190. package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
  191. package/carbon/src/client/components/effects/index.ts +2 -0
  192. package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
  193. package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
  194. package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
  195. package/carbon/src/client/components/scroll/index.ts +2 -0
  196. package/carbon/src/client/components/ui/accordion.tsx +71 -0
  197. package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
  198. package/carbon/src/client/components/ui/alert.tsx +73 -0
  199. package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
  200. package/carbon/src/client/components/ui/avatar.tsx +91 -0
  201. package/carbon/src/client/components/ui/badge.tsx +50 -0
  202. package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
  203. package/carbon/src/client/components/ui/button-group.tsx +78 -0
  204. package/carbon/src/client/components/ui/button.tsx +120 -0
  205. package/carbon/src/client/components/ui/calendar.tsx +182 -0
  206. package/carbon/src/client/components/ui/card.tsx +85 -0
  207. package/carbon/src/client/components/ui/carousel.tsx +227 -0
  208. package/carbon/src/client/components/ui/chart.tsx +357 -0
  209. package/carbon/src/client/components/ui/checkbox.tsx +27 -0
  210. package/carbon/src/client/components/ui/collapsible.tsx +15 -0
  211. package/carbon/src/client/components/ui/command.tsx +178 -0
  212. package/carbon/src/client/components/ui/context-menu.tsx +233 -0
  213. package/carbon/src/client/components/ui/dialog.tsx +132 -0
  214. package/carbon/src/client/components/ui/drawer.tsx +118 -0
  215. package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
  216. package/carbon/src/client/components/ui/empty.tsx +94 -0
  217. package/carbon/src/client/components/ui/field.tsx +226 -0
  218. package/carbon/src/client/components/ui/hover-card.tsx +44 -0
  219. package/carbon/src/client/components/ui/input-group.tsx +146 -0
  220. package/carbon/src/client/components/ui/input-otp.tsx +83 -0
  221. package/carbon/src/client/components/ui/input.tsx +20 -0
  222. package/carbon/src/client/components/ui/item.tsx +188 -0
  223. package/carbon/src/client/components/ui/kbd.tsx +26 -0
  224. package/carbon/src/client/components/ui/label.tsx +21 -0
  225. package/carbon/src/client/components/ui/menubar.tsx +250 -0
  226. package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
  227. package/carbon/src/client/components/ui/pagination.tsx +102 -0
  228. package/carbon/src/client/components/ui/popover.tsx +75 -0
  229. package/carbon/src/client/components/ui/progress.tsx +66 -0
  230. package/carbon/src/client/components/ui/radio-group.tsx +36 -0
  231. package/carbon/src/client/components/ui/resizable.tsx +46 -0
  232. package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
  233. package/carbon/src/client/components/ui/select.tsx +186 -0
  234. package/carbon/src/client/components/ui/separator.tsx +21 -0
  235. package/carbon/src/client/components/ui/sheet.tsx +124 -0
  236. package/carbon/src/client/components/ui/sidebar.tsx +702 -0
  237. package/carbon/src/client/components/ui/skeleton.tsx +13 -0
  238. package/carbon/src/client/components/ui/slider.tsx +57 -0
  239. package/carbon/src/client/components/ui/sonner.tsx +45 -0
  240. package/carbon/src/client/components/ui/spinner.tsx +15 -0
  241. package/carbon/src/client/components/ui/switch.tsx +30 -0
  242. package/carbon/src/client/components/ui/table.tsx +89 -0
  243. package/carbon/src/client/components/ui/tabs.tsx +73 -0
  244. package/carbon/src/client/components/ui/textarea.tsx +18 -0
  245. package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
  246. package/carbon/src/client/components/ui/toggle.tsx +44 -0
  247. package/carbon/src/client/components/ui/tooltip.tsx +56 -0
  248. package/carbon/src/client/hooks/api/index.ts +14 -0
  249. package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
  250. package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
  251. package/carbon/src/client/hooks/use-mobile.ts +21 -0
  252. package/carbon/src/client/hooks/useMousePosition.ts +91 -0
  253. package/carbon/src/client/hooks/useNotifications.tsx +124 -0
  254. package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
  255. package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
  256. package/carbon/src/client/hooks/usePackageManager.ts +69 -0
  257. package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
  258. package/carbon/src/client/hooks/useRunningServices.ts +114 -0
  259. package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
  260. package/carbon/src/client/index.css +467 -0
  261. package/carbon/src/client/index.html +56 -0
  262. package/carbon/src/client/lib/admin-services.ts +151 -0
  263. package/carbon/src/client/lib/api.ts +32 -0
  264. package/carbon/src/client/lib/blog.ts +35 -0
  265. package/carbon/src/client/lib/changelog.ts +33 -0
  266. package/carbon/src/client/lib/docs-search.ts +101 -0
  267. package/carbon/src/client/lib/docs.ts +37 -0
  268. package/carbon/src/client/lib/i18n.ts +32 -0
  269. package/carbon/src/client/lib/supabase.ts +72 -0
  270. package/carbon/src/client/lib/tailwind-colors.ts +357 -0
  271. package/carbon/src/client/lib/theme.ts +117 -0
  272. package/carbon/src/client/lib/utils.ts +22 -0
  273. package/carbon/src/client/locales/de.json +529 -0
  274. package/carbon/src/client/locales/en.json +461 -0
  275. package/carbon/src/client/locales/es.json +529 -0
  276. package/carbon/src/client/locales/fr.json +529 -0
  277. package/carbon/src/client/locales/pt.json +529 -0
  278. package/carbon/src/client/main.tsx +56 -0
  279. package/carbon/src/client/mdx.d.ts +13 -0
  280. package/carbon/src/client/pages/ApiDocs.tsx +76 -0
  281. package/carbon/src/client/pages/AuthCallback.tsx +34 -0
  282. package/carbon/src/client/pages/Blog.tsx +167 -0
  283. package/carbon/src/client/pages/Changelog.tsx +171 -0
  284. package/carbon/src/client/pages/Charts.tsx +388 -0
  285. package/carbon/src/client/pages/Checkout.tsx +227 -0
  286. package/carbon/src/client/pages/Contact.tsx +174 -0
  287. package/carbon/src/client/pages/Dashboard.tsx +368 -0
  288. package/carbon/src/client/pages/Docs.tsx +372 -0
  289. package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
  290. package/carbon/src/client/pages/Home.tsx +187 -0
  291. package/carbon/src/client/pages/Legal.tsx +100 -0
  292. package/carbon/src/client/pages/Login.tsx +408 -0
  293. package/carbon/src/client/pages/MFAVerify.tsx +156 -0
  294. package/carbon/src/client/pages/NotFound.tsx +21 -0
  295. package/carbon/src/client/pages/Onboarding.tsx +246 -0
  296. package/carbon/src/client/pages/ResetPassword.tsx +200 -0
  297. package/carbon/src/client/pages/UIComponents.tsx +390 -0
  298. package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
  299. package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
  300. package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
  301. package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
  302. package/carbon/src/client/pages/admin/Logs.tsx +18 -0
  303. package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
  304. package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
  305. package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
  306. package/carbon/src/client/pages/admin/Settings.tsx +314 -0
  307. package/carbon/src/client/pages/admin/Theme.tsx +465 -0
  308. package/carbon/src/client/pages/admin/Users.tsx +365 -0
  309. package/carbon/src/client/pages/api-docs.css +156 -0
  310. package/carbon/src/client/pages/organizations/Details.tsx +200 -0
  311. package/carbon/src/client/pages/organizations/Members.tsx +402 -0
  312. package/carbon/src/client/pages/settings/Billing.tsx +473 -0
  313. package/carbon/src/client/pages/settings/Profile.tsx +160 -0
  314. package/carbon/src/client/pages/settings/Security.tsx +341 -0
  315. package/carbon/src/client/public/favicon.svg +19 -0
  316. package/carbon/src/client/public/robots.txt +8 -0
  317. package/carbon/src/server/billing/index.ts +104 -0
  318. package/carbon/src/server/billing/provider.ts +81 -0
  319. package/carbon/src/server/billing/providers/paddle.ts +314 -0
  320. package/carbon/src/server/billing/providers/polar.ts +325 -0
  321. package/carbon/src/server/billing/providers/stripe.ts +233 -0
  322. package/carbon/src/server/emails/templates.ts +116 -0
  323. package/carbon/src/server/index.ts +554 -0
  324. package/carbon/src/server/lib/auth.ts +6 -0
  325. package/carbon/src/server/lib/email.ts +64 -0
  326. package/carbon/src/server/lib/env.ts +112 -0
  327. package/carbon/src/server/lib/errors.ts +21 -0
  328. package/carbon/src/server/lib/logger.ts +17 -0
  329. package/carbon/src/server/lib/rate-limiter.ts +288 -0
  330. package/carbon/src/server/lib/request.ts +34 -0
  331. package/carbon/src/server/lib/stripe.ts +42 -0
  332. package/carbon/src/server/lib/supabase.ts +65 -0
  333. package/carbon/src/server/middleware/requirePlan.ts +80 -0
  334. package/carbon/src/server/routes/_internal/services-status.ts +958 -0
  335. package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
  336. package/carbon/src/server/routes/health.ts +48 -0
  337. package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
  338. package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
  339. package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
  340. package/carbon/src/server/routes/v1/auth.ts +390 -0
  341. package/carbon/src/server/routes/v1/billing.ts +718 -0
  342. package/carbon/src/server/routes/v1/contact.ts +93 -0
  343. package/carbon/src/server/routes/v1/index.ts +1333 -0
  344. package/carbon/src/server/routes/v1/newsletter.ts +181 -0
  345. package/carbon/src/server/routes/v1/performance.ts +157 -0
  346. package/carbon/src/server/routes/v1/stats.ts +170 -0
  347. package/carbon/src/server/routes/v1/theme.ts +106 -0
  348. package/carbon/src/server/routes/webhooks/billing.ts +376 -0
  349. package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
  350. package/carbon/src/server/types.ts +11 -0
  351. package/carbon/src/shared/pricing.ts +155 -0
  352. package/carbon/src/shared/types.ts +338 -0
  353. package/carbon/supabase/migrations/00001_init.sql +717 -0
  354. package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
  355. package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
  356. package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
  357. package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
  358. package/carbon/supabase/seed.sql +16 -0
  359. package/carbon/tests/_helpers/app.ts +45 -0
  360. package/carbon/tests/_helpers/env.ts +37 -0
  361. package/carbon/tests/_helpers/factories.ts +69 -0
  362. package/carbon/tests/_helpers/jwt.ts +23 -0
  363. package/carbon/tests/_helpers/setup-integration.ts +20 -0
  364. package/carbon/tests/_helpers/setup-rtl.ts +12 -0
  365. package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
  366. package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
  367. package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
  368. package/carbon/tests/integration/server/routes/health.test.ts +61 -0
  369. package/carbon/tests/structural/i18n-parity.test.ts +42 -0
  370. package/carbon/tests/unit/client/utils.test.ts +49 -0
  371. package/carbon/tests/unit/shared/pricing.test.ts +93 -0
  372. package/carbon/tsconfig.json +27 -0
  373. package/carbon/tsconfig.server.json +27 -0
  374. package/carbon/tsconfig.test.json +9 -0
  375. package/carbon/vite.config.ts +110 -0
  376. package/carbon/vitest.config.ts +74 -0
  377. package/carbon/volumes/db/jwt.sql +57 -0
  378. package/carbon/volumes/db/metabase-init.sh +29 -0
  379. package/carbon/volumes/db/n8n-init.sh +25 -0
  380. package/carbon/volumes/db/realtime.sql +33 -0
  381. package/carbon/volumes/db/roles.sql +93 -0
  382. package/carbon/volumes/db/set-passwords.sh +12 -0
  383. package/carbon/volumes/db/super-admin.dev.sql +113 -0
  384. package/carbon/volumes/db/super-admin.generated.sql +113 -0
  385. package/carbon/volumes/db/super-admin.sql +114 -0
  386. package/carbon/volumes/grafana/dashboards/logs.json +179 -0
  387. package/carbon/volumes/grafana/dashboards/overview.json +523 -0
  388. package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
  389. package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
  390. package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
  391. package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
  392. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
  393. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
  394. package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
  395. package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
  396. package/carbon/volumes/kong/kong.yml +208 -0
  397. package/carbon/volumes/loki/loki-config.yml +58 -0
  398. package/carbon/volumes/n8n/hooks.js +131 -0
  399. package/carbon/volumes/n8n/scripts/setup.sh +66 -0
  400. package/carbon/volumes/prometheus/prometheus.yml +43 -0
  401. package/carbon/volumes/promtail/promtail-config.yml +64 -0
  402. package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
  403. package/carbon/volumes/traefik/middlewares.yml +95 -0
  404. package/carbon/volumes/traefik/vite-dev.yml +20 -0
  405. package/package.json +95 -0
  406. package/src/access.js +354 -0
  407. package/src/activate.js +187 -0
  408. package/src/add.js +718 -0
  409. package/src/backup.js +786 -0
  410. package/src/cli.js +350 -0
  411. package/src/configure.js +967 -0
  412. package/src/console.js +155 -0
  413. package/src/create.js +1499 -0
  414. package/src/deploy.js +311 -0
  415. package/src/destroy.js +2033 -0
  416. package/src/diagnose.js +735 -0
  417. package/src/down.js +80 -0
  418. package/src/failover.js +1032 -0
  419. package/src/lib/backup-s3.js +179 -0
  420. package/src/lib/build.js +33 -0
  421. package/src/lib/checksum.js +28 -0
  422. package/src/lib/ci-setup.js +666 -0
  423. package/src/lib/cli/help.js +129 -0
  424. package/src/lib/cli/parse-flags.js +160 -0
  425. package/src/lib/cli/select-action.js +65 -0
  426. package/src/lib/cli/select-environment.js +108 -0
  427. package/src/lib/cli/tty-guard.js +75 -0
  428. package/src/lib/cloudflare.js +447 -0
  429. package/src/lib/colors.js +52 -0
  430. package/src/lib/command.js +361 -0
  431. package/src/lib/config.js +359 -0
  432. package/src/lib/cost.js +103 -0
  433. package/src/lib/deploy/bundle.js +231 -0
  434. package/src/lib/deploy/compose/acme-verify.js +121 -0
  435. package/src/lib/deploy/compose/build-args.js +78 -0
  436. package/src/lib/deploy/compose/ha.js +1874 -0
  437. package/src/lib/deploy/compose/index.js +1294 -0
  438. package/src/lib/deploy/compose/reconcile.js +74 -0
  439. package/src/lib/deploy/github.js +382 -0
  440. package/src/lib/deploy/image.js +191 -0
  441. package/src/lib/deploy/index.js +119 -0
  442. package/src/lib/deploy/k8s/LICENSE +99 -0
  443. package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
  444. package/src/lib/deploy/k8s/ha/index.js +1000 -0
  445. package/src/lib/deploy/k8s/index.js +62 -0
  446. package/src/lib/deploy/k8s/k3s.js +2515 -0
  447. package/src/lib/deploy/orchestrator.js +1401 -0
  448. package/src/lib/deploy/prompts.js +545 -0
  449. package/src/lib/deploy/remote-build.js +130 -0
  450. package/src/lib/deploy/state.js +120 -0
  451. package/src/lib/deploy/utils.js +328 -0
  452. package/src/lib/deploy-logger.js +93 -0
  453. package/src/lib/dns-propagation.js +48 -0
  454. package/src/lib/environment.js +58 -0
  455. package/src/lib/fetch-retry.js +103 -0
  456. package/src/lib/github-environments.js +335 -0
  457. package/src/lib/hetzner-dns.js +377 -0
  458. package/src/lib/hetzner-guided-setup.js +275 -0
  459. package/src/lib/host-keys.js +37 -0
  460. package/src/lib/iac/cloud-init.js +52 -0
  461. package/src/lib/iac/index.js +325 -0
  462. package/src/lib/iac/programs/hetzner-compose.js +130 -0
  463. package/src/lib/iac/programs/hetzner-k8s.js +320 -0
  464. package/src/lib/kubectl.js +81 -0
  465. package/src/lib/licensing/index.js +259 -0
  466. package/src/lib/licensing/tiers.js +181 -0
  467. package/src/lib/licensing/validator.js +171 -0
  468. package/src/lib/merge-package-json.js +90 -0
  469. package/src/lib/operator-ip.js +381 -0
  470. package/src/lib/package-manager.js +111 -0
  471. package/src/lib/perf.js +71 -0
  472. package/src/lib/project-guard.js +39 -0
  473. package/src/lib/project.js +334 -0
  474. package/src/lib/providers/base.js +276 -0
  475. package/src/lib/providers/hetzner-s3.js +656 -0
  476. package/src/lib/providers/hetzner.js +755 -0
  477. package/src/lib/providers/index.js +164 -0
  478. package/src/lib/s3.js +510 -0
  479. package/src/lib/secret-scan.js +583 -0
  480. package/src/lib/secrets.js +63 -0
  481. package/src/lib/server-types.js +195 -0
  482. package/src/lib/shell.js +91 -0
  483. package/src/lib/ssh.js +241 -0
  484. package/src/lib/tracker.js +242 -0
  485. package/src/lib/upgrade-policy.js +170 -0
  486. package/src/lib/validators.js +105 -0
  487. package/src/lib/version.js +5 -0
  488. package/src/remove.js +292 -0
  489. package/src/reset.js +97 -0
  490. package/src/restore.js +871 -0
  491. package/src/scale.js +1734 -0
  492. package/src/shell.js +222 -0
  493. package/src/status.js +981 -0
  494. package/src/up.js +264 -0
  495. package/src/upgrade.js +721 -0
@@ -0,0 +1,100 @@
1
+ import type { ComponentType } from 'react';
2
+ import { Link, Navigate, useLocation, useParams } from 'react-router-dom';
3
+ import { Wordmark } from '@/components/Logo';
4
+ import { Nav } from '@/components/Nav';
5
+ import { SEO } from '@/components/SEO';
6
+ import type { MdxProps } from '@/lib/docs';
7
+
8
+ interface LegalPage {
9
+ slug: string;
10
+ title: string;
11
+ description: string;
12
+ Component: ComponentType<MdxProps>;
13
+ }
14
+
15
+ const legalModules = import.meta.glob<{
16
+ default: ComponentType<MdxProps>;
17
+ frontmatter: { title: string; description: string };
18
+ }>('/../../content/docs/legal/*.mdx', { eager: true });
19
+
20
+ const legalPages: LegalPage[] = Object.entries(legalModules).map(([filepath, mod]) => {
21
+ const slug = (filepath.split('/').pop() ?? '').replace('.mdx', '');
22
+ return {
23
+ slug,
24
+ title: mod.frontmatter.title,
25
+ description: mod.frontmatter.description,
26
+ Component: mod.default,
27
+ };
28
+ });
29
+
30
+ /** Map short URL paths to MDX file slugs */
31
+ const pathToSlug: Record<string, string> = {
32
+ '/privacy': 'privacy-policy',
33
+ '/terms': 'terms-of-service',
34
+ };
35
+
36
+ function getLegalPage(slug: string): LegalPage | undefined {
37
+ return legalPages.find((p) => p.slug === slug);
38
+ }
39
+
40
+ export default function LegalRoute() {
41
+ const { slug: paramSlug } = useParams<{ slug: string }>();
42
+ const { pathname } = useLocation();
43
+
44
+ // Resolve slug from param (/legal/:slug) or from path alias (/privacy, /terms)
45
+ const slug = paramSlug || pathToSlug[pathname];
46
+
47
+ if (!slug) {
48
+ return <Navigate to="/privacy" replace />;
49
+ }
50
+
51
+ const page = getLegalPage(slug);
52
+
53
+ if (!page) {
54
+ return <Navigate to="/" replace />;
55
+ }
56
+
57
+ return (
58
+ <div className="min-h-screen bg-background">
59
+ <Nav />
60
+ <SEO title={page.title} description={page.description} />
61
+
62
+ <div className="mx-auto max-w-3xl px-6 pt-28 pb-24">
63
+ <header className="mb-10">
64
+ <h1 className="mb-3 text-4xl font-black tracking-tight">{page.title}</h1>
65
+ <p className="text-lg text-muted-foreground">{page.description}</p>
66
+ </header>
67
+
68
+ <article className="prose prose-invert max-w-none">
69
+ <page.Component />
70
+ </article>
71
+ </div>
72
+
73
+ <footer className="border-t border-border py-8">
74
+ <div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-4 px-6 sm:flex-row">
75
+ <Wordmark size="sm" />
76
+ <div className="flex items-center gap-6">
77
+ <Link
78
+ to="/"
79
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
80
+ >
81
+ Home
82
+ </Link>
83
+ <Link
84
+ to="/privacy"
85
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
86
+ >
87
+ Privacy
88
+ </Link>
89
+ <Link
90
+ to="/terms"
91
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
92
+ >
93
+ Terms
94
+ </Link>
95
+ </div>
96
+ </div>
97
+ </footer>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,408 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
4
+ import { useAuth } from '../components/auth/AuthProvider';
5
+ import { Wordmark } from '../components/Logo';
6
+ import { Button } from '../components/ui/button';
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
8
+ import { Input } from '../components/ui/input';
9
+ import { useAuthSettings } from '../hooks/api';
10
+
11
+ type OAuthProvider = 'google' | 'microsoft' | 'github' | 'apple' | 'discord';
12
+
13
+ const providerConfig: Record<OAuthProvider, { label: string; icon: React.ReactNode }> = {
14
+ google: {
15
+ label: 'Google',
16
+ icon: (
17
+ <svg className="size-4" viewBox="0 0 24 24" aria-hidden="true">
18
+ <path
19
+ fill="currentColor"
20
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
21
+ />
22
+ <path
23
+ fill="currentColor"
24
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
25
+ />
26
+ <path
27
+ fill="currentColor"
28
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
29
+ />
30
+ <path
31
+ fill="currentColor"
32
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
33
+ />
34
+ </svg>
35
+ ),
36
+ },
37
+ microsoft: {
38
+ label: 'Microsoft',
39
+ icon: (
40
+ <svg className="size-4" viewBox="0 0 23 23" aria-hidden="true">
41
+ <path fill="#f35325" d="M1 1h10v10H1z" />
42
+ <path fill="#81bc06" d="M12 1h10v10H12z" />
43
+ <path fill="#05a6f0" d="M1 12h10v10H1z" />
44
+ <path fill="#ffba08" d="M12 12h10v10H12z" />
45
+ </svg>
46
+ ),
47
+ },
48
+ github: {
49
+ label: 'GitHub',
50
+ icon: (
51
+ <svg className="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
52
+ <path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" />
53
+ </svg>
54
+ ),
55
+ },
56
+ apple: {
57
+ label: 'Apple',
58
+ icon: (
59
+ <svg className="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
60
+ <path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
61
+ </svg>
62
+ ),
63
+ },
64
+ discord: {
65
+ label: 'Discord',
66
+ icon: (
67
+ <svg className="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
68
+ <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
69
+ </svg>
70
+ ),
71
+ },
72
+ };
73
+
74
+ /**
75
+ * Validate a redirect URL is safe (not an open redirect).
76
+ * Allows relative paths and absolute URLs to sibling subdomains
77
+ * (e.g., n8n.localhost when the app is on localhost).
78
+ */
79
+ function getSafeRedirect(redirectParam: string | null): string {
80
+ const fallback = '/dashboard';
81
+ if (!redirectParam) return fallback;
82
+
83
+ // Relative paths: must start with / and not // (protocol-relative)
84
+ if (redirectParam.startsWith('/') && !redirectParam.startsWith('//')) {
85
+ return redirectParam;
86
+ }
87
+
88
+ // Absolute URLs: allow only same base domain (subdomain routing)
89
+ try {
90
+ const url = new URL(redirectParam);
91
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return fallback;
92
+
93
+ const currentHost = window.location.hostname;
94
+
95
+ // Extract base domain: *.localhost → localhost, *.example.com → example.com
96
+ const baseDomain = (host: string) => {
97
+ if (host === 'localhost' || host.endsWith('.localhost')) return 'localhost';
98
+ const parts = host.split('.');
99
+ return parts.length >= 2 ? parts.slice(-2).join('.') : host;
100
+ };
101
+
102
+ if (baseDomain(url.hostname) === baseDomain(currentHost)) {
103
+ return redirectParam; // Trusted subdomain
104
+ }
105
+ } catch {
106
+ // Invalid URL
107
+ }
108
+
109
+ return fallback;
110
+ }
111
+
112
+ /**
113
+ * Navigate to redirect target — uses window.location for absolute URLs
114
+ * (cross-subdomain) and react-router navigate for relative paths.
115
+ */
116
+ function performRedirect(redirect: string, navigate: ReturnType<typeof useNavigate>) {
117
+ if (redirect.startsWith('http://') || redirect.startsWith('https://')) {
118
+ window.location.href = redirect;
119
+ } else {
120
+ navigate(redirect, { replace: true });
121
+ }
122
+ }
123
+
124
+ export default function Login() {
125
+ const { t } = useTranslation();
126
+ const navigate = useNavigate();
127
+ const location = useLocation();
128
+ const [searchParams] = useSearchParams();
129
+ const { user, signIn, signUp } = useAuth();
130
+ const { data: authSettings } = useAuthSettings();
131
+
132
+ // Default to signup mode if on /signup route
133
+ const [isSignUp, setIsSignUp] = useState(location.pathname === '/signup');
134
+ const [email, setEmail] = useState('');
135
+ const [password, setPassword] = useState('');
136
+ const [name, setName] = useState('');
137
+ const [error, setError] = useState('');
138
+ const [loading, setLoading] = useState(false);
139
+ const [magicLinkSent, setMagicLinkSent] = useState(false);
140
+ const [showMagicLink, setShowMagicLink] = useState(false);
141
+
142
+ // Derive enabled providers from settings
143
+ const providers = authSettings?.settings?.providers;
144
+ const enabledProviders: OAuthProvider[] = providers
145
+ ? (Object.entries(providers) as [OAuthProvider, boolean][])
146
+ .filter(([, enabled]) => enabled)
147
+ .map(([provider]) => provider)
148
+ : [];
149
+ const magicLinkEnabled = authSettings?.settings?.magicLinkEnabled ?? true;
150
+
151
+ // Sync isSignUp state with route
152
+ useEffect(() => {
153
+ setIsSignUp(location.pathname === '/signup');
154
+ }, [location.pathname]);
155
+
156
+ // Redirect if already logged in
157
+ useEffect(() => {
158
+ if (user) {
159
+ const safeRedirect = getSafeRedirect(searchParams.get('redirect'));
160
+ performRedirect(safeRedirect, navigate);
161
+ }
162
+ }, [user, navigate, searchParams]);
163
+
164
+ const handleSubmit = async (e: React.FormEvent) => {
165
+ e.preventDefault();
166
+ setError('');
167
+ setLoading(true);
168
+
169
+ try {
170
+ const safeRedirect = getSafeRedirect(searchParams.get('redirect'));
171
+
172
+ if (isSignUp) {
173
+ await signUp(email, password, name);
174
+ performRedirect(safeRedirect, navigate);
175
+ } else {
176
+ const { requiresMfa } = await signIn.email(email, password);
177
+ if (requiresMfa) {
178
+ // Redirect to MFA verification with the intended destination
179
+ navigate(`/mfa-verify?redirect=${encodeURIComponent(safeRedirect)}`);
180
+ } else {
181
+ performRedirect(safeRedirect, navigate);
182
+ }
183
+ }
184
+ } catch (err) {
185
+ setError(err instanceof Error ? err.message : t('auth.authFailed'));
186
+ } finally {
187
+ setLoading(false);
188
+ }
189
+ };
190
+
191
+ const handleMagicLink = async (e: React.FormEvent) => {
192
+ e.preventDefault();
193
+ if (!email) return;
194
+ setError('');
195
+ setLoading(true);
196
+
197
+ try {
198
+ await signIn.magicLink(email);
199
+ setMagicLinkSent(true);
200
+ } catch (err) {
201
+ setError(err instanceof Error ? err.message : t('auth.magicLinkFailed'));
202
+ } finally {
203
+ setLoading(false);
204
+ }
205
+ };
206
+
207
+ const handleOAuth = async (provider: OAuthProvider) => {
208
+ setLoading(true);
209
+ setError('');
210
+ try {
211
+ await signIn[provider]();
212
+ } catch (err) {
213
+ setError(err instanceof Error ? err.message : t('auth.oauthFailed'));
214
+ setLoading(false);
215
+ }
216
+ };
217
+
218
+ return (
219
+ <div className="min-h-screen flex flex-col bg-background">
220
+ {/* Header */}
221
+ <header className="h-20">
222
+ <div className="mx-auto max-w-7xl h-full px-6 flex items-center">
223
+ <Link to="/" className="group">
224
+ <Wordmark size="lg" />
225
+ </Link>
226
+ </div>
227
+ </header>
228
+
229
+ {/* Main content */}
230
+ <div className="flex-1 flex items-center justify-center p-4">
231
+ <Card className="w-full max-w-md shadow-lg shadow-primary/5">
232
+ <CardHeader className="text-center">
233
+ <CardTitle className="text-2xl text-primary">
234
+ {magicLinkSent
235
+ ? t('auth.checkYourEmail')
236
+ : isSignUp
237
+ ? t('auth.createAccount')
238
+ : t('auth.signIn')}
239
+ </CardTitle>
240
+ <CardDescription>
241
+ {magicLinkSent ? t('auth.magicLinkSent', { email }) : t('auth.poweredBy')}
242
+ </CardDescription>
243
+ </CardHeader>
244
+ <CardContent className="space-y-6">
245
+ {magicLinkSent ? (
246
+ <div className="text-center space-y-4">
247
+ <p className="text-sm text-muted-foreground">{t('auth.magicLinkExpiry')}</p>
248
+ <Button
249
+ variant="outline"
250
+ onClick={() => {
251
+ setMagicLinkSent(false);
252
+ setShowMagicLink(false);
253
+ }}
254
+ >
255
+ {t('auth.backToSignIn')}
256
+ </Button>
257
+ </div>
258
+ ) : showMagicLink ? (
259
+ <>
260
+ <form onSubmit={handleMagicLink} className="space-y-4">
261
+ {error && (
262
+ <div className="p-3 text-sm text-destructive bg-destructive/10 rounded-lg">
263
+ {error}
264
+ </div>
265
+ )}
266
+ <Input
267
+ type="email"
268
+ placeholder={t('auth.emailPlaceholder')}
269
+ value={email}
270
+ onChange={(e) => setEmail(e.target.value)}
271
+ required
272
+ autoFocus
273
+ />
274
+ <Button type="submit" disabled={loading} className="w-full">
275
+ {loading ? t('auth.sending') : t('auth.sendMagicLink')}
276
+ </Button>
277
+ </form>
278
+ <button
279
+ type="button"
280
+ onClick={() => setShowMagicLink(false)}
281
+ className="w-full text-center text-sm text-primary hover:underline"
282
+ >
283
+ {t('auth.passwordInstead')}
284
+ </button>
285
+ </>
286
+ ) : (
287
+ <>
288
+ <form onSubmit={handleSubmit} className="space-y-4">
289
+ {error && (
290
+ <div className="p-3 text-sm text-destructive bg-destructive/10 rounded-lg">
291
+ {error}
292
+ </div>
293
+ )}
294
+
295
+ {isSignUp && (
296
+ <Input
297
+ type="text"
298
+ placeholder={t('auth.namePlaceholder')}
299
+ value={name}
300
+ onChange={(e) => setName(e.target.value)}
301
+ />
302
+ )}
303
+
304
+ <Input
305
+ type="email"
306
+ placeholder={t('auth.emailPlaceholder')}
307
+ value={email}
308
+ onChange={(e) => setEmail(e.target.value)}
309
+ required
310
+ autoFocus
311
+ />
312
+
313
+ <Input
314
+ type="password"
315
+ placeholder={t('auth.passwordPlaceholder')}
316
+ value={password}
317
+ onChange={(e) => setPassword(e.target.value)}
318
+ required
319
+ minLength={8}
320
+ />
321
+
322
+ <div className="flex justify-between items-center">
323
+ {magicLinkEnabled && !isSignUp && (
324
+ <button
325
+ type="button"
326
+ onClick={() => setShowMagicLink(true)}
327
+ className="text-sm text-primary hover:underline cursor-pointer"
328
+ >
329
+ {t('auth.signInWithMagicLink')}
330
+ </button>
331
+ )}
332
+ {!isSignUp && (
333
+ <Link
334
+ to="/forgot-password"
335
+ className="text-sm text-primary hover:underline ml-auto"
336
+ >
337
+ {t('auth.forgotPassword')}
338
+ </Link>
339
+ )}
340
+ </div>
341
+
342
+ <Button type="submit" disabled={loading} className="w-full">
343
+ {loading
344
+ ? t('common.loading')
345
+ : isSignUp
346
+ ? t('auth.signUp')
347
+ : t('auth.signInAction')}
348
+ </Button>
349
+ </form>
350
+
351
+ {enabledProviders.length > 0 && (
352
+ <>
353
+ <div className="relative">
354
+ <div className="absolute inset-0 flex items-center">
355
+ <div className="w-full border-t border-border/50" />
356
+ </div>
357
+ <div className="relative flex justify-center text-sm">
358
+ <span className="px-2 bg-transparent text-muted-foreground">
359
+ {t('auth.orContinueWith')}
360
+ </span>
361
+ </div>
362
+ </div>
363
+
364
+ <div
365
+ className={`grid gap-4 ${
366
+ enabledProviders.length === 1
367
+ ? 'grid-cols-1'
368
+ : enabledProviders.length === 2
369
+ ? 'grid-cols-2'
370
+ : enabledProviders.length === 3
371
+ ? 'grid-cols-3'
372
+ : 'grid-cols-2'
373
+ }`}
374
+ >
375
+ {enabledProviders.map((provider) => (
376
+ <Button
377
+ key={provider}
378
+ type="button"
379
+ variant="outline"
380
+ onClick={() => handleOAuth(provider)}
381
+ disabled={loading}
382
+ >
383
+ {providerConfig[provider].icon}
384
+ {providerConfig[provider].label}
385
+ </Button>
386
+ ))}
387
+ </div>
388
+ </>
389
+ )}
390
+
391
+ <p className="text-center text-sm text-muted-foreground">
392
+ {isSignUp ? t('auth.alreadyHaveAccount') : t('auth.dontHaveAccount')}{' '}
393
+ <button
394
+ type="button"
395
+ onClick={() => navigate(isSignUp ? '/login' : '/signup')}
396
+ className="text-primary hover:underline font-medium cursor-pointer"
397
+ >
398
+ {isSignUp ? t('auth.signIn') : t('nav.signUp')}
399
+ </button>
400
+ </p>
401
+ </>
402
+ )}
403
+ </CardContent>
404
+ </Card>
405
+ </div>
406
+ </div>
407
+ );
408
+ }
@@ -0,0 +1,156 @@
1
+ import { useState } from 'react';
2
+ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
3
+ import { useAuth } from '../components/auth/AuthProvider';
4
+ import { Wordmark } from '../components/Logo';
5
+ import { Button } from '../components/ui/button';
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
7
+ import { InputOTP, InputOTPGroup, InputOTPSlot } from '../components/ui/input-otp';
8
+
9
+ export default function MFAVerify() {
10
+ const navigate = useNavigate();
11
+ const [searchParams] = useSearchParams();
12
+ const { pendingMfaChallenge, mfa, clearMfaChallenge, signOut } = useAuth();
13
+
14
+ const [code, setCode] = useState('');
15
+ const [error, setError] = useState('');
16
+ const [loading, setLoading] = useState(false);
17
+
18
+ const handleVerify = async () => {
19
+ if (code.length !== 6) {
20
+ setError('Please enter a 6-digit code');
21
+ return;
22
+ }
23
+
24
+ if (!pendingMfaChallenge) {
25
+ setError('No pending MFA challenge. Please sign in again.');
26
+ return;
27
+ }
28
+
29
+ setError('');
30
+ setLoading(true);
31
+
32
+ try {
33
+ await mfa.verify(pendingMfaChallenge.factorId, pendingMfaChallenge.challengeId, code);
34
+
35
+ // Get redirect URL
36
+ const redirectParam = searchParams.get('redirect') || '/dashboard';
37
+ const safeRedirect =
38
+ redirectParam.startsWith('/') &&
39
+ !redirectParam.startsWith('//') &&
40
+ !redirectParam.includes(':')
41
+ ? redirectParam
42
+ : '/dashboard';
43
+
44
+ navigate(safeRedirect);
45
+ } catch (err) {
46
+ setError(err instanceof Error ? err.message : 'Invalid code. Please try again.');
47
+ setCode('');
48
+ } finally {
49
+ setLoading(false);
50
+ }
51
+ };
52
+
53
+ const handleCancel = async () => {
54
+ clearMfaChallenge();
55
+ await signOut();
56
+ navigate('/login');
57
+ };
58
+
59
+ // If no pending challenge, show message
60
+ if (!pendingMfaChallenge) {
61
+ return (
62
+ <div className="min-h-screen flex flex-col bg-background">
63
+ <header className="h-20">
64
+ <div className="mx-auto max-w-7xl h-full px-6 flex items-center">
65
+ <Link to="/" className="group">
66
+ <Wordmark size="lg" />
67
+ </Link>
68
+ </div>
69
+ </header>
70
+
71
+ <div className="flex-1 flex items-center justify-center p-4">
72
+ <Card className="w-full max-w-md shadow-lg shadow-primary/5">
73
+ <CardHeader className="text-center">
74
+ <CardTitle className="text-2xl text-primary">Session expired</CardTitle>
75
+ <CardDescription>
76
+ Your authentication session has expired. Please sign in again.
77
+ </CardDescription>
78
+ </CardHeader>
79
+ <CardContent>
80
+ <Link
81
+ to="/login"
82
+ className="inline-flex items-center justify-center w-full h-9 px-3 rounded-4xl bg-primary text-primary-foreground hover:bg-primary/80 text-sm font-medium transition-all"
83
+ >
84
+ Sign in
85
+ </Link>
86
+ </CardContent>
87
+ </Card>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <div className="min-h-screen flex flex-col bg-background">
95
+ {/* Header */}
96
+ <header className="h-20">
97
+ <div className="mx-auto max-w-7xl h-full px-6 flex items-center">
98
+ <Link to="/" className="group">
99
+ <Wordmark size="lg" />
100
+ </Link>
101
+ </div>
102
+ </header>
103
+
104
+ {/* Main content */}
105
+ <div className="flex-1 flex items-center justify-center p-4">
106
+ <Card className="w-full max-w-md shadow-lg shadow-primary/5">
107
+ <CardHeader className="text-center">
108
+ <CardTitle className="text-2xl text-primary">Two-factor authentication</CardTitle>
109
+ <CardDescription>Enter the 6-digit code from your authenticator app</CardDescription>
110
+ </CardHeader>
111
+ <CardContent className="space-y-6">
112
+ {error && (
113
+ <div className="p-3 text-sm text-destructive bg-destructive/10 rounded-lg">
114
+ {error}
115
+ </div>
116
+ )}
117
+
118
+ <div className="flex justify-center">
119
+ <InputOTP
120
+ maxLength={6}
121
+ value={code}
122
+ onChange={setCode}
123
+ onComplete={handleVerify}
124
+ disabled={loading}
125
+ >
126
+ <InputOTPGroup>
127
+ <InputOTPSlot index={0} />
128
+ <InputOTPSlot index={1} />
129
+ <InputOTPSlot index={2} />
130
+ <InputOTPSlot index={3} />
131
+ <InputOTPSlot index={4} />
132
+ <InputOTPSlot index={5} />
133
+ </InputOTPGroup>
134
+ </InputOTP>
135
+ </div>
136
+
137
+ <div className="space-y-2">
138
+ <Button onClick={handleVerify} disabled={loading} className="w-full">
139
+ {loading ? 'Verifying...' : 'Verify'}
140
+ </Button>
141
+
142
+ <Button variant="ghost" onClick={handleCancel} disabled={loading} className="w-full">
143
+ Cancel and sign out
144
+ </Button>
145
+ </div>
146
+
147
+ <p className="text-center text-sm text-muted-foreground">
148
+ Open your authenticator app (Google Authenticator, Authy, etc.) and enter the code
149
+ shown for this account.
150
+ </p>
151
+ </CardContent>
152
+ </Card>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
@@ -0,0 +1,21 @@
1
+ import { ArrowLeft } from 'lucide-react';
2
+ import { Link } from 'react-router-dom';
3
+
4
+ export default function NotFound() {
5
+ return (
6
+ <div className="flex min-h-screen flex-col items-center justify-center bg-background px-4 text-center">
7
+ <p className="text-7xl font-bold text-muted-foreground/30">404</p>
8
+ <h1 className="mt-4 text-2xl font-bold text-foreground">Page not found</h1>
9
+ <p className="mt-2 text-muted-foreground">
10
+ The page you're looking for doesn't exist or has been moved.
11
+ </p>
12
+ <Link
13
+ to="/"
14
+ className="mt-6 inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
15
+ >
16
+ <ArrowLeft className="size-4" />
17
+ Back to Home
18
+ </Link>
19
+ </div>
20
+ );
21
+ }