create-ej-app 25.0.0 → 27.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 +83 -28
- 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 -0
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -40
- 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 +8 -0
- package/template/saas-starter/.env.example +25 -0
- package/template/saas-starter/.vscode/extensions.json +11 -0
- package/template/saas-starter/.vscode/settings.json +3 -0
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/Dockerfile +15 -0
- package/template/saas-starter/README.md +125 -0
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/components.json +25 -0
- package/template/saas-starter/drizzle.config.ts +10 -0
- package/template/saas-starter/eslint.config.mjs +24 -0
- package/template/saas-starter/gitignore +14 -0
- package/template/saas-starter/next.config.ts +32 -0
- package/template/saas-starter/package.json +97 -0
- package/template/saas-starter/postcss.config.mjs +7 -0
- package/template/saas-starter/prettier.config.mjs +11 -0
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/public/placeholder.svg +1 -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 +183 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- 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 +53 -0
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +60 -0
- 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 +377 -0
- 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 +216 -0
- 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 +189 -0
- 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 +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +31 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +92 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +357 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +130 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +222 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +115 -0
- 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 +168 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +604 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +33 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +55 -0
- 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 +31 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +26 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +30 -0
- 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 +37 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +200 -0
- package/template/saas-starter/src/app/(app)/app/layout.tsx +64 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +117 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +40 -0
- package/template/saas-starter/src/app/(app)/app/page.tsx +76 -0
- 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 +131 -0
- package/template/saas-starter/src/app/(auth)/login/page.tsx +105 -0
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +137 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +94 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +153 -0
- 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 +48 -0
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +24 -0
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +63 -0
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +41 -0
- 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/api/auth/[...all]/route.ts +4 -0
- package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/favicon.ico +0 -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 +269 -0
- package/template/saas-starter/src/app/layout.tsx +77 -0
- package/template/saas-starter/src/app/not-found.tsx +49 -0
- package/template/saas-starter/src/app/robots.ts +36 -0
- package/template/saas-starter/src/app/sitemap.ts +21 -0
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +70 -0
- package/template/saas-starter/src/components/data-table/enum.ts +5 -0
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +961 -0
- package/template/saas-starter/src/components/data-table/types.ts +76 -0
- package/template/saas-starter/src/components/error.tsx +71 -0
- package/template/saas-starter/src/components/file-upload.tsx +254 -0
- package/template/saas-starter/src/components/loader.tsx +41 -0
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +140 -0
- package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
- package/template/saas-starter/src/components/page-heading.tsx +38 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +51 -0
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +76 -0
- package/template/saas-starter/src/components/site-footer.tsx +39 -0
- 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 +115 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +75 -0
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
- package/template/saas-starter/src/components/ui/badge.tsx +54 -0
- package/template/saas-starter/src/components/ui/button.tsx +69 -0
- package/template/saas-starter/src/components/ui/card.tsx +103 -0
- package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
- package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
- package/template/saas-starter/src/components/ui/command.tsx +192 -0
- package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
- package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/saas-starter/src/components/ui/field.tsx +238 -0
- package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
- package/template/saas-starter/src/components/ui/input.tsx +19 -0
- package/template/saas-starter/src/components/ui/label.tsx +24 -0
- package/template/saas-starter/src/components/ui/popover.tsx +89 -0
- package/template/saas-starter/src/components/ui/select.tsx +195 -0
- package/template/saas-starter/src/components/ui/separator.tsx +28 -0
- package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
- package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
- package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
- package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
- package/template/saas-starter/src/components/ui/switch.tsx +33 -0
- package/template/saas-starter/src/components/ui/table.tsx +116 -0
- package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
- package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
- package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
- package/template/saas-starter/src/components/user-menu.tsx +213 -0
- package/template/saas-starter/src/db/common.ts +63 -0
- package/template/saas-starter/src/db/enums.ts +50 -0
- package/template/saas-starter/src/db/index.ts +60 -0
- package/template/saas-starter/src/db/relations.ts +112 -0
- package/template/saas-starter/src/db/schema.ts +342 -0
- package/template/saas-starter/src/emails/components/email-layout.tsx +79 -0
- package/template/saas-starter/src/emails/invitation.tsx +69 -0
- package/template/saas-starter/src/emails/magic-link.tsx +51 -0
- package/template/saas-starter/src/emails/test.tsx +34 -0
- package/template/saas-starter/src/emails/welcome.tsx +37 -0
- package/template/saas-starter/src/env.ts +66 -0
- package/template/saas-starter/src/hooks/use-mobile.ts +21 -0
- package/template/saas-starter/src/instrumentation.ts +44 -0
- package/template/saas-starter/src/lib/actions.ts +55 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +63 -0
- package/template/saas-starter/src/lib/app-navigation.ts +63 -0
- package/template/saas-starter/src/lib/auth-client.ts +34 -0
- package/template/saas-starter/src/lib/auth-permissions.ts +61 -0
- package/template/saas-starter/src/lib/auth.ts +312 -0
- package/template/saas-starter/src/lib/billing.ts +152 -0
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/cn.ts +4 -0
- package/template/saas-starter/src/lib/constants.ts +91 -0
- package/template/saas-starter/src/lib/delay.ts +12 -0
- package/template/saas-starter/src/lib/email-service.ts +118 -0
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +25 -0
- package/template/saas-starter/src/lib/format-date.ts +75 -0
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +67 -0
- package/template/saas-starter/src/lib/organization-access.ts +73 -0
- package/template/saas-starter/src/lib/otel.ts +117 -0
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +90 -0
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +126 -0
- package/template/saas-starter/src/lib/safe-action.ts +187 -0
- 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 +20 -0
- package/template/saas-starter/src/lib/storage.ts +311 -0
- package/template/saas-starter/src/lib/stripe.ts +9 -0
- package/template/saas-starter/src/lib/toast-message.ts +101 -0
- package/template/saas-starter/src/lib/validations.ts +95 -0
- 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/reset-db.ts +22 -0
- package/template/saas-starter/src/scripts/seed.ts +85 -0
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +206 -0
- package/template/saas-starter/src/scripts/test-mail.ts +43 -0
- 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 +52 -0
- package/cli.js +0 -143
- 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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { env } from '~/env'
|
|
4
|
+
|
|
5
|
+
export type PlanId = 'free' | 'pro' | 'ultimate'
|
|
6
|
+
|
|
7
|
+
type PlanPrice = {
|
|
8
|
+
id: string
|
|
9
|
+
/** Minor units, as Stripe stores them: 2000 is $20.00. */
|
|
10
|
+
amount: number
|
|
11
|
+
currency: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Plan = {
|
|
15
|
+
id: PlanId
|
|
16
|
+
name: string
|
|
17
|
+
maxMembers: number
|
|
18
|
+
stripePrice?: { monthly: PlanPrice; annual: PlanPrice }
|
|
19
|
+
canUseTrial?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Every plan, cheapest first. Stripe prices come from `bun run stripe:setup`. */
|
|
23
|
+
export const PLANS: Plan[] = [
|
|
24
|
+
{
|
|
25
|
+
id: 'free',
|
|
26
|
+
name: 'Free',
|
|
27
|
+
maxMembers: 1
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'pro',
|
|
31
|
+
name: 'Pro',
|
|
32
|
+
maxMembers: 25,
|
|
33
|
+
stripePrice: {
|
|
34
|
+
monthly: {
|
|
35
|
+
id: env.STRIPE_PRICE_PRO_MONTHLY,
|
|
36
|
+
amount: 2000,
|
|
37
|
+
currency: 'USD'
|
|
38
|
+
},
|
|
39
|
+
annual: {
|
|
40
|
+
id: env.STRIPE_PRICE_PRO_ANNUAL,
|
|
41
|
+
amount: 20000,
|
|
42
|
+
currency: 'USD'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
canUseTrial: true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'ultimate',
|
|
49
|
+
name: 'Ultimate',
|
|
50
|
+
maxMembers: 100,
|
|
51
|
+
stripePrice: {
|
|
52
|
+
monthly: {
|
|
53
|
+
id: env.STRIPE_PRICE_ULTIMATE_MONTHLY,
|
|
54
|
+
amount: 10000,
|
|
55
|
+
currency: 'USD'
|
|
56
|
+
},
|
|
57
|
+
annual: {
|
|
58
|
+
id: env.STRIPE_PRICE_ULTIMATE_ANNUAL,
|
|
59
|
+
amount: 100000,
|
|
60
|
+
currency: 'USD'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
/** Subscription statuses that keep the paid plan's features switched on. */
|
|
67
|
+
export const ACTIVE_SUBSCRIPTION_STATUSES = [
|
|
68
|
+
'trialing',
|
|
69
|
+
'active',
|
|
70
|
+
'past_due'
|
|
71
|
+
] as const
|
|
72
|
+
|
|
73
|
+
export function isActiveSubscriptionStatus(status: string): boolean {
|
|
74
|
+
return (ACTIVE_SUBSCRIPTION_STATUSES as readonly string[]).includes(status)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Statuses with no live Stripe subscription behind them. Any other status
|
|
79
|
+
* (including `unpaid`, `paused`, and `incomplete`) still bills or can resume,
|
|
80
|
+
* so changes go through the billing portal instead of a new checkout.
|
|
81
|
+
*/
|
|
82
|
+
export const ENDED_SUBSCRIPTION_STATUSES = [
|
|
83
|
+
'free',
|
|
84
|
+
'canceled',
|
|
85
|
+
'incomplete_expired'
|
|
86
|
+
] as const
|
|
87
|
+
|
|
88
|
+
export function hasLiveSubscription(status: string): boolean {
|
|
89
|
+
return !(ENDED_SUBSCRIPTION_STATUSES as readonly string[]).includes(status)
|
|
90
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization role predicates and labels, shared by server and client.
|
|
3
|
+
*
|
|
4
|
+
* The roles are the Better Auth organization plugin defaults:
|
|
5
|
+
* - owner: full control, including ownership transfer and deletion
|
|
6
|
+
* - admin: manages settings, billing, and members, but not ownership
|
|
7
|
+
* - member: standard access to the organization's workspace
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type OrganizationRole = 'owner' | 'admin' | 'member'
|
|
11
|
+
|
|
12
|
+
/** Roles an owner or admin can give to someone. Ownership moves only by transfer. */
|
|
13
|
+
export const ASSIGNABLE_ROLES = ['admin', 'member'] as const
|
|
14
|
+
|
|
15
|
+
export type AssignableRole = (typeof ASSIGNABLE_ROLES)[number]
|
|
16
|
+
|
|
17
|
+
/** True for the manager tier: owner and admin. */
|
|
18
|
+
export function isManagerRole(role: string | null | undefined): boolean {
|
|
19
|
+
return role === 'owner' || role === 'admin'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Descriptions shown when you pick a role for a member or an invitation. */
|
|
23
|
+
export const ROLE_DESCRIPTIONS: Record<AssignableRole, string> = {
|
|
24
|
+
admin: 'Admin: manage settings, billing, and members',
|
|
25
|
+
member: 'Member: use the workspace'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Shown to an owner before they transfer ownership to another member. */
|
|
29
|
+
export const OWNERSHIP_DEMOTION_NOTICE =
|
|
30
|
+
'After the transfer, your role becomes admin: you keep every permission except ownership transfer and organization deletion.'
|
|
31
|
+
|
|
32
|
+
/** Role with its article, for sentences such as "join Acme as an admin". */
|
|
33
|
+
export function roleArticle(role: string): string {
|
|
34
|
+
switch (role) {
|
|
35
|
+
case 'owner':
|
|
36
|
+
return 'an owner'
|
|
37
|
+
case 'admin':
|
|
38
|
+
return 'an admin'
|
|
39
|
+
case 'member':
|
|
40
|
+
return 'a member'
|
|
41
|
+
default:
|
|
42
|
+
return `a ${role}`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useMutation,
|
|
5
|
+
useQuery,
|
|
6
|
+
type UseMutationOptions,
|
|
7
|
+
type UseQueryOptions
|
|
8
|
+
} from '@tanstack/react-query'
|
|
9
|
+
import type { SafeActionResult } from 'next-safe-action'
|
|
10
|
+
|
|
11
|
+
/** Type helper for any server action function */
|
|
12
|
+
export type AnyAction = (
|
|
13
|
+
...args: any[]
|
|
14
|
+
) => Promise<SafeActionResult<any, any, any, any, any>>
|
|
15
|
+
|
|
16
|
+
/** Extracts the input type from a server action */
|
|
17
|
+
type ActionInput<T extends AnyAction> = Parameters<T>[0]
|
|
18
|
+
|
|
19
|
+
/** Extracts the data type from a server action result */
|
|
20
|
+
type ActionData<T extends AnyAction> = Awaited<ReturnType<T>>['data']
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Client-side error for server action failures.
|
|
24
|
+
* Carries the user-facing `serverError` string (including unmasked AppError
|
|
25
|
+
* messages) and optional Zod validation errors.
|
|
26
|
+
*/
|
|
27
|
+
class SafeActionError extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
public message: string,
|
|
30
|
+
public validationErrors?: Record<string, string[] | undefined>
|
|
31
|
+
) {
|
|
32
|
+
super(message)
|
|
33
|
+
this.name = 'SafeActionError'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* React Query hook for server actions that fetch data
|
|
39
|
+
* Wraps server actions with proper error handling and caching support
|
|
40
|
+
*
|
|
41
|
+
* @param key - Unique query key for caching
|
|
42
|
+
* @param action - Server action function to execute
|
|
43
|
+
* @param input - Input parameters for the server action (optional)
|
|
44
|
+
* @param options - React Query options (staleTime, refetchInterval, etc.)
|
|
45
|
+
*
|
|
46
|
+
* @returns React Query result with data, loading, and error states
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const { data, isLoading } = useSafeActionQuery('cart-items', getCartItems)
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function useSafeActionQuery<TAction extends AnyAction>(
|
|
54
|
+
key: string,
|
|
55
|
+
action: TAction,
|
|
56
|
+
input?: ActionInput<TAction>,
|
|
57
|
+
options?: Omit<UseQueryOptions<ActionData<TAction>>, 'queryKey' | 'queryFn'>
|
|
58
|
+
) {
|
|
59
|
+
return useQuery({
|
|
60
|
+
queryKey: [key, input],
|
|
61
|
+
queryFn: async () => {
|
|
62
|
+
const result = await action(input)
|
|
63
|
+
|
|
64
|
+
if (result?.serverError) {
|
|
65
|
+
throw new SafeActionError(result.serverError)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result?.validationErrors) {
|
|
69
|
+
throw new SafeActionError(
|
|
70
|
+
'Check the highlighted fields and try again.',
|
|
71
|
+
result.validationErrors
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result?.data
|
|
76
|
+
},
|
|
77
|
+
...options
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* React Query mutation hook for server actions that modify data
|
|
83
|
+
* Wraps server actions with proper error handling and optimistic updates support
|
|
84
|
+
*
|
|
85
|
+
* @param action - Server action function to execute
|
|
86
|
+
* @param options - React Query mutation options (onSuccess, onError, etc.)
|
|
87
|
+
*
|
|
88
|
+
* @returns React Query mutation result with mutate function and states
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* const { mutate, isPending } = useSafeActionMutation(addToCart, {
|
|
93
|
+
* onSuccess: (data) => {
|
|
94
|
+
* toastSuccessMessage('Added to cart!')
|
|
95
|
+
* queryClient.invalidateQueries({ queryKey: ['cart-items'] })
|
|
96
|
+
* },
|
|
97
|
+
* onError: (error) => {
|
|
98
|
+
* toastActionError(error, 'Failed to add to cart')
|
|
99
|
+
* }
|
|
100
|
+
* })
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function useSafeActionMutation<TAction extends AnyAction>(
|
|
104
|
+
action: TAction,
|
|
105
|
+
options?: UseMutationOptions<ActionData<TAction>, Error, ActionInput<TAction>>
|
|
106
|
+
) {
|
|
107
|
+
return useMutation({
|
|
108
|
+
mutationFn: async (input: ActionInput<TAction>) => {
|
|
109
|
+
const result = await action(input)
|
|
110
|
+
|
|
111
|
+
if (result?.serverError) {
|
|
112
|
+
throw new SafeActionError(result.serverError)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (result?.validationErrors) {
|
|
116
|
+
throw new SafeActionError(
|
|
117
|
+
'Check the highlighted fields and try again.',
|
|
118
|
+
result.validationErrors
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result?.data
|
|
123
|
+
},
|
|
124
|
+
...options
|
|
125
|
+
})
|
|
126
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { createSafeActionClient } from 'next-safe-action'
|
|
4
|
+
import { getAuthSession } from './auth'
|
|
5
|
+
import { AppError, AppErrorCode, isAppError } from './errors'
|
|
6
|
+
import { flattenForWideEvent, recordError, recordWideEvent } from './otel'
|
|
7
|
+
|
|
8
|
+
/** Shown for every unexpected error; details stay in telemetry. */
|
|
9
|
+
export const UNEXPECTED_ERROR_MESSAGE =
|
|
10
|
+
'The request did not finish. Try again in a few minutes.'
|
|
11
|
+
|
|
12
|
+
type ActionServerArgs = {
|
|
13
|
+
parsedInput?: unknown
|
|
14
|
+
clientInput?: unknown
|
|
15
|
+
[key: string]: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wrap a safe-action server function with wide-event telemetry.
|
|
20
|
+
*
|
|
21
|
+
* Records `action.name`, flattened `parsedInput` (or `clientInput`), and on
|
|
22
|
+
* success `outcome: 'success'` plus `duration_ms`. Failures keep error fields
|
|
23
|
+
* from `handleServerError`; duration is still recorded.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* export const myAction = authActionClient
|
|
27
|
+
* .inputSchema(schema)
|
|
28
|
+
* .action(
|
|
29
|
+
* withWideEvent('myAction', async ({ parsedInput, ctx }) => {
|
|
30
|
+
* // business logic only
|
|
31
|
+
* })
|
|
32
|
+
* )
|
|
33
|
+
*/
|
|
34
|
+
export function withWideEvent<TArgs extends ActionServerArgs, TResult>(
|
|
35
|
+
actionName: string,
|
|
36
|
+
serverCode: (args: TArgs) => Promise<TResult>
|
|
37
|
+
): (args: TArgs) => Promise<TResult> {
|
|
38
|
+
return async args => {
|
|
39
|
+
const startedAt = Date.now()
|
|
40
|
+
const input = args.parsedInput ?? args.clientInput
|
|
41
|
+
|
|
42
|
+
recordWideEvent({
|
|
43
|
+
'action.name': actionName,
|
|
44
|
+
...flattenForWideEvent(input, 'input')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await serverCode(args)
|
|
49
|
+
recordWideEvent({
|
|
50
|
+
outcome: 'success',
|
|
51
|
+
duration_ms: Date.now() - startedAt
|
|
52
|
+
})
|
|
53
|
+
return result
|
|
54
|
+
} catch (error) {
|
|
55
|
+
recordWideEvent({
|
|
56
|
+
outcome: 'error',
|
|
57
|
+
duration_ms: Date.now() - startedAt
|
|
58
|
+
})
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Configured safe action client with authentication middleware
|
|
66
|
+
* This client wraps server actions with:
|
|
67
|
+
* - Automatic input/output validation using Zod
|
|
68
|
+
* - Type-safe error handling ({@link AppError} messages reach the client)
|
|
69
|
+
* - Server-side authentication injection
|
|
70
|
+
*
|
|
71
|
+
* All server actions using this client automatically receive the current auth session
|
|
72
|
+
* in the context, so protected actions can access user information.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* export const myAction = actionClient
|
|
76
|
+
* .inputSchema(z.object({ name: z.string() }))
|
|
77
|
+
* .action(async ({ parsedInput: { name }, ctx }) => {
|
|
78
|
+
* const userId = ctx.user?.id // Auth injected automatically
|
|
79
|
+
* return { success: true }
|
|
80
|
+
* })
|
|
81
|
+
*/
|
|
82
|
+
export const actionClient = createSafeActionClient({
|
|
83
|
+
handleServerError(e) {
|
|
84
|
+
if (isAppError(e)) {
|
|
85
|
+
recordWideEvent({
|
|
86
|
+
'error.name': e.name,
|
|
87
|
+
'error.code': e.code,
|
|
88
|
+
'error.message': e.message,
|
|
89
|
+
'error.expected': true,
|
|
90
|
+
outcome: 'error'
|
|
91
|
+
})
|
|
92
|
+
return e.message
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
recordError(e)
|
|
96
|
+
recordWideEvent({
|
|
97
|
+
'error.name': e.name,
|
|
98
|
+
'error.expected': false,
|
|
99
|
+
'error.message': e.message,
|
|
100
|
+
outcome: 'error'
|
|
101
|
+
})
|
|
102
|
+
return UNEXPECTED_ERROR_MESSAGE
|
|
103
|
+
}
|
|
104
|
+
}).use(async ({ next, ctx }) => {
|
|
105
|
+
// Retrieve current authentication session
|
|
106
|
+
const authSession = await getAuthSession()
|
|
107
|
+
|
|
108
|
+
// Record user information in telemetry for all actions, if available
|
|
109
|
+
if (authSession) {
|
|
110
|
+
recordWideEvent({
|
|
111
|
+
'user.id': authSession.user.id,
|
|
112
|
+
'user.admin': authSession.isAdmin,
|
|
113
|
+
'user.superAdmin': authSession.isSuperAdmin,
|
|
114
|
+
'session.id': authSession.session.id
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Inject auth session into action context for all downstream actions
|
|
119
|
+
return next({
|
|
120
|
+
ctx: {
|
|
121
|
+
...ctx,
|
|
122
|
+
...authSession
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
export const authActionClient = actionClient.use(async ({ ctx, next }) => {
|
|
128
|
+
if (!ctx?.user || !ctx?.session) {
|
|
129
|
+
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Sign in to continue.')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return next({
|
|
133
|
+
ctx: {
|
|
134
|
+
...ctx,
|
|
135
|
+
user: ctx.user,
|
|
136
|
+
session: ctx.session
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Action client for the admin panel.
|
|
143
|
+
*
|
|
144
|
+
* Allows both `admin` and `superadmin` roles. Use for actions that any
|
|
145
|
+
* admin-panel user may run (support tickets, feedback, and contact leads).
|
|
146
|
+
*/
|
|
147
|
+
export const adminActionClient = authActionClient.use(async ({ ctx, next }) => {
|
|
148
|
+
if (!ctx.isAdmin) {
|
|
149
|
+
throw new AppError(
|
|
150
|
+
AppErrorCode.FORBIDDEN,
|
|
151
|
+
'You do not have permission to do this.'
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return next({
|
|
156
|
+
ctx: {
|
|
157
|
+
...ctx,
|
|
158
|
+
user: ctx.user,
|
|
159
|
+
session: ctx.session
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Action client restricted to the `superadmin` role.
|
|
166
|
+
*
|
|
167
|
+
* Use for privileged operations, such as user management, role changes, and
|
|
168
|
+
* impersonation, that `admin` users cannot perform.
|
|
169
|
+
*/
|
|
170
|
+
export const superAdminActionClient = adminActionClient.use(
|
|
171
|
+
async ({ ctx, next }) => {
|
|
172
|
+
if (!ctx.isSuperAdmin) {
|
|
173
|
+
throw new AppError(
|
|
174
|
+
AppErrorCode.FORBIDDEN,
|
|
175
|
+
'You do not have permission to do this.'
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return next({
|
|
180
|
+
ctx: {
|
|
181
|
+
...ctx,
|
|
182
|
+
user: ctx.user,
|
|
183
|
+
session: ctx.session
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape characters that are special in SQL LIKE / ILIKE patterns.
|
|
3
|
+
*
|
|
4
|
+
* Postgres uses `\` as the default escape character. Without escaping, a
|
|
5
|
+
* user-typed `%` or `_` becomes a wildcard and can force full-table scans.
|
|
6
|
+
*/
|
|
7
|
+
export function escapeLikePattern(value: string): string {
|
|
8
|
+
return value.replace(/[\\%_]/g, char => `\\${char}`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a case-insensitive contains pattern: `%value%` with wildcards escaped.
|
|
13
|
+
*/
|
|
14
|
+
export function likeContains(value: string): string {
|
|
15
|
+
return `%${escapeLikePattern(value)}%`
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Brand identity and site-wide copy.
|
|
3
|
+
*
|
|
4
|
+
* Layouts, emails, legal pages, and metadata read from here. Never hardcode
|
|
5
|
+
* the product name anywhere else.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/siteConfig
|
|
8
|
+
*/
|
|
9
|
+
export const siteConfig = {
|
|
10
|
+
name: 'Project Name',
|
|
11
|
+
description: 'Project Description',
|
|
12
|
+
logo: '/logo.svg',
|
|
13
|
+
logoDark: '/logo-dark.svg',
|
|
14
|
+
contact: {
|
|
15
|
+
email: 'hello@example.com'
|
|
16
|
+
},
|
|
17
|
+
legal: {
|
|
18
|
+
lastUpdated: 'September 25, 2026'
|
|
19
|
+
}
|
|
20
|
+
}
|