@nextsparkjs/core 0.1.0-beta.84 → 0.1.0-beta.85
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/dist/styles/classes.json +1 -1
- package/dist/templates/app/(auth)/forgot-password/page.tsx +216 -0
- package/dist/templates/app/(auth)/layout.tsx +51 -0
- package/dist/templates/app/(auth)/login/page.tsx +21 -0
- package/dist/templates/app/(auth)/reset-password/page.tsx +212 -0
- package/dist/templates/app/(auth)/signup/page.tsx +21 -0
- package/dist/templates/app/(auth)/verify-email/page.tsx +190 -0
- package/dist/templates/app/(public)/[...slug]/page.tsx +378 -0
- package/dist/templates/app/(public)/docs/[section]/[page]/page.tsx +90 -0
- package/dist/templates/app/(public)/docs/layout.tsx +25 -0
- package/dist/templates/app/(public)/docs/page.tsx +81 -0
- package/dist/templates/app/(public)/layout.tsx +41 -0
- package/dist/templates/app/(public)/page.tsx +19 -0
- package/dist/templates/app/403/page.tsx +89 -0
- package/dist/templates/app/api/auth/[...all]/route.ts +78 -0
- package/dist/templates/app/api/cron/billing/lifecycle/route.ts +98 -0
- package/dist/templates/app/api/csp-report/route.ts +175 -0
- package/dist/templates/app/api/devtools/config/entities/route.ts +108 -0
- package/dist/templates/app/api/devtools/config/theme/route.ts +66 -0
- package/dist/templates/app/api/devtools/tests/[...path]/route.ts +130 -0
- package/dist/templates/app/api/devtools/tests/route.ts +134 -0
- package/dist/templates/app/api/health/route.ts +29 -0
- package/dist/templates/app/api/internal/user-metadata/route.ts +36 -0
- package/dist/templates/app/api/superadmin/subscriptions/route.ts +310 -0
- package/dist/templates/app/api/superadmin/teams/[teamId]/route.ts +286 -0
- package/dist/templates/app/api/superadmin/teams/route.ts +188 -0
- package/dist/templates/app/api/superadmin/users/[userId]/route.ts +540 -0
- package/dist/templates/app/api/superadmin/users/route.ts +323 -0
- package/dist/templates/app/api/user/delete-account/route.ts +55 -0
- package/dist/templates/app/api/user/plan-flags/route.ts +283 -0
- package/dist/templates/app/api/user/profile/route.ts +133 -0
- package/dist/templates/app/api/v1/[entity]/[id]/child/[childType]/[childId]/route.ts +210 -0
- package/dist/templates/app/api/v1/[entity]/[id]/child/[childType]/route.ts +331 -0
- package/dist/templates/app/api/v1/[entity]/[id]/route.ts +35 -0
- package/dist/templates/app/api/v1/[entity]/docs.md +369 -0
- package/dist/templates/app/api/v1/[entity]/presets.ts +194 -0
- package/dist/templates/app/api/v1/[entity]/route.ts +31 -0
- package/dist/templates/app/api/v1/api-keys/[id]/route.ts +303 -0
- package/dist/templates/app/api/v1/api-keys/docs.md +101 -0
- package/dist/templates/app/api/v1/api-keys/presets.ts +31 -0
- package/dist/templates/app/api/v1/api-keys/route.ts +250 -0
- package/dist/templates/app/api/v1/auth/docs.md +184 -0
- package/dist/templates/app/api/v1/auth/presets.ts +44 -0
- package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +227 -0
- package/dist/templates/app/api/v1/billing/cancel/route.ts +206 -0
- package/dist/templates/app/api/v1/billing/change-plan/route.ts +97 -0
- package/dist/templates/app/api/v1/billing/check-action/route.ts +81 -0
- package/dist/templates/app/api/v1/billing/checkout/route.ts +124 -0
- package/dist/templates/app/api/v1/billing/docs.md +209 -0
- package/dist/templates/app/api/v1/billing/plans/route.ts +85 -0
- package/dist/templates/app/api/v1/billing/portal/route.ts +90 -0
- package/dist/templates/app/api/v1/billing/presets.ts +121 -0
- package/dist/templates/app/api/v1/billing/webhooks/stripe/route.ts +428 -0
- package/dist/templates/app/api/v1/blocks/[slug]/route.ts +29 -0
- package/dist/templates/app/api/v1/blocks/docs.md +173 -0
- package/dist/templates/app/api/v1/blocks/presets.ts +121 -0
- package/dist/templates/app/api/v1/blocks/route.ts +45 -0
- package/dist/templates/app/api/v1/blocks/validate/route.ts +45 -0
- package/dist/templates/app/api/v1/cron/docs.md +116 -0
- package/dist/templates/app/api/v1/cron/presets.ts +26 -0
- package/dist/templates/app/api/v1/cron/process/route.ts +108 -0
- package/dist/templates/app/api/v1/devtools/blocks/route.ts +82 -0
- package/dist/templates/app/api/v1/devtools/docs/route.ts +150 -0
- package/dist/templates/app/api/v1/devtools/docs.md +204 -0
- package/dist/templates/app/api/v1/devtools/features/route.ts +61 -0
- package/dist/templates/app/api/v1/devtools/flows/route.ts +61 -0
- package/dist/templates/app/api/v1/devtools/presets.ts +113 -0
- package/dist/templates/app/api/v1/devtools/scheduled-actions/route.ts +120 -0
- package/dist/templates/app/api/v1/devtools/testing/route.ts +82 -0
- package/dist/templates/app/api/v1/media/docs.md +117 -0
- package/dist/templates/app/api/v1/media/presets.ts +24 -0
- package/dist/templates/app/api/v1/media/upload/route.ts +150 -0
- package/dist/templates/app/api/v1/patterns/[id]/usages/route.ts +116 -0
- package/dist/templates/app/api/v1/plugin/[...path]/route.ts +373 -0
- package/dist/templates/app/api/v1/plugin/docs.md +79 -0
- package/dist/templates/app/api/v1/plugin/presets.ts +21 -0
- package/dist/templates/app/api/v1/plugin/route.ts +96 -0
- package/dist/templates/app/api/v1/post-categories/[id]/route.ts +255 -0
- package/dist/templates/app/api/v1/post-categories/docs.md +134 -0
- package/dist/templates/app/api/v1/post-categories/presets.ts +78 -0
- package/dist/templates/app/api/v1/post-categories/route.ts +119 -0
- package/dist/templates/app/api/v1/team-invitations/[token]/accept/route.ts +179 -0
- package/dist/templates/app/api/v1/team-invitations/[token]/decline/route.ts +120 -0
- package/dist/templates/app/api/v1/team-invitations/[token]/route.ts +89 -0
- package/dist/templates/app/api/v1/team-invitations/docs.md +88 -0
- package/dist/templates/app/api/v1/team-invitations/presets.ts +43 -0
- package/dist/templates/app/api/v1/team-invitations/route.ts +114 -0
- package/dist/templates/app/api/v1/teams/[teamId]/invitations/route.ts +171 -0
- package/dist/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +105 -0
- package/dist/templates/app/api/v1/teams/[teamId]/invoices/route.ts +125 -0
- package/dist/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +263 -0
- package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +358 -0
- package/dist/templates/app/api/v1/teams/[teamId]/route.ts +322 -0
- package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +50 -0
- package/dist/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +91 -0
- package/dist/templates/app/api/v1/teams/docs.md +320 -0
- package/dist/templates/app/api/v1/teams/presets.ts +178 -0
- package/dist/templates/app/api/v1/teams/route.ts +293 -0
- package/dist/templates/app/api/v1/teams/switch/route.ts +88 -0
- package/dist/templates/app/api/v1/theme/[...path]/route.ts +361 -0
- package/dist/templates/app/api/v1/theme/docs.md +74 -0
- package/dist/templates/app/api/v1/theme/presets.ts +21 -0
- package/dist/templates/app/api/v1/theme/route.ts +96 -0
- package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +363 -0
- package/dist/templates/app/api/v1/users/[id]/route.ts +302 -0
- package/dist/templates/app/api/v1/users/docs.md +93 -0
- package/dist/templates/app/api/v1/users/presets.ts +59 -0
- package/dist/templates/app/api/v1/users/route.ts +197 -0
- package/dist/templates/app/dashboard/(main)/[entity]/[id]/edit/page.tsx +117 -0
- package/dist/templates/app/dashboard/(main)/[entity]/[id]/page.tsx +103 -0
- package/dist/templates/app/dashboard/(main)/[entity]/create/page.tsx +95 -0
- package/dist/templates/app/dashboard/(main)/[entity]/error.tsx +51 -0
- package/dist/templates/app/dashboard/(main)/[entity]/layout.tsx +113 -0
- package/dist/templates/app/dashboard/(main)/[entity]/loading.tsx +61 -0
- package/dist/templates/app/dashboard/(main)/[entity]/page.tsx +90 -0
- package/dist/templates/app/dashboard/(main)/layout.tsx +98 -0
- package/dist/templates/app/dashboard/(main)/loading.tsx +5 -0
- package/dist/templates/app/dashboard/(main)/page.tsx +201 -0
- package/dist/templates/app/dashboard/(main)/patterns/[id]/edit/page.tsx +114 -0
- package/dist/templates/app/dashboard/(main)/patterns/[id]/page.tsx +20 -0
- package/dist/templates/app/dashboard/(main)/patterns/[id]/reports/page.tsx +171 -0
- package/dist/templates/app/dashboard/(main)/patterns/create/page.tsx +86 -0
- package/dist/templates/app/dashboard/(main)/patterns/page.tsx +444 -0
- package/dist/templates/app/dashboard/features/analytics/page.tsx +35 -0
- package/dist/templates/app/dashboard/features/automation/page.tsx +35 -0
- package/dist/templates/app/dashboard/features/layout.tsx +13 -0
- package/dist/templates/app/dashboard/features/loading.tsx +5 -0
- package/dist/templates/app/dashboard/features/webhooks/page.tsx +35 -0
- package/dist/templates/app/dashboard/layout.tsx +86 -0
- package/dist/templates/app/dashboard/permission-denied/page.tsx +29 -0
- package/dist/templates/app/dashboard/settings/api-keys/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/api-keys/page.tsx +513 -0
- package/dist/templates/app/dashboard/settings/billing/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/billing/page.tsx +284 -0
- package/dist/templates/app/dashboard/settings/invoices/[invoiceNumber]/page.tsx +222 -0
- package/dist/templates/app/dashboard/settings/invoices/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/invoices/page.tsx +82 -0
- package/dist/templates/app/dashboard/settings/layout.tsx +151 -0
- package/dist/templates/app/dashboard/settings/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/notifications/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/notifications/page.tsx +462 -0
- package/dist/templates/app/dashboard/settings/page.tsx +92 -0
- package/dist/templates/app/dashboard/settings/password/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/password/page.tsx +306 -0
- package/dist/templates/app/dashboard/settings/plans/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/plans/page.tsx +40 -0
- package/dist/templates/app/dashboard/settings/profile/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/profile/page.tsx +686 -0
- package/dist/templates/app/dashboard/settings/security/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/security/page.tsx +505 -0
- package/dist/templates/app/dashboard/settings/teams/loading.tsx +5 -0
- package/dist/templates/app/dashboard/settings/teams/page.tsx +272 -0
- package/dist/templates/app/dashboard/settings/teams/permissions/page.tsx +92 -0
- package/dist/templates/app/devtools/blocks/[slug]/page.tsx +39 -0
- package/dist/templates/app/devtools/blocks/page.tsx +31 -0
- package/dist/templates/app/devtools/config/page.tsx +31 -0
- package/dist/templates/app/devtools/features/page.tsx +31 -0
- package/dist/templates/app/devtools/flows/page.tsx +31 -0
- package/dist/templates/app/devtools/layout.tsx +58 -0
- package/dist/templates/app/devtools/page.tsx +121 -0
- package/dist/templates/app/devtools/scheduled-actions/page.tsx +157 -0
- package/dist/templates/app/devtools/style/page.tsx +330 -0
- package/dist/templates/app/devtools/tags/page.tsx +31 -0
- package/dist/templates/app/devtools/tests/[[...path]]/page.tsx +47 -0
- package/dist/templates/app/favicon.ico +0 -0
- package/dist/templates/app/globals.css +12 -0
- package/dist/templates/app/layout.tsx +96 -0
- package/dist/templates/app/public/page.tsx +30 -0
- package/dist/templates/app/superadmin/docs/[section]/[page]/page.tsx +92 -0
- package/dist/templates/app/superadmin/docs/page.tsx +75 -0
- package/dist/templates/app/superadmin/layout.tsx +67 -0
- package/dist/templates/app/superadmin/page.tsx +149 -0
- package/dist/templates/app/superadmin/subscriptions/page.tsx +655 -0
- package/dist/templates/app/superadmin/team-roles/page.tsx +493 -0
- package/dist/templates/app/superadmin/teams/[teamId]/page.tsx +687 -0
- package/dist/templates/app/superadmin/teams/page.tsx +302 -0
- package/dist/templates/app/superadmin/users/[userId]/page.tsx +548 -0
- package/dist/templates/app/superadmin/users/page.tsx +528 -0
- package/package.json +2 -2
- package/templates/app/(auth)/forgot-password/page.tsx +216 -0
- package/templates/app/(auth)/layout.tsx +51 -0
- package/templates/app/(auth)/login/page.tsx +21 -0
- package/templates/app/(auth)/reset-password/page.tsx +212 -0
- package/templates/app/(auth)/signup/page.tsx +21 -0
- package/templates/app/(auth)/verify-email/page.tsx +190 -0
- package/templates/app/(public)/[...slug]/page.tsx +378 -0
- package/templates/app/(public)/docs/[section]/[page]/page.tsx +90 -0
- package/templates/app/(public)/docs/layout.tsx +25 -0
- package/templates/app/(public)/docs/page.tsx +81 -0
- package/templates/app/(public)/layout.tsx +41 -0
- package/templates/app/(public)/page.tsx +19 -0
- package/templates/app/403/page.tsx +89 -0
- package/templates/app/api/auth/[...all]/route.ts +78 -0
- package/templates/app/api/cron/billing/lifecycle/route.ts +98 -0
- package/templates/app/api/csp-report/route.ts +175 -0
- package/templates/app/api/devtools/config/entities/route.ts +108 -0
- package/templates/app/api/devtools/config/theme/route.ts +66 -0
- package/templates/app/api/devtools/tests/[...path]/route.ts +130 -0
- package/templates/app/api/devtools/tests/route.ts +134 -0
- package/templates/app/api/health/route.ts +29 -0
- package/templates/app/api/internal/user-metadata/route.ts +36 -0
- package/templates/app/api/superadmin/subscriptions/route.ts +310 -0
- package/templates/app/api/superadmin/teams/[teamId]/route.ts +286 -0
- package/templates/app/api/superadmin/teams/route.ts +188 -0
- package/templates/app/api/superadmin/users/[userId]/route.ts +540 -0
- package/templates/app/api/superadmin/users/route.ts +323 -0
- package/templates/app/api/user/delete-account/route.ts +55 -0
- package/templates/app/api/user/plan-flags/route.ts +283 -0
- package/templates/app/api/user/profile/route.ts +133 -0
- package/templates/app/api/v1/[entity]/[id]/child/[childType]/[childId]/route.ts +210 -0
- package/templates/app/api/v1/[entity]/[id]/child/[childType]/route.ts +331 -0
- package/templates/app/api/v1/[entity]/[id]/route.ts +35 -0
- package/templates/app/api/v1/[entity]/docs.md +369 -0
- package/templates/app/api/v1/[entity]/presets.ts +194 -0
- package/templates/app/api/v1/[entity]/route.ts +31 -0
- package/templates/app/api/v1/api-keys/[id]/route.ts +303 -0
- package/templates/app/api/v1/api-keys/docs.md +101 -0
- package/templates/app/api/v1/api-keys/presets.ts +31 -0
- package/templates/app/api/v1/api-keys/route.ts +250 -0
- package/templates/app/api/v1/auth/docs.md +184 -0
- package/templates/app/api/v1/auth/presets.ts +44 -0
- package/templates/app/api/v1/auth/signup-with-invite/route.ts +227 -0
- package/templates/app/api/v1/billing/cancel/route.ts +206 -0
- package/templates/app/api/v1/billing/change-plan/route.ts +97 -0
- package/templates/app/api/v1/billing/check-action/route.ts +81 -0
- package/templates/app/api/v1/billing/checkout/route.ts +124 -0
- package/templates/app/api/v1/billing/docs.md +209 -0
- package/templates/app/api/v1/billing/plans/route.ts +85 -0
- package/templates/app/api/v1/billing/portal/route.ts +90 -0
- package/templates/app/api/v1/billing/presets.ts +121 -0
- package/templates/app/api/v1/billing/webhooks/stripe/route.ts +428 -0
- package/templates/app/api/v1/blocks/[slug]/route.ts +29 -0
- package/templates/app/api/v1/blocks/docs.md +173 -0
- package/templates/app/api/v1/blocks/presets.ts +121 -0
- package/templates/app/api/v1/blocks/route.ts +45 -0
- package/templates/app/api/v1/blocks/validate/route.ts +45 -0
- package/templates/app/api/v1/cron/docs.md +116 -0
- package/templates/app/api/v1/cron/presets.ts +26 -0
- package/templates/app/api/v1/cron/process/route.ts +108 -0
- package/templates/app/api/v1/devtools/blocks/route.ts +82 -0
- package/templates/app/api/v1/devtools/docs/route.ts +150 -0
- package/templates/app/api/v1/devtools/docs.md +204 -0
- package/templates/app/api/v1/devtools/features/route.ts +61 -0
- package/templates/app/api/v1/devtools/flows/route.ts +61 -0
- package/templates/app/api/v1/devtools/presets.ts +113 -0
- package/templates/app/api/v1/devtools/scheduled-actions/route.ts +120 -0
- package/templates/app/api/v1/devtools/testing/route.ts +82 -0
- package/templates/app/api/v1/media/docs.md +117 -0
- package/templates/app/api/v1/media/presets.ts +24 -0
- package/templates/app/api/v1/media/upload/route.ts +150 -0
- package/templates/app/api/v1/patterns/[id]/usages/route.ts +116 -0
- package/templates/app/api/v1/plugin/[...path]/route.ts +373 -0
- package/templates/app/api/v1/plugin/docs.md +79 -0
- package/templates/app/api/v1/plugin/presets.ts +21 -0
- package/templates/app/api/v1/plugin/route.ts +96 -0
- package/templates/app/api/v1/post-categories/[id]/route.ts +255 -0
- package/templates/app/api/v1/post-categories/docs.md +134 -0
- package/templates/app/api/v1/post-categories/presets.ts +78 -0
- package/templates/app/api/v1/post-categories/route.ts +119 -0
- package/templates/app/api/v1/team-invitations/[token]/accept/route.ts +179 -0
- package/templates/app/api/v1/team-invitations/[token]/decline/route.ts +120 -0
- package/templates/app/api/v1/team-invitations/[token]/route.ts +89 -0
- package/templates/app/api/v1/team-invitations/docs.md +88 -0
- package/templates/app/api/v1/team-invitations/presets.ts +43 -0
- package/templates/app/api/v1/team-invitations/route.ts +114 -0
- package/templates/app/api/v1/teams/[teamId]/invitations/route.ts +171 -0
- package/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +105 -0
- package/templates/app/api/v1/teams/[teamId]/invoices/route.ts +125 -0
- package/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +263 -0
- package/templates/app/api/v1/teams/[teamId]/members/route.ts +358 -0
- package/templates/app/api/v1/teams/[teamId]/route.ts +322 -0
- package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +50 -0
- package/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +91 -0
- package/templates/app/api/v1/teams/docs.md +320 -0
- package/templates/app/api/v1/teams/presets.ts +178 -0
- package/templates/app/api/v1/teams/route.ts +293 -0
- package/templates/app/api/v1/teams/switch/route.ts +88 -0
- package/templates/app/api/v1/theme/[...path]/route.ts +361 -0
- package/templates/app/api/v1/theme/docs.md +74 -0
- package/templates/app/api/v1/theme/presets.ts +21 -0
- package/templates/app/api/v1/theme/route.ts +96 -0
- package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +363 -0
- package/templates/app/api/v1/users/[id]/route.ts +302 -0
- package/templates/app/api/v1/users/docs.md +93 -0
- package/templates/app/api/v1/users/presets.ts +59 -0
- package/templates/app/api/v1/users/route.ts +197 -0
- package/templates/app/dashboard/(main)/[entity]/[id]/edit/page.tsx +117 -0
- package/templates/app/dashboard/(main)/[entity]/[id]/page.tsx +103 -0
- package/templates/app/dashboard/(main)/[entity]/create/page.tsx +95 -0
- package/templates/app/dashboard/(main)/[entity]/error.tsx +51 -0
- package/templates/app/dashboard/(main)/[entity]/layout.tsx +113 -0
- package/templates/app/dashboard/(main)/[entity]/loading.tsx +61 -0
- package/templates/app/dashboard/(main)/[entity]/page.tsx +90 -0
- package/templates/app/dashboard/(main)/layout.tsx +98 -0
- package/templates/app/dashboard/(main)/loading.tsx +5 -0
- package/templates/app/dashboard/(main)/page.tsx +201 -0
- package/templates/app/dashboard/(main)/patterns/[id]/edit/page.tsx +114 -0
- package/templates/app/dashboard/(main)/patterns/[id]/page.tsx +20 -0
- package/templates/app/dashboard/(main)/patterns/[id]/reports/page.tsx +171 -0
- package/templates/app/dashboard/(main)/patterns/create/page.tsx +86 -0
- package/templates/app/dashboard/(main)/patterns/page.tsx +444 -0
- package/templates/app/dashboard/features/analytics/page.tsx +35 -0
- package/templates/app/dashboard/features/automation/page.tsx +35 -0
- package/templates/app/dashboard/features/layout.tsx +13 -0
- package/templates/app/dashboard/features/loading.tsx +5 -0
- package/templates/app/dashboard/features/webhooks/page.tsx +35 -0
- package/templates/app/dashboard/layout.tsx +86 -0
- package/templates/app/dashboard/permission-denied/page.tsx +29 -0
- package/templates/app/dashboard/settings/api-keys/loading.tsx +5 -0
- package/templates/app/dashboard/settings/api-keys/page.tsx +513 -0
- package/templates/app/dashboard/settings/billing/loading.tsx +5 -0
- package/templates/app/dashboard/settings/billing/page.tsx +284 -0
- package/templates/app/dashboard/settings/invoices/[invoiceNumber]/page.tsx +222 -0
- package/templates/app/dashboard/settings/invoices/loading.tsx +5 -0
- package/templates/app/dashboard/settings/invoices/page.tsx +82 -0
- package/templates/app/dashboard/settings/layout.tsx +151 -0
- package/templates/app/dashboard/settings/loading.tsx +5 -0
- package/templates/app/dashboard/settings/notifications/loading.tsx +5 -0
- package/templates/app/dashboard/settings/notifications/page.tsx +462 -0
- package/templates/app/dashboard/settings/page.tsx +92 -0
- package/templates/app/dashboard/settings/password/loading.tsx +5 -0
- package/templates/app/dashboard/settings/password/page.tsx +306 -0
- package/templates/app/dashboard/settings/plans/loading.tsx +5 -0
- package/templates/app/dashboard/settings/plans/page.tsx +40 -0
- package/templates/app/dashboard/settings/profile/loading.tsx +5 -0
- package/templates/app/dashboard/settings/profile/page.tsx +686 -0
- package/templates/app/dashboard/settings/security/loading.tsx +5 -0
- package/templates/app/dashboard/settings/security/page.tsx +505 -0
- package/templates/app/dashboard/settings/teams/loading.tsx +5 -0
- package/templates/app/dashboard/settings/teams/page.tsx +272 -0
- package/templates/app/dashboard/settings/teams/permissions/page.tsx +92 -0
- package/templates/app/devtools/blocks/[slug]/page.tsx +39 -0
- package/templates/app/devtools/blocks/page.tsx +31 -0
- package/templates/app/devtools/config/page.tsx +31 -0
- package/templates/app/devtools/features/page.tsx +31 -0
- package/templates/app/devtools/flows/page.tsx +31 -0
- package/templates/app/devtools/layout.tsx +58 -0
- package/templates/app/devtools/page.tsx +121 -0
- package/templates/app/devtools/scheduled-actions/page.tsx +157 -0
- package/templates/app/devtools/style/page.tsx +330 -0
- package/templates/app/devtools/tags/page.tsx +31 -0
- package/templates/app/devtools/tests/[[...path]]/page.tsx +47 -0
- package/templates/app/favicon.ico +0 -0
- package/templates/app/globals.css +12 -0
- package/templates/app/layout.tsx +96 -0
- package/templates/app/public/page.tsx +30 -0
- package/templates/app/superadmin/docs/[section]/[page]/page.tsx +92 -0
- package/templates/app/superadmin/docs/page.tsx +75 -0
- package/templates/app/superadmin/layout.tsx +67 -0
- package/templates/app/superadmin/page.tsx +149 -0
- package/templates/app/superadmin/subscriptions/page.tsx +655 -0
- package/templates/app/superadmin/team-roles/page.tsx +493 -0
- package/templates/app/superadmin/teams/[teamId]/page.tsx +687 -0
- package/templates/app/superadmin/teams/page.tsx +302 -0
- package/templates/app/superadmin/users/[userId]/page.tsx +548 -0
- package/templates/app/superadmin/users/page.tsx +528 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Auth API
|
|
2
|
+
|
|
3
|
+
Authentication endpoints powered by Better Auth with NextSpark extensions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Auth API provides authentication functionality including sign up, sign in, password reset, and specialized flows like invitation-based registration. Core auth endpoints are handled by Better Auth at `/api/auth/*`, while custom extensions are available at `/api/v1/auth/*`.
|
|
8
|
+
|
|
9
|
+
## Better Auth Endpoints
|
|
10
|
+
|
|
11
|
+
These endpoints are handled by Better Auth at `/api/auth/*`:
|
|
12
|
+
|
|
13
|
+
### Sign Up
|
|
14
|
+
`POST /api/auth/sign-up/email`
|
|
15
|
+
|
|
16
|
+
Register a new user account.
|
|
17
|
+
|
|
18
|
+
**Request Body:**
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"email": "user@example.com",
|
|
22
|
+
"password": "securepassword",
|
|
23
|
+
"name": "John Doe"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Sign In
|
|
28
|
+
`POST /api/auth/sign-in/email`
|
|
29
|
+
|
|
30
|
+
Authenticate with email and password.
|
|
31
|
+
|
|
32
|
+
**Request Body:**
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"email": "user@example.com",
|
|
36
|
+
"password": "securepassword"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Sign Out
|
|
41
|
+
`POST /api/auth/sign-out`
|
|
42
|
+
|
|
43
|
+
End the current session.
|
|
44
|
+
|
|
45
|
+
### Get Session
|
|
46
|
+
`GET /api/auth/session`
|
|
47
|
+
|
|
48
|
+
Get current user session.
|
|
49
|
+
|
|
50
|
+
**Response:**
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"user": {
|
|
54
|
+
"id": "user_123",
|
|
55
|
+
"email": "user@example.com",
|
|
56
|
+
"name": "John Doe",
|
|
57
|
+
"emailVerified": true
|
|
58
|
+
},
|
|
59
|
+
"session": {
|
|
60
|
+
"id": "session_abc",
|
|
61
|
+
"expiresAt": "2024-02-15T10:00:00Z"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Forgot Password
|
|
67
|
+
`POST /api/auth/forget-password`
|
|
68
|
+
|
|
69
|
+
Request password reset email.
|
|
70
|
+
|
|
71
|
+
**Request Body:**
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"email": "user@example.com"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Reset Password
|
|
79
|
+
`POST /api/auth/reset-password`
|
|
80
|
+
|
|
81
|
+
Reset password with token from email.
|
|
82
|
+
|
|
83
|
+
**Request Body:**
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"token": "reset_token_from_email",
|
|
87
|
+
"newPassword": "newsecurepassword"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## NextSpark Extensions
|
|
94
|
+
|
|
95
|
+
### Sign Up with Invitation
|
|
96
|
+
`POST /api/v1/auth/signup-with-invite`
|
|
97
|
+
|
|
98
|
+
Create an account and automatically join a team via invitation. This is a single-step flow that:
|
|
99
|
+
1. Validates the invitation token
|
|
100
|
+
2. Creates the user account
|
|
101
|
+
3. Skips email verification (invitation proves ownership)
|
|
102
|
+
4. Adds user to the invited team with specified role
|
|
103
|
+
|
|
104
|
+
**Request Body:**
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"email": "user@example.com",
|
|
108
|
+
"password": "securepassword",
|
|
109
|
+
"firstName": "John",
|
|
110
|
+
"lastName": "Doe",
|
|
111
|
+
"inviteToken": "invitation_token_from_email"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Success Response (201):**
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"success": true,
|
|
119
|
+
"data": {
|
|
120
|
+
"user": {
|
|
121
|
+
"id": "user_123",
|
|
122
|
+
"email": "user@example.com",
|
|
123
|
+
"firstName": "John",
|
|
124
|
+
"lastName": "Doe",
|
|
125
|
+
"emailVerified": true
|
|
126
|
+
},
|
|
127
|
+
"teamId": "team_abc123",
|
|
128
|
+
"redirectTo": "/dashboard/settings/teams"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Validation Rules:**
|
|
134
|
+
- Email must match the invitation recipient
|
|
135
|
+
- Password minimum 8 characters
|
|
136
|
+
- Invitation must be pending and not expired
|
|
137
|
+
- Email format validation
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Error Responses
|
|
142
|
+
|
|
143
|
+
| Status | Code | Description |
|
|
144
|
+
|--------|------|-------------|
|
|
145
|
+
| 400 | MISSING_FIELDS | Required fields not provided |
|
|
146
|
+
| 400 | INVALID_EMAIL | Email format is invalid |
|
|
147
|
+
| 400 | INVALID_PASSWORD | Password doesn't meet requirements |
|
|
148
|
+
| 403 | EMAIL_MISMATCH | Email doesn't match invitation |
|
|
149
|
+
| 404 | INVITATION_NOT_FOUND | Invalid invitation token |
|
|
150
|
+
| 409 | USER_ALREADY_EXISTS | Account with email already exists |
|
|
151
|
+
| 409 | INVITATION_NOT_PENDING | Invitation already used or cancelled |
|
|
152
|
+
| 410 | INVITATION_EXPIRED | Invitation has expired |
|
|
153
|
+
| 500 | SIGNUP_FAILED | Account creation failed |
|
|
154
|
+
|
|
155
|
+
## OAuth Providers
|
|
156
|
+
|
|
157
|
+
If configured, these OAuth endpoints are available:
|
|
158
|
+
|
|
159
|
+
- `GET /api/auth/callback/google` - Google OAuth callback
|
|
160
|
+
- `GET /api/auth/callback/github` - GitHub OAuth callback
|
|
161
|
+
- `GET /api/auth/callback/microsoft` - Microsoft OAuth callback
|
|
162
|
+
|
|
163
|
+
## Session Management
|
|
164
|
+
|
|
165
|
+
Sessions are managed via secure HTTP-only cookies. Session duration and refresh behavior are configured in the Better Auth settings.
|
|
166
|
+
|
|
167
|
+
## API Key Authentication
|
|
168
|
+
|
|
169
|
+
For server-to-server requests, API keys can be used instead of session cookies:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
Authorization: Bearer sk_live_xxx
|
|
173
|
+
# or
|
|
174
|
+
x-api-key: sk_live_xxx
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
See the API Keys documentation for more information.
|
|
178
|
+
|
|
179
|
+
## Related APIs
|
|
180
|
+
|
|
181
|
+
- **[API Keys](/api/v1/api-keys)** - Create and manage API keys for server-to-server auth
|
|
182
|
+
- **[Teams](/api/v1/teams)** - Team management and invitation acceptance
|
|
183
|
+
- **[Team Invitations](/api/v1/team-invitations)** - Manage team invitations
|
|
184
|
+
- **[Users](/api/v1/users)** - User profile management after authentication
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Presets for Auth
|
|
3
|
+
*
|
|
4
|
+
* These presets appear in the DevTools API Explorer's "Presets" tab.
|
|
5
|
+
* Note: Core Better Auth endpoints are at /api/auth/*, not /api/v1/auth/*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineApiEndpoint } from '@nextsparkjs/core/types/api-presets'
|
|
9
|
+
|
|
10
|
+
export default defineApiEndpoint({
|
|
11
|
+
endpoint: '/api/v1/auth',
|
|
12
|
+
summary: 'Authentication with Better Auth and NextSpark extensions',
|
|
13
|
+
presets: [
|
|
14
|
+
// NextSpark extension: signup with invite
|
|
15
|
+
{
|
|
16
|
+
id: 'signup-with-invite',
|
|
17
|
+
title: 'Sign Up with Invitation',
|
|
18
|
+
description: 'Create account and join team via invitation token',
|
|
19
|
+
method: 'POST',
|
|
20
|
+
path: '/signup-with-invite',
|
|
21
|
+
payload: {
|
|
22
|
+
email: 'newuser@example.com',
|
|
23
|
+
password: 'Test1234',
|
|
24
|
+
firstName: 'John',
|
|
25
|
+
lastName: 'Doe',
|
|
26
|
+
inviteToken: 'invitation_token_here'
|
|
27
|
+
},
|
|
28
|
+
tags: ['write', 'signup']
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'signup-with-invite-minimal',
|
|
32
|
+
title: 'Sign Up (Minimal)',
|
|
33
|
+
description: 'Sign up with only required fields',
|
|
34
|
+
method: 'POST',
|
|
35
|
+
path: '/signup-with-invite',
|
|
36
|
+
payload: {
|
|
37
|
+
email: 'user@example.com',
|
|
38
|
+
password: 'SecurePass123',
|
|
39
|
+
inviteToken: 'invitation_token_here'
|
|
40
|
+
},
|
|
41
|
+
tags: ['write', 'signup']
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
})
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { auth } from '@nextsparkjs/core/lib/auth'
|
|
3
|
+
import { queryOneWithRLS, mutateWithRLS, getTransactionClient } from '@nextsparkjs/core/lib/db'
|
|
4
|
+
import {
|
|
5
|
+
createApiResponse,
|
|
6
|
+
createApiError,
|
|
7
|
+
withApiLogging,
|
|
8
|
+
handleCorsPreflightRequest,
|
|
9
|
+
addCorsHeaders,
|
|
10
|
+
} from '@nextsparkjs/core/lib/api/helpers'
|
|
11
|
+
import type { TeamInvitation, TeamMember } from '@nextsparkjs/core/lib/teams/types'
|
|
12
|
+
import { I18N_CONFIG } from '@nextsparkjs/core/lib/config'
|
|
13
|
+
import { withSignupContext } from '@nextsparkjs/core/lib/auth-context'
|
|
14
|
+
|
|
15
|
+
// Handle CORS preflight
|
|
16
|
+
export async function OPTIONS() {
|
|
17
|
+
return handleCorsPreflightRequest()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SignupWithInviteBody {
|
|
21
|
+
email: string
|
|
22
|
+
password: string
|
|
23
|
+
firstName?: string
|
|
24
|
+
lastName?: string
|
|
25
|
+
inviteToken: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// POST /api/v1/auth/signup-with-invite - Create account and auto-accept invitation
|
|
29
|
+
export const POST = withApiLogging(
|
|
30
|
+
async (req: NextRequest): Promise<NextResponse> => {
|
|
31
|
+
try {
|
|
32
|
+
const body: SignupWithInviteBody = await req.json()
|
|
33
|
+
const { email, password, firstName, lastName, inviteToken } = body
|
|
34
|
+
|
|
35
|
+
// Validate required fields
|
|
36
|
+
if (!email || !password || !inviteToken) {
|
|
37
|
+
const response = createApiError(
|
|
38
|
+
'Email, password, and invitation token are required',
|
|
39
|
+
400,
|
|
40
|
+
null,
|
|
41
|
+
'MISSING_FIELDS'
|
|
42
|
+
)
|
|
43
|
+
return addCorsHeaders(response)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate email format
|
|
47
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
48
|
+
if (!emailRegex.test(email)) {
|
|
49
|
+
const response = createApiError('Invalid email format', 400, null, 'INVALID_EMAIL')
|
|
50
|
+
return addCorsHeaders(response)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate password length (min 8 characters as per Better Auth config)
|
|
54
|
+
if (password.length < 8) {
|
|
55
|
+
const response = createApiError(
|
|
56
|
+
'Password must be at least 8 characters',
|
|
57
|
+
400,
|
|
58
|
+
null,
|
|
59
|
+
'INVALID_PASSWORD'
|
|
60
|
+
)
|
|
61
|
+
return addCorsHeaders(response)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Step 1: Validate invitation token (without RLS since user doesn't exist yet)
|
|
65
|
+
const invitation = await queryOneWithRLS<TeamInvitation>(
|
|
66
|
+
'SELECT * FROM "team_invitations" WHERE token = $1',
|
|
67
|
+
[inviteToken],
|
|
68
|
+
'system' // Use system context for initial validation
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (!invitation) {
|
|
72
|
+
const response = createApiError('Invitation not found', 404, null, 'INVITATION_NOT_FOUND')
|
|
73
|
+
return addCorsHeaders(response)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Verify invitation is for the correct email
|
|
77
|
+
if (invitation.email.toLowerCase() !== email.toLowerCase()) {
|
|
78
|
+
const response = createApiError(
|
|
79
|
+
'This invitation was sent to a different email address',
|
|
80
|
+
403,
|
|
81
|
+
null,
|
|
82
|
+
'EMAIL_MISMATCH'
|
|
83
|
+
)
|
|
84
|
+
return addCorsHeaders(response)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if invitation is pending
|
|
88
|
+
if (invitation.status !== 'pending') {
|
|
89
|
+
const response = createApiError(
|
|
90
|
+
`Invitation has already been ${invitation.status}`,
|
|
91
|
+
409,
|
|
92
|
+
null,
|
|
93
|
+
'INVITATION_NOT_PENDING'
|
|
94
|
+
)
|
|
95
|
+
return addCorsHeaders(response)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if invitation has expired
|
|
99
|
+
const expiresAt = new Date(invitation.expiresAt)
|
|
100
|
+
if (expiresAt < new Date()) {
|
|
101
|
+
const response = createApiError('Invitation has expired', 410, null, 'INVITATION_EXPIRED')
|
|
102
|
+
return addCorsHeaders(response)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 2: Create user using Better Auth's internal API
|
|
106
|
+
// Wrap in signup context to skip automatic team creation
|
|
107
|
+
// (user will be added to the invited team instead)
|
|
108
|
+
const signUpRequest = new Request(`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/sign-up/email`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
email,
|
|
115
|
+
password,
|
|
116
|
+
name: firstName && lastName ? `${firstName} ${lastName}` : firstName || '',
|
|
117
|
+
firstName,
|
|
118
|
+
lastName,
|
|
119
|
+
language: I18N_CONFIG.defaultLocale,
|
|
120
|
+
}),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Use signup context to signal that we should skip team creation
|
|
124
|
+
// The user will be added to the invited team instead
|
|
125
|
+
const signUpResponse = await withSignupContext(
|
|
126
|
+
{ skipTeamCreation: true, invitedTeamId: invitation.teamId },
|
|
127
|
+
() => auth.handler(signUpRequest)
|
|
128
|
+
) as Response
|
|
129
|
+
|
|
130
|
+
if (!signUpResponse.ok) {
|
|
131
|
+
const errorData = await signUpResponse.json() as { message?: string; code?: string }
|
|
132
|
+
|
|
133
|
+
// Check if user already exists
|
|
134
|
+
if (errorData.message?.includes('already exists') || errorData.code === 'USER_ALREADY_EXISTS') {
|
|
135
|
+
const response = createApiError(
|
|
136
|
+
'An account with this email already exists. Please sign in instead.',
|
|
137
|
+
409,
|
|
138
|
+
null,
|
|
139
|
+
'USER_ALREADY_EXISTS'
|
|
140
|
+
)
|
|
141
|
+
return addCorsHeaders(response)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const response = createApiError(
|
|
145
|
+
errorData.message || 'Failed to create account',
|
|
146
|
+
signUpResponse.status,
|
|
147
|
+
null,
|
|
148
|
+
'SIGNUP_FAILED'
|
|
149
|
+
)
|
|
150
|
+
return addCorsHeaders(response)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parse response to get user data
|
|
154
|
+
const signUpData = await signUpResponse.json()
|
|
155
|
+
const userId = signUpData.user?.id
|
|
156
|
+
|
|
157
|
+
if (!userId) {
|
|
158
|
+
const response = createApiError(
|
|
159
|
+
'Failed to create account - no user ID returned',
|
|
160
|
+
500,
|
|
161
|
+
null,
|
|
162
|
+
'SIGNUP_FAILED'
|
|
163
|
+
)
|
|
164
|
+
return addCorsHeaders(response)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Step 3: Mark email as verified (skip email verification since invitation proves email ownership)
|
|
168
|
+
await mutateWithRLS(
|
|
169
|
+
'UPDATE "users" SET "emailVerified" = true, "updatedAt" = CURRENT_TIMESTAMP WHERE id = $1',
|
|
170
|
+
[userId],
|
|
171
|
+
userId
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// Step 4: Accept the invitation (add user to team)
|
|
175
|
+
const tx = await getTransactionClient(userId)
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Add user as team member
|
|
179
|
+
const [member] = await tx.query<TeamMember>(
|
|
180
|
+
`INSERT INTO "team_members" ("teamId", "userId", role, "invitedBy", "joinedAt")
|
|
181
|
+
VALUES ($1, $2, $3, $4, NOW())
|
|
182
|
+
RETURNING *`,
|
|
183
|
+
[invitation.teamId, userId, invitation.role, invitation.invitedBy]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if (!member) {
|
|
187
|
+
throw new Error('Failed to create team member')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Update invitation status
|
|
191
|
+
await tx.query(
|
|
192
|
+
`UPDATE "team_invitations"
|
|
193
|
+
SET status = 'accepted', "acceptedAt" = NOW(), "updatedAt" = CURRENT_TIMESTAMP
|
|
194
|
+
WHERE id = $1`,
|
|
195
|
+
[invitation.id]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
await tx.commit()
|
|
199
|
+
|
|
200
|
+
// Step 5: Return success with user info and redirect URL
|
|
201
|
+
const response = createApiResponse(
|
|
202
|
+
{
|
|
203
|
+
user: {
|
|
204
|
+
id: userId,
|
|
205
|
+
email,
|
|
206
|
+
firstName,
|
|
207
|
+
lastName,
|
|
208
|
+
emailVerified: true,
|
|
209
|
+
},
|
|
210
|
+
teamId: invitation.teamId,
|
|
211
|
+
redirectTo: '/dashboard/settings/teams',
|
|
212
|
+
},
|
|
213
|
+
{ created: true },
|
|
214
|
+
201
|
|
215
|
+
)
|
|
216
|
+
return addCorsHeaders(response)
|
|
217
|
+
} catch (error) {
|
|
218
|
+
await tx.rollback()
|
|
219
|
+
throw error
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Error in signup-with-invite:', error)
|
|
223
|
+
const response = createApiError('Internal server error', 500)
|
|
224
|
+
return addCorsHeaders(response)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cancel Subscription Endpoint
|
|
3
|
+
*
|
|
4
|
+
* Allows users to cancel their subscription directly without using Stripe Portal.
|
|
5
|
+
* Supports both soft cancel (at period end) and hard cancel (immediate).
|
|
6
|
+
*
|
|
7
|
+
* P1-4: Cancel subscription directo
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { NextRequest } from 'next/server'
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
import { authenticateRequest, createAuthError } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
13
|
+
import { SubscriptionService, MembershipService } from '@nextsparkjs/core/lib/services'
|
|
14
|
+
import {
|
|
15
|
+
cancelSubscriptionAtPeriodEnd,
|
|
16
|
+
cancelSubscriptionImmediately,
|
|
17
|
+
reactivateSubscription
|
|
18
|
+
} from '@nextsparkjs/core/lib/billing/gateways/stripe'
|
|
19
|
+
import { queryWithRLS } from '@nextsparkjs/core/lib/db'
|
|
20
|
+
|
|
21
|
+
const cancelSchema = z.object({
|
|
22
|
+
immediate: z.boolean().optional().default(false),
|
|
23
|
+
reason: z.string().optional()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* POST /api/v1/billing/cancel
|
|
28
|
+
* Cancel the team's active subscription
|
|
29
|
+
*
|
|
30
|
+
* Body:
|
|
31
|
+
* - immediate: boolean (default: false) - If true, cancels immediately. If false, cancels at period end.
|
|
32
|
+
* - reason: string (optional) - Reason for cancellation (stored in metadata)
|
|
33
|
+
*/
|
|
34
|
+
export async function POST(request: NextRequest) {
|
|
35
|
+
// 1. Dual authentication
|
|
36
|
+
const authResult = await authenticateRequest(request)
|
|
37
|
+
|
|
38
|
+
if (!authResult.success || !authResult.user) {
|
|
39
|
+
return createAuthError('Unauthorized', 401)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Get team context
|
|
43
|
+
const teamId = request.headers.get('x-team-id') || authResult.user.defaultTeamId
|
|
44
|
+
|
|
45
|
+
if (!teamId) {
|
|
46
|
+
return Response.json(
|
|
47
|
+
{
|
|
48
|
+
success: false,
|
|
49
|
+
error: 'No team context available. Please provide x-team-id header.'
|
|
50
|
+
},
|
|
51
|
+
{ status: 400 }
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. Permission check using MembershipService
|
|
56
|
+
const membership = await MembershipService.get(authResult.user.id, teamId)
|
|
57
|
+
const actionResult = membership.canPerformAction('billing.cancel')
|
|
58
|
+
|
|
59
|
+
if (!actionResult.allowed) {
|
|
60
|
+
return Response.json(
|
|
61
|
+
{
|
|
62
|
+
success: false,
|
|
63
|
+
error: actionResult.message,
|
|
64
|
+
reason: actionResult.reason,
|
|
65
|
+
meta: actionResult.meta,
|
|
66
|
+
},
|
|
67
|
+
{ status: 403 }
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Parse and validate request body
|
|
72
|
+
let body: Record<string, unknown>
|
|
73
|
+
try {
|
|
74
|
+
body = await request.json()
|
|
75
|
+
} catch {
|
|
76
|
+
return Response.json(
|
|
77
|
+
{ success: false, error: 'Invalid JSON body' },
|
|
78
|
+
{ status: 400 }
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if this is a reactivation request
|
|
83
|
+
if (body.action === 'reactivate') {
|
|
84
|
+
return handleReactivation(teamId)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Otherwise, it's a cancel request
|
|
88
|
+
const parseResult = cancelSchema.safeParse(body)
|
|
89
|
+
if (!parseResult.success) {
|
|
90
|
+
return Response.json(
|
|
91
|
+
{ success: false, error: 'Invalid request body', details: parseResult.error.issues },
|
|
92
|
+
{ status: 400 }
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { immediate, reason } = parseResult.data
|
|
97
|
+
|
|
98
|
+
// 5. Get active subscription
|
|
99
|
+
const subscription = await SubscriptionService.getActive(teamId)
|
|
100
|
+
|
|
101
|
+
if (!subscription || !subscription.externalSubscriptionId) {
|
|
102
|
+
return Response.json(
|
|
103
|
+
{ success: false, error: 'No active subscription found' },
|
|
104
|
+
{ status: 404 }
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 6. Cancel via Stripe
|
|
109
|
+
try {
|
|
110
|
+
if (immediate) {
|
|
111
|
+
await cancelSubscriptionImmediately(subscription.externalSubscriptionId)
|
|
112
|
+
} else {
|
|
113
|
+
await cancelSubscriptionAtPeriodEnd(subscription.externalSubscriptionId)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 7. Update local DB
|
|
117
|
+
await queryWithRLS(
|
|
118
|
+
`UPDATE subscriptions
|
|
119
|
+
SET "cancelAtPeriodEnd" = $1,
|
|
120
|
+
"canceledAt" = $2,
|
|
121
|
+
metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{cancelReason}', $3::jsonb),
|
|
122
|
+
"updatedAt" = now()
|
|
123
|
+
WHERE id = $4`,
|
|
124
|
+
[
|
|
125
|
+
!immediate, // cancelAtPeriodEnd is true for soft cancel
|
|
126
|
+
immediate ? new Date() : null,
|
|
127
|
+
JSON.stringify(reason || 'User requested'),
|
|
128
|
+
subscription.id
|
|
129
|
+
]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return Response.json({
|
|
133
|
+
success: true,
|
|
134
|
+
data: {
|
|
135
|
+
canceledAt: immediate ? new Date().toISOString() : null,
|
|
136
|
+
cancelAtPeriodEnd: !immediate,
|
|
137
|
+
periodEnd: subscription.currentPeriodEnd?.toISOString() || null,
|
|
138
|
+
message: immediate
|
|
139
|
+
? 'Subscription canceled immediately'
|
|
140
|
+
: 'Subscription will cancel at the end of the current billing period'
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('[cancel] Error canceling subscription:', error)
|
|
145
|
+
return Response.json(
|
|
146
|
+
{
|
|
147
|
+
success: false,
|
|
148
|
+
error: error instanceof Error ? error.message : 'Failed to cancel subscription'
|
|
149
|
+
},
|
|
150
|
+
{ status: 500 }
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handle reactivation of a subscription that was scheduled to cancel
|
|
157
|
+
*/
|
|
158
|
+
async function handleReactivation(teamId: string) {
|
|
159
|
+
const subscription = await SubscriptionService.getActive(teamId)
|
|
160
|
+
|
|
161
|
+
if (!subscription || !subscription.externalSubscriptionId) {
|
|
162
|
+
return Response.json(
|
|
163
|
+
{ success: false, error: 'No active subscription found' },
|
|
164
|
+
{ status: 404 }
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!subscription.cancelAtPeriodEnd) {
|
|
169
|
+
return Response.json(
|
|
170
|
+
{ success: false, error: 'Subscription is not scheduled for cancellation' },
|
|
171
|
+
{ status: 400 }
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await reactivateSubscription(subscription.externalSubscriptionId)
|
|
177
|
+
|
|
178
|
+
// Update local DB
|
|
179
|
+
await queryWithRLS(
|
|
180
|
+
`UPDATE subscriptions
|
|
181
|
+
SET "cancelAtPeriodEnd" = false,
|
|
182
|
+
"canceledAt" = NULL,
|
|
183
|
+
metadata = metadata - 'cancelReason',
|
|
184
|
+
"updatedAt" = now()
|
|
185
|
+
WHERE id = $1`,
|
|
186
|
+
[subscription.id]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return Response.json({
|
|
190
|
+
success: true,
|
|
191
|
+
data: {
|
|
192
|
+
reactivated: true,
|
|
193
|
+
message: 'Subscription reactivated successfully'
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('[cancel] Error reactivating subscription:', error)
|
|
198
|
+
return Response.json(
|
|
199
|
+
{
|
|
200
|
+
success: false,
|
|
201
|
+
error: error instanceof Error ? error.message : 'Failed to reactivate subscription'
|
|
202
|
+
},
|
|
203
|
+
{ status: 500 }
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|