create-ej-app 26.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 +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 +46 -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 +49 -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,6 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Drizzle table definitions.
|
|
3
|
+
*
|
|
4
|
+
* Enums live in `./enums.ts` and reusable column helpers in `./common.ts`.
|
|
5
|
+
* Every foreign key leads an index and declares an explicit `onDelete`;
|
|
6
|
+
* `tests/db/fk-indexes.test.ts` enforces both.
|
|
7
|
+
*/
|
|
1
8
|
import { sql } from 'drizzle-orm'
|
|
2
9
|
import {
|
|
3
10
|
boolean,
|
|
11
|
+
check,
|
|
4
12
|
index,
|
|
5
13
|
integer,
|
|
6
14
|
jsonb,
|
|
@@ -9,27 +17,35 @@ import {
|
|
|
9
17
|
uniqueIndex,
|
|
10
18
|
varchar
|
|
11
19
|
} from 'drizzle-orm/pg-core'
|
|
12
|
-
import { commonFieldDefs } from './common'
|
|
20
|
+
import { commonFieldDefs, nonNegativeCheck } from './common'
|
|
13
21
|
import {
|
|
14
22
|
organizationSubscriptionIntervalEnum,
|
|
15
23
|
organizationSubscriptionPlanEnum,
|
|
16
|
-
organizationSubscriptionStatusEnum
|
|
24
|
+
organizationSubscriptionStatusEnum,
|
|
25
|
+
supportTicketPriorityEnum,
|
|
26
|
+
supportTicketStatusEnum
|
|
17
27
|
} from './enums'
|
|
18
28
|
|
|
19
29
|
export * from './enums'
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// ─── Auth (Better Auth) ─────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export const usersTable = pgTable(
|
|
34
|
+
'users',
|
|
35
|
+
{
|
|
36
|
+
id: commonFieldDefs.id('user'),
|
|
37
|
+
name: text('name').notNull(),
|
|
38
|
+
email: text('email').notNull().unique(),
|
|
39
|
+
emailVerified: boolean('email_verified').default(false).notNull(),
|
|
40
|
+
image: text('image'),
|
|
41
|
+
role: text('role'),
|
|
42
|
+
banned: boolean('banned').default(false),
|
|
43
|
+
banReason: text('ban_reason'),
|
|
44
|
+
banExpires: commonFieldDefs.date('ban_expires'),
|
|
45
|
+
...commonFieldDefs.dates
|
|
46
|
+
},
|
|
47
|
+
table => [index().on(table.createdAt), index().on(table.role)]
|
|
48
|
+
)
|
|
33
49
|
|
|
34
50
|
export const sessionsTable = pgTable(
|
|
35
51
|
'sessions',
|
|
@@ -70,13 +86,19 @@ export const accountsTable = pgTable(
|
|
|
70
86
|
table => [index().on(table.userId)]
|
|
71
87
|
)
|
|
72
88
|
|
|
73
|
-
export const verificationsTable = pgTable(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
export const verificationsTable = pgTable(
|
|
90
|
+
'verifications',
|
|
91
|
+
{
|
|
92
|
+
id: commonFieldDefs.id('verification'),
|
|
93
|
+
identifier: text('identifier').notNull(),
|
|
94
|
+
value: text('value').notNull(),
|
|
95
|
+
expiresAt: commonFieldDefs.date('expires_at').notNull(),
|
|
96
|
+
...commonFieldDefs.dates
|
|
97
|
+
},
|
|
98
|
+
table => [index().on(table.identifier)]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
// ─── Organizations (Better Auth) ────────────────────────────────
|
|
80
102
|
|
|
81
103
|
export const organizationsTable = pgTable('organizations', {
|
|
82
104
|
id: commonFieldDefs.id('organization'),
|
|
@@ -100,7 +122,10 @@ export const membersTable = pgTable(
|
|
|
100
122
|
role: text('role').notNull(),
|
|
101
123
|
...commonFieldDefs.dates
|
|
102
124
|
},
|
|
103
|
-
table => [
|
|
125
|
+
table => [
|
|
126
|
+
uniqueIndex().on(table.organizationId, table.userId),
|
|
127
|
+
index().on(table.userId)
|
|
128
|
+
]
|
|
104
129
|
)
|
|
105
130
|
|
|
106
131
|
export const invitationsTable = pgTable(
|
|
@@ -116,20 +141,21 @@ export const invitationsTable = pgTable(
|
|
|
116
141
|
expiresAt: commonFieldDefs.date('expires_at'),
|
|
117
142
|
inviterId: text('inviter_id')
|
|
118
143
|
.notNull()
|
|
119
|
-
.references(() => usersTable.id, {
|
|
120
|
-
onDelete: 'cascade'
|
|
121
|
-
}),
|
|
144
|
+
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
|
122
145
|
...commonFieldDefs.dates
|
|
123
146
|
},
|
|
124
147
|
table => [
|
|
125
148
|
index().on(table.organizationId),
|
|
126
149
|
index().on(table.email),
|
|
150
|
+
index().on(table.inviterId),
|
|
127
151
|
uniqueIndex()
|
|
128
152
|
.on(table.organizationId, table.email)
|
|
129
153
|
.where(sql`${table.status} = 'pending'`)
|
|
130
154
|
]
|
|
131
155
|
)
|
|
132
156
|
|
|
157
|
+
// ─── Billing ────────────────────────────────────────────────────
|
|
158
|
+
|
|
133
159
|
export const organizationSubscriptionsTable = pgTable(
|
|
134
160
|
'organization_subscriptions',
|
|
135
161
|
{
|
|
@@ -137,7 +163,7 @@ export const organizationSubscriptionsTable = pgTable(
|
|
|
137
163
|
organizationId: text('organization_id')
|
|
138
164
|
.notNull()
|
|
139
165
|
.unique()
|
|
140
|
-
.references(() => organizationsTable.id),
|
|
166
|
+
.references(() => organizationsTable.id, { onDelete: 'cascade' }),
|
|
141
167
|
plan: organizationSubscriptionPlanEnum('plan').notNull().default('free'),
|
|
142
168
|
status: organizationSubscriptionStatusEnum('status')
|
|
143
169
|
.notNull()
|
|
@@ -154,6 +180,7 @@ export const organizationSubscriptionsTable = pgTable(
|
|
|
154
180
|
}
|
|
155
181
|
)
|
|
156
182
|
|
|
183
|
+
/** One row per Stripe event, so a redelivered webhook is processed once. */
|
|
157
184
|
export const stripeWebhooksEventsTable = pgTable('stripe_webhooks_events', {
|
|
158
185
|
id: text('id').primaryKey(),
|
|
159
186
|
type: text('type').notNull(),
|
|
@@ -161,31 +188,155 @@ export const stripeWebhooksEventsTable = pgTable('stripe_webhooks_events', {
|
|
|
161
188
|
processed: boolean('processed').notNull().default(false),
|
|
162
189
|
processedAt: commonFieldDefs.date('processed_at'),
|
|
163
190
|
error: text('error'),
|
|
164
|
-
|
|
191
|
+
// Stored only when processing fails, for replay and debugging.
|
|
192
|
+
payload: jsonb('payload')
|
|
165
193
|
})
|
|
166
194
|
|
|
195
|
+
// ─── Support ────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/** Single-row counter behind `TICKET-000001` numbers; see `scripts/setup-db.ts`. */
|
|
198
|
+
export const supportTicketCounterTable = pgTable(
|
|
199
|
+
'support_ticket_counter',
|
|
200
|
+
{
|
|
201
|
+
id: integer('id').primaryKey().default(1),
|
|
202
|
+
counter: integer('counter').notNull().default(0)
|
|
203
|
+
},
|
|
204
|
+
table => [
|
|
205
|
+
nonNegativeCheck('support_ticket_counter_counter_non_negative', [
|
|
206
|
+
table.counter
|
|
207
|
+
])
|
|
208
|
+
]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A ticket with an `organizationId` belongs to that organization's managers.
|
|
213
|
+
* A ticket without one is an account ticket, visible only to its author.
|
|
214
|
+
* Platform admins see both.
|
|
215
|
+
*/
|
|
216
|
+
export const supportTicketsTable = pgTable(
|
|
217
|
+
'support_tickets',
|
|
218
|
+
{
|
|
219
|
+
id: commonFieldDefs.id('support_ticket'),
|
|
220
|
+
// The database fills this in from a trigger; see `scripts/setup-db.ts`.
|
|
221
|
+
ticketNumber: varchar('ticket_number')
|
|
222
|
+
.notNull()
|
|
223
|
+
.unique()
|
|
224
|
+
.$defaultFn(() => ''),
|
|
225
|
+
organizationId: varchar('organization_id').references(
|
|
226
|
+
() => organizationsTable.id,
|
|
227
|
+
{ onDelete: 'set null' }
|
|
228
|
+
),
|
|
229
|
+
userId: varchar('user_id').references(() => usersTable.id, {
|
|
230
|
+
onDelete: 'set null'
|
|
231
|
+
}),
|
|
232
|
+
subject: varchar('subject').notNull(),
|
|
233
|
+
status: supportTicketStatusEnum('status').notNull().default('open'),
|
|
234
|
+
priority: supportTicketPriorityEnum('priority'),
|
|
235
|
+
resolvedAt: commonFieldDefs.date('resolved_at'),
|
|
236
|
+
...commonFieldDefs.dates
|
|
237
|
+
},
|
|
238
|
+
table => [
|
|
239
|
+
index().on(table.userId, table.createdAt),
|
|
240
|
+
index().on(table.organizationId, table.createdAt),
|
|
241
|
+
index().on(table.status),
|
|
242
|
+
index().on(table.priority)
|
|
243
|
+
]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
export const supportTicketMessagesTable = pgTable(
|
|
247
|
+
'support_ticket_messages',
|
|
248
|
+
{
|
|
249
|
+
id: commonFieldDefs.id('support_ticket_message'),
|
|
250
|
+
ticketId: varchar('ticket_id')
|
|
251
|
+
.notNull()
|
|
252
|
+
.references(() => supportTicketsTable.id, { onDelete: 'cascade' }),
|
|
253
|
+
userId: varchar('user_id').references(() => usersTable.id, {
|
|
254
|
+
onDelete: 'set null'
|
|
255
|
+
}),
|
|
256
|
+
message: text('message').notNull(),
|
|
257
|
+
/** True when the support team wrote it, even if the author is also a customer. */
|
|
258
|
+
isAdmin: boolean('is_admin').notNull().default(false),
|
|
259
|
+
...commonFieldDefs.dates
|
|
260
|
+
},
|
|
261
|
+
table => [index().on(table.ticketId), index().on(table.userId)]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
export const feedbacksTable = pgTable(
|
|
265
|
+
'feedbacks',
|
|
266
|
+
{
|
|
267
|
+
id: commonFieldDefs.id('feedback'),
|
|
268
|
+
userId: varchar('user_id')
|
|
269
|
+
.notNull()
|
|
270
|
+
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
|
271
|
+
organizationId: varchar('organization_id')
|
|
272
|
+
.notNull()
|
|
273
|
+
.references(() => organizationsTable.id, { onDelete: 'cascade' }),
|
|
274
|
+
rating: integer('rating').notNull(),
|
|
275
|
+
comment: text('comment'),
|
|
276
|
+
...commonFieldDefs.dates
|
|
277
|
+
},
|
|
278
|
+
table => [
|
|
279
|
+
index().on(table.userId),
|
|
280
|
+
index().on(table.organizationId, table.createdAt),
|
|
281
|
+
index().on(table.createdAt),
|
|
282
|
+
check('feedbacks_rating_range', sql`${table.rating} BETWEEN 1 AND 5`)
|
|
283
|
+
]
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
export const contactLeadsTable = pgTable(
|
|
287
|
+
'contact_leads',
|
|
288
|
+
{
|
|
289
|
+
id: commonFieldDefs.id('contact_lead'),
|
|
290
|
+
name: varchar('name').notNull(),
|
|
291
|
+
email: varchar('email').notNull(),
|
|
292
|
+
phone: varchar('phone'),
|
|
293
|
+
subject: varchar('subject').notNull(),
|
|
294
|
+
message: text('message').notNull(),
|
|
295
|
+
...commonFieldDefs.dates
|
|
296
|
+
},
|
|
297
|
+
table => [index().on(table.createdAt)]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
// ─── Uploads ────────────────────────────────────────────────────
|
|
301
|
+
|
|
167
302
|
export const fileUploadsTable = pgTable(
|
|
168
303
|
'file_uploads',
|
|
169
304
|
{
|
|
170
|
-
id:
|
|
171
|
-
// Object
|
|
305
|
+
id: commonFieldDefs.id('file_upload'),
|
|
306
|
+
// Object key, such as uploads/organizations/<organizationId>/<file>.jpg
|
|
172
307
|
key: text('key').notNull().unique(),
|
|
173
308
|
originalName: varchar('original_name').notNull(),
|
|
174
309
|
mimeType: varchar('mime_type').notNull(),
|
|
175
|
-
// In
|
|
310
|
+
// In bytes
|
|
176
311
|
size: integer('size').notNull(),
|
|
177
312
|
isTemp: boolean('is_temp').notNull().default(true),
|
|
178
313
|
uploadedBy: varchar('uploaded_by').references(() => usersTable.id, {
|
|
179
314
|
onDelete: 'set null'
|
|
180
315
|
}),
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
316
|
+
// `restrict` forces organization deletion to remove the R2 objects first;
|
|
317
|
+
// see `deleteOrganizationUploads`.
|
|
318
|
+
organizationId: varchar('organization_id')
|
|
319
|
+
.notNull()
|
|
320
|
+
.references(() => organizationsTable.id, { onDelete: 'restrict' }),
|
|
185
321
|
...commonFieldDefs.dates
|
|
186
322
|
},
|
|
187
323
|
table => [
|
|
188
324
|
index().on(table.uploadedBy),
|
|
189
|
-
index().on(table.
|
|
325
|
+
index().on(table.organizationId),
|
|
326
|
+
index().on(table.isTemp, table.createdAt),
|
|
327
|
+
nonNegativeCheck('file_uploads_size_non_negative', [table.size])
|
|
190
328
|
]
|
|
191
329
|
)
|
|
330
|
+
|
|
331
|
+
// ─── Seed ───────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Singleton marker for a successful `seedDatabase` run. While
|
|
335
|
+
* {@link SEED_RUN_MARKER_ID} exists, the seed script does nothing.
|
|
336
|
+
*/
|
|
337
|
+
export const SEED_RUN_MARKER_ID = 'seed_run_default'
|
|
338
|
+
|
|
339
|
+
export const seedRunsTable = pgTable('seed_runs', {
|
|
340
|
+
id: varchar('id').primaryKey(),
|
|
341
|
+
...commonFieldDefs.dates
|
|
342
|
+
})
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared email shell: a brand rule and name at the top, the
|
|
3
|
+
* content, and a copyright footer. Every template wraps its body in it.
|
|
4
|
+
*
|
|
5
|
+
* @module emails/components/email-layout
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import {
|
|
2
9
|
Body,
|
|
3
10
|
Container,
|
|
4
11
|
Head,
|
|
5
|
-
Hr,
|
|
6
12
|
Html,
|
|
7
13
|
pixelBasedPreset,
|
|
8
14
|
Preview,
|
|
@@ -11,7 +17,11 @@ import {
|
|
|
11
17
|
Text
|
|
12
18
|
} from 'react-email'
|
|
13
19
|
|
|
20
|
+
/** Brand color. Email clients strip CSS variables, so it is a literal here. */
|
|
21
|
+
const BRAND = '#171717'
|
|
22
|
+
|
|
14
23
|
interface EmailLayoutProps {
|
|
24
|
+
/** Text email clients show in the inbox before the email is opened. */
|
|
15
25
|
previewText: string
|
|
16
26
|
companyName: string
|
|
17
27
|
children: React.ReactNode
|
|
@@ -28,31 +38,37 @@ export default function EmailLayout({
|
|
|
28
38
|
<Preview>{previewText}</Preview>
|
|
29
39
|
<Tailwind
|
|
30
40
|
config={{
|
|
31
|
-
presets: [pixelBasedPreset]
|
|
41
|
+
presets: [pixelBasedPreset],
|
|
42
|
+
theme: { extend: { colors: { brand: BRAND } } }
|
|
32
43
|
}}
|
|
33
44
|
>
|
|
34
45
|
<Body className="bg-gray-100 font-sans">
|
|
35
46
|
<Container className="mx-auto my-8 max-w-xl rounded-lg bg-white shadow-sm">
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
<Section
|
|
48
|
+
className="rounded-t-lg bg-white px-8 py-5"
|
|
49
|
+
style={{
|
|
50
|
+
borderTop: `4px solid ${BRAND}`,
|
|
51
|
+
borderBottom: '1px solid #E5E7EB'
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<Text className="m-0 text-lg font-bold tracking-tight text-gray-900">
|
|
39
55
|
{companyName}
|
|
40
56
|
</Text>
|
|
41
57
|
</Section>
|
|
42
58
|
|
|
43
|
-
{/* Content */}
|
|
44
59
|
<Section className="px-8 py-6">{children}</Section>
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
<Section
|
|
62
|
+
className="rounded-b-lg bg-gray-50 px-8 py-6"
|
|
63
|
+
style={{ borderTop: '1px solid #E5E7EB' }}
|
|
64
|
+
>
|
|
49
65
|
<Text className="m-0 text-center text-sm text-gray-500">
|
|
50
66
|
© {new Date().getUTCFullYear()} {companyName}. All rights
|
|
51
67
|
reserved.
|
|
52
68
|
</Text>
|
|
53
69
|
<Text className="m-0 mt-2 text-center text-xs text-gray-400">
|
|
54
|
-
This is an automated email.
|
|
55
|
-
|
|
70
|
+
This is an automated email. Replies to this address are not
|
|
71
|
+
read.
|
|
56
72
|
</Text>
|
|
57
73
|
</Section>
|
|
58
74
|
</Container>
|
|
@@ -1,56 +1,69 @@
|
|
|
1
1
|
import { Button, Heading, Text } from 'react-email'
|
|
2
|
-
import EmailLayout from '
|
|
2
|
+
import EmailLayout from '~/emails/components/email-layout'
|
|
3
|
+
import { INVITATION_EXPIRES_IN_DAYS } from '~/lib/constants'
|
|
4
|
+
import { roleArticle } from '~/lib/rbac'
|
|
5
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
3
6
|
|
|
4
7
|
export interface InvitationEmailProps {
|
|
5
|
-
|
|
8
|
+
inviteUrl: string
|
|
6
9
|
organizationName: string
|
|
7
|
-
|
|
10
|
+
inviterName: string
|
|
11
|
+
role: string
|
|
8
12
|
companyName: string
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
/** Invitation to join an organization, sent by the Better Auth organization plugin. */
|
|
11
16
|
export default function InvitationEmail({
|
|
12
|
-
|
|
17
|
+
inviteUrl,
|
|
13
18
|
organizationName,
|
|
14
|
-
|
|
19
|
+
inviterName,
|
|
20
|
+
role,
|
|
15
21
|
companyName
|
|
16
22
|
}: InvitationEmailProps) {
|
|
17
|
-
const previewText = `${inviterName} invited you to join ${organizationName}`
|
|
18
|
-
|
|
19
23
|
return (
|
|
20
|
-
<EmailLayout
|
|
24
|
+
<EmailLayout
|
|
25
|
+
previewText={`${inviterName} invited you to join ${organizationName}`}
|
|
26
|
+
companyName={companyName}
|
|
27
|
+
>
|
|
21
28
|
<Heading className="mb-4 text-2xl font-bold text-gray-900">
|
|
22
|
-
You
|
|
29
|
+
You're Invited to Join {organizationName}
|
|
23
30
|
</Heading>
|
|
24
31
|
|
|
25
32
|
<Text className="mb-4 text-base text-gray-700">
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
{inviterName} invited you to join <strong>{organizationName}</strong> on{' '}
|
|
34
|
+
{companyName} as {roleArticle(role)}.
|
|
35
|
+
</Text>
|
|
36
|
+
|
|
37
|
+
<Text className="mb-4 text-base text-gray-700">
|
|
38
|
+
Select the button below to accept. You'll sign in with this email
|
|
39
|
+
address first.
|
|
28
40
|
</Text>
|
|
29
41
|
|
|
30
42
|
<Button
|
|
31
|
-
href={
|
|
32
|
-
className="mb-4 rounded-lg
|
|
43
|
+
href={inviteUrl}
|
|
44
|
+
className="bg-brand mb-4 rounded-lg px-6 py-3 text-center text-base font-semibold text-white no-underline"
|
|
33
45
|
>
|
|
34
46
|
Accept Invitation
|
|
35
47
|
</Button>
|
|
36
48
|
|
|
37
49
|
<Text className="mb-4 text-sm text-gray-600">
|
|
38
|
-
Or copy
|
|
50
|
+
Or copy this link into your browser:
|
|
39
51
|
</Text>
|
|
40
52
|
|
|
41
|
-
<Text className="mb-4 text-sm break-all
|
|
53
|
+
<Text className="text-brand mb-4 text-sm break-all">{inviteUrl}</Text>
|
|
42
54
|
|
|
43
55
|
<Text className="text-sm text-gray-500">
|
|
44
|
-
|
|
45
|
-
email.
|
|
56
|
+
This invitation expires in {INVITATION_EXPIRES_IN_DAYS} days. If you
|
|
57
|
+
weren't expecting it, you can ignore this email.
|
|
46
58
|
</Text>
|
|
47
59
|
</EmailLayout>
|
|
48
60
|
)
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
InvitationEmail.PreviewProps = {
|
|
52
|
-
|
|
53
|
-
organizationName: 'Acme Inc',
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
inviteUrl: 'https://example.com/accept-invitation/your_invitation_id_here',
|
|
65
|
+
organizationName: 'Acme Inc.',
|
|
66
|
+
inviterName: 'Alex Morgan',
|
|
67
|
+
role: 'member',
|
|
68
|
+
companyName: siteConfig.name
|
|
56
69
|
} satisfies InvitationEmailProps
|
|
@@ -1,49 +1,51 @@
|
|
|
1
1
|
import { Button, Heading, Text } from 'react-email'
|
|
2
|
-
import EmailLayout from '
|
|
2
|
+
import EmailLayout from '~/emails/components/email-layout'
|
|
3
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
3
4
|
|
|
4
5
|
export interface MagicLinkEmailProps {
|
|
5
6
|
magicLink: string
|
|
6
7
|
companyName: string
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
/** Passwordless sign-in link. The link works once and expires in 5 minutes. */
|
|
9
11
|
export default function MagicLinkEmail({
|
|
10
12
|
magicLink,
|
|
11
13
|
companyName
|
|
12
14
|
}: MagicLinkEmailProps) {
|
|
13
|
-
const previewText = `Your magic link to sign in to ${companyName}`
|
|
14
|
-
|
|
15
15
|
return (
|
|
16
|
-
<EmailLayout
|
|
16
|
+
<EmailLayout
|
|
17
|
+
previewText={`Your link to sign in to ${companyName}`}
|
|
18
|
+
companyName={companyName}
|
|
19
|
+
>
|
|
17
20
|
<Heading className="mb-4 text-2xl font-bold text-gray-900">
|
|
18
|
-
Sign
|
|
21
|
+
Sign In to {companyName}
|
|
19
22
|
</Heading>
|
|
20
23
|
|
|
21
24
|
<Text className="mb-4 text-base text-gray-700">
|
|
22
|
-
|
|
23
|
-
will expire in 5 minutes.
|
|
25
|
+
Select the button below to sign in. The link expires in 5 minutes.
|
|
24
26
|
</Text>
|
|
25
27
|
|
|
26
28
|
<Button
|
|
27
29
|
href={magicLink}
|
|
28
|
-
className="mb-4 rounded-lg
|
|
30
|
+
className="bg-brand mb-4 rounded-lg px-6 py-3 text-center text-base font-semibold text-white no-underline"
|
|
29
31
|
>
|
|
30
32
|
Sign In
|
|
31
33
|
</Button>
|
|
32
34
|
|
|
33
35
|
<Text className="mb-4 text-sm text-gray-600">
|
|
34
|
-
Or copy
|
|
36
|
+
Or copy this link into your browser:
|
|
35
37
|
</Text>
|
|
36
38
|
|
|
37
|
-
<Text className="mb-4 text-sm break-all
|
|
39
|
+
<Text className="text-brand mb-4 text-sm break-all">{magicLink}</Text>
|
|
38
40
|
|
|
39
41
|
<Text className="text-sm text-gray-500">
|
|
40
|
-
If you didn
|
|
42
|
+
If you didn't ask to sign in, you can ignore this email.
|
|
41
43
|
</Text>
|
|
42
44
|
</EmailLayout>
|
|
43
45
|
)
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
MagicLinkEmail.PreviewProps = {
|
|
47
|
-
magicLink: 'https://example.com/magic-link?token=
|
|
48
|
-
companyName:
|
|
49
|
+
magicLink: 'https://example.com/magic-link?token=your_magic_link_token_here',
|
|
50
|
+
companyName: siteConfig.name
|
|
49
51
|
} satisfies MagicLinkEmailProps
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { Button, Heading, Text } from 'react-email'
|
|
2
|
-
import EmailLayout from '
|
|
2
|
+
import EmailLayout from '~/emails/components/email-layout'
|
|
3
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
3
4
|
|
|
4
5
|
interface TestEmailProps {
|
|
5
6
|
name: string
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
/** Sent by `bun run email:test` to check SMTP settings. */
|
|
8
10
|
export default function TestEmail({ name }: TestEmailProps) {
|
|
9
11
|
return (
|
|
10
|
-
<EmailLayout
|
|
12
|
+
<EmailLayout
|
|
13
|
+
previewText="Test email from your app"
|
|
14
|
+
companyName={siteConfig.name}
|
|
15
|
+
>
|
|
11
16
|
<Heading className="mb-4 text-2xl font-bold text-gray-900">
|
|
12
17
|
Test Email for {name}
|
|
13
18
|
</Heading>
|
|
14
|
-
<Text className="mb-4 text-base text-gray-700">
|
|
15
|
-
This is a test email template to verify email functionality.
|
|
16
|
-
</Text>
|
|
17
19
|
<Text className="mb-6 text-base text-gray-700">
|
|
18
|
-
|
|
20
|
+
Your SMTP settings work: this email reached your inbox.
|
|
19
21
|
</Text>
|
|
20
22
|
<Button
|
|
21
23
|
href="https://example.com"
|
|
22
|
-
className="rounded-md
|
|
24
|
+
className="bg-brand rounded-md px-5 py-3 text-center text-sm font-medium text-white"
|
|
23
25
|
>
|
|
24
26
|
Test Button
|
|
25
27
|
</Button>
|
|
@@ -28,5 +30,5 @@ export default function TestEmail({ name }: TestEmailProps) {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
TestEmail.PreviewProps = {
|
|
31
|
-
name: '
|
|
32
|
-
}
|
|
33
|
+
name: 'Alex Morgan'
|
|
34
|
+
} satisfies TestEmailProps
|
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
import { Heading, Text } from 'react-email'
|
|
2
|
-
import EmailLayout from '
|
|
2
|
+
import EmailLayout from '~/emails/components/email-layout'
|
|
3
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
3
4
|
|
|
4
5
|
export interface WelcomeEmailProps {
|
|
5
6
|
userName: string
|
|
6
7
|
companyName: string
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
/** Sent once, right after an account is created. */
|
|
9
11
|
export default function WelcomeEmail({
|
|
10
12
|
userName,
|
|
11
13
|
companyName
|
|
12
14
|
}: WelcomeEmailProps) {
|
|
13
|
-
const previewText = `Welcome to ${companyName}! Your account has been created successfully.`
|
|
14
|
-
|
|
15
15
|
return (
|
|
16
|
-
<EmailLayout
|
|
16
|
+
<EmailLayout
|
|
17
|
+
previewText={`Welcome to ${companyName}. Your account is ready.`}
|
|
18
|
+
companyName={companyName}
|
|
19
|
+
>
|
|
17
20
|
<Heading className="mb-4 text-2xl font-bold text-gray-900">
|
|
18
|
-
Welcome to {companyName}
|
|
21
|
+
Welcome to {companyName}
|
|
19
22
|
</Heading>
|
|
20
23
|
|
|
21
24
|
<Text className="mb-4 text-base text-gray-700">Hi {userName},</Text>
|
|
22
25
|
|
|
23
26
|
<Text className="mb-4 text-base text-gray-700">
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
Your account is ready. Sign in any time with the email address this
|
|
28
|
+
message was sent to.
|
|
26
29
|
</Text>
|
|
27
30
|
</EmailLayout>
|
|
28
31
|
)
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
WelcomeEmail.PreviewProps = {
|
|
32
|
-
userName: '
|
|
33
|
-
companyName:
|
|
35
|
+
userName: 'Alex Morgan',
|
|
36
|
+
companyName: siteConfig.name
|
|
34
37
|
} satisfies WelcomeEmailProps
|
|
@@ -15,7 +15,7 @@ export const env = createEnv({
|
|
|
15
15
|
EMAIL_SERVER_USER: z.string(),
|
|
16
16
|
EMAIL_SERVER_PASSWORD: z.string(),
|
|
17
17
|
EMAIL_SERVER_HOST: z.string(),
|
|
18
|
-
EMAIL_SERVER_PORT: z.
|
|
18
|
+
EMAIL_SERVER_PORT: z.coerce.number().int().min(1).max(65535),
|
|
19
19
|
EMAIL_FROM: z.email(),
|
|
20
20
|
STRIPE_SECRET_KEY: z.string(),
|
|
21
21
|
STRIPE_WEBHOOK_SECRET: z.string(),
|