create-ej-app 26.0.0 → 28.0.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/README.md +125 -226
- package/cli.ts +79 -29
- package/package.json +2 -2
- package/template/api/.vscode/settings.json +1 -1
- package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
- package/template/api/CLAUDE.md +1 -0
- package/template/api/package.json +10 -10
- package/template/nextjs/.dockerignore +1 -0
- package/template/nextjs/AGENTS.md +260 -0
- package/template/nextjs/CLAUDE.md +1 -0
- package/template/nextjs/README.md +109 -0
- package/template/nextjs/bunfig.toml +3 -0
- package/template/nextjs/eslint.config.mjs +6 -5
- package/template/nextjs/gitignore +6 -1
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -39
- package/template/nextjs/public/logo-dark.svg +4 -0
- package/template/nextjs/public/logo.svg +4 -0
- package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
- package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
- package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
- package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
- package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
- package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
- package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
- package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
- package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
- package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
- package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
- package/template/nextjs/src/app/error.tsx +37 -0
- package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/index.ts +60 -0
- package/template/nextjs/src/app/global-error.tsx +38 -0
- package/template/nextjs/src/app/globals.css +29 -0
- package/template/nextjs/src/app/layout.tsx +45 -18
- package/template/nextjs/src/app/not-found.tsx +25 -3
- package/template/nextjs/src/app/robots.ts +29 -0
- package/template/nextjs/src/app/sitemap.ts +21 -0
- package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
- package/template/nextjs/src/components/dashboard-header.tsx +70 -0
- package/template/nextjs/src/components/data-table/enum.ts +5 -0
- package/template/nextjs/src/components/data-table/features.ts +49 -0
- package/template/nextjs/src/components/data-table/index.tsx +961 -0
- package/template/nextjs/src/components/data-table/types.ts +76 -0
- package/template/nextjs/src/components/error.tsx +71 -0
- package/template/nextjs/src/components/loader.tsx +18 -2
- package/template/nextjs/src/components/logo.tsx +49 -0
- package/template/nextjs/src/components/navigation-search.tsx +140 -0
- package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
- package/template/nextjs/src/components/page-heading.tsx +38 -0
- package/template/nextjs/src/components/react-query-provider.tsx +41 -2
- package/template/nextjs/src/components/route-error.tsx +55 -0
- package/template/nextjs/src/components/screen-size.tsx +36 -0
- package/template/nextjs/src/components/site-footer.tsx +20 -4
- package/template/nextjs/src/components/theme-provider.tsx +48 -0
- package/template/nextjs/src/components/theme-toggle.tsx +58 -5
- package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
- package/template/nextjs/src/components/ui/avatar.tsx +112 -0
- package/template/nextjs/src/components/ui/badge.tsx +54 -0
- package/template/nextjs/src/components/ui/button.tsx +3 -1
- package/template/nextjs/src/components/ui/card.tsx +4 -4
- package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
- package/template/nextjs/src/components/ui/command.tsx +192 -0
- package/template/nextjs/src/components/ui/dialog.tsx +164 -0
- package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/nextjs/src/components/ui/input-group.tsx +156 -0
- package/template/nextjs/src/components/ui/select.tsx +195 -0
- package/template/nextjs/src/components/ui/sheet.tsx +143 -0
- package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
- package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
- package/template/nextjs/src/components/ui/table.tsx +116 -0
- package/template/nextjs/src/components/ui/textarea.tsx +18 -0
- package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
- package/template/nextjs/src/components/user-menu.tsx +206 -0
- package/template/nextjs/src/db/common.ts +10 -0
- package/template/nextjs/src/db/index.ts +16 -6
- package/template/nextjs/src/db/relations.ts +21 -0
- package/template/nextjs/src/db/schema.ts +85 -46
- package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
- package/template/nextjs/src/emails/magic-link.tsx +15 -13
- package/template/nextjs/src/emails/test.tsx +11 -9
- package/template/nextjs/src/emails/welcome.tsx +12 -9
- package/template/nextjs/src/env.ts +1 -1
- package/template/nextjs/src/hooks/use-mobile.ts +14 -12
- package/template/nextjs/src/instrumentation.ts +20 -2
- package/template/nextjs/src/lib/actions.ts +55 -0
- package/template/nextjs/src/lib/admin-navigation.ts +43 -0
- package/template/nextjs/src/lib/auth-client.ts +20 -9
- package/template/nextjs/src/lib/auth-permissions.ts +61 -0
- package/template/nextjs/src/lib/auth.ts +33 -3
- package/template/nextjs/src/lib/callback-url.ts +28 -0
- package/template/nextjs/src/lib/constants.ts +21 -0
- package/template/nextjs/src/lib/delay.ts +10 -0
- package/template/nextjs/src/lib/email-service.ts +21 -0
- package/template/nextjs/src/lib/errors.ts +74 -0
- package/template/nextjs/src/lib/format-currency.ts +22 -24
- package/template/nextjs/src/lib/format-date.ts +69 -6
- package/template/nextjs/src/lib/nodemailer.ts +27 -3
- package/template/nextjs/src/lib/otel.ts +92 -2
- package/template/nextjs/src/lib/safe-action-client.ts +60 -9
- package/template/nextjs/src/lib/safe-action.ts +152 -16
- package/template/nextjs/src/lib/search.ts +16 -0
- package/template/nextjs/src/lib/seo.ts +7 -0
- package/template/nextjs/src/lib/siteConfig.ts +17 -1
- package/template/nextjs/src/lib/toast-message.ts +87 -1
- package/template/nextjs/src/lib/validations.ts +30 -0
- package/template/nextjs/src/proxy.ts +48 -0
- package/template/nextjs/src/scripts/reset-db.ts +22 -0
- package/template/nextjs/src/scripts/seed.ts +59 -8
- package/template/nextjs/src/scripts/test-mail.ts +28 -20
- package/template/nextjs/tests/admin/users.test.ts +64 -0
- package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
- package/template/nextjs/tests/db/seed.test.ts +49 -0
- package/template/nextjs/tests/helpers/action.ts +71 -0
- package/template/nextjs/tests/helpers/fixtures.ts +78 -0
- package/template/nextjs/tests/helpers/index.ts +2 -0
- package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
- package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/nextjs/tests/lib/errors.test.ts +10 -0
- package/template/nextjs/tests/lib/search.test.ts +23 -0
- package/template/nextjs/tests/setup/pglite-db.ts +63 -0
- package/template/nextjs/tests/setup/preload.ts +95 -0
- package/template/nextjs/tsconfig.json +1 -0
- package/template/saas-starter/.dockerignore +1 -0
- package/template/saas-starter/.env.example +15 -15
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/README.md +124 -2
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/eslint.config.mjs +6 -5
- package/template/saas-starter/gitignore +6 -1
- package/template/saas-starter/next.config.ts +19 -1
- package/template/saas-starter/package.json +50 -44
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
- package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
- package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
- package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
- package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
- package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
- package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
- package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
- package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
- package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
- package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
- package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
- package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
- package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
- package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
- package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
- package/template/saas-starter/src/app/actions/uploads.ts +36 -0
- package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/index.ts +60 -0
- package/template/saas-starter/src/app/global-error.tsx +38 -0
- package/template/saas-starter/src/app/globals.css +29 -0
- package/template/saas-starter/src/app/layout.tsx +43 -19
- package/template/saas-starter/src/app/not-found.tsx +25 -3
- package/template/saas-starter/src/app/robots.ts +14 -10
- package/template/saas-starter/src/app/sitemap.ts +2 -1
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +65 -64
- package/template/saas-starter/src/components/data-table/types.ts +5 -4
- package/template/saas-starter/src/components/error.tsx +45 -0
- package/template/saas-starter/src/components/file-upload.tsx +65 -26
- package/template/saas-starter/src/components/loader.tsx +18 -2
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +2 -2
- package/template/saas-starter/src/components/page-heading.tsx +6 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +36 -0
- package/template/saas-starter/src/components/site-footer.tsx +20 -4
- package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
- package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
- package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
- package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
- package/template/saas-starter/src/components/theme-provider.tsx +48 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/badge.tsx +6 -1
- package/template/saas-starter/src/components/ui/button.tsx +2 -0
- package/template/saas-starter/src/components/ui/command.tsx +1 -1
- package/template/saas-starter/src/components/user-menu.tsx +147 -24
- package/template/saas-starter/src/db/common.ts +33 -1
- package/template/saas-starter/src/db/enums.ts +24 -0
- package/template/saas-starter/src/db/index.ts +14 -5
- package/template/saas-starter/src/db/relations.ts +40 -3
- package/template/saas-starter/src/db/schema.ts +186 -35
- package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
- package/template/saas-starter/src/emails/invitation.tsx +34 -21
- package/template/saas-starter/src/emails/magic-link.tsx +15 -13
- package/template/saas-starter/src/emails/test.tsx +11 -9
- package/template/saas-starter/src/emails/welcome.tsx +12 -9
- package/template/saas-starter/src/env.ts +1 -1
- package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
- package/template/saas-starter/src/instrumentation.ts +20 -2
- package/template/saas-starter/src/lib/actions.ts +34 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
- package/template/saas-starter/src/lib/app-navigation.ts +45 -41
- package/template/saas-starter/src/lib/auth-client.ts +8 -3
- package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
- package/template/saas-starter/src/lib/auth.ts +122 -21
- package/template/saas-starter/src/lib/billing.ts +59 -24
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/constants.ts +78 -9
- package/template/saas-starter/src/lib/delay.ts +10 -0
- package/template/saas-starter/src/lib/email-service.ts +30 -1
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +22 -24
- package/template/saas-starter/src/lib/format-date.ts +69 -6
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +27 -3
- package/template/saas-starter/src/lib/organization-access.ts +56 -76
- package/template/saas-starter/src/lib/otel.ts +92 -2
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +40 -13
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
- package/template/saas-starter/src/lib/safe-action.ts +137 -17
- package/template/saas-starter/src/lib/search.ts +16 -0
- package/template/saas-starter/src/lib/seo.ts +7 -0
- package/template/saas-starter/src/lib/siteConfig.ts +17 -1
- package/template/saas-starter/src/lib/storage.ts +176 -46
- package/template/saas-starter/src/lib/stripe.ts +4 -1
- package/template/saas-starter/src/lib/toast-message.ts +87 -1
- package/template/saas-starter/src/lib/validations.ts +85 -2
- package/template/saas-starter/src/proxy.ts +48 -0
- package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
- package/template/saas-starter/src/scripts/seed.ts +82 -8
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
- package/template/saas-starter/src/scripts/test-mail.ts +28 -20
- package/template/saas-starter/tests/admin/users.test.ts +64 -0
- package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
- package/template/saas-starter/tests/db/seed.test.ts +57 -0
- package/template/saas-starter/tests/helpers/action.ts +71 -0
- package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
- package/template/saas-starter/tests/helpers/index.ts +2 -0
- package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
- package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/saas-starter/tests/lib/errors.test.ts +10 -0
- package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
- package/template/saas-starter/tests/lib/search.test.ts +23 -0
- package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
- package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
- package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
- package/template/saas-starter/tests/setup/preload.ts +108 -0
- package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
- package/template/saas-starter/tsconfig.json +1 -0
- package/cli.js +0 -147
- package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
- package/template/nextjs/src/app/(app)/layout.tsx +0 -20
- package/template/nextjs/terraform/artifact-registry.tf +0 -5
- package/template/nextjs/terraform/backend-service.tf +0 -13
- package/template/nextjs/terraform/cleanup.sh +0 -9
- package/template/nextjs/terraform/cloudbuild.tf +0 -70
- package/template/nextjs/terraform/cloudrun.tf +0 -68
- package/template/nextjs/terraform/database.tf +0 -60
- package/template/nextjs/terraform/forwarding-rule.tf +0 -43
- package/template/nextjs/terraform/gitignore +0 -4
- package/template/nextjs/terraform/ip-address.tf +0 -11
- package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
- package/template/nextjs/terraform/network.tf +0 -21
- package/template/nextjs/terraform/pre.sh +0 -3
- package/template/nextjs/terraform/providers.tf +0 -18
- package/template/nextjs/terraform/secret.txt +0 -11
- package/template/nextjs/terraform/setup.sh +0 -25
- package/template/nextjs/terraform/ssl-certs.tf +0 -32
- package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
- package/template/nextjs/terraform/target-https-proxy.tf +0 -14
- package/template/nextjs/terraform/terraform.tfvars +0 -4
- package/template/nextjs/terraform/url-map.tf +0 -23
- package/template/nextjs/terraform/variables.tf +0 -21
- package/template/saas-starter/next-env.d.ts +0 -6
- package/template/saas-starter/src/actions/uploads.ts +0 -28
- package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
- package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
- package/template/saas-starter/src/app/proxy.ts +0 -28
- package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
- package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
|
@@ -2,18 +2,20 @@ import * as React from 'react'
|
|
|
2
2
|
|
|
3
3
|
const MOBILE_BREAKPOINT = 768
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const
|
|
5
|
+
function subscribe(onStoreChange: () => void) {
|
|
6
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
7
|
+
mql.addEventListener('change', onStoreChange)
|
|
8
|
+
return () => mql.removeEventListener('change', onStoreChange)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getSnapshot() {
|
|
12
|
+
return window.innerWidth < MOBILE_BREAKPOINT
|
|
13
|
+
}
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
-
}
|
|
13
|
-
mql.addEventListener('change', onChange)
|
|
14
|
-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
-
return () => mql.removeEventListener('change', onChange)
|
|
16
|
-
}, [])
|
|
15
|
+
function getServerSnapshot() {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
export function useIsMobile() {
|
|
20
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
19
21
|
}
|
|
@@ -5,12 +5,25 @@ import {
|
|
|
5
5
|
SimpleSpanProcessor
|
|
6
6
|
} from '@opentelemetry/sdk-trace-base'
|
|
7
7
|
import { registerOTel } from '@vercel/otel'
|
|
8
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Custom SpanProcessor that samples spans to reduce telemetry volume.
|
|
12
|
+
* Exports every error span, exports every span slower than 5s.
|
|
13
|
+
*/
|
|
9
14
|
class SmartSamplingSpanProcessor extends SimpleSpanProcessor {
|
|
15
|
+
/**
|
|
16
|
+
* Called when a span ends. Evaluates whether the span should be exported based on its properties.
|
|
17
|
+
*
|
|
18
|
+
* @param span - The readable span that has just ended
|
|
19
|
+
*/
|
|
10
20
|
onEnd(span: ReadableSpan): void {
|
|
11
21
|
const isError = span.status.code === SpanStatusCode.ERROR
|
|
22
|
+
// span.duration is [seconds, nanoseconds]
|
|
23
|
+
// Spans slower than 5s
|
|
12
24
|
const isSlow = span.duration[0] * 1000 + span.duration[1] / 1e6 > 5000
|
|
13
|
-
|
|
25
|
+
// Random sampling: Adjust probability as needed (e.g. 0.1 for 10%)
|
|
26
|
+
const isLucky = Math.random() < 0 // Currently disabled for non-essential traces
|
|
14
27
|
|
|
15
28
|
if (isError || isSlow || isLucky) {
|
|
16
29
|
super.onEnd(span)
|
|
@@ -18,9 +31,14 @@ class SmartSamplingSpanProcessor extends SimpleSpanProcessor {
|
|
|
18
31
|
}
|
|
19
32
|
}
|
|
20
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Registers OpenTelemetry instrumentation for the application.
|
|
36
|
+
* This function should be exported as `register` from `src/instrumentation.ts`
|
|
37
|
+
* to be automatically picked up by Next.js.
|
|
38
|
+
*/
|
|
21
39
|
export function register() {
|
|
22
40
|
registerOTel({
|
|
23
|
-
serviceName:
|
|
41
|
+
serviceName: siteConfig.name,
|
|
24
42
|
spanProcessors: [new SmartSamplingSpanProcessor(new ConsoleSpanExporter())]
|
|
25
43
|
})
|
|
26
44
|
}
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable Zod fragments for paginated, sortable list actions.
|
|
3
|
+
*/
|
|
1
4
|
import { z } from 'zod'
|
|
2
5
|
import { SortOrderEnum } from '~/components/data-table/enum'
|
|
3
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Returns a reusable Zod schema fragment for cursor-less offset pagination.
|
|
9
|
+
*
|
|
10
|
+
* - `page`: 1-based page number (defaults to 1)
|
|
11
|
+
* - `limit`: Number of items per page; clamped between 10 and 100 (defaults to `defaultPageSize`)
|
|
12
|
+
*
|
|
13
|
+
* Spread the returned object into a `z.object({})` call to add pagination
|
|
14
|
+
* to any query schema.
|
|
15
|
+
*
|
|
16
|
+
* @param defaultPageSize - Default number of items per page (defaults to 10)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* z.object({
|
|
20
|
+
* ...getPaginationSchema(20),
|
|
21
|
+
* ...getSortSchema(['title', 'createdAt'])
|
|
22
|
+
* })
|
|
23
|
+
*/
|
|
4
24
|
export const getPaginationSchema = (defaultPageSize = 10) => ({
|
|
5
25
|
page: z.coerce.number().int().positive().optional().default(1),
|
|
6
26
|
limit: z.coerce
|
|
@@ -13,6 +33,20 @@ export const getPaginationSchema = (defaultPageSize = 10) => ({
|
|
|
13
33
|
.default(defaultPageSize)
|
|
14
34
|
})
|
|
15
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Generates a reusable Zod schema fragment for sort parameters.
|
|
38
|
+
*
|
|
39
|
+
* @param keys - A non-empty tuple of the allowed `sortBy` field names for
|
|
40
|
+
* the resource (e.g. `['title', 'createdAt', 'updatedAt']`).
|
|
41
|
+
* @returns An object with `sortOrder` and `sortBy` Zod validators that can
|
|
42
|
+
* be spread into a `z.object({})` query schema.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* z.object({
|
|
46
|
+
* ...paginationSchema,
|
|
47
|
+
* ...getSortSchema(['title', 'createdAt', 'updatedAt'])
|
|
48
|
+
* })
|
|
49
|
+
*/
|
|
16
50
|
export const getSortSchema = <T extends string>(
|
|
17
51
|
keys: readonly [T, ...T[]]
|
|
18
52
|
) => ({
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Admin panel navigation, shared by the sidebar and the
|
|
3
|
+
* command search.
|
|
4
|
+
*
|
|
5
|
+
* @module lib/admin-navigation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Home, MessageCircle, MessageSquare, Ticket, Users } from 'lucide-react'
|
|
2
9
|
import { SidebarNavGroup } from '~/components/navigation-sidebar'
|
|
3
10
|
|
|
4
11
|
const adminNavigation: SidebarNavGroup[] = [
|
|
@@ -13,8 +20,29 @@ const adminNavigation: SidebarNavGroup[] = [
|
|
|
13
20
|
}
|
|
14
21
|
]
|
|
15
22
|
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Customers',
|
|
25
|
+
items: [
|
|
26
|
+
{
|
|
27
|
+
title: 'Support Tickets',
|
|
28
|
+
url: '/admin/support-tickets',
|
|
29
|
+
icon: Ticket
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: 'Feedback',
|
|
33
|
+
url: '/admin/feedbacks',
|
|
34
|
+
icon: MessageCircle
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: 'Contact Leads',
|
|
38
|
+
url: '/admin/contact-leads',
|
|
39
|
+
icon: MessageSquare
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
16
43
|
{
|
|
17
44
|
label: 'Settings',
|
|
45
|
+
superadminOnly: true,
|
|
18
46
|
items: [
|
|
19
47
|
{
|
|
20
48
|
title: 'Users & Roles',
|
|
@@ -25,6 +53,10 @@ const adminNavigation: SidebarNavGroup[] = [
|
|
|
25
53
|
}
|
|
26
54
|
]
|
|
27
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The admin navigation visible to the current user. Superadmin-only groups
|
|
58
|
+
* are hidden from `admin` users.
|
|
59
|
+
*/
|
|
28
60
|
export const getAdminNavigation = (isSuperAdmin: boolean): SidebarNavGroup[] =>
|
|
29
61
|
isSuperAdmin
|
|
30
62
|
? adminNavigation
|
|
@@ -1,58 +1,62 @@
|
|
|
1
|
-
import { CreditCard, Home, Settings, Users } from 'lucide-react'
|
|
1
|
+
import { CreditCard, Home, Settings, Ticket, Users } from 'lucide-react'
|
|
2
2
|
import { SidebarNavGroup } from '~/components/navigation-sidebar'
|
|
3
|
+
import { isManagerRole } from './rbac'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Sidebar and command-search entries for one organization. Managers (owner
|
|
7
|
+
* and admin) also get support and settings pages.
|
|
8
|
+
*/
|
|
9
|
+
export function getAppNavigation(
|
|
10
|
+
organizationId: string,
|
|
9
11
|
role: string
|
|
10
|
-
|
|
11
|
-
memberId: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const canManageOrganization = (role: string): boolean =>
|
|
15
|
-
role === 'owner' || role === 'admin'
|
|
16
|
-
|
|
17
|
-
export const getAppNavigation = (
|
|
18
|
-
orgId: string,
|
|
19
|
-
role: string
|
|
20
|
-
): SidebarNavGroup[] => {
|
|
12
|
+
): SidebarNavGroup[] {
|
|
21
13
|
const groups: SidebarNavGroup[] = [
|
|
22
14
|
{
|
|
23
|
-
label: '
|
|
15
|
+
label: 'Workspace',
|
|
24
16
|
items: [
|
|
25
17
|
{
|
|
26
18
|
title: 'Dashboard',
|
|
27
|
-
url: `/app/${
|
|
19
|
+
url: `/app/${organizationId}/dashboard`,
|
|
28
20
|
icon: Home
|
|
29
21
|
}
|
|
30
22
|
]
|
|
31
23
|
}
|
|
32
24
|
]
|
|
33
25
|
|
|
34
|
-
if (
|
|
35
|
-
groups.push(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
26
|
+
if (isManagerRole(role)) {
|
|
27
|
+
groups.push(
|
|
28
|
+
{
|
|
29
|
+
label: 'Help',
|
|
30
|
+
items: [
|
|
31
|
+
{
|
|
32
|
+
title: 'Support',
|
|
33
|
+
url: `/app/${organizationId}/support-tickets`,
|
|
34
|
+
icon: Ticket
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Settings',
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
title: 'General',
|
|
43
|
+
url: `/app/${organizationId}/settings`,
|
|
44
|
+
icon: Settings,
|
|
45
|
+
exact: true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: 'Members',
|
|
49
|
+
url: `/app/${organizationId}/settings/members`,
|
|
50
|
+
icon: Users
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
title: 'Billing',
|
|
54
|
+
url: `/app/${organizationId}/settings/billing`,
|
|
55
|
+
icon: CreditCard
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
)
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
return groups
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Browser auth client. The admin plugin gets the same access control as the
|
|
5
|
+
* server so `admin.hasPermission` checks agree on both sides.
|
|
6
|
+
*/
|
|
3
7
|
import {
|
|
4
8
|
adminClient,
|
|
5
9
|
inferAdditionalFields,
|
|
@@ -9,20 +13,21 @@ import {
|
|
|
9
13
|
} from 'better-auth/client/plugins'
|
|
10
14
|
import { createAuthClient } from 'better-auth/react'
|
|
11
15
|
import type { auth } from './auth'
|
|
16
|
+
import { ac, roles } from './auth-permissions'
|
|
12
17
|
|
|
13
18
|
export const {
|
|
14
19
|
signIn,
|
|
15
20
|
signOut,
|
|
16
21
|
useSession,
|
|
22
|
+
updateUser,
|
|
17
23
|
getLastUsedLoginMethod,
|
|
18
24
|
admin,
|
|
19
|
-
organization
|
|
20
|
-
useListOrganizations
|
|
25
|
+
organization
|
|
21
26
|
} = createAuthClient({
|
|
22
27
|
plugins: [
|
|
23
28
|
inferAdditionalFields<typeof auth>(),
|
|
24
29
|
magicLinkClient(),
|
|
25
|
-
adminClient(),
|
|
30
|
+
adminClient({ ac, roles }),
|
|
26
31
|
organizationClient(),
|
|
27
32
|
lastLoginMethodClient()
|
|
28
33
|
]
|
|
@@ -1,22 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Access-control definitions for the Better Auth admin plugin.
|
|
3
|
+
*
|
|
4
|
+
* The platform has three roles:
|
|
5
|
+
*
|
|
6
|
+
* - `superadmin`: full control of the admin panel. Can list, view, create,
|
|
7
|
+
* update, change the role of, ban and impersonate any user (including other
|
|
8
|
+
* admins and superadmins).
|
|
9
|
+
* - `admin`: can open the admin panel and work with customer-facing data
|
|
10
|
+
* (support tickets, feedback, and contact leads), but is intentionally
|
|
11
|
+
* granted no user-management or impersonation permissions.
|
|
12
|
+
* - `user`: a regular customer with no admin-panel access
|
|
13
|
+
*
|
|
14
|
+
* These statements/roles are shared by both the server (`auth.ts`) and the
|
|
15
|
+
* client (`auth-client.ts`) so permission checks stay consistent everywhere.
|
|
16
|
+
*
|
|
17
|
+
* @module lib/auth-permissions
|
|
18
|
+
*/
|
|
19
|
+
|
|
1
20
|
import { createAccessControl } from 'better-auth/plugins/access'
|
|
2
21
|
import { defaultStatements } from 'better-auth/plugins/admin/access'
|
|
3
22
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The full set of permissions available across the admin panel.
|
|
25
|
+
*
|
|
26
|
+
* `defaultStatements` contributes the admin plugin's built-in `user` and
|
|
27
|
+
* `session` resources.
|
|
28
|
+
*/
|
|
29
|
+
export const statement = { ...defaultStatements } as const
|
|
7
30
|
|
|
8
31
|
export const ac = createAccessControl(statement)
|
|
9
32
|
|
|
10
|
-
|
|
33
|
+
/** Regular customer with no admin-panel privileges. */
|
|
34
|
+
export const user = ac.newRole({})
|
|
11
35
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Operational admin who can open the admin panel but has no
|
|
38
|
+
* `user` or `session` permissions. User management and impersonation are
|
|
39
|
+
* therefore denied.
|
|
40
|
+
*/
|
|
41
|
+
export const admin = ac.newRole({})
|
|
16
42
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Full-control admin who receives every built-in user/session permission,
|
|
45
|
+
* including `impersonate-admins`.
|
|
46
|
+
*
|
|
47
|
+
* Each resource is granted explicitly rather than spreading
|
|
48
|
+
* `defaultStatements` (which is the statement *definition*, not a grant).
|
|
49
|
+
* This way a newly added resource is denied by default until it is
|
|
50
|
+
* deliberately listed here.
|
|
51
|
+
*/
|
|
52
|
+
export const superadmin = ac.newRole({
|
|
53
|
+
user: [...defaultStatements.user],
|
|
54
|
+
session: [...defaultStatements.session]
|
|
20
55
|
})
|
|
21
56
|
|
|
57
|
+
/** Role map passed to the admin plugin on both server and client. */
|
|
22
58
|
export const roles = { user, admin, superadmin }
|
|
59
|
+
|
|
60
|
+
/** Roles that are allowed to reach the admin panel at all. */
|
|
61
|
+
export const adminRoles = ['admin', 'superadmin'] as const
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
1
3
|
import { validateEmail } from '@ejekanshjain/simple-email-validator'
|
|
2
4
|
import { createId } from '@paralleldrive/cuid2'
|
|
3
5
|
import { APIError, betterAuth } from 'better-auth'
|
|
@@ -9,7 +11,8 @@ import {
|
|
|
9
11
|
magicLink,
|
|
10
12
|
organization
|
|
11
13
|
} from 'better-auth/plugins'
|
|
12
|
-
import { eq } from 'drizzle-orm'
|
|
14
|
+
import { and, count, eq, isNull, ne } from 'drizzle-orm'
|
|
15
|
+
import { alias } from 'drizzle-orm/pg-core'
|
|
13
16
|
import { headers } from 'next/headers'
|
|
14
17
|
import { cache } from 'react'
|
|
15
18
|
import { start } from 'workflow/api'
|
|
@@ -18,23 +21,77 @@ import {
|
|
|
18
21
|
accountsTable,
|
|
19
22
|
invitationsTable,
|
|
20
23
|
membersTable,
|
|
21
|
-
organizationsTable,
|
|
22
24
|
organizationSubscriptionsTable,
|
|
25
|
+
organizationsTable,
|
|
23
26
|
sessionsTable,
|
|
24
27
|
usersTable,
|
|
25
28
|
verificationsTable
|
|
26
29
|
} from '~/db/schema'
|
|
27
30
|
import { env } from '~/env'
|
|
28
|
-
import { ac, roles } from './auth-permissions'
|
|
31
|
+
import { ac, adminRoles, roles } from './auth-permissions'
|
|
32
|
+
import {
|
|
33
|
+
INVITATION_EXPIRES_IN_DAYS,
|
|
34
|
+
MAX_ORGANIZATIONS_PER_USER
|
|
35
|
+
} from './constants'
|
|
29
36
|
import {
|
|
30
37
|
sendInvitationEmail,
|
|
31
38
|
sendMagicLinkEmail,
|
|
32
39
|
sendWelcomeEmail
|
|
33
40
|
} from './email-service'
|
|
34
|
-
import {
|
|
41
|
+
import { getOrganizationPlan } from './plan-limits'
|
|
35
42
|
import { siteConfig } from './siteConfig'
|
|
43
|
+
import { deleteOrganizationUploads } from './storage'
|
|
36
44
|
import { stripeClient } from './stripe'
|
|
37
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Blocks organization logos set through the Better Auth client. Logos go
|
|
48
|
+
* through `updateOrganizationAction`, which checks the upload belongs to the
|
|
49
|
+
* organization before saving it.
|
|
50
|
+
*/
|
|
51
|
+
function rejectClientLogo(logo: string | null | undefined) {
|
|
52
|
+
if (logo) {
|
|
53
|
+
throw new APIError('BAD_REQUEST', {
|
|
54
|
+
message: 'Change the logo from the organization settings page.'
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when `userId` is the only owner of at least one organization. */
|
|
60
|
+
async function ownsOrganizationAlone(userId: string): Promise<boolean> {
|
|
61
|
+
const otherOwners = alias(membersTable, 'other_owners')
|
|
62
|
+
|
|
63
|
+
const [row] = await db
|
|
64
|
+
.select({ organizationId: membersTable.organizationId })
|
|
65
|
+
.from(membersTable)
|
|
66
|
+
.leftJoin(
|
|
67
|
+
otherOwners,
|
|
68
|
+
and(
|
|
69
|
+
eq(otherOwners.organizationId, membersTable.organizationId),
|
|
70
|
+
eq(otherOwners.role, 'owner'),
|
|
71
|
+
ne(otherOwners.userId, userId)
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
.where(
|
|
75
|
+
and(
|
|
76
|
+
eq(membersTable.userId, userId),
|
|
77
|
+
eq(membersTable.role, 'owner'),
|
|
78
|
+
isNull(otherOwners.id)
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
.limit(1)
|
|
82
|
+
|
|
83
|
+
return Boolean(row)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Better Auth configuration.
|
|
88
|
+
*
|
|
89
|
+
* Features:
|
|
90
|
+
* - Magic link email sign-in
|
|
91
|
+
* - OAuth (GitHub, Google) with account linking
|
|
92
|
+
* - Admin plugin with `user`, `admin`, and `superadmin` roles
|
|
93
|
+
* - Organization plugin with owner, admin, and member roles
|
|
94
|
+
*/
|
|
38
95
|
export const auth = betterAuth({
|
|
39
96
|
database: drizzleAdapter(db, {
|
|
40
97
|
provider: 'pg',
|
|
@@ -73,6 +130,11 @@ export const auth = betterAuth({
|
|
|
73
130
|
databaseHooks: {
|
|
74
131
|
user: {
|
|
75
132
|
create: {
|
|
133
|
+
// Magic-link sign-up sends an empty name; fall back to the email's
|
|
134
|
+
// local part so greetings and avatars never render blank.
|
|
135
|
+
before: async user => ({
|
|
136
|
+
data: { ...user, name: user.name.trim() || user.email.split('@')[0]! }
|
|
137
|
+
}),
|
|
76
138
|
after: async user => {
|
|
77
139
|
try {
|
|
78
140
|
await start(sendWelcomeEmail, [
|
|
@@ -86,6 +148,16 @@ export const auth = betterAuth({
|
|
|
86
148
|
console.error('Failed to send welcome email:', error)
|
|
87
149
|
}
|
|
88
150
|
}
|
|
151
|
+
},
|
|
152
|
+
delete: {
|
|
153
|
+
before: async user => {
|
|
154
|
+
if (await ownsOrganizationAlone(user.id)) {
|
|
155
|
+
throw new APIError('BAD_REQUEST', {
|
|
156
|
+
message:
|
|
157
|
+
'This user is the only owner of an organization. Transfer ownership or delete the organization first.'
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
89
161
|
}
|
|
90
162
|
}
|
|
91
163
|
},
|
|
@@ -107,20 +179,29 @@ export const auth = betterAuth({
|
|
|
107
179
|
}),
|
|
108
180
|
admin({
|
|
109
181
|
ac,
|
|
110
|
-
roles
|
|
182
|
+
roles,
|
|
183
|
+
adminRoles: [...adminRoles],
|
|
184
|
+
defaultRole: 'user'
|
|
111
185
|
}),
|
|
112
186
|
organization({
|
|
113
|
-
organizationLimit:
|
|
114
|
-
invitationExpiresIn:
|
|
187
|
+
organizationLimit: MAX_ORGANIZATIONS_PER_USER,
|
|
188
|
+
invitationExpiresIn: INVITATION_EXPIRES_IN_DAYS * 24 * 60 * 60,
|
|
115
189
|
cancelPendingInvitationsOnReInvite: true,
|
|
116
|
-
sendInvitationEmail: async ({
|
|
190
|
+
sendInvitationEmail: async ({
|
|
191
|
+
id,
|
|
192
|
+
email,
|
|
193
|
+
role,
|
|
194
|
+
organization,
|
|
195
|
+
inviter
|
|
196
|
+
}) => {
|
|
117
197
|
try {
|
|
118
198
|
await start(sendInvitationEmail, [
|
|
119
199
|
email,
|
|
120
200
|
{
|
|
121
201
|
inviterName: inviter.user.name,
|
|
122
202
|
organizationName: organization.name,
|
|
123
|
-
|
|
203
|
+
inviteUrl: `${env.BETTER_AUTH_URL}/accept-invitation/${id}`,
|
|
204
|
+
role,
|
|
124
205
|
companyName: siteConfig.name
|
|
125
206
|
}
|
|
126
207
|
])
|
|
@@ -129,17 +210,28 @@ export const auth = betterAuth({
|
|
|
129
210
|
}
|
|
130
211
|
},
|
|
131
212
|
organizationHooks: {
|
|
213
|
+
beforeCreateOrganization: async ({ organization }) => {
|
|
214
|
+
rejectClientLogo(organization.logo)
|
|
215
|
+
},
|
|
132
216
|
afterCreateOrganization: async ({ organization }) => {
|
|
133
217
|
await db.insert(organizationSubscriptionsTable).values({
|
|
134
218
|
organizationId: organization.id
|
|
135
219
|
})
|
|
136
220
|
},
|
|
221
|
+
beforeUpdateOrganization: async ({ organization }) => {
|
|
222
|
+
rejectClientLogo(organization.logo)
|
|
223
|
+
},
|
|
224
|
+
// Files go first: if R2 fails, the organization and its paid plan
|
|
225
|
+
// both stay intact. Stripe is cancelled only once nothing else can fail.
|
|
137
226
|
beforeDeleteOrganization: async ({ organization }) => {
|
|
227
|
+
await deleteOrganizationUploads(organization.id)
|
|
228
|
+
|
|
138
229
|
const sub = await db.query.organizationSubscriptionsTable.findFirst({
|
|
139
230
|
where: eq(
|
|
140
231
|
organizationSubscriptionsTable.organizationId,
|
|
141
232
|
organization.id
|
|
142
|
-
)
|
|
233
|
+
),
|
|
234
|
+
columns: { stripeSubscriptionId: true }
|
|
143
235
|
})
|
|
144
236
|
|
|
145
237
|
if (sub?.stripeSubscriptionId) {
|
|
@@ -147,29 +239,31 @@ export const auth = betterAuth({
|
|
|
147
239
|
await stripeClient.subscriptions.cancel(sub.stripeSubscriptionId)
|
|
148
240
|
} catch (error) {
|
|
149
241
|
console.error(
|
|
150
|
-
`Failed to cancel Stripe subscription ${sub.stripeSubscriptionId}
|
|
242
|
+
`Failed to cancel Stripe subscription ${sub.stripeSubscriptionId} while deleting organization ${organization.id}:`,
|
|
151
243
|
error
|
|
152
244
|
)
|
|
153
245
|
throw new APIError('BAD_REQUEST', {
|
|
154
246
|
message:
|
|
155
|
-
'
|
|
247
|
+
'We could not cancel the active subscription, so the organization was kept. Try again in a few minutes.'
|
|
156
248
|
})
|
|
157
249
|
}
|
|
158
250
|
}
|
|
159
|
-
|
|
160
|
-
await db
|
|
161
|
-
.delete(organizationSubscriptionsTable)
|
|
162
|
-
.where(
|
|
163
|
-
eq(organizationSubscriptionsTable.organizationId, organization.id)
|
|
164
|
-
)
|
|
165
251
|
}
|
|
166
252
|
},
|
|
253
|
+
// Better Auth caps pending invitations at this number, so leave room
|
|
254
|
+
// only for the seats current members do not fill.
|
|
167
255
|
invitationLimit: async ({ organization }) => {
|
|
168
|
-
const { plan } = await
|
|
169
|
-
|
|
256
|
+
const [{ plan }, [row]] = await Promise.all([
|
|
257
|
+
getOrganizationPlan(organization.id),
|
|
258
|
+
db
|
|
259
|
+
.select({ count: count() })
|
|
260
|
+
.from(membersTable)
|
|
261
|
+
.where(eq(membersTable.organizationId, organization.id))
|
|
262
|
+
])
|
|
263
|
+
return Math.max(0, plan.maxMembers - (row?.count ?? 0))
|
|
170
264
|
},
|
|
171
265
|
membershipLimit: async (_, organization) => {
|
|
172
|
-
const { plan } = await
|
|
266
|
+
const { plan } = await getOrganizationPlan(organization.id)
|
|
173
267
|
return plan.maxMembers
|
|
174
268
|
}
|
|
175
269
|
}),
|
|
@@ -196,6 +290,13 @@ export const auth = betterAuth({
|
|
|
196
290
|
}
|
|
197
291
|
})
|
|
198
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The current session, cached per request.
|
|
295
|
+
*
|
|
296
|
+
* Returns null when signed out. `isAdmin` is true for both `admin` and
|
|
297
|
+
* `superadmin` (admin panel access). `isSuperAdmin` is true only for
|
|
298
|
+
* `superadmin` (user management, role changes, and impersonation).
|
|
299
|
+
*/
|
|
199
300
|
export const getAuthSession = cache(async () => {
|
|
200
301
|
const authSession = await auth.api.getSession({
|
|
201
302
|
headers: await headers()
|