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.
- package/LICENSE +663 -0
- package/README.md +188 -0
- package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
- package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
- package/carbon/.claude/settings.json +29 -0
- package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
- package/carbon/.dockerignore +57 -0
- package/carbon/.env.example +219 -0
- package/carbon/.github/copilot-instructions.md +3 -0
- package/carbon/.github/workflows/deploy.yml +346 -0
- package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
- package/carbon/.windsurfrules +74 -0
- package/carbon/AGENTS.md +422 -0
- package/carbon/CLAUDE.md +3 -0
- package/carbon/DEVELOPMENT.md +187 -0
- package/carbon/Dockerfile +98 -0
- package/carbon/LICENSE +21 -0
- package/carbon/PRODUCTION.md +364 -0
- package/carbon/README.md +193 -0
- package/carbon/TESTING.md +138 -0
- package/carbon/backup/Dockerfile +75 -0
- package/carbon/backup/backup.sh +95 -0
- package/carbon/backup/compose-backup.sh +140 -0
- package/carbon/biome.json +83 -0
- package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
- package/carbon/cloud-init/k3s/master-init.sh +215 -0
- package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
- package/carbon/cloud-init/k3s/worker-init.sh +147 -0
- package/carbon/components.json +24 -0
- package/carbon/content/blog/authentication-guide.mdx +34 -0
- package/carbon/content/blog/getting-started.mdx +41 -0
- package/carbon/content/changelog/v0-1-0.mdx +40 -0
- package/carbon/content/docs/analytics.mdx +90 -0
- package/carbon/content/docs/authentication.mdx +231 -0
- package/carbon/content/docs/background-jobs.mdx +116 -0
- package/carbon/content/docs/cli.mdx +630 -0
- package/carbon/content/docs/database.mdx +236 -0
- package/carbon/content/docs/deployment.mdx +227 -0
- package/carbon/content/docs/development.mdx +238 -0
- package/carbon/content/docs/environments.mdx +84 -0
- package/carbon/content/docs/getting-started.mdx +112 -0
- package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
- package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
- package/carbon/content/docs/optional-services.mdx +160 -0
- package/carbon/db/Dockerfile +23 -0
- package/carbon/docker-compose.dev-init.yml +5 -0
- package/carbon/docker-compose.metabase.override.yml +14 -0
- package/carbon/docker-compose.metabase.prod.yml +28 -0
- package/carbon/docker-compose.metabase.yml +84 -0
- package/carbon/docker-compose.n8n.override.yml +14 -0
- package/carbon/docker-compose.n8n.prod.yml +31 -0
- package/carbon/docker-compose.n8n.yml +97 -0
- package/carbon/docker-compose.observability.override.yml +18 -0
- package/carbon/docker-compose.observability.prod.yml +28 -0
- package/carbon/docker-compose.observability.yml +125 -0
- package/carbon/docker-compose.override.yml +28 -0
- package/carbon/docker-compose.prod.yml +294 -0
- package/carbon/docker-compose.yml +508 -0
- package/carbon/docker-entrypoint.sh +12 -0
- package/carbon/ha/activate-standby.sh +52 -0
- package/carbon/ha/primary-init.sql +36 -0
- package/carbon/ha/standby-init.sh +74 -0
- package/carbon/k8s/LICENSE +99 -0
- package/carbon/k8s/README.md +272 -0
- package/carbon/k8s/base/app/deployment.yaml +130 -0
- package/carbon/k8s/base/app/hpa.yaml +44 -0
- package/carbon/k8s/base/app/kustomization.yaml +10 -0
- package/carbon/k8s/base/app/network-policy.yaml +124 -0
- package/carbon/k8s/base/app/pdb.yaml +14 -0
- package/carbon/k8s/base/app/rbac.yaml +36 -0
- package/carbon/k8s/base/app/service.yaml +16 -0
- package/carbon/k8s/base/backup/cronjob.yaml +120 -0
- package/carbon/k8s/base/backup/kustomization.yaml +7 -0
- package/carbon/k8s/base/backup/network-policy.yaml +31 -0
- package/carbon/k8s/base/backup/rbac.yaml +7 -0
- package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
- package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
- package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
- package/carbon/k8s/base/config/configmap.yaml +48 -0
- package/carbon/k8s/base/config/kustomization.yaml +8 -0
- package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
- package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
- package/carbon/k8s/base/kustomization.yaml +43 -0
- package/carbon/k8s/base/namespace.yaml +7 -0
- package/carbon/k8s/base/network-policies.yaml +95 -0
- package/carbon/k8s/base/registry/kustomization.yaml +5 -0
- package/carbon/k8s/base/registry/local-registry.yaml +193 -0
- package/carbon/k8s/base/traefik/certificate.yaml +21 -0
- package/carbon/k8s/base/traefik/configmap.yaml +13 -0
- package/carbon/k8s/base/traefik/deployment.yaml +165 -0
- package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
- package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
- package/carbon/k8s/base/traefik/middleware.yaml +109 -0
- package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
- package/carbon/k8s/base/traefik/service.yaml +38 -0
- package/carbon/k8s/flux/README.md +49 -0
- package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
- package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
- package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
- package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
- package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
- package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
- package/carbon/k8s/infra/kustomization.yaml +21 -0
- package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
- package/carbon/k8s/overlays/local/configmap.yaml +23 -0
- package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
- package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
- package/carbon/k8s/overlays/local/namespace.yaml +8 -0
- package/carbon/k8s/overlays/local/secrets.yaml +32 -0
- package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
- package/carbon/k8s/test-local.sh +318 -0
- package/carbon/k8s/values/supabase.values.yaml +133 -0
- package/carbon/package.json +154 -0
- package/carbon/runtime.Dockerfile +17 -0
- package/carbon/scripts/_dev-jwt.mjs +45 -0
- package/carbon/scripts/check-rls.ts +53 -0
- package/carbon/scripts/dev-init.js +191 -0
- package/carbon/scripts/dev.js +191 -0
- package/carbon/scripts/docker-down.js +63 -0
- package/carbon/scripts/docker-logs.js +59 -0
- package/carbon/scripts/docker-up.js +222 -0
- package/carbon/scripts/generate-dev-configs.sh +131 -0
- package/carbon/scripts/generate-rss.ts +116 -0
- package/carbon/scripts/generate-sitemap.ts +102 -0
- package/carbon/scripts/k8s-apply.js +71 -0
- package/carbon/scripts/k8s-delete.js +75 -0
- package/carbon/scripts/lib/manifest.js +176 -0
- package/carbon/scripts/secret-scan.mjs +278 -0
- package/carbon/scripts/validate-dev-configs.sh +101 -0
- package/carbon/src/client/App.tsx +202 -0
- package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
- package/carbon/src/client/assets/hyperformant-light.svg +29 -0
- package/carbon/src/client/assets/logos/aider.svg +1 -0
- package/carbon/src/client/assets/logos/antigravity.svg +1 -0
- package/carbon/src/client/assets/logos/bolt.svg +1 -0
- package/carbon/src/client/assets/logos/claude-code.svg +1 -0
- package/carbon/src/client/assets/logos/copilot.svg +1 -0
- package/carbon/src/client/assets/logos/cursor.svg +1 -0
- package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
- package/carbon/src/client/assets/logos/lovable.svg +1 -0
- package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
- package/carbon/src/client/assets/logos/v0.svg +1 -0
- package/carbon/src/client/assets/logos/windsurf.svg +1 -0
- package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
- package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
- package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
- package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
- package/carbon/src/client/components/AppSidebar.tsx +760 -0
- package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
- package/carbon/src/client/components/CTAFooter.tsx +59 -0
- package/carbon/src/client/components/ComparisonSection.tsx +132 -0
- package/carbon/src/client/components/ContentPanel.tsx +66 -0
- package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
- package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
- package/carbon/src/client/components/FAQSection.tsx +76 -0
- package/carbon/src/client/components/FileUpload.tsx +210 -0
- package/carbon/src/client/components/HeaderActions.tsx +17 -0
- package/carbon/src/client/components/Hero.tsx +608 -0
- package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
- package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
- package/carbon/src/client/components/Logo.tsx +87 -0
- package/carbon/src/client/components/LogoStrip.tsx +76 -0
- package/carbon/src/client/components/MetricsStrip.tsx +48 -0
- package/carbon/src/client/components/Nav.tsx +195 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
- package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
- package/carbon/src/client/components/PageHeader.tsx +24 -0
- package/carbon/src/client/components/PlanGate.tsx +36 -0
- package/carbon/src/client/components/PricingSection.tsx +371 -0
- package/carbon/src/client/components/SEO.tsx +34 -0
- package/carbon/src/client/components/SmoothScroll.tsx +45 -0
- package/carbon/src/client/components/TechStackSection.tsx +165 -0
- package/carbon/src/client/components/WorkflowSection.tsx +604 -0
- package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
- package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
- package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
- package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
- package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
- package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
- package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
- package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
- package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
- package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
- package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
- package/carbon/src/client/components/effects/index.ts +2 -0
- package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
- package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
- package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
- package/carbon/src/client/components/scroll/index.ts +2 -0
- package/carbon/src/client/components/ui/accordion.tsx +71 -0
- package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
- package/carbon/src/client/components/ui/alert.tsx +73 -0
- package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
- package/carbon/src/client/components/ui/avatar.tsx +91 -0
- package/carbon/src/client/components/ui/badge.tsx +50 -0
- package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
- package/carbon/src/client/components/ui/button-group.tsx +78 -0
- package/carbon/src/client/components/ui/button.tsx +120 -0
- package/carbon/src/client/components/ui/calendar.tsx +182 -0
- package/carbon/src/client/components/ui/card.tsx +85 -0
- package/carbon/src/client/components/ui/carousel.tsx +227 -0
- package/carbon/src/client/components/ui/chart.tsx +357 -0
- package/carbon/src/client/components/ui/checkbox.tsx +27 -0
- package/carbon/src/client/components/ui/collapsible.tsx +15 -0
- package/carbon/src/client/components/ui/command.tsx +178 -0
- package/carbon/src/client/components/ui/context-menu.tsx +233 -0
- package/carbon/src/client/components/ui/dialog.tsx +132 -0
- package/carbon/src/client/components/ui/drawer.tsx +118 -0
- package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
- package/carbon/src/client/components/ui/empty.tsx +94 -0
- package/carbon/src/client/components/ui/field.tsx +226 -0
- package/carbon/src/client/components/ui/hover-card.tsx +44 -0
- package/carbon/src/client/components/ui/input-group.tsx +146 -0
- package/carbon/src/client/components/ui/input-otp.tsx +83 -0
- package/carbon/src/client/components/ui/input.tsx +20 -0
- package/carbon/src/client/components/ui/item.tsx +188 -0
- package/carbon/src/client/components/ui/kbd.tsx +26 -0
- package/carbon/src/client/components/ui/label.tsx +21 -0
- package/carbon/src/client/components/ui/menubar.tsx +250 -0
- package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
- package/carbon/src/client/components/ui/pagination.tsx +102 -0
- package/carbon/src/client/components/ui/popover.tsx +75 -0
- package/carbon/src/client/components/ui/progress.tsx +66 -0
- package/carbon/src/client/components/ui/radio-group.tsx +36 -0
- package/carbon/src/client/components/ui/resizable.tsx +46 -0
- package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
- package/carbon/src/client/components/ui/select.tsx +186 -0
- package/carbon/src/client/components/ui/separator.tsx +21 -0
- package/carbon/src/client/components/ui/sheet.tsx +124 -0
- package/carbon/src/client/components/ui/sidebar.tsx +702 -0
- package/carbon/src/client/components/ui/skeleton.tsx +13 -0
- package/carbon/src/client/components/ui/slider.tsx +57 -0
- package/carbon/src/client/components/ui/sonner.tsx +45 -0
- package/carbon/src/client/components/ui/spinner.tsx +15 -0
- package/carbon/src/client/components/ui/switch.tsx +30 -0
- package/carbon/src/client/components/ui/table.tsx +89 -0
- package/carbon/src/client/components/ui/tabs.tsx +73 -0
- package/carbon/src/client/components/ui/textarea.tsx +18 -0
- package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
- package/carbon/src/client/components/ui/toggle.tsx +44 -0
- package/carbon/src/client/components/ui/tooltip.tsx +56 -0
- package/carbon/src/client/hooks/api/index.ts +14 -0
- package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
- package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
- package/carbon/src/client/hooks/use-mobile.ts +21 -0
- package/carbon/src/client/hooks/useMousePosition.ts +91 -0
- package/carbon/src/client/hooks/useNotifications.tsx +124 -0
- package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
- package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
- package/carbon/src/client/hooks/usePackageManager.ts +69 -0
- package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
- package/carbon/src/client/hooks/useRunningServices.ts +114 -0
- package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
- package/carbon/src/client/index.css +467 -0
- package/carbon/src/client/index.html +56 -0
- package/carbon/src/client/lib/admin-services.ts +151 -0
- package/carbon/src/client/lib/api.ts +32 -0
- package/carbon/src/client/lib/blog.ts +35 -0
- package/carbon/src/client/lib/changelog.ts +33 -0
- package/carbon/src/client/lib/docs-search.ts +101 -0
- package/carbon/src/client/lib/docs.ts +37 -0
- package/carbon/src/client/lib/i18n.ts +32 -0
- package/carbon/src/client/lib/supabase.ts +72 -0
- package/carbon/src/client/lib/tailwind-colors.ts +357 -0
- package/carbon/src/client/lib/theme.ts +117 -0
- package/carbon/src/client/lib/utils.ts +22 -0
- package/carbon/src/client/locales/de.json +529 -0
- package/carbon/src/client/locales/en.json +461 -0
- package/carbon/src/client/locales/es.json +529 -0
- package/carbon/src/client/locales/fr.json +529 -0
- package/carbon/src/client/locales/pt.json +529 -0
- package/carbon/src/client/main.tsx +56 -0
- package/carbon/src/client/mdx.d.ts +13 -0
- package/carbon/src/client/pages/ApiDocs.tsx +76 -0
- package/carbon/src/client/pages/AuthCallback.tsx +34 -0
- package/carbon/src/client/pages/Blog.tsx +167 -0
- package/carbon/src/client/pages/Changelog.tsx +171 -0
- package/carbon/src/client/pages/Charts.tsx +388 -0
- package/carbon/src/client/pages/Checkout.tsx +227 -0
- package/carbon/src/client/pages/Contact.tsx +174 -0
- package/carbon/src/client/pages/Dashboard.tsx +368 -0
- package/carbon/src/client/pages/Docs.tsx +372 -0
- package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
- package/carbon/src/client/pages/Home.tsx +187 -0
- package/carbon/src/client/pages/Legal.tsx +100 -0
- package/carbon/src/client/pages/Login.tsx +408 -0
- package/carbon/src/client/pages/MFAVerify.tsx +156 -0
- package/carbon/src/client/pages/NotFound.tsx +21 -0
- package/carbon/src/client/pages/Onboarding.tsx +246 -0
- package/carbon/src/client/pages/ResetPassword.tsx +200 -0
- package/carbon/src/client/pages/UIComponents.tsx +390 -0
- package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
- package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
- package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
- package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
- package/carbon/src/client/pages/admin/Logs.tsx +18 -0
- package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
- package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
- package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
- package/carbon/src/client/pages/admin/Settings.tsx +314 -0
- package/carbon/src/client/pages/admin/Theme.tsx +465 -0
- package/carbon/src/client/pages/admin/Users.tsx +365 -0
- package/carbon/src/client/pages/api-docs.css +156 -0
- package/carbon/src/client/pages/organizations/Details.tsx +200 -0
- package/carbon/src/client/pages/organizations/Members.tsx +402 -0
- package/carbon/src/client/pages/settings/Billing.tsx +473 -0
- package/carbon/src/client/pages/settings/Profile.tsx +160 -0
- package/carbon/src/client/pages/settings/Security.tsx +341 -0
- package/carbon/src/client/public/favicon.svg +19 -0
- package/carbon/src/client/public/robots.txt +8 -0
- package/carbon/src/server/billing/index.ts +104 -0
- package/carbon/src/server/billing/provider.ts +81 -0
- package/carbon/src/server/billing/providers/paddle.ts +314 -0
- package/carbon/src/server/billing/providers/polar.ts +325 -0
- package/carbon/src/server/billing/providers/stripe.ts +233 -0
- package/carbon/src/server/emails/templates.ts +116 -0
- package/carbon/src/server/index.ts +554 -0
- package/carbon/src/server/lib/auth.ts +6 -0
- package/carbon/src/server/lib/email.ts +64 -0
- package/carbon/src/server/lib/env.ts +112 -0
- package/carbon/src/server/lib/errors.ts +21 -0
- package/carbon/src/server/lib/logger.ts +17 -0
- package/carbon/src/server/lib/rate-limiter.ts +288 -0
- package/carbon/src/server/lib/request.ts +34 -0
- package/carbon/src/server/lib/stripe.ts +42 -0
- package/carbon/src/server/lib/supabase.ts +65 -0
- package/carbon/src/server/middleware/requirePlan.ts +80 -0
- package/carbon/src/server/routes/_internal/services-status.ts +958 -0
- package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
- package/carbon/src/server/routes/health.ts +48 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
- package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
- package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
- package/carbon/src/server/routes/v1/auth.ts +390 -0
- package/carbon/src/server/routes/v1/billing.ts +718 -0
- package/carbon/src/server/routes/v1/contact.ts +93 -0
- package/carbon/src/server/routes/v1/index.ts +1333 -0
- package/carbon/src/server/routes/v1/newsletter.ts +181 -0
- package/carbon/src/server/routes/v1/performance.ts +157 -0
- package/carbon/src/server/routes/v1/stats.ts +170 -0
- package/carbon/src/server/routes/v1/theme.ts +106 -0
- package/carbon/src/server/routes/webhooks/billing.ts +376 -0
- package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
- package/carbon/src/server/types.ts +11 -0
- package/carbon/src/shared/pricing.ts +155 -0
- package/carbon/src/shared/types.ts +338 -0
- package/carbon/supabase/migrations/00001_init.sql +717 -0
- package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
- package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
- package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
- package/carbon/supabase/seed.sql +16 -0
- package/carbon/tests/_helpers/app.ts +45 -0
- package/carbon/tests/_helpers/env.ts +37 -0
- package/carbon/tests/_helpers/factories.ts +69 -0
- package/carbon/tests/_helpers/jwt.ts +23 -0
- package/carbon/tests/_helpers/setup-integration.ts +20 -0
- package/carbon/tests/_helpers/setup-rtl.ts +12 -0
- package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
- package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
- package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
- package/carbon/tests/integration/server/routes/health.test.ts +61 -0
- package/carbon/tests/structural/i18n-parity.test.ts +42 -0
- package/carbon/tests/unit/client/utils.test.ts +49 -0
- package/carbon/tests/unit/shared/pricing.test.ts +93 -0
- package/carbon/tsconfig.json +27 -0
- package/carbon/tsconfig.server.json +27 -0
- package/carbon/tsconfig.test.json +9 -0
- package/carbon/vite.config.ts +110 -0
- package/carbon/vitest.config.ts +74 -0
- package/carbon/volumes/db/jwt.sql +57 -0
- package/carbon/volumes/db/metabase-init.sh +29 -0
- package/carbon/volumes/db/n8n-init.sh +25 -0
- package/carbon/volumes/db/realtime.sql +33 -0
- package/carbon/volumes/db/roles.sql +93 -0
- package/carbon/volumes/db/set-passwords.sh +12 -0
- package/carbon/volumes/db/super-admin.dev.sql +113 -0
- package/carbon/volumes/db/super-admin.generated.sql +113 -0
- package/carbon/volumes/db/super-admin.sql +114 -0
- package/carbon/volumes/grafana/dashboards/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards/overview.json +523 -0
- package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
- package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
- package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
- package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
- package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
- package/carbon/volumes/kong/kong.yml +208 -0
- package/carbon/volumes/loki/loki-config.yml +58 -0
- package/carbon/volumes/n8n/hooks.js +131 -0
- package/carbon/volumes/n8n/scripts/setup.sh +66 -0
- package/carbon/volumes/prometheus/prometheus.yml +43 -0
- package/carbon/volumes/promtail/promtail-config.yml +64 -0
- package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
- package/carbon/volumes/traefik/middlewares.yml +95 -0
- package/carbon/volumes/traefik/vite-dev.yml +20 -0
- package/package.json +95 -0
- package/src/access.js +354 -0
- package/src/activate.js +187 -0
- package/src/add.js +718 -0
- package/src/backup.js +786 -0
- package/src/cli.js +350 -0
- package/src/configure.js +967 -0
- package/src/console.js +155 -0
- package/src/create.js +1499 -0
- package/src/deploy.js +311 -0
- package/src/destroy.js +2033 -0
- package/src/diagnose.js +735 -0
- package/src/down.js +80 -0
- package/src/failover.js +1032 -0
- package/src/lib/backup-s3.js +179 -0
- package/src/lib/build.js +33 -0
- package/src/lib/checksum.js +28 -0
- package/src/lib/ci-setup.js +666 -0
- package/src/lib/cli/help.js +129 -0
- package/src/lib/cli/parse-flags.js +160 -0
- package/src/lib/cli/select-action.js +65 -0
- package/src/lib/cli/select-environment.js +108 -0
- package/src/lib/cli/tty-guard.js +75 -0
- package/src/lib/cloudflare.js +447 -0
- package/src/lib/colors.js +52 -0
- package/src/lib/command.js +361 -0
- package/src/lib/config.js +359 -0
- package/src/lib/cost.js +103 -0
- package/src/lib/deploy/bundle.js +231 -0
- package/src/lib/deploy/compose/acme-verify.js +121 -0
- package/src/lib/deploy/compose/build-args.js +78 -0
- package/src/lib/deploy/compose/ha.js +1874 -0
- package/src/lib/deploy/compose/index.js +1294 -0
- package/src/lib/deploy/compose/reconcile.js +74 -0
- package/src/lib/deploy/github.js +382 -0
- package/src/lib/deploy/image.js +191 -0
- package/src/lib/deploy/index.js +119 -0
- package/src/lib/deploy/k8s/LICENSE +99 -0
- package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
- package/src/lib/deploy/k8s/ha/index.js +1000 -0
- package/src/lib/deploy/k8s/index.js +62 -0
- package/src/lib/deploy/k8s/k3s.js +2515 -0
- package/src/lib/deploy/orchestrator.js +1401 -0
- package/src/lib/deploy/prompts.js +545 -0
- package/src/lib/deploy/remote-build.js +130 -0
- package/src/lib/deploy/state.js +120 -0
- package/src/lib/deploy/utils.js +328 -0
- package/src/lib/deploy-logger.js +93 -0
- package/src/lib/dns-propagation.js +48 -0
- package/src/lib/environment.js +58 -0
- package/src/lib/fetch-retry.js +103 -0
- package/src/lib/github-environments.js +335 -0
- package/src/lib/hetzner-dns.js +377 -0
- package/src/lib/hetzner-guided-setup.js +275 -0
- package/src/lib/host-keys.js +37 -0
- package/src/lib/iac/cloud-init.js +52 -0
- package/src/lib/iac/index.js +325 -0
- package/src/lib/iac/programs/hetzner-compose.js +130 -0
- package/src/lib/iac/programs/hetzner-k8s.js +320 -0
- package/src/lib/kubectl.js +81 -0
- package/src/lib/licensing/index.js +259 -0
- package/src/lib/licensing/tiers.js +181 -0
- package/src/lib/licensing/validator.js +171 -0
- package/src/lib/merge-package-json.js +90 -0
- package/src/lib/operator-ip.js +381 -0
- package/src/lib/package-manager.js +111 -0
- package/src/lib/perf.js +71 -0
- package/src/lib/project-guard.js +39 -0
- package/src/lib/project.js +334 -0
- package/src/lib/providers/base.js +276 -0
- package/src/lib/providers/hetzner-s3.js +656 -0
- package/src/lib/providers/hetzner.js +755 -0
- package/src/lib/providers/index.js +164 -0
- package/src/lib/s3.js +510 -0
- package/src/lib/secret-scan.js +583 -0
- package/src/lib/secrets.js +63 -0
- package/src/lib/server-types.js +195 -0
- package/src/lib/shell.js +91 -0
- package/src/lib/ssh.js +241 -0
- package/src/lib/tracker.js +242 -0
- package/src/lib/upgrade-policy.js +170 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/version.js +5 -0
- package/src/remove.js +292 -0
- package/src/reset.js +97 -0
- package/src/restore.js +871 -0
- package/src/scale.js +1734 -0
- package/src/shell.js +222 -0
- package/src/status.js +981 -0
- package/src/up.js +264 -0
- package/src/upgrade.js +721 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type { Factor } from '@supabase/supabase-js';
|
|
2
|
+
import { ShieldCheck, ShieldOff, Smartphone } from 'lucide-react';
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { useSearchParams } from 'react-router-dom';
|
|
6
|
+
import { useAuth } from '@/components/auth/AuthProvider';
|
|
7
|
+
import { PageHeader } from '@/components/PageHeader';
|
|
8
|
+
import { Button } from '@/components/ui/button';
|
|
9
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
} from '@/components/ui/dialog';
|
|
18
|
+
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
|
|
19
|
+
import { Switch } from '@/components/ui/switch';
|
|
20
|
+
import { ContentPanel } from '../../components/ContentPanel';
|
|
21
|
+
|
|
22
|
+
export default function Security() {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const { mfa, hasMfaEnabled, mfaRequired } = useAuth();
|
|
25
|
+
const [searchParams] = useSearchParams();
|
|
26
|
+
|
|
27
|
+
const [factors, setFactors] = useState<Factor[]>([]);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
const [error, setError] = useState('');
|
|
30
|
+
|
|
31
|
+
// Enrollment state
|
|
32
|
+
const [enrolling, setEnrolling] = useState(false);
|
|
33
|
+
const [enrollmentData, setEnrollmentData] = useState<{
|
|
34
|
+
id: string;
|
|
35
|
+
qrCode: string;
|
|
36
|
+
secret: string;
|
|
37
|
+
} | null>(null);
|
|
38
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
39
|
+
const [verifying, setVerifying] = useState(false);
|
|
40
|
+
|
|
41
|
+
// Unenroll state
|
|
42
|
+
const [unenrolling, setUnenrolling] = useState(false);
|
|
43
|
+
const [factorToUnenroll, setFactorToUnenroll] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
// Show setup prompt if redirected from login with MFA required
|
|
46
|
+
const setupRequired = searchParams.get('setup') === 'required';
|
|
47
|
+
|
|
48
|
+
const loadFactors = useCallback(async () => {
|
|
49
|
+
try {
|
|
50
|
+
const factorList = await mfa.listFactors();
|
|
51
|
+
setFactors(factorList.filter((f) => f.status === 'verified'));
|
|
52
|
+
} catch {
|
|
53
|
+
setError('Failed to load MFA factors');
|
|
54
|
+
} finally {
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
}, [mfa]);
|
|
58
|
+
|
|
59
|
+
// Load factors on mount
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
loadFactors();
|
|
62
|
+
}, [loadFactors]);
|
|
63
|
+
|
|
64
|
+
const handleStartEnrollment = async () => {
|
|
65
|
+
setEnrolling(true);
|
|
66
|
+
setError('');
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const data = await mfa.enroll('Authenticator App');
|
|
70
|
+
if (data && data.type === 'totp' && 'totp' in data) {
|
|
71
|
+
setEnrollmentData({
|
|
72
|
+
id: data.id,
|
|
73
|
+
qrCode: data.totp.qr_code,
|
|
74
|
+
secret: data.totp.secret,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setError(err instanceof Error ? err.message : 'Failed to start enrollment');
|
|
79
|
+
setEnrolling(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleVerifyEnrollment = async () => {
|
|
84
|
+
if (!enrollmentData || verificationCode.length !== 6) return;
|
|
85
|
+
|
|
86
|
+
setVerifying(true);
|
|
87
|
+
setError('');
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Create challenge and verify
|
|
91
|
+
const challenge = await mfa.challenge(enrollmentData.id);
|
|
92
|
+
if (challenge) {
|
|
93
|
+
await mfa.verify(enrollmentData.id, challenge.id, verificationCode);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Success - refresh factors and close dialog
|
|
97
|
+
await loadFactors();
|
|
98
|
+
setEnrollmentData(null);
|
|
99
|
+
setEnrolling(false);
|
|
100
|
+
setVerificationCode('');
|
|
101
|
+
} catch (err) {
|
|
102
|
+
setError(err instanceof Error ? err.message : 'Invalid verification code');
|
|
103
|
+
setVerificationCode('');
|
|
104
|
+
} finally {
|
|
105
|
+
setVerifying(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleCancelEnrollment = async () => {
|
|
110
|
+
// If we have an unverified factor, unenroll it
|
|
111
|
+
if (enrollmentData) {
|
|
112
|
+
try {
|
|
113
|
+
await mfa.unenroll(enrollmentData.id);
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore error - factor may not exist
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
setEnrollmentData(null);
|
|
119
|
+
setEnrolling(false);
|
|
120
|
+
setVerificationCode('');
|
|
121
|
+
setError('');
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleUnenroll = async () => {
|
|
125
|
+
if (!factorToUnenroll) return;
|
|
126
|
+
|
|
127
|
+
setUnenrolling(true);
|
|
128
|
+
setError('');
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await mfa.unenroll(factorToUnenroll);
|
|
132
|
+
await loadFactors();
|
|
133
|
+
setFactorToUnenroll(null);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
setError(err instanceof Error ? err.message : 'Failed to disable 2FA');
|
|
136
|
+
} finally {
|
|
137
|
+
setUnenrolling(false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<>
|
|
143
|
+
<PageHeader title={t('security.title')} description={t('security.description')} />
|
|
144
|
+
|
|
145
|
+
<ContentPanel variant="narrow">
|
|
146
|
+
{setupRequired && !hasMfaEnabled && (
|
|
147
|
+
<div className="p-4 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900 rounded-lg">
|
|
148
|
+
<div className="flex items-start gap-3">
|
|
149
|
+
<ShieldOff className="size-5 text-amber-600 dark:text-amber-500 mt-0.5" />
|
|
150
|
+
<div>
|
|
151
|
+
<p className="font-medium text-amber-800 dark:text-amber-200">
|
|
152
|
+
{t('security.mfaRequired')}
|
|
153
|
+
</p>
|
|
154
|
+
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
|
|
155
|
+
{t('security.mfaRequiredDescription')}
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{error && (
|
|
163
|
+
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-lg">{error}</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<Card>
|
|
167
|
+
<CardHeader>
|
|
168
|
+
<div className="flex items-start justify-between gap-4">
|
|
169
|
+
<div className="space-y-1">
|
|
170
|
+
<CardTitle className="flex items-center gap-2">
|
|
171
|
+
<Smartphone className="size-5" />
|
|
172
|
+
{t('security.twoFactor')}
|
|
173
|
+
</CardTitle>
|
|
174
|
+
<CardDescription>{t('security.twoFactorDescription')}</CardDescription>
|
|
175
|
+
</div>
|
|
176
|
+
{!loading && (
|
|
177
|
+
<Switch
|
|
178
|
+
checked={hasMfaEnabled}
|
|
179
|
+
disabled={enrolling || unenrolling || mfaRequired}
|
|
180
|
+
onCheckedChange={(checked) => {
|
|
181
|
+
if (checked) {
|
|
182
|
+
handleStartEnrollment();
|
|
183
|
+
} else if (factors.length > 0) {
|
|
184
|
+
setFactorToUnenroll(factors[0].id);
|
|
185
|
+
}
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</CardHeader>
|
|
191
|
+
{!loading && (hasMfaEnabled || mfaRequired) && (
|
|
192
|
+
<CardContent className="space-y-4">
|
|
193
|
+
{hasMfaEnabled && (
|
|
194
|
+
<>
|
|
195
|
+
<div className="flex items-center gap-3 p-3 bg-emerald-50 dark:bg-emerald-950/30 rounded-lg">
|
|
196
|
+
<ShieldCheck className="size-4 text-emerald-600 dark:text-emerald-500 shrink-0" />
|
|
197
|
+
<p className="text-sm text-emerald-700 dark:text-emerald-300">
|
|
198
|
+
{t('security.mfaEnabledDescription')}
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{factors.length > 0 && (
|
|
203
|
+
<div className="space-y-2">
|
|
204
|
+
<p className="text-sm font-medium">{t('security.enrolledAuthenticators')}</p>
|
|
205
|
+
{factors.map((factor) => (
|
|
206
|
+
<div
|
|
207
|
+
key={factor.id}
|
|
208
|
+
className="flex items-center justify-between p-3 border rounded-lg"
|
|
209
|
+
>
|
|
210
|
+
<div className="flex items-center gap-3">
|
|
211
|
+
<Smartphone className="size-4 text-muted-foreground" />
|
|
212
|
+
<span className="text-sm">
|
|
213
|
+
{factor.friendly_name || t('security.authenticatorApp')}
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
<Button
|
|
217
|
+
variant="ghost"
|
|
218
|
+
size="sm"
|
|
219
|
+
className="text-destructive hover:text-destructive"
|
|
220
|
+
onClick={() => setFactorToUnenroll(factor.id)}
|
|
221
|
+
>
|
|
222
|
+
{t('common.remove')}
|
|
223
|
+
</Button>
|
|
224
|
+
</div>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{mfaRequired && (
|
|
232
|
+
<p className="text-sm text-muted-foreground">{t('security.mfaRequiredByOrg')}</p>
|
|
233
|
+
)}
|
|
234
|
+
</CardContent>
|
|
235
|
+
)}
|
|
236
|
+
</Card>
|
|
237
|
+
</ContentPanel>
|
|
238
|
+
|
|
239
|
+
{/* Enrollment Dialog */}
|
|
240
|
+
<Dialog open={!!enrollmentData} onOpenChange={() => handleCancelEnrollment()}>
|
|
241
|
+
<DialogContent className="sm:max-w-md">
|
|
242
|
+
<DialogHeader>
|
|
243
|
+
<DialogTitle>{t('security.setupMfa')}</DialogTitle>
|
|
244
|
+
<DialogDescription>{t('security.setupMfaDescription')}</DialogDescription>
|
|
245
|
+
</DialogHeader>
|
|
246
|
+
|
|
247
|
+
<div className="space-y-6">
|
|
248
|
+
{/* QR Code */}
|
|
249
|
+
<div className="flex justify-center">
|
|
250
|
+
{enrollmentData?.qrCode && (
|
|
251
|
+
<div
|
|
252
|
+
className="p-4 bg-white rounded-lg"
|
|
253
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: QR code is SVG from Supabase
|
|
254
|
+
dangerouslySetInnerHTML={{ __html: enrollmentData.qrCode }}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Manual entry */}
|
|
260
|
+
<div className="space-y-2">
|
|
261
|
+
<p className="text-sm text-muted-foreground text-center">{t('security.cantScan')}</p>
|
|
262
|
+
<code className="block text-center text-sm bg-muted p-2 rounded font-mono break-all">
|
|
263
|
+
{enrollmentData?.secret}
|
|
264
|
+
</code>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Verification code input */}
|
|
268
|
+
<div className="space-y-2">
|
|
269
|
+
<p className="text-sm font-medium text-center">{t('security.enterCode')}</p>
|
|
270
|
+
<div className="flex justify-center">
|
|
271
|
+
<InputOTP
|
|
272
|
+
maxLength={6}
|
|
273
|
+
value={verificationCode}
|
|
274
|
+
onChange={setVerificationCode}
|
|
275
|
+
onComplete={handleVerifyEnrollment}
|
|
276
|
+
disabled={verifying}
|
|
277
|
+
>
|
|
278
|
+
<InputOTPGroup>
|
|
279
|
+
<InputOTPSlot index={0} />
|
|
280
|
+
<InputOTPSlot index={1} />
|
|
281
|
+
<InputOTPSlot index={2} />
|
|
282
|
+
<InputOTPSlot index={3} />
|
|
283
|
+
<InputOTPSlot index={4} />
|
|
284
|
+
<InputOTPSlot index={5} />
|
|
285
|
+
</InputOTPGroup>
|
|
286
|
+
</InputOTP>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{error && (
|
|
291
|
+
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-lg text-center">
|
|
292
|
+
{error}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
298
|
+
<Button variant="ghost" onClick={handleCancelEnrollment} disabled={verifying}>
|
|
299
|
+
{t('common.cancel')}
|
|
300
|
+
</Button>
|
|
301
|
+
<Button
|
|
302
|
+
onClick={handleVerifyEnrollment}
|
|
303
|
+
disabled={verifying || verificationCode.length !== 6}
|
|
304
|
+
>
|
|
305
|
+
{verifying ? t('security.verifying') : t('security.verifyAndEnable')}
|
|
306
|
+
</Button>
|
|
307
|
+
</DialogFooter>
|
|
308
|
+
</DialogContent>
|
|
309
|
+
</Dialog>
|
|
310
|
+
|
|
311
|
+
{/* Unenroll Confirmation Dialog */}
|
|
312
|
+
<Dialog open={!!factorToUnenroll} onOpenChange={() => setFactorToUnenroll(null)}>
|
|
313
|
+
<DialogContent className="sm:max-w-md">
|
|
314
|
+
<DialogHeader>
|
|
315
|
+
<DialogTitle>{t('security.disableMfa')}</DialogTitle>
|
|
316
|
+
<DialogDescription>{t('security.disableMfaDescription')}</DialogDescription>
|
|
317
|
+
</DialogHeader>
|
|
318
|
+
|
|
319
|
+
{mfaRequired && (
|
|
320
|
+
<div className="p-3 text-sm text-amber-700 bg-amber-50 dark:bg-amber-950/30 dark:text-amber-300 rounded-lg">
|
|
321
|
+
{t('security.disableMfaWarning')}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
326
|
+
<Button
|
|
327
|
+
variant="ghost"
|
|
328
|
+
onClick={() => setFactorToUnenroll(null)}
|
|
329
|
+
disabled={unenrolling}
|
|
330
|
+
>
|
|
331
|
+
{t('common.cancel')}
|
|
332
|
+
</Button>
|
|
333
|
+
<Button variant="destructive" onClick={handleUnenroll} disabled={unenrolling}>
|
|
334
|
+
{unenrolling ? t('security.disabling') : t('security.disable2fa')}
|
|
335
|
+
</Button>
|
|
336
|
+
</DialogFooter>
|
|
337
|
+
</DialogContent>
|
|
338
|
+
</Dialog>
|
|
339
|
+
</>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg width="877" height="1012" viewBox="0 0 877 1012" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g clip-path="url(#clip0_77_141)">
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M434.346 2.3064C436.82 0.879051 439.867 0.879058 442.342 2.30641L872.685 250.582C875.162 252.01 876.688 254.652 876.688 257.511V754.051C876.688 756.91 875.162 759.552 872.685 760.981L442.342 1009.26C439.867 1010.68 436.82 1010.68 434.346 1009.26L4.00221 760.981C1.52575 759.552 0 756.91 0 754.051V257.511C0 254.652 1.52576 252.01 4.00222 250.582L434.346 2.3064ZM708.094 505.781C708.094 577.323 679.674 645.935 629.086 696.523C578.498 747.111 509.886 775.531 438.344 775.531C366.802 775.531 298.19 747.111 247.602 696.523C197.014 645.935 168.594 577.323 168.594 505.781C168.594 434.239 197.014 365.627 247.602 315.039C298.19 264.451 366.802 236.031 438.344 236.031C509.886 236.031 578.498 264.451 629.086 315.039C679.674 365.627 708.094 434.239 708.094 505.781Z" fill="url(#paint0_linear_77_141)"/>
|
|
4
|
+
<path d="M876.688 252.891V472.062L438.344 606.938L0 472.062V252.891L438.344 0L876.688 252.891Z" fill="url(#paint1_linear_77_141)"/>
|
|
5
|
+
</g>
|
|
6
|
+
<defs>
|
|
7
|
+
<linearGradient id="paint0_linear_77_141" x1="0" y1="0" x2="0" y2="1011.56" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#5EEAD4"/>
|
|
9
|
+
<stop offset="1" stop-color="#0D9488"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="paint1_linear_77_141" x1="175.338" y1="-5.42644e-06" x2="516.194" y2="492.348" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop stop-color="white" stop-opacity="0.5"/>
|
|
13
|
+
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<clipPath id="clip0_77_141">
|
|
16
|
+
<rect width="876.688" height="1011.56" fill="white"/>
|
|
17
|
+
</clipPath>
|
|
18
|
+
</defs>
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing provider factory.
|
|
3
|
+
*
|
|
4
|
+
* Selects the active payment provider based on the BILLING_PROVIDER env var
|
|
5
|
+
* (defaults to 'stripe'). The provider instance is cached so it's created once
|
|
6
|
+
* per process lifetime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { env } from '../lib/env';
|
|
10
|
+
import type { BillingProvider, BillingProviderType } from './provider';
|
|
11
|
+
import { PaddleProvider } from './providers/paddle';
|
|
12
|
+
import { PolarProvider } from './providers/polar';
|
|
13
|
+
import { StripeProvider } from './providers/stripe';
|
|
14
|
+
|
|
15
|
+
// Re-export types for convenience
|
|
16
|
+
export type {
|
|
17
|
+
BillingProvider,
|
|
18
|
+
BillingProviderType,
|
|
19
|
+
CheckoutResult,
|
|
20
|
+
PortalResult,
|
|
21
|
+
SubscriptionInfo,
|
|
22
|
+
} from './provider';
|
|
23
|
+
|
|
24
|
+
/** Cached provider instance (one per process). */
|
|
25
|
+
let cachedProvider: BillingProvider | null = null;
|
|
26
|
+
let cachedProviderType: BillingProviderType | null = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the active billing provider instance.
|
|
30
|
+
*
|
|
31
|
+
* The provider is determined by the BILLING_PROVIDER env var and cached
|
|
32
|
+
* for the lifetime of the process.
|
|
33
|
+
*
|
|
34
|
+
* @throws {Error} if the BILLING_PROVIDER value is unsupported
|
|
35
|
+
*/
|
|
36
|
+
export function getBillingProvider(): BillingProvider {
|
|
37
|
+
const providerType = env.BILLING_PROVIDER;
|
|
38
|
+
|
|
39
|
+
// Return cached instance if the provider type hasn't changed
|
|
40
|
+
if (cachedProvider && cachedProviderType === providerType) {
|
|
41
|
+
return cachedProvider;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
switch (providerType) {
|
|
45
|
+
case 'stripe':
|
|
46
|
+
cachedProvider = new StripeProvider();
|
|
47
|
+
break;
|
|
48
|
+
case 'paddle':
|
|
49
|
+
cachedProvider = new PaddleProvider();
|
|
50
|
+
break;
|
|
51
|
+
case 'polar':
|
|
52
|
+
cachedProvider = new PolarProvider();
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unknown billing provider: ${providerType}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cachedProviderType = providerType;
|
|
59
|
+
return cachedProvider;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if the active billing provider is properly configured.
|
|
64
|
+
*
|
|
65
|
+
* Returns false if the provider type is unknown or the provider's
|
|
66
|
+
* required credentials are missing.
|
|
67
|
+
*/
|
|
68
|
+
export function isBillingConfigured(): boolean {
|
|
69
|
+
try {
|
|
70
|
+
return getBillingProvider().isConfigured();
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the provider-specific price IDs from environment variables.
|
|
78
|
+
*
|
|
79
|
+
* Used by the pricing endpoint and webhook handler to map price IDs
|
|
80
|
+
* back to plan names.
|
|
81
|
+
*/
|
|
82
|
+
export function getProviderPriceIds(): { starter?: string; pro?: string } {
|
|
83
|
+
const providerType = env.BILLING_PROVIDER;
|
|
84
|
+
|
|
85
|
+
switch (providerType) {
|
|
86
|
+
case 'stripe':
|
|
87
|
+
return {
|
|
88
|
+
starter: env.STRIPE_PRICE_STARTER,
|
|
89
|
+
pro: env.STRIPE_PRICE_PRO,
|
|
90
|
+
};
|
|
91
|
+
case 'paddle':
|
|
92
|
+
return {
|
|
93
|
+
starter: env.PADDLE_PRICE_STARTER,
|
|
94
|
+
pro: env.PADDLE_PRICE_PRO,
|
|
95
|
+
};
|
|
96
|
+
case 'polar':
|
|
97
|
+
return {
|
|
98
|
+
starter: env.POLAR_PRICE_STARTER,
|
|
99
|
+
pro: env.POLAR_PRICE_PRO,
|
|
100
|
+
};
|
|
101
|
+
default:
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing provider abstraction layer.
|
|
3
|
+
*
|
|
4
|
+
* Defines a common interface that all payment providers (Stripe, Paddle, Polar)
|
|
5
|
+
* must implement. This allows the billing routes and webhook handlers to remain
|
|
6
|
+
* provider-agnostic, selecting the active provider at runtime via BILLING_PROVIDER.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type BillingProviderType = 'stripe' | 'paddle' | 'polar';
|
|
10
|
+
|
|
11
|
+
export interface CheckoutResult {
|
|
12
|
+
url: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PortalResult {
|
|
17
|
+
url: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SubscriptionInfo {
|
|
21
|
+
id: string;
|
|
22
|
+
status: string;
|
|
23
|
+
planId: string;
|
|
24
|
+
priceId: string;
|
|
25
|
+
currentPeriodEnd: string | null;
|
|
26
|
+
cancelAtPeriodEnd: boolean;
|
|
27
|
+
price: {
|
|
28
|
+
unitAmount: number;
|
|
29
|
+
currency: string;
|
|
30
|
+
interval: string;
|
|
31
|
+
};
|
|
32
|
+
product: {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BillingProvider {
|
|
39
|
+
readonly type: BillingProviderType;
|
|
40
|
+
|
|
41
|
+
/** Returns true if the provider has its required API keys configured. */
|
|
42
|
+
isConfigured(): boolean;
|
|
43
|
+
|
|
44
|
+
/** Create a checkout session and return the redirect URL. */
|
|
45
|
+
createCheckout(params: {
|
|
46
|
+
customerId: string;
|
|
47
|
+
priceId: string;
|
|
48
|
+
successUrl: string;
|
|
49
|
+
cancelUrl: string;
|
|
50
|
+
metadata: Record<string, string>;
|
|
51
|
+
mode?: 'subscription' | 'payment';
|
|
52
|
+
}): Promise<CheckoutResult>;
|
|
53
|
+
|
|
54
|
+
/** Create a customer portal session and return the redirect URL. */
|
|
55
|
+
createPortalSession(params: { customerId: string; returnUrl: string }): Promise<PortalResult>;
|
|
56
|
+
|
|
57
|
+
/** Retrieve subscription details for a customer. */
|
|
58
|
+
getSubscription(params: {
|
|
59
|
+
customerId: string;
|
|
60
|
+
subscriptionId: string;
|
|
61
|
+
}): Promise<SubscriptionInfo | null>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Verify and parse an incoming webhook payload.
|
|
65
|
+
* Returns a normalized event type and data payload.
|
|
66
|
+
*/
|
|
67
|
+
handleWebhook(params: {
|
|
68
|
+
body: string;
|
|
69
|
+
signature: string;
|
|
70
|
+
}): Promise<{ type: string; data: Record<string, unknown> }>;
|
|
71
|
+
|
|
72
|
+
/** Create a customer in the provider and return the provider's customer ID. */
|
|
73
|
+
createCustomer(params: {
|
|
74
|
+
email: string;
|
|
75
|
+
name: string;
|
|
76
|
+
metadata: Record<string, string>;
|
|
77
|
+
}): Promise<string>;
|
|
78
|
+
|
|
79
|
+
/** Delete a customer in the provider. */
|
|
80
|
+
deleteCustomer(customerId: string): Promise<void>;
|
|
81
|
+
}
|