create-ej-app 25.0.0 → 27.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -226
- package/cli.ts +83 -28
- package/package.json +2 -2
- package/template/api/.vscode/settings.json +1 -1
- package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
- package/template/api/CLAUDE.md +1 -0
- package/template/api/package.json +10 -10
- package/template/nextjs/.dockerignore +1 -0
- package/template/nextjs/AGENTS.md +260 -0
- package/template/nextjs/CLAUDE.md +1 -0
- package/template/nextjs/README.md +109 -0
- package/template/nextjs/bunfig.toml +3 -0
- package/template/nextjs/eslint.config.mjs +6 -5
- package/template/nextjs/gitignore +6 -0
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -40
- package/template/nextjs/public/logo-dark.svg +4 -0
- package/template/nextjs/public/logo.svg +4 -0
- package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
- package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
- package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
- package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
- package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
- package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
- package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
- package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
- package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
- package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
- package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
- package/template/nextjs/src/app/error.tsx +37 -0
- package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/index.ts +60 -0
- package/template/nextjs/src/app/global-error.tsx +38 -0
- package/template/nextjs/src/app/globals.css +29 -0
- package/template/nextjs/src/app/layout.tsx +45 -18
- package/template/nextjs/src/app/not-found.tsx +25 -3
- package/template/nextjs/src/app/robots.ts +29 -0
- package/template/nextjs/src/app/sitemap.ts +21 -0
- package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
- package/template/nextjs/src/components/dashboard-header.tsx +70 -0
- package/template/nextjs/src/components/data-table/enum.ts +5 -0
- package/template/nextjs/src/components/data-table/features.ts +49 -0
- package/template/nextjs/src/components/data-table/index.tsx +961 -0
- package/template/nextjs/src/components/data-table/types.ts +76 -0
- package/template/nextjs/src/components/error.tsx +71 -0
- package/template/nextjs/src/components/loader.tsx +18 -2
- package/template/nextjs/src/components/logo.tsx +49 -0
- package/template/nextjs/src/components/navigation-search.tsx +140 -0
- package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
- package/template/nextjs/src/components/page-heading.tsx +38 -0
- package/template/nextjs/src/components/react-query-provider.tsx +41 -2
- package/template/nextjs/src/components/route-error.tsx +55 -0
- package/template/nextjs/src/components/screen-size.tsx +36 -0
- package/template/nextjs/src/components/site-footer.tsx +20 -4
- package/template/nextjs/src/components/theme-provider.tsx +48 -0
- package/template/nextjs/src/components/theme-toggle.tsx +58 -5
- package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
- package/template/nextjs/src/components/ui/avatar.tsx +112 -0
- package/template/nextjs/src/components/ui/badge.tsx +54 -0
- package/template/nextjs/src/components/ui/button.tsx +3 -1
- package/template/nextjs/src/components/ui/card.tsx +4 -4
- package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
- package/template/nextjs/src/components/ui/command.tsx +192 -0
- package/template/nextjs/src/components/ui/dialog.tsx +164 -0
- package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/nextjs/src/components/ui/input-group.tsx +156 -0
- package/template/nextjs/src/components/ui/select.tsx +195 -0
- package/template/nextjs/src/components/ui/sheet.tsx +143 -0
- package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
- package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
- package/template/nextjs/src/components/ui/table.tsx +116 -0
- package/template/nextjs/src/components/ui/textarea.tsx +18 -0
- package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
- package/template/nextjs/src/components/user-menu.tsx +206 -0
- package/template/nextjs/src/db/common.ts +10 -0
- package/template/nextjs/src/db/index.ts +16 -6
- package/template/nextjs/src/db/relations.ts +21 -0
- package/template/nextjs/src/db/schema.ts +85 -46
- package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
- package/template/nextjs/src/emails/magic-link.tsx +15 -13
- package/template/nextjs/src/emails/test.tsx +11 -9
- package/template/nextjs/src/emails/welcome.tsx +12 -9
- package/template/nextjs/src/env.ts +1 -1
- package/template/nextjs/src/hooks/use-mobile.ts +14 -12
- package/template/nextjs/src/instrumentation.ts +20 -2
- package/template/nextjs/src/lib/actions.ts +55 -0
- package/template/nextjs/src/lib/admin-navigation.ts +43 -0
- package/template/nextjs/src/lib/auth-client.ts +20 -9
- package/template/nextjs/src/lib/auth-permissions.ts +61 -0
- package/template/nextjs/src/lib/auth.ts +33 -3
- package/template/nextjs/src/lib/callback-url.ts +28 -0
- package/template/nextjs/src/lib/constants.ts +21 -0
- package/template/nextjs/src/lib/delay.ts +10 -0
- package/template/nextjs/src/lib/email-service.ts +21 -0
- package/template/nextjs/src/lib/errors.ts +74 -0
- package/template/nextjs/src/lib/format-currency.ts +22 -24
- package/template/nextjs/src/lib/format-date.ts +69 -6
- package/template/nextjs/src/lib/nodemailer.ts +27 -3
- package/template/nextjs/src/lib/otel.ts +92 -2
- package/template/nextjs/src/lib/safe-action-client.ts +60 -9
- package/template/nextjs/src/lib/safe-action.ts +152 -16
- package/template/nextjs/src/lib/search.ts +16 -0
- package/template/nextjs/src/lib/seo.ts +7 -0
- package/template/nextjs/src/lib/siteConfig.ts +17 -1
- package/template/nextjs/src/lib/toast-message.ts +87 -1
- package/template/nextjs/src/lib/validations.ts +30 -0
- package/template/nextjs/src/proxy.ts +48 -0
- package/template/nextjs/src/scripts/reset-db.ts +22 -0
- package/template/nextjs/src/scripts/seed.ts +59 -8
- package/template/nextjs/src/scripts/test-mail.ts +28 -20
- package/template/nextjs/tests/admin/users.test.ts +64 -0
- package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
- package/template/nextjs/tests/db/seed.test.ts +49 -0
- package/template/nextjs/tests/helpers/action.ts +71 -0
- package/template/nextjs/tests/helpers/fixtures.ts +78 -0
- package/template/nextjs/tests/helpers/index.ts +2 -0
- package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
- package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/nextjs/tests/lib/errors.test.ts +10 -0
- package/template/nextjs/tests/lib/search.test.ts +23 -0
- package/template/nextjs/tests/setup/pglite-db.ts +63 -0
- package/template/nextjs/tests/setup/preload.ts +95 -0
- package/template/nextjs/tsconfig.json +1 -0
- package/template/saas-starter/.dockerignore +8 -0
- package/template/saas-starter/.env.example +25 -0
- package/template/saas-starter/.vscode/extensions.json +11 -0
- package/template/saas-starter/.vscode/settings.json +3 -0
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/Dockerfile +15 -0
- package/template/saas-starter/README.md +125 -0
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/components.json +25 -0
- package/template/saas-starter/drizzle.config.ts +10 -0
- package/template/saas-starter/eslint.config.mjs +24 -0
- package/template/saas-starter/gitignore +14 -0
- package/template/saas-starter/next.config.ts +32 -0
- package/template/saas-starter/package.json +97 -0
- package/template/saas-starter/postcss.config.mjs +7 -0
- package/template/saas-starter/prettier.config.mjs +11 -0
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/public/placeholder.svg +1 -0
- package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
- package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +183 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
- package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +60 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +216 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +189 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +31 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +92 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +357 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +130 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +222 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +115 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +168 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +604 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +33 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +55 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +31 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +26 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +30 -0
- package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +37 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +200 -0
- package/template/saas-starter/src/app/(app)/app/layout.tsx +64 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +117 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +40 -0
- package/template/saas-starter/src/app/(app)/app/page.tsx +76 -0
- package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +131 -0
- package/template/saas-starter/src/app/(auth)/login/page.tsx +105 -0
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +137 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +94 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +153 -0
- package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +48 -0
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +24 -0
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +63 -0
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +41 -0
- package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
- package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
- package/template/saas-starter/src/app/actions/uploads.ts +36 -0
- package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
- package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/favicon.ico +0 -0
- package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/index.ts +60 -0
- package/template/saas-starter/src/app/global-error.tsx +38 -0
- package/template/saas-starter/src/app/globals.css +269 -0
- package/template/saas-starter/src/app/layout.tsx +77 -0
- package/template/saas-starter/src/app/not-found.tsx +49 -0
- package/template/saas-starter/src/app/robots.ts +36 -0
- package/template/saas-starter/src/app/sitemap.ts +21 -0
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +70 -0
- package/template/saas-starter/src/components/data-table/enum.ts +5 -0
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +961 -0
- package/template/saas-starter/src/components/data-table/types.ts +76 -0
- package/template/saas-starter/src/components/error.tsx +71 -0
- package/template/saas-starter/src/components/file-upload.tsx +254 -0
- package/template/saas-starter/src/components/loader.tsx +41 -0
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +140 -0
- package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
- package/template/saas-starter/src/components/page-heading.tsx +38 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +51 -0
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +76 -0
- package/template/saas-starter/src/components/site-footer.tsx +39 -0
- package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
- package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
- package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
- package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
- package/template/saas-starter/src/components/theme-provider.tsx +115 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +75 -0
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
- package/template/saas-starter/src/components/ui/badge.tsx +54 -0
- package/template/saas-starter/src/components/ui/button.tsx +69 -0
- package/template/saas-starter/src/components/ui/card.tsx +103 -0
- package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
- package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
- package/template/saas-starter/src/components/ui/command.tsx +192 -0
- package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
- package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/saas-starter/src/components/ui/field.tsx +238 -0
- package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
- package/template/saas-starter/src/components/ui/input.tsx +19 -0
- package/template/saas-starter/src/components/ui/label.tsx +24 -0
- package/template/saas-starter/src/components/ui/popover.tsx +89 -0
- package/template/saas-starter/src/components/ui/select.tsx +195 -0
- package/template/saas-starter/src/components/ui/separator.tsx +28 -0
- package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
- package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
- package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
- package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
- package/template/saas-starter/src/components/ui/switch.tsx +33 -0
- package/template/saas-starter/src/components/ui/table.tsx +116 -0
- package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
- package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
- package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
- package/template/saas-starter/src/components/user-menu.tsx +213 -0
- package/template/saas-starter/src/db/common.ts +63 -0
- package/template/saas-starter/src/db/enums.ts +50 -0
- package/template/saas-starter/src/db/index.ts +60 -0
- package/template/saas-starter/src/db/relations.ts +112 -0
- package/template/saas-starter/src/db/schema.ts +342 -0
- package/template/saas-starter/src/emails/components/email-layout.tsx +79 -0
- package/template/saas-starter/src/emails/invitation.tsx +69 -0
- package/template/saas-starter/src/emails/magic-link.tsx +51 -0
- package/template/saas-starter/src/emails/test.tsx +34 -0
- package/template/saas-starter/src/emails/welcome.tsx +37 -0
- package/template/saas-starter/src/env.ts +66 -0
- package/template/saas-starter/src/hooks/use-mobile.ts +21 -0
- package/template/saas-starter/src/instrumentation.ts +44 -0
- package/template/saas-starter/src/lib/actions.ts +55 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +63 -0
- package/template/saas-starter/src/lib/app-navigation.ts +63 -0
- package/template/saas-starter/src/lib/auth-client.ts +34 -0
- package/template/saas-starter/src/lib/auth-permissions.ts +61 -0
- package/template/saas-starter/src/lib/auth.ts +312 -0
- package/template/saas-starter/src/lib/billing.ts +152 -0
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/cn.ts +4 -0
- package/template/saas-starter/src/lib/constants.ts +91 -0
- package/template/saas-starter/src/lib/delay.ts +12 -0
- package/template/saas-starter/src/lib/email-service.ts +118 -0
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +25 -0
- package/template/saas-starter/src/lib/format-date.ts +75 -0
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +67 -0
- package/template/saas-starter/src/lib/organization-access.ts +73 -0
- package/template/saas-starter/src/lib/otel.ts +117 -0
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +90 -0
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +126 -0
- package/template/saas-starter/src/lib/safe-action.ts +187 -0
- package/template/saas-starter/src/lib/search.ts +16 -0
- package/template/saas-starter/src/lib/seo.ts +7 -0
- package/template/saas-starter/src/lib/siteConfig.ts +20 -0
- package/template/saas-starter/src/lib/storage.ts +311 -0
- package/template/saas-starter/src/lib/stripe.ts +9 -0
- package/template/saas-starter/src/lib/toast-message.ts +101 -0
- package/template/saas-starter/src/lib/validations.ts +95 -0
- package/template/saas-starter/src/proxy.ts +48 -0
- package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
- package/template/saas-starter/src/scripts/reset-db.ts +22 -0
- package/template/saas-starter/src/scripts/seed.ts +85 -0
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +206 -0
- package/template/saas-starter/src/scripts/test-mail.ts +43 -0
- package/template/saas-starter/tests/admin/users.test.ts +64 -0
- package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
- package/template/saas-starter/tests/db/seed.test.ts +57 -0
- package/template/saas-starter/tests/helpers/action.ts +71 -0
- package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
- package/template/saas-starter/tests/helpers/index.ts +2 -0
- package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
- package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/saas-starter/tests/lib/errors.test.ts +10 -0
- package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
- package/template/saas-starter/tests/lib/search.test.ts +23 -0
- package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
- package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
- package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
- package/template/saas-starter/tests/setup/preload.ts +108 -0
- package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
- package/template/saas-starter/tsconfig.json +52 -0
- package/cli.js +0 -143
- package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
- package/template/nextjs/src/app/(app)/layout.tsx +0 -20
- package/template/nextjs/terraform/artifact-registry.tf +0 -5
- package/template/nextjs/terraform/backend-service.tf +0 -13
- package/template/nextjs/terraform/cleanup.sh +0 -9
- package/template/nextjs/terraform/cloudbuild.tf +0 -70
- package/template/nextjs/terraform/cloudrun.tf +0 -68
- package/template/nextjs/terraform/database.tf +0 -60
- package/template/nextjs/terraform/forwarding-rule.tf +0 -43
- package/template/nextjs/terraform/gitignore +0 -4
- package/template/nextjs/terraform/ip-address.tf +0 -11
- package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
- package/template/nextjs/terraform/network.tf +0 -21
- package/template/nextjs/terraform/pre.sh +0 -3
- package/template/nextjs/terraform/providers.tf +0 -18
- package/template/nextjs/terraform/secret.txt +0 -11
- package/template/nextjs/terraform/setup.sh +0 -25
- package/template/nextjs/terraform/ssl-certs.tf +0 -32
- package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
- package/template/nextjs/terraform/target-https-proxy.tf +0 -14
- package/template/nextjs/terraform/terraform.tfvars +0 -4
- package/template/nextjs/terraform/url-map.tf +0 -23
- package/template/nextjs/terraform/variables.tf +0 -21
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
4
|
+
import { Mail, ShieldCheck } from 'lucide-react'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'
|
|
9
|
+
import { Badge } from '~/components/ui/badge'
|
|
10
|
+
import { Button } from '~/components/ui/button'
|
|
11
|
+
import {
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardDescription,
|
|
15
|
+
CardHeader,
|
|
16
|
+
CardTitle
|
|
17
|
+
} from '~/components/ui/card'
|
|
18
|
+
import { Field, FieldError, FieldLabel } from '~/components/ui/field'
|
|
19
|
+
import { Input } from '~/components/ui/input'
|
|
20
|
+
import { Skeleton } from '~/components/ui/skeleton'
|
|
21
|
+
import { updateUser, useSession } from '~/lib/auth-client'
|
|
22
|
+
import {
|
|
23
|
+
toastActionError,
|
|
24
|
+
toastErrorMessage,
|
|
25
|
+
toastSuccessMessage
|
|
26
|
+
} from '~/lib/toast-message'
|
|
27
|
+
import { stringValidation } from '~/lib/validations'
|
|
28
|
+
|
|
29
|
+
const profileFormSchema = z.object({
|
|
30
|
+
name: stringValidation
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
type ProfileFormValues = z.infer<typeof profileFormSchema>
|
|
34
|
+
|
|
35
|
+
export function ProfileForm() {
|
|
36
|
+
const { data: session, isPending, refetch } = useSession()
|
|
37
|
+
|
|
38
|
+
if (isPending) {
|
|
39
|
+
return <ProfileFormSkeleton />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!session?.user) {
|
|
43
|
+
return (
|
|
44
|
+
<Card>
|
|
45
|
+
<CardContent className="py-8 text-center">
|
|
46
|
+
<p className="text-muted-foreground text-sm">
|
|
47
|
+
You are not signed in.
|
|
48
|
+
</p>
|
|
49
|
+
<Button asChild className="mt-4">
|
|
50
|
+
<Link href="/login?callbackUrl=/profile">Sign In</Link>
|
|
51
|
+
</Button>
|
|
52
|
+
</CardContent>
|
|
53
|
+
</Card>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return <ProfileFormInner user={session.user} onUpdated={refetch} />
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ProfileFormInner({
|
|
61
|
+
user,
|
|
62
|
+
onUpdated
|
|
63
|
+
}: {
|
|
64
|
+
user: {
|
|
65
|
+
name: string
|
|
66
|
+
email: string
|
|
67
|
+
image?: string | null
|
|
68
|
+
emailVerified?: boolean
|
|
69
|
+
role?: string | null
|
|
70
|
+
}
|
|
71
|
+
onUpdated: () => Promise<void>
|
|
72
|
+
}) {
|
|
73
|
+
const form = useForm<ProfileFormValues>({
|
|
74
|
+
resolver: zodResolver(profileFormSchema),
|
|
75
|
+
defaultValues: {
|
|
76
|
+
name: user.name ?? ''
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const isSubmitting = form.formState.isSubmitting
|
|
81
|
+
const isDirty = form.formState.isDirty
|
|
82
|
+
|
|
83
|
+
const initials =
|
|
84
|
+
user.name
|
|
85
|
+
?.split(' ')
|
|
86
|
+
.slice(0, 2)
|
|
87
|
+
.map(part => part[0])
|
|
88
|
+
.join('')
|
|
89
|
+
.toUpperCase() || 'NA'
|
|
90
|
+
|
|
91
|
+
async function onSubmit(values: ProfileFormValues) {
|
|
92
|
+
if (values.name.trim() === user.name) return
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const res = await updateUser({ name: values.name.trim() })
|
|
96
|
+
if (res?.error) throw res.error
|
|
97
|
+
await onUpdated()
|
|
98
|
+
form.reset({ name: values.name.trim() })
|
|
99
|
+
toastSuccessMessage('Profile updated')
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Failed to update profile:', err)
|
|
102
|
+
if (
|
|
103
|
+
typeof err === 'object' &&
|
|
104
|
+
err &&
|
|
105
|
+
'message' in err &&
|
|
106
|
+
typeof err.message === 'string'
|
|
107
|
+
)
|
|
108
|
+
toastActionError(err)
|
|
109
|
+
else toastErrorMessage('Your profile was not saved. Try again.')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="space-y-6">
|
|
115
|
+
<Card>
|
|
116
|
+
<CardHeader>
|
|
117
|
+
<CardTitle>Account</CardTitle>
|
|
118
|
+
<CardDescription>
|
|
119
|
+
Details from your account. Only your name can change here.
|
|
120
|
+
</CardDescription>
|
|
121
|
+
</CardHeader>
|
|
122
|
+
<CardContent className="space-y-6">
|
|
123
|
+
<div className="flex items-center gap-4">
|
|
124
|
+
<Avatar className="size-16">
|
|
125
|
+
<AvatarFallback className="text-foreground text-lg">
|
|
126
|
+
{initials}
|
|
127
|
+
</AvatarFallback>
|
|
128
|
+
{user.image ? (
|
|
129
|
+
<AvatarImage src={user.image} alt={user.name} />
|
|
130
|
+
) : null}
|
|
131
|
+
</Avatar>
|
|
132
|
+
<div className="space-y-1">
|
|
133
|
+
<p className="text-base font-medium">{user.name}</p>
|
|
134
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
135
|
+
{user.emailVerified ? (
|
|
136
|
+
<Badge variant="success" className="gap-1">
|
|
137
|
+
<ShieldCheck className="size-3" />
|
|
138
|
+
Email verified
|
|
139
|
+
</Badge>
|
|
140
|
+
) : null}
|
|
141
|
+
{user.role ? (
|
|
142
|
+
<Badge variant="outline" className="capitalize">
|
|
143
|
+
{user.role}
|
|
144
|
+
</Badge>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<DetailRow icon={Mail} label="Email" value={user.email} />
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
|
|
154
|
+
<Card>
|
|
155
|
+
<CardHeader>
|
|
156
|
+
<CardTitle>Edit Profile</CardTitle>
|
|
157
|
+
<CardDescription>Update your display name.</CardDescription>
|
|
158
|
+
</CardHeader>
|
|
159
|
+
<CardContent>
|
|
160
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
161
|
+
<Controller
|
|
162
|
+
control={form.control}
|
|
163
|
+
name="name"
|
|
164
|
+
render={({ field, fieldState }) => (
|
|
165
|
+
<Field data-invalid={fieldState.invalid}>
|
|
166
|
+
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
|
167
|
+
<Input
|
|
168
|
+
id={field.name}
|
|
169
|
+
placeholder="Your full name"
|
|
170
|
+
autoComplete="name"
|
|
171
|
+
aria-invalid={fieldState.invalid}
|
|
172
|
+
{...field}
|
|
173
|
+
/>
|
|
174
|
+
<FieldError
|
|
175
|
+
errors={fieldState.error ? [fieldState.error] : undefined}
|
|
176
|
+
/>
|
|
177
|
+
</Field>
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<div className="flex justify-end">
|
|
182
|
+
<Button type="submit" disabled={isSubmitting || !isDirty}>
|
|
183
|
+
{isSubmitting ? 'Saving…' : 'Save'}
|
|
184
|
+
</Button>
|
|
185
|
+
</div>
|
|
186
|
+
</form>
|
|
187
|
+
</CardContent>
|
|
188
|
+
</Card>
|
|
189
|
+
</div>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function DetailRow({
|
|
194
|
+
icon: Icon,
|
|
195
|
+
label,
|
|
196
|
+
value
|
|
197
|
+
}: {
|
|
198
|
+
icon: React.FC<{ className?: string }>
|
|
199
|
+
label: string
|
|
200
|
+
value: string
|
|
201
|
+
}) {
|
|
202
|
+
return (
|
|
203
|
+
<div className="flex items-start gap-3 rounded-lg border p-3">
|
|
204
|
+
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-md">
|
|
205
|
+
<Icon className="size-4" />
|
|
206
|
+
</div>
|
|
207
|
+
<div className="min-w-0 space-y-0.5">
|
|
208
|
+
<p className="text-muted-foreground text-xs tracking-wide uppercase">
|
|
209
|
+
{label}
|
|
210
|
+
</p>
|
|
211
|
+
<p className="text-sm wrap-break-word">{value}</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ProfileFormSkeleton() {
|
|
218
|
+
return (
|
|
219
|
+
<div className="space-y-6">
|
|
220
|
+
<Card>
|
|
221
|
+
<CardHeader>
|
|
222
|
+
<Skeleton className="h-5 w-32" />
|
|
223
|
+
<Skeleton className="mt-2 h-4 w-64" />
|
|
224
|
+
</CardHeader>
|
|
225
|
+
<CardContent className="space-y-6">
|
|
226
|
+
<div className="flex items-center gap-4">
|
|
227
|
+
<Skeleton className="size-16 rounded-full" />
|
|
228
|
+
<div className="space-y-2">
|
|
229
|
+
<Skeleton className="h-4 w-40" />
|
|
230
|
+
<Skeleton className="h-4 w-24" />
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
<Skeleton className="h-16 w-full" />
|
|
234
|
+
</CardContent>
|
|
235
|
+
</Card>
|
|
236
|
+
<Card>
|
|
237
|
+
<CardHeader>
|
|
238
|
+
<Skeleton className="h-5 w-32" />
|
|
239
|
+
</CardHeader>
|
|
240
|
+
<CardContent className="space-y-4">
|
|
241
|
+
<Skeleton className="h-10 w-full" />
|
|
242
|
+
<Skeleton className="h-10 w-32 self-end" />
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email-based login form with magic link
|
|
3
|
+
*
|
|
4
|
+
* Sends a passwordless authentication link to the user's email.
|
|
5
|
+
* Shows success message after link is sent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
'use client'
|
|
2
9
|
|
|
3
10
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
@@ -6,21 +13,41 @@ import { useState } from 'react'
|
|
|
6
13
|
import { Controller, useForm } from 'react-hook-form'
|
|
7
14
|
import { z } from 'zod'
|
|
8
15
|
import { Button } from '~/components/ui/button'
|
|
9
|
-
import {
|
|
10
|
-
Field,
|
|
11
|
-
FieldContent,
|
|
12
|
-
FieldError,
|
|
13
|
-
FieldLabel
|
|
14
|
-
} from '~/components/ui/field'
|
|
16
|
+
import { Field, FieldError, FieldLabel } from '~/components/ui/field'
|
|
15
17
|
import { Input } from '~/components/ui/input'
|
|
16
18
|
import { signIn } from '~/lib/auth-client'
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
toastActionError,
|
|
21
|
+
toastErrorMessage,
|
|
22
|
+
toastSuccessMessage
|
|
23
|
+
} from '~/lib/toast-message'
|
|
24
|
+
import { emailValidation } from '~/lib/validations'
|
|
18
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Email login form schema
|
|
28
|
+
* Validates email format
|
|
29
|
+
*/
|
|
19
30
|
const emailLoginFormSchema = z.object({
|
|
20
|
-
email:
|
|
31
|
+
email: emailValidation
|
|
21
32
|
})
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Email Login Form Component
|
|
36
|
+
*
|
|
37
|
+
* Allows users to sign in via magic link sent to their email.
|
|
38
|
+
* Shows confirmation message after sending the link.
|
|
39
|
+
*
|
|
40
|
+
* **States:**
|
|
41
|
+
* - Form: Ready for email input
|
|
42
|
+
* - Submitted: Shows confirmation after sending link
|
|
43
|
+
*
|
|
44
|
+
* @returns {JSX.Element} Email input form or confirmation message
|
|
45
|
+
*/
|
|
46
|
+
export const EmailLoginForm = ({
|
|
47
|
+
callbackUrl
|
|
48
|
+
}: {
|
|
49
|
+
callbackUrl?: string | null
|
|
50
|
+
}) => {
|
|
24
51
|
const form = useForm<z.infer<typeof emailLoginFormSchema>>({
|
|
25
52
|
resolver: zodResolver(emailLoginFormSchema),
|
|
26
53
|
defaultValues: {
|
|
@@ -40,11 +67,12 @@ export const EmailLoginForm = () => {
|
|
|
40
67
|
try {
|
|
41
68
|
const res = await signIn.magicLink({
|
|
42
69
|
email,
|
|
43
|
-
name: ''
|
|
70
|
+
name: '',
|
|
71
|
+
callbackURL: callbackUrl || '/login'
|
|
44
72
|
})
|
|
45
73
|
if (res?.error) throw res.error
|
|
46
74
|
setIsSent(email)
|
|
47
|
-
toastSuccessMessage('Magic link sent
|
|
75
|
+
toastSuccessMessage('Magic link sent. Check your email.')
|
|
48
76
|
} catch (err) {
|
|
49
77
|
console.error('Error sending magic link:', err)
|
|
50
78
|
if (
|
|
@@ -53,8 +81,8 @@ export const EmailLoginForm = () => {
|
|
|
53
81
|
'message' in err &&
|
|
54
82
|
typeof err.message === 'string'
|
|
55
83
|
)
|
|
56
|
-
|
|
57
|
-
else toastErrorMessage('
|
|
84
|
+
toastActionError(err)
|
|
85
|
+
else toastErrorMessage('The magic link was not sent. Try again.')
|
|
58
86
|
} finally {
|
|
59
87
|
setIsLoading(false)
|
|
60
88
|
}
|
|
@@ -64,7 +92,7 @@ export const EmailLoginForm = () => {
|
|
|
64
92
|
return (
|
|
65
93
|
<div className="rounded-lg border p-4 text-center">
|
|
66
94
|
<Mail className="text-primary mx-auto mb-2 size-6" />
|
|
67
|
-
<h3 className="mb-1 text-sm font-medium">Check
|
|
95
|
+
<h3 className="mb-1 text-sm font-medium">Check Your Email</h3>
|
|
68
96
|
<p className="text-muted-foreground text-xs">
|
|
69
97
|
We've sent a magic link to <strong>{isSent}</strong>
|
|
70
98
|
</p>
|
|
@@ -79,25 +107,24 @@ export const EmailLoginForm = () => {
|
|
|
79
107
|
name="email"
|
|
80
108
|
render={({ field, fieldState }) => (
|
|
81
109
|
<Field data-invalid={fieldState.invalid}>
|
|
82
|
-
<FieldLabel htmlFor=
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</FieldContent>
|
|
110
|
+
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
|
111
|
+
<Input
|
|
112
|
+
id={field.name}
|
|
113
|
+
placeholder="name@example.com"
|
|
114
|
+
autoCapitalize="off"
|
|
115
|
+
autoComplete="email"
|
|
116
|
+
autoFocus
|
|
117
|
+
aria-invalid={fieldState.invalid}
|
|
118
|
+
{...field}
|
|
119
|
+
/>
|
|
120
|
+
<FieldError
|
|
121
|
+
errors={fieldState.error ? [fieldState.error] : undefined}
|
|
122
|
+
/>
|
|
96
123
|
</Field>
|
|
97
124
|
)}
|
|
98
125
|
/>
|
|
99
126
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
100
|
-
{isLoading ? 'Sending
|
|
127
|
+
{isLoading ? 'Sending…' : 'Send Magic Link'}
|
|
101
128
|
</Button>
|
|
102
129
|
</form>
|
|
103
130
|
)
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Login/Sign-in Page
|
|
3
|
+
*
|
|
4
|
+
* Authentication entry point supporting multiple sign-in methods:
|
|
5
|
+
* - Email with magic link (passwordless)
|
|
6
|
+
* - OAuth (Google, GitHub)
|
|
7
|
+
*
|
|
8
|
+
* **Behavior:**
|
|
9
|
+
* - Redirects to /admin for admin users
|
|
10
|
+
* - Redirects to /app for regular authenticated users
|
|
11
|
+
* - Shows login form for anonymous users
|
|
12
|
+
*
|
|
13
|
+
* @module app/(auth)/login/page
|
|
14
|
+
*/
|
|
15
|
+
|
|
2
16
|
import Link from 'next/link'
|
|
3
17
|
import { redirect } from 'next/navigation'
|
|
18
|
+
import { Logo } from '~/components/logo'
|
|
4
19
|
import {
|
|
5
20
|
Card,
|
|
6
21
|
CardContent,
|
|
@@ -11,22 +26,41 @@ import {
|
|
|
11
26
|
} from '~/components/ui/card'
|
|
12
27
|
import { Separator } from '~/components/ui/separator'
|
|
13
28
|
import { getAuthSession } from '~/lib/auth'
|
|
29
|
+
import { sanitizeCallbackUrl } from '~/lib/callback-url'
|
|
14
30
|
import { EmailLoginForm } from './email-login-form'
|
|
15
31
|
import { SocialLoginButtons } from './social-login-buttons'
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Login Page Component
|
|
35
|
+
*
|
|
36
|
+
* Renders authentication page with multiple login options.
|
|
37
|
+
* Checks session and redirects already-authenticated users.
|
|
38
|
+
*
|
|
39
|
+
* @returns {JSX.Element} Login form with email and OAuth options
|
|
40
|
+
*/
|
|
41
|
+
export default async function LoginPage({
|
|
42
|
+
searchParams
|
|
43
|
+
}: {
|
|
44
|
+
searchParams?: Promise<Record<string, string | string[] | undefined>>
|
|
45
|
+
}) {
|
|
18
46
|
const authSession = await getAuthSession()
|
|
47
|
+
const resolvedParams = (await searchParams) ?? {}
|
|
48
|
+
const callbackUrl = sanitizeCallbackUrl(resolvedParams.callbackUrl)
|
|
19
49
|
|
|
20
|
-
if (authSession)
|
|
50
|
+
if (authSession?.isAdmin) {
|
|
51
|
+
return redirect(callbackUrl ?? '/admin')
|
|
52
|
+
} else if (authSession) {
|
|
53
|
+
return redirect(callbackUrl ?? '/app')
|
|
54
|
+
}
|
|
21
55
|
|
|
22
56
|
return (
|
|
23
57
|
<div className="flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
|
24
58
|
<Card className="w-full max-w-md">
|
|
25
59
|
<CardHeader className="space-y-1 text-center">
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
</
|
|
29
|
-
<CardTitle className="text-2xl font-bold">Welcome
|
|
60
|
+
<Link href="/" className="mx-auto mb-4 inline-flex">
|
|
61
|
+
<Logo size={40} priority className="text-xl" />
|
|
62
|
+
</Link>
|
|
63
|
+
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
|
|
30
64
|
<CardDescription className="text-muted-foreground text-sm">
|
|
31
65
|
Sign in to your account to continue
|
|
32
66
|
</CardDescription>
|
|
@@ -35,12 +69,11 @@ export default async function LoginPage() {
|
|
|
35
69
|
<div className="border-muted-foreground/50 bg-muted/50 rounded-lg border border-dashed p-4">
|
|
36
70
|
<p className="text-muted-foreground text-sm">
|
|
37
71
|
<strong>New to our site?</strong> No need to create a separate
|
|
38
|
-
account.
|
|
39
|
-
log in.
|
|
72
|
+
account. Use either option below to sign up and log in.
|
|
40
73
|
</p>
|
|
41
74
|
</div>
|
|
42
75
|
|
|
43
|
-
<EmailLoginForm />
|
|
76
|
+
<EmailLoginForm callbackUrl={callbackUrl} />
|
|
44
77
|
|
|
45
78
|
<div className="relative">
|
|
46
79
|
<div className="absolute inset-0 flex items-center">
|
|
@@ -53,7 +86,7 @@ export default async function LoginPage() {
|
|
|
53
86
|
</div>
|
|
54
87
|
</div>
|
|
55
88
|
|
|
56
|
-
<SocialLoginButtons />
|
|
89
|
+
<SocialLoginButtons callbackUrl={callbackUrl} />
|
|
57
90
|
</CardContent>
|
|
58
91
|
<CardFooter className="flex flex-col space-y-4">
|
|
59
92
|
<Separator className="my-2" />
|
|
@@ -62,7 +95,7 @@ export default async function LoginPage() {
|
|
|
62
95
|
href="/"
|
|
63
96
|
className="text-primary underline-offset-4 hover:underline"
|
|
64
97
|
>
|
|
65
|
-
Return to
|
|
98
|
+
Return to Home Page
|
|
66
99
|
</Link>
|
|
67
100
|
</div>
|
|
68
101
|
</CardFooter>
|
|
@@ -1,26 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social authentication button components
|
|
3
|
+
*
|
|
4
|
+
* Provides OAuth sign-in buttons for Google and GitHub.
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
'use client'
|
|
2
8
|
|
|
9
|
+
import { useState, useSyncExternalStore } from 'react'
|
|
3
10
|
import { Button } from '~/components/ui/button'
|
|
4
11
|
import { getLastUsedLoginMethod, signIn } from '~/lib/auth-client'
|
|
12
|
+
import { toastActionError, toastErrorMessage } from '~/lib/toast-message'
|
|
13
|
+
|
|
14
|
+
type OAuthProvider = 'google' | 'github'
|
|
15
|
+
|
|
16
|
+
const PROVIDER_LABELS: Record<OAuthProvider, string> = {
|
|
17
|
+
google: 'Google',
|
|
18
|
+
github: 'GitHub'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const emptySubscribe = () => () => {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Social Login Buttons Component
|
|
25
|
+
*
|
|
26
|
+
* Renders OAuth provider buttons (Google, GitHub).
|
|
27
|
+
* Highlights the last-used login method if available.
|
|
28
|
+
*
|
|
29
|
+
* @returns {JSX.Element} Flex container with OAuth provider buttons
|
|
30
|
+
*/
|
|
31
|
+
export const SocialLoginButtons = ({
|
|
32
|
+
callbackUrl
|
|
33
|
+
}: {
|
|
34
|
+
callbackUrl?: string | null
|
|
35
|
+
}) => {
|
|
36
|
+
// Cookie/local storage is only available on the client; server snapshot is null.
|
|
37
|
+
const lastUsedLoginMethod = useSyncExternalStore(
|
|
38
|
+
emptySubscribe,
|
|
39
|
+
() => getLastUsedLoginMethod() ?? null,
|
|
40
|
+
() => null
|
|
41
|
+
)
|
|
42
|
+
const [pendingProvider, setPendingProvider] = useState<OAuthProvider | null>(
|
|
43
|
+
null
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const target = callbackUrl || '/login'
|
|
47
|
+
const isPending = pendingProvider !== null
|
|
48
|
+
|
|
49
|
+
async function startOAuth(provider: OAuthProvider) {
|
|
50
|
+
if (pendingProvider) return
|
|
5
51
|
|
|
6
|
-
|
|
7
|
-
|
|
52
|
+
setPendingProvider(provider)
|
|
53
|
+
try {
|
|
54
|
+
const res = await signIn.social({
|
|
55
|
+
provider,
|
|
56
|
+
callbackURL: target
|
|
57
|
+
})
|
|
58
|
+
if (res?.error) throw res.error
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`Error starting ${provider} sign-in:`, err)
|
|
61
|
+
if (
|
|
62
|
+
typeof err === 'object' &&
|
|
63
|
+
err &&
|
|
64
|
+
'message' in err &&
|
|
65
|
+
typeof err.message === 'string'
|
|
66
|
+
) {
|
|
67
|
+
toastActionError(err)
|
|
68
|
+
} else {
|
|
69
|
+
toastErrorMessage(
|
|
70
|
+
`Sign-in with ${PROVIDER_LABELS[provider]} did not start. Try again.`
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
setPendingProvider(null)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
8
76
|
|
|
9
77
|
return (
|
|
10
|
-
<div className="flex flex-col
|
|
78
|
+
<div className="flex flex-col gap-2">
|
|
11
79
|
<Button
|
|
80
|
+
type="button"
|
|
12
81
|
variant="outline"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
provider: 'google'
|
|
16
|
-
})
|
|
17
|
-
}}
|
|
82
|
+
disabled={isPending}
|
|
83
|
+
onClick={() => startOAuth('google')}
|
|
18
84
|
>
|
|
19
85
|
<svg
|
|
20
86
|
viewBox="0 0 24 24"
|
|
21
87
|
width="16"
|
|
22
88
|
height="16"
|
|
23
89
|
xmlns="http://www.w3.org/2000/svg"
|
|
90
|
+
aria-hidden
|
|
24
91
|
>
|
|
25
92
|
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
|
|
26
93
|
<path
|
|
@@ -41,28 +108,29 @@ export const SocialLoginButtons = () => {
|
|
|
41
108
|
/>
|
|
42
109
|
</g>
|
|
43
110
|
</svg>
|
|
44
|
-
|
|
45
|
-
|
|
111
|
+
{pendingProvider === 'google'
|
|
112
|
+
? 'Continuing with Google…'
|
|
113
|
+
: `Continue with Google${lastUsedLoginMethod === 'google' ? ' (last used)' : ''}`}
|
|
46
114
|
</Button>
|
|
47
115
|
|
|
48
116
|
<Button
|
|
117
|
+
type="button"
|
|
49
118
|
variant="outline"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
provider: 'github'
|
|
53
|
-
})
|
|
54
|
-
}}
|
|
119
|
+
disabled={isPending}
|
|
120
|
+
onClick={() => startOAuth('github')}
|
|
55
121
|
>
|
|
56
122
|
<svg
|
|
57
123
|
viewBox="0 0 24 24"
|
|
58
124
|
width="16"
|
|
59
125
|
height="16"
|
|
60
126
|
xmlns="http://www.w3.org/2000/svg"
|
|
127
|
+
aria-hidden
|
|
61
128
|
>
|
|
62
129
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
63
130
|
</svg>
|
|
64
|
-
|
|
65
|
-
|
|
131
|
+
{pendingProvider === 'github'
|
|
132
|
+
? 'Continuing with GitHub…'
|
|
133
|
+
: `Continue with GitHub${lastUsedLoginMethod === 'github' ? ' (last used)' : ''}`}
|
|
66
134
|
</Button>
|
|
67
135
|
</div>
|
|
68
136
|
)
|
|
@@ -1,14 +1,45 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { Logo } from '~/components/logo'
|
|
1
3
|
import { SiteFooter } from '~/components/site-footer'
|
|
4
|
+
import { Button } from '~/components/ui/button'
|
|
5
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
2
6
|
|
|
3
|
-
export default
|
|
7
|
+
export default function Layout({
|
|
4
8
|
children
|
|
5
9
|
}: Readonly<{
|
|
6
10
|
children: React.ReactNode
|
|
7
11
|
}>) {
|
|
8
12
|
return (
|
|
9
|
-
<div className="flex min-h-screen flex-col
|
|
10
|
-
<
|
|
11
|
-
|
|
13
|
+
<div className="bg-background text-foreground flex min-h-screen flex-col">
|
|
14
|
+
<a
|
|
15
|
+
href="#main-content"
|
|
16
|
+
className="bg-background text-foreground focus:ring-ring/50 sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-200 focus:rounded-md focus:px-4 focus:py-2 focus:ring-3"
|
|
17
|
+
>
|
|
18
|
+
Skip to content
|
|
19
|
+
</a>
|
|
20
|
+
<header className="bg-background/80 sticky top-0 z-50 border-b backdrop-blur">
|
|
21
|
+
<nav
|
|
22
|
+
aria-label="Primary"
|
|
23
|
+
className="container flex h-14 items-center justify-between gap-4"
|
|
24
|
+
>
|
|
25
|
+
<Link href="/" className="flex items-center">
|
|
26
|
+
<Logo size={28} priority className="text-lg" />
|
|
27
|
+
</Link>
|
|
28
|
+
<div className="flex items-center gap-2">
|
|
29
|
+
<Button asChild size="sm">
|
|
30
|
+
<Link href="/login">Log In</Link>
|
|
31
|
+
</Button>
|
|
32
|
+
</div>
|
|
33
|
+
</nav>
|
|
34
|
+
</header>
|
|
35
|
+
<main
|
|
36
|
+
id="main-content"
|
|
37
|
+
tabIndex={-1}
|
|
38
|
+
className="focus-visible:outline-primary flex-1 focus-visible:outline-2 focus-visible:-outline-offset-2"
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</main>
|
|
42
|
+
<SiteFooter name={siteConfig.name} />
|
|
12
43
|
</div>
|
|
13
44
|
)
|
|
14
45
|
}
|