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
|
@@ -1,30 +1,132 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
1
3
|
import { createSafeActionClient } from 'next-safe-action'
|
|
2
4
|
import { getAuthSession } from './auth'
|
|
3
|
-
import {
|
|
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
|
+
}
|
|
4
17
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
})
|
|
8
46
|
|
|
9
|
-
|
|
47
|
+
try {
|
|
48
|
+
const result = await serverCode(args)
|
|
10
49
|
recordWideEvent({
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
'session.id': authSession.session.id
|
|
50
|
+
outcome: 'success',
|
|
51
|
+
duration_ms: Date.now() - startedAt
|
|
14
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
|
+
}
|
|
15
63
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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'
|
|
21
101
|
})
|
|
102
|
+
return UNEXPECTED_ERROR_MESSAGE
|
|
22
103
|
}
|
|
23
|
-
)
|
|
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
|
+
})
|
|
24
126
|
|
|
25
127
|
export const authActionClient = actionClient.use(async ({ ctx, next }) => {
|
|
26
128
|
if (!ctx?.user || !ctx?.session) {
|
|
27
|
-
throw new
|
|
129
|
+
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Sign in to continue.')
|
|
28
130
|
}
|
|
29
131
|
|
|
30
132
|
return next({
|
|
@@ -36,9 +138,18 @@ export const authActionClient = actionClient.use(async ({ ctx, next }) => {
|
|
|
36
138
|
})
|
|
37
139
|
})
|
|
38
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, such as reading customer data.
|
|
146
|
+
*/
|
|
39
147
|
export const adminActionClient = authActionClient.use(async ({ ctx, next }) => {
|
|
40
148
|
if (!ctx.isAdmin) {
|
|
41
|
-
throw new
|
|
149
|
+
throw new AppError(
|
|
150
|
+
AppErrorCode.FORBIDDEN,
|
|
151
|
+
'You do not have permission to do this.'
|
|
152
|
+
)
|
|
42
153
|
}
|
|
43
154
|
|
|
44
155
|
return next({
|
|
@@ -49,3 +160,28 @@ export const adminActionClient = authActionClient.use(async ({ ctx, next }) => {
|
|
|
49
160
|
}
|
|
50
161
|
})
|
|
51
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
|
+
}
|
|
@@ -1,4 +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
|
+
*/
|
|
1
9
|
export const siteConfig = {
|
|
2
10
|
name: 'Project Name',
|
|
3
|
-
description: 'Project Description'
|
|
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
|
+
}
|
|
4
20
|
}
|
|
@@ -2,14 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
import { toast } from 'sonner'
|
|
4
4
|
|
|
5
|
+
const DEFAULT_ERROR = 'The request did not finish. Try again in a few minutes.'
|
|
6
|
+
|
|
7
|
+
type ActionResultLike = {
|
|
8
|
+
serverError?: string
|
|
9
|
+
validationErrors?: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Displays a success toast notification to the user.
|
|
14
|
+
*/
|
|
5
15
|
export const toastSuccessMessage = (message: string) =>
|
|
6
16
|
toast.success(message, {
|
|
7
17
|
richColors: true,
|
|
8
18
|
position: 'top-center'
|
|
9
19
|
})
|
|
10
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Displays an informational toast (neutral, non-error feedback).
|
|
23
|
+
*/
|
|
24
|
+
export const toastInfoMessage = (message: string) =>
|
|
25
|
+
toast.info(message, {
|
|
26
|
+
richColors: true,
|
|
27
|
+
position: 'top-center'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Displays an error toast. Uses a default message when none is provided.
|
|
32
|
+
*/
|
|
11
33
|
export const toastErrorMessage = (message?: string) =>
|
|
12
|
-
toast.error(message ||
|
|
34
|
+
toast.error(message || DEFAULT_ERROR, {
|
|
13
35
|
richColors: true,
|
|
14
36
|
position: 'top-center'
|
|
15
37
|
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Toast an action failure from `await someAction(...)`.
|
|
41
|
+
* Returns true when a toast was shown so callers can bail.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const result = await upsertTableAction(values)
|
|
45
|
+
* if (toastIfActionFailed(result)) return
|
|
46
|
+
*/
|
|
47
|
+
export function toastIfActionFailed(
|
|
48
|
+
result: ActionResultLike | undefined | null
|
|
49
|
+
): boolean {
|
|
50
|
+
if (!result) return false
|
|
51
|
+
if (result.serverError) {
|
|
52
|
+
toastErrorMessage(result.serverError)
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
if (result.validationErrors) {
|
|
56
|
+
toastErrorMessage('Check the highlighted fields and try again.')
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Toast a failure from hooks (`useSafeActionMutation`, `useAction`) or a thrown Error.
|
|
64
|
+
*
|
|
65
|
+
* Accepts:
|
|
66
|
+
* - `SafeActionError` / `Error` (`message`)
|
|
67
|
+
* - `useAction` error bag (`{ serverError, validationErrors }`)
|
|
68
|
+
* - plain string
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* onError: error => toastActionError(error, 'Failed to save')
|
|
72
|
+
* onError: ({ error }) => toastActionError(error, 'Failed to save')
|
|
73
|
+
*/
|
|
74
|
+
export function toastActionError(
|
|
75
|
+
error: unknown,
|
|
76
|
+
fallback = DEFAULT_ERROR
|
|
77
|
+
): void {
|
|
78
|
+
if (typeof error === 'string' && error.trim()) {
|
|
79
|
+
toastErrorMessage(error)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (error && typeof error === 'object') {
|
|
84
|
+
const bag = error as ActionResultLike & { message?: string }
|
|
85
|
+
|
|
86
|
+
if (typeof bag.serverError === 'string' && bag.serverError) {
|
|
87
|
+
toastErrorMessage(bag.serverError)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (bag.validationErrors) {
|
|
91
|
+
toastErrorMessage('Check the highlighted fields and try again.')
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (typeof bag.message === 'string' && bag.message.trim()) {
|
|
95
|
+
toastErrorMessage(bag.message)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
toastErrorMessage(fallback)
|
|
101
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Zod fragments for action input and forms.
|
|
3
|
+
*
|
|
4
|
+
* Client-safe: no env, DB, or server-only imports, so forms can reuse the
|
|
5
|
+
* exact schema their action validates with.
|
|
6
|
+
*/
|
|
7
|
+
import z from 'zod'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Phone number: 7 to 15 digits once separators are removed (E.164 maximum).
|
|
11
|
+
* Allowed characters are digits, `+`, `-`, `()`, `.`, and spaces.
|
|
12
|
+
*/
|
|
13
|
+
export const phoneValidation = z
|
|
14
|
+
.string()
|
|
15
|
+
.trim()
|
|
16
|
+
.max(20)
|
|
17
|
+
.regex(/^[\d+\-().\s]+$/, 'Enter a valid phone number.')
|
|
18
|
+
.refine(value => {
|
|
19
|
+
const digits = value.replace(/\D/g, '').length
|
|
20
|
+
return digits >= 7 && digits <= 15
|
|
21
|
+
}, 'Enter a phone number with 7 to 15 digits.')
|
|
22
|
+
|
|
23
|
+
/** Email address, trimmed and lowercased. */
|
|
24
|
+
export const emailValidation = z.email().trim().toLowerCase()
|
|
25
|
+
|
|
26
|
+
/** Required short string, 1 to 255 characters after trimming. */
|
|
27
|
+
export const stringValidation = z.string().trim().min(1).max(255)
|
|
28
|
+
|
|
29
|
+
/** Optional short string (max 255). Empty string is allowed after trim. */
|
|
30
|
+
export const optionalStringValidation = z.string().trim().max(255).optional()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Proxy middleware function
|
|
5
|
+
*
|
|
6
|
+
* Forwards the request pathname (+ search) as `x-pathname` so server layouts
|
|
7
|
+
* can build a same-origin login callback without losing deep links.
|
|
8
|
+
* @returns NextResponse that continues the request chain
|
|
9
|
+
*/
|
|
10
|
+
export function proxy(request: NextRequest) {
|
|
11
|
+
const requestHeaders = new Headers(request.headers)
|
|
12
|
+
requestHeaders.set(
|
|
13
|
+
'x-pathname',
|
|
14
|
+
`${request.nextUrl.pathname}${request.nextUrl.search}`
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
return NextResponse.next({
|
|
18
|
+
request: { headers: requestHeaders }
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Middleware configuration
|
|
24
|
+
*
|
|
25
|
+
* Defines which routes this middleware should be applied to using the matcher pattern.
|
|
26
|
+
*/
|
|
27
|
+
export const config = {
|
|
28
|
+
matcher: [
|
|
29
|
+
{
|
|
30
|
+
/**
|
|
31
|
+
* Route matching pattern
|
|
32
|
+
*
|
|
33
|
+
* This regex pattern matches all routes EXCEPT:
|
|
34
|
+
* - /_next/static - Next.js static assets
|
|
35
|
+
* - /_next/image - Next.js image optimization
|
|
36
|
+
* - /favicon.ico - Browser favicon
|
|
37
|
+
* - /.well-known/workflow/ - Internal workflow paths
|
|
38
|
+
*
|
|
39
|
+
* Excluding these paths ensures that:
|
|
40
|
+
* 1. Static assets load efficiently without middleware overhead
|
|
41
|
+
* 2. Image optimization is not interrupted
|
|
42
|
+
* 3. Workflow execution and resumption function correctly
|
|
43
|
+
*/
|
|
44
|
+
source:
|
|
45
|
+
'/((?!_next/static|_next/image|favicon.ico|.well-known/workflow/).*)'
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
import { db } from '~/db'
|
|
3
|
+
import { env } from '~/env'
|
|
4
|
+
|
|
5
|
+
const resetDb = async () => {
|
|
6
|
+
if (env.APP_ENV === 'production') {
|
|
7
|
+
throw new Error('Cannot reset database in production environment.')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
await db.execute(sql`DROP SCHEMA IF EXISTS public CASCADE;`)
|
|
11
|
+
await db.execute(sql`CREATE SCHEMA public;`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (import.meta.main) {
|
|
15
|
+
try {
|
|
16
|
+
await resetDb()
|
|
17
|
+
process.exit(0)
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('Error running reset-db:', err)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,11 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Seeds a local database with a superadmin and a regular user.
|
|
3
|
+
*
|
|
4
|
+
* Sign in with a magic link sent to either seeded address, so point them at
|
|
5
|
+
* inboxes you control. A successful run writes a `seed_runs` marker and later
|
|
6
|
+
* runs do nothing; reset the database to seed again. Everything runs in one
|
|
7
|
+
* transaction, so a failure leaves no partial data.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { db } from '~/db'
|
|
11
|
+
import { SEED_RUN_MARKER_ID, seedRunsTable, usersTable } from '~/db/schema'
|
|
12
|
+
|
|
13
|
+
/** Seeds `database` once. Returns `skipped: true` when a seed already ran. */
|
|
14
|
+
export async function seedDatabase(
|
|
15
|
+
database: typeof db = db
|
|
16
|
+
): Promise<{ skipped: boolean }> {
|
|
17
|
+
const [existingSeedRun] = await database
|
|
18
|
+
.select({ id: seedRunsTable.id })
|
|
19
|
+
.from(seedRunsTable)
|
|
20
|
+
.limit(1)
|
|
21
|
+
|
|
22
|
+
if (existingSeedRun) return { skipped: true }
|
|
23
|
+
|
|
24
|
+
await database.transaction(async tx => {
|
|
25
|
+
const superadminId = usersTable.id.defaultFn!()
|
|
26
|
+
const userId = usersTable.id.defaultFn!()
|
|
27
|
+
|
|
28
|
+
await tx.insert(usersTable).values([
|
|
29
|
+
{
|
|
30
|
+
id: superadminId,
|
|
31
|
+
name: 'Super Admin',
|
|
32
|
+
email: 'superadmin@example.com',
|
|
33
|
+
emailVerified: true,
|
|
34
|
+
role: 'superadmin'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: userId,
|
|
38
|
+
name: 'Demo User',
|
|
39
|
+
email: 'user@example.com',
|
|
40
|
+
emailVerified: true,
|
|
41
|
+
role: 'user'
|
|
42
|
+
}
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
// Marker last, so a failed seed never blocks a retry.
|
|
46
|
+
await tx.insert(seedRunsTable).values({ id: SEED_RUN_MARKER_ID })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return { skipped: false }
|
|
50
|
+
}
|
|
2
51
|
|
|
3
52
|
if (import.meta.main) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
53
|
+
seedDatabase(db)
|
|
54
|
+
.then(result => {
|
|
55
|
+
if (result.skipped) console.info('Database already seeded; skipping.')
|
|
56
|
+
process.exit(0)
|
|
57
|
+
})
|
|
58
|
+
.catch(err => {
|
|
59
|
+
console.error('Error running seed:', err)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
})
|
|
11
62
|
}
|
|
@@ -1,35 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Sends one test email to check SMTP settings.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```bash
|
|
6
|
+
* bun run email:test you@example.com
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
import { render, toPlainText } from '@react-email/render'
|
|
2
11
|
import TestEmail from '~/emails/test'
|
|
3
12
|
import { sendEmail } from '~/lib/nodemailer'
|
|
4
13
|
|
|
5
|
-
async function sendTestMail() {
|
|
6
|
-
const html = await render(
|
|
7
|
-
TestEmail({
|
|
8
|
-
name: 'John Doe'
|
|
9
|
-
}),
|
|
10
|
-
{
|
|
11
|
-
pretty: true
|
|
12
|
-
}
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
const text = toPlainText(html)
|
|
14
|
+
async function sendTestMail(to: string) {
|
|
15
|
+
const html = await render(TestEmail({ name: to }), { pretty: true })
|
|
16
16
|
|
|
17
17
|
const result = await sendEmail({
|
|
18
|
-
to
|
|
18
|
+
to,
|
|
19
19
|
subject: 'Test Email',
|
|
20
|
-
text,
|
|
20
|
+
text: toPlainText(html),
|
|
21
21
|
html
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
console.info('Test email sent
|
|
24
|
+
console.info('Test email sent', result)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (import.meta.main) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.error(
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const to = process.argv[2]
|
|
29
|
+
|
|
30
|
+
if (!to) {
|
|
31
|
+
console.error(
|
|
32
|
+
'Pass a recipient address: bun run email:test you@example.com'
|
|
33
|
+
)
|
|
34
|
+
process.exit(1)
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
sendTestMail(to)
|
|
38
|
+
.then(() => process.exit(0))
|
|
39
|
+
.catch(err => {
|
|
40
|
+
console.error('Error sending test mail:', err)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
})
|
|
35
43
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterEach, describe, test } from 'bun:test'
|
|
2
|
+
import { createUser, getUsers, updateUser } from '~/app/(admin)/actions/users'
|
|
3
|
+
import {
|
|
4
|
+
asAnonymous,
|
|
5
|
+
asUser,
|
|
6
|
+
expectData,
|
|
7
|
+
expectServerError,
|
|
8
|
+
Fixtures
|
|
9
|
+
} from '../helpers'
|
|
10
|
+
|
|
11
|
+
describe('admin user management', () => {
|
|
12
|
+
const fx = new Fixtures()
|
|
13
|
+
afterEach(() => fx.cleanup())
|
|
14
|
+
|
|
15
|
+
test('signed-out and regular users cannot reach admin actions', async () => {
|
|
16
|
+
const user = await fx.user()
|
|
17
|
+
|
|
18
|
+
expectServerError(
|
|
19
|
+
await asAnonymous(() => getUsers({ page: 1, limit: 10 })),
|
|
20
|
+
'sign in'
|
|
21
|
+
)
|
|
22
|
+
expectServerError(
|
|
23
|
+
await asUser(user, () => getUsers({ page: 1, limit: 10 })),
|
|
24
|
+
'permission'
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('a superadmin cannot remove their own superadmin role', async () => {
|
|
29
|
+
const superadmin = await fx.platformUser('superadmin')
|
|
30
|
+
|
|
31
|
+
const result = await asUser(superadmin, () =>
|
|
32
|
+
updateUser({
|
|
33
|
+
id: superadmin.id,
|
|
34
|
+
name: superadmin.name,
|
|
35
|
+
email: superadmin.email,
|
|
36
|
+
role: 'admin'
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expectServerError(result, 'your own superadmin role')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('admins cannot manage users; superadmins can', async () => {
|
|
44
|
+
const admin = await fx.platformUser('admin')
|
|
45
|
+
const superadmin = await fx.platformUser('superadmin')
|
|
46
|
+
|
|
47
|
+
expectServerError(
|
|
48
|
+
await asUser(admin, () => getUsers({ page: 1, limit: 10 })),
|
|
49
|
+
'permission'
|
|
50
|
+
)
|
|
51
|
+
expectData(await asUser(superadmin, () => getUsers({ page: 1, limit: 10 })))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('creating a user with a taken email explains the conflict', async () => {
|
|
55
|
+
const superadmin = await fx.platformUser('superadmin')
|
|
56
|
+
const existing = await fx.user()
|
|
57
|
+
|
|
58
|
+
const result = await asUser(superadmin, () =>
|
|
59
|
+
createUser({ name: 'Duplicate', email: existing.email, role: 'user' })
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expectServerError(result, 'already uses this email')
|
|
63
|
+
})
|
|
64
|
+
})
|