create-ej-app 26.0.0 → 28.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/README.md +125 -226
  2. package/cli.ts +79 -29
  3. package/package.json +2 -2
  4. package/template/api/.vscode/settings.json +1 -1
  5. package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
  6. package/template/api/CLAUDE.md +1 -0
  7. package/template/api/package.json +10 -10
  8. package/template/nextjs/.dockerignore +1 -0
  9. package/template/nextjs/AGENTS.md +260 -0
  10. package/template/nextjs/CLAUDE.md +1 -0
  11. package/template/nextjs/README.md +109 -0
  12. package/template/nextjs/bunfig.toml +3 -0
  13. package/template/nextjs/eslint.config.mjs +6 -5
  14. package/template/nextjs/gitignore +6 -1
  15. package/template/nextjs/next.config.ts +8 -1
  16. package/template/nextjs/package.json +47 -39
  17. package/template/nextjs/public/logo-dark.svg +4 -0
  18. package/template/nextjs/public/logo.svg +4 -0
  19. package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
  20. package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
  21. package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  22. package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
  23. package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
  24. package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
  25. package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
  26. package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  27. package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  28. package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
  29. package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  30. package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  31. package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
  32. package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
  33. package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
  34. package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
  35. package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  36. package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
  37. package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
  38. package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
  39. package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
  40. package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
  41. package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
  42. package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
  43. package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
  44. package/template/nextjs/src/app/error.tsx +37 -0
  45. package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  46. package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  47. package/template/nextjs/src/app/fonts/index.ts +60 -0
  48. package/template/nextjs/src/app/global-error.tsx +38 -0
  49. package/template/nextjs/src/app/globals.css +29 -0
  50. package/template/nextjs/src/app/layout.tsx +45 -18
  51. package/template/nextjs/src/app/not-found.tsx +25 -3
  52. package/template/nextjs/src/app/robots.ts +29 -0
  53. package/template/nextjs/src/app/sitemap.ts +21 -0
  54. package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
  55. package/template/nextjs/src/components/dashboard-header.tsx +70 -0
  56. package/template/nextjs/src/components/data-table/enum.ts +5 -0
  57. package/template/nextjs/src/components/data-table/features.ts +49 -0
  58. package/template/nextjs/src/components/data-table/index.tsx +961 -0
  59. package/template/nextjs/src/components/data-table/types.ts +76 -0
  60. package/template/nextjs/src/components/error.tsx +71 -0
  61. package/template/nextjs/src/components/loader.tsx +18 -2
  62. package/template/nextjs/src/components/logo.tsx +49 -0
  63. package/template/nextjs/src/components/navigation-search.tsx +140 -0
  64. package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
  65. package/template/nextjs/src/components/page-heading.tsx +38 -0
  66. package/template/nextjs/src/components/react-query-provider.tsx +41 -2
  67. package/template/nextjs/src/components/route-error.tsx +55 -0
  68. package/template/nextjs/src/components/screen-size.tsx +36 -0
  69. package/template/nextjs/src/components/site-footer.tsx +20 -4
  70. package/template/nextjs/src/components/theme-provider.tsx +48 -0
  71. package/template/nextjs/src/components/theme-toggle.tsx +58 -5
  72. package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
  73. package/template/nextjs/src/components/ui/avatar.tsx +112 -0
  74. package/template/nextjs/src/components/ui/badge.tsx +54 -0
  75. package/template/nextjs/src/components/ui/button.tsx +3 -1
  76. package/template/nextjs/src/components/ui/card.tsx +4 -4
  77. package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
  78. package/template/nextjs/src/components/ui/command.tsx +192 -0
  79. package/template/nextjs/src/components/ui/dialog.tsx +164 -0
  80. package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
  81. package/template/nextjs/src/components/ui/input-group.tsx +156 -0
  82. package/template/nextjs/src/components/ui/select.tsx +195 -0
  83. package/template/nextjs/src/components/ui/sheet.tsx +143 -0
  84. package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
  85. package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
  86. package/template/nextjs/src/components/ui/table.tsx +116 -0
  87. package/template/nextjs/src/components/ui/textarea.tsx +18 -0
  88. package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
  89. package/template/nextjs/src/components/user-menu.tsx +206 -0
  90. package/template/nextjs/src/db/common.ts +10 -0
  91. package/template/nextjs/src/db/index.ts +16 -6
  92. package/template/nextjs/src/db/relations.ts +21 -0
  93. package/template/nextjs/src/db/schema.ts +85 -46
  94. package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
  95. package/template/nextjs/src/emails/magic-link.tsx +15 -13
  96. package/template/nextjs/src/emails/test.tsx +11 -9
  97. package/template/nextjs/src/emails/welcome.tsx +12 -9
  98. package/template/nextjs/src/env.ts +1 -1
  99. package/template/nextjs/src/hooks/use-mobile.ts +14 -12
  100. package/template/nextjs/src/instrumentation.ts +20 -2
  101. package/template/nextjs/src/lib/actions.ts +55 -0
  102. package/template/nextjs/src/lib/admin-navigation.ts +43 -0
  103. package/template/nextjs/src/lib/auth-client.ts +20 -9
  104. package/template/nextjs/src/lib/auth-permissions.ts +61 -0
  105. package/template/nextjs/src/lib/auth.ts +33 -3
  106. package/template/nextjs/src/lib/callback-url.ts +28 -0
  107. package/template/nextjs/src/lib/constants.ts +21 -0
  108. package/template/nextjs/src/lib/delay.ts +10 -0
  109. package/template/nextjs/src/lib/email-service.ts +21 -0
  110. package/template/nextjs/src/lib/errors.ts +74 -0
  111. package/template/nextjs/src/lib/format-currency.ts +22 -24
  112. package/template/nextjs/src/lib/format-date.ts +69 -6
  113. package/template/nextjs/src/lib/nodemailer.ts +27 -3
  114. package/template/nextjs/src/lib/otel.ts +92 -2
  115. package/template/nextjs/src/lib/safe-action-client.ts +60 -9
  116. package/template/nextjs/src/lib/safe-action.ts +152 -16
  117. package/template/nextjs/src/lib/search.ts +16 -0
  118. package/template/nextjs/src/lib/seo.ts +7 -0
  119. package/template/nextjs/src/lib/siteConfig.ts +17 -1
  120. package/template/nextjs/src/lib/toast-message.ts +87 -1
  121. package/template/nextjs/src/lib/validations.ts +30 -0
  122. package/template/nextjs/src/proxy.ts +48 -0
  123. package/template/nextjs/src/scripts/reset-db.ts +22 -0
  124. package/template/nextjs/src/scripts/seed.ts +59 -8
  125. package/template/nextjs/src/scripts/test-mail.ts +28 -20
  126. package/template/nextjs/tests/admin/users.test.ts +64 -0
  127. package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
  128. package/template/nextjs/tests/db/seed.test.ts +49 -0
  129. package/template/nextjs/tests/helpers/action.ts +71 -0
  130. package/template/nextjs/tests/helpers/fixtures.ts +78 -0
  131. package/template/nextjs/tests/helpers/index.ts +2 -0
  132. package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
  133. package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
  134. package/template/nextjs/tests/lib/errors.test.ts +10 -0
  135. package/template/nextjs/tests/lib/search.test.ts +23 -0
  136. package/template/nextjs/tests/setup/pglite-db.ts +63 -0
  137. package/template/nextjs/tests/setup/preload.ts +95 -0
  138. package/template/nextjs/tsconfig.json +1 -0
  139. package/template/saas-starter/.dockerignore +1 -0
  140. package/template/saas-starter/.env.example +15 -15
  141. package/template/saas-starter/AGENTS.md +302 -0
  142. package/template/saas-starter/CLAUDE.md +1 -0
  143. package/template/saas-starter/README.md +124 -2
  144. package/template/saas-starter/bunfig.toml +3 -0
  145. package/template/saas-starter/eslint.config.mjs +6 -5
  146. package/template/saas-starter/gitignore +6 -1
  147. package/template/saas-starter/next.config.ts +19 -1
  148. package/template/saas-starter/package.json +50 -44
  149. package/template/saas-starter/public/logo-dark.svg +4 -0
  150. package/template/saas-starter/public/logo.svg +4 -0
  151. package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
  152. package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
  153. package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
  154. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
  155. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
  156. package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
  157. package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
  158. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
  159. package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
  160. package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
  161. package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  162. package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  163. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
  164. package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  165. package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  166. package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
  167. package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
  168. package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
  169. package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
  170. package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
  171. package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
  172. package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
  173. package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
  174. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
  175. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
  176. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
  177. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
  178. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
  179. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
  180. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
  181. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
  182. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
  183. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
  184. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
  185. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
  186. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
  187. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
  188. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
  189. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
  190. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
  191. package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
  192. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
  193. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
  194. package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
  195. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
  196. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
  197. package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
  198. package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
  199. package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
  200. package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  201. package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
  202. package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
  203. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
  204. package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
  205. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
  206. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
  207. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
  208. package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
  209. package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
  210. package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
  211. package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
  212. package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
  213. package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
  214. package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
  215. package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
  216. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
  217. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
  218. package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
  219. package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
  220. package/template/saas-starter/src/app/actions/uploads.ts +36 -0
  221. package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
  222. package/template/saas-starter/src/app/error.tsx +37 -0
  223. package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  224. package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  225. package/template/saas-starter/src/app/fonts/index.ts +60 -0
  226. package/template/saas-starter/src/app/global-error.tsx +38 -0
  227. package/template/saas-starter/src/app/globals.css +29 -0
  228. package/template/saas-starter/src/app/layout.tsx +43 -19
  229. package/template/saas-starter/src/app/not-found.tsx +25 -3
  230. package/template/saas-starter/src/app/robots.ts +14 -10
  231. package/template/saas-starter/src/app/sitemap.ts +2 -1
  232. package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
  233. package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
  234. package/template/saas-starter/src/components/data-table/features.ts +49 -0
  235. package/template/saas-starter/src/components/data-table/index.tsx +65 -64
  236. package/template/saas-starter/src/components/data-table/types.ts +5 -4
  237. package/template/saas-starter/src/components/error.tsx +45 -0
  238. package/template/saas-starter/src/components/file-upload.tsx +65 -26
  239. package/template/saas-starter/src/components/loader.tsx +18 -2
  240. package/template/saas-starter/src/components/logo.tsx +49 -0
  241. package/template/saas-starter/src/components/navigation-search.tsx +2 -2
  242. package/template/saas-starter/src/components/page-heading.tsx +6 -0
  243. package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
  244. package/template/saas-starter/src/components/route-error.tsx +55 -0
  245. package/template/saas-starter/src/components/screen-size.tsx +36 -0
  246. package/template/saas-starter/src/components/site-footer.tsx +20 -4
  247. package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
  248. package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
  249. package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
  250. package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
  251. package/template/saas-starter/src/components/theme-provider.tsx +48 -0
  252. package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
  253. package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
  254. package/template/saas-starter/src/components/ui/badge.tsx +6 -1
  255. package/template/saas-starter/src/components/ui/button.tsx +2 -0
  256. package/template/saas-starter/src/components/ui/command.tsx +1 -1
  257. package/template/saas-starter/src/components/user-menu.tsx +147 -24
  258. package/template/saas-starter/src/db/common.ts +33 -1
  259. package/template/saas-starter/src/db/enums.ts +24 -0
  260. package/template/saas-starter/src/db/index.ts +14 -5
  261. package/template/saas-starter/src/db/relations.ts +40 -3
  262. package/template/saas-starter/src/db/schema.ts +186 -35
  263. package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
  264. package/template/saas-starter/src/emails/invitation.tsx +34 -21
  265. package/template/saas-starter/src/emails/magic-link.tsx +15 -13
  266. package/template/saas-starter/src/emails/test.tsx +11 -9
  267. package/template/saas-starter/src/emails/welcome.tsx +12 -9
  268. package/template/saas-starter/src/env.ts +1 -1
  269. package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
  270. package/template/saas-starter/src/instrumentation.ts +20 -2
  271. package/template/saas-starter/src/lib/actions.ts +34 -0
  272. package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
  273. package/template/saas-starter/src/lib/app-navigation.ts +45 -41
  274. package/template/saas-starter/src/lib/auth-client.ts +8 -3
  275. package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
  276. package/template/saas-starter/src/lib/auth.ts +122 -21
  277. package/template/saas-starter/src/lib/billing.ts +59 -24
  278. package/template/saas-starter/src/lib/callback-url.ts +28 -0
  279. package/template/saas-starter/src/lib/constants.ts +78 -9
  280. package/template/saas-starter/src/lib/delay.ts +10 -0
  281. package/template/saas-starter/src/lib/email-service.ts +30 -1
  282. package/template/saas-starter/src/lib/errors.ts +74 -0
  283. package/template/saas-starter/src/lib/format-currency.ts +22 -24
  284. package/template/saas-starter/src/lib/format-date.ts +69 -6
  285. package/template/saas-starter/src/lib/image-url.ts +54 -0
  286. package/template/saas-starter/src/lib/nodemailer.ts +27 -3
  287. package/template/saas-starter/src/lib/organization-access.ts +56 -76
  288. package/template/saas-starter/src/lib/otel.ts +92 -2
  289. package/template/saas-starter/src/lib/plan-limits.ts +37 -0
  290. package/template/saas-starter/src/lib/plans.ts +40 -13
  291. package/template/saas-starter/src/lib/rbac.ts +44 -0
  292. package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
  293. package/template/saas-starter/src/lib/safe-action.ts +137 -17
  294. package/template/saas-starter/src/lib/search.ts +16 -0
  295. package/template/saas-starter/src/lib/seo.ts +7 -0
  296. package/template/saas-starter/src/lib/siteConfig.ts +17 -1
  297. package/template/saas-starter/src/lib/storage.ts +176 -46
  298. package/template/saas-starter/src/lib/stripe.ts +4 -1
  299. package/template/saas-starter/src/lib/toast-message.ts +87 -1
  300. package/template/saas-starter/src/lib/validations.ts +85 -2
  301. package/template/saas-starter/src/proxy.ts +48 -0
  302. package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
  303. package/template/saas-starter/src/scripts/seed.ts +82 -8
  304. package/template/saas-starter/src/scripts/setup-db.ts +72 -0
  305. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
  306. package/template/saas-starter/src/scripts/test-mail.ts +28 -20
  307. package/template/saas-starter/tests/admin/users.test.ts +64 -0
  308. package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
  309. package/template/saas-starter/tests/db/seed.test.ts +57 -0
  310. package/template/saas-starter/tests/helpers/action.ts +71 -0
  311. package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
  312. package/template/saas-starter/tests/helpers/index.ts +2 -0
  313. package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
  314. package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
  315. package/template/saas-starter/tests/lib/errors.test.ts +10 -0
  316. package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
  317. package/template/saas-starter/tests/lib/search.test.ts +23 -0
  318. package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
  319. package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
  320. package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
  321. package/template/saas-starter/tests/setup/preload.ts +108 -0
  322. package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
  323. package/template/saas-starter/tsconfig.json +1 -0
  324. package/cli.js +0 -147
  325. package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
  326. package/template/nextjs/src/app/(app)/layout.tsx +0 -20
  327. package/template/nextjs/terraform/artifact-registry.tf +0 -5
  328. package/template/nextjs/terraform/backend-service.tf +0 -13
  329. package/template/nextjs/terraform/cleanup.sh +0 -9
  330. package/template/nextjs/terraform/cloudbuild.tf +0 -70
  331. package/template/nextjs/terraform/cloudrun.tf +0 -68
  332. package/template/nextjs/terraform/database.tf +0 -60
  333. package/template/nextjs/terraform/forwarding-rule.tf +0 -43
  334. package/template/nextjs/terraform/gitignore +0 -4
  335. package/template/nextjs/terraform/ip-address.tf +0 -11
  336. package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
  337. package/template/nextjs/terraform/network.tf +0 -21
  338. package/template/nextjs/terraform/pre.sh +0 -3
  339. package/template/nextjs/terraform/providers.tf +0 -18
  340. package/template/nextjs/terraform/secret.txt +0 -11
  341. package/template/nextjs/terraform/setup.sh +0 -25
  342. package/template/nextjs/terraform/ssl-certs.tf +0 -32
  343. package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
  344. package/template/nextjs/terraform/target-https-proxy.tf +0 -14
  345. package/template/nextjs/terraform/terraform.tfvars +0 -4
  346. package/template/nextjs/terraform/url-map.tf +0 -23
  347. package/template/nextjs/terraform/variables.tf +0 -21
  348. package/template/saas-starter/next-env.d.ts +0 -6
  349. package/template/saas-starter/src/actions/uploads.ts +0 -28
  350. package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
  351. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
  352. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
  353. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
  354. package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
  355. package/template/saas-starter/src/app/proxy.ts +0 -28
  356. package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
  357. package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
@@ -2,30 +2,25 @@ import { Building2, Plus } from 'lucide-react'
2
2
  import Image from 'next/image'
3
3
  import Link from 'next/link'
4
4
  import { redirect } from 'next/navigation'
5
- import {
6
- Card,
7
- CardDescription,
8
- CardHeader,
9
- CardTitle
10
- } from '~/components/ui/card'
11
- import { getUserOrganizationsCached } from '~/lib/organization-access'
5
+ import { Card, CardHeader, CardTitle } from '~/components/ui/card'
12
6
  import { siteConfig } from '~/lib/siteConfig'
7
+ import { getOrganizationsCached } from '../actions/organizations'
13
8
 
14
- export default async function Page() {
15
- const organizations = await getUserOrganizationsCached()
9
+ export default async function AppPage() {
10
+ const organizations = (await getOrganizationsCached())?.data ?? []
16
11
 
17
12
  if (organizations.length === 0) {
18
13
  return redirect('/app/onboarding')
19
14
  }
20
15
 
21
16
  return (
22
- <div className="mx-auto flex w-full max-w-4xl flex-col gap-8 px-2 py-8">
17
+ <div className="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-12">
23
18
  <div className="flex flex-col gap-2">
24
- <h1 className="text-3xl font-bold tracking-tight">
19
+ <h1 className="text-4xl font-bold tracking-tight">
25
20
  Welcome to {siteConfig.name}
26
21
  </h1>
27
- <p className="text-muted-foreground">
28
- Select an organization to manage, or create a new one.
22
+ <p className="text-muted-foreground text-lg">
23
+ Choose an organization to open, or create a new one.
29
24
  </p>
30
25
  </div>
31
26
 
@@ -38,15 +33,15 @@ export default async function Page() {
38
33
  >
39
34
  <Card className="hover:border-primary h-full cursor-pointer transition-all duration-200 hover:shadow-md">
40
35
  <CardHeader className="flex flex-row items-center gap-4">
41
- {org.logo ? (
36
+ {org.logoUrl ? (
42
37
  <div className="relative size-10 shrink-0 overflow-hidden rounded-lg">
43
38
  <Image
44
- src={org.logo}
39
+ src={org.logoUrl}
45
40
  alt={org.name}
46
41
  fill
47
42
  sizes="40px"
48
43
  className="object-cover"
49
- unoptimized={org.logo.startsWith('data:')}
44
+ unoptimized={org.logoUrl.startsWith('data:')}
50
45
  />
51
46
  </div>
52
47
  ) : (
@@ -56,22 +51,22 @@ export default async function Page() {
56
51
  )}
57
52
  <div className="min-w-0 flex-1">
58
53
  <CardTitle className="truncate text-lg">{org.name}</CardTitle>
59
- <CardDescription className="truncate text-xs capitalize">
54
+ <p className="text-muted-foreground truncate text-xs capitalize">
60
55
  {org.role}
61
- </CardDescription>
56
+ </p>
62
57
  </div>
63
58
  </CardHeader>
64
59
  </Card>
65
60
  </Link>
66
61
  ))}
67
62
 
68
- <Link href="/app/onboarding" className="group">
69
- <Card className="hover:border-primary hover:bg-muted/50 flex h-full min-h-28 cursor-pointer flex-col items-center justify-center border-dashed transition-all">
63
+ <Link href="/app/onboarding">
64
+ <Card className="hover:border-primary hover:bg-muted/50 flex h-full cursor-pointer flex-col items-center justify-center border-dashed py-8 transition-all">
70
65
  <div className="bg-muted mb-2 flex size-10 items-center justify-center rounded-full">
71
66
  <Plus className="size-5" />
72
67
  </div>
73
68
  <div className="text-muted-foreground font-semibold">
74
- Create organization
69
+ Create Organization
75
70
  </div>
76
71
  </Card>
77
72
  </Link>
@@ -0,0 +1,64 @@
1
+ import { ArrowLeft } from 'lucide-react'
2
+ import type { Metadata } from 'next'
3
+ import { headers } from 'next/headers'
4
+ import Link from 'next/link'
5
+ import { redirect } from 'next/navigation'
6
+ import { NuqsAdapter } from 'nuqs/adapters/next/app'
7
+ import { Logo } from '~/components/logo'
8
+ import { SiteFooter } from '~/components/site-footer'
9
+ import { Button } from '~/components/ui/button'
10
+ import { UserMenu } from '~/components/user-menu'
11
+ import { getAuthSession } from '~/lib/auth'
12
+ import { sanitizeCallbackUrl } from '~/lib/callback-url'
13
+ import { privatePageRobots } from '~/lib/seo'
14
+ import { siteConfig } from '~/lib/siteConfig'
15
+
16
+ export const metadata: Metadata = {
17
+ robots: privatePageRobots
18
+ }
19
+
20
+ export default async function AccountLayout({
21
+ children
22
+ }: {
23
+ children: React.ReactNode
24
+ }) {
25
+ const authSession = await getAuthSession()
26
+
27
+ if (!authSession) {
28
+ const headerStore = await headers()
29
+ const callbackUrl =
30
+ sanitizeCallbackUrl(headerStore.get('x-pathname') ?? undefined) ??
31
+ '/profile'
32
+ redirect(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`)
33
+ }
34
+
35
+ return (
36
+ <NuqsAdapter>
37
+ <div className="flex min-h-screen flex-col">
38
+ <header className="bg-background sticky top-0 z-50 flex h-14 shrink-0 items-center gap-2 border-b px-4">
39
+ <Link href="/" className="flex shrink-0 items-center">
40
+ <Logo size={32} priority className="text-lg" />
41
+ </Link>
42
+
43
+ <div className="flex flex-1 items-center justify-end gap-2">
44
+ <Button asChild variant="ghost" size="sm">
45
+ <Link href="/app">
46
+ <ArrowLeft className="size-4" />
47
+ Back to App
48
+ </Link>
49
+ </Button>
50
+ <UserMenu
51
+ user={authSession.user}
52
+ isSuperAdmin={authSession.isSuperAdmin}
53
+ isImpersonating={!!authSession.session.impersonatedBy}
54
+ />
55
+ </div>
56
+ </header>
57
+
58
+ <main className="flex-1">{children}</main>
59
+
60
+ <SiteFooter name={siteConfig.name} />
61
+ </div>
62
+ </NuqsAdapter>
63
+ )
64
+ }
@@ -0,0 +1,16 @@
1
+ import { UserCog } from 'lucide-react'
2
+ import { PageHeading } from '~/components/page-heading'
3
+ import { ProfileForm } from './profile-form'
4
+
5
+ export default function ProfilePage() {
6
+ return (
7
+ <div className="mx-auto w-full max-w-3xl space-y-6 p-4 sm:p-6">
8
+ <PageHeading
9
+ title="Your Profile"
10
+ description="Update your name and review your account details."
11
+ icon={UserCog}
12
+ />
13
+ <ProfileForm />
14
+ </div>
15
+ )
16
+ }
@@ -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
+ }
@@ -0,0 +1,19 @@
1
+ import { SupportTicketConversation } from '~/components/support/support-ticket-conversation'
2
+
3
+ export default async function AccountSupportTicketPage({
4
+ params
5
+ }: {
6
+ params: Promise<{ ticketId: string }>
7
+ }) {
8
+ const { ticketId } = await params
9
+
10
+ return (
11
+ <div className="mx-auto w-full max-w-5xl p-4 sm:p-6">
12
+ <SupportTicketConversation
13
+ mode="app"
14
+ ticketId={ticketId}
15
+ backHref="/support"
16
+ />
17
+ </div>
18
+ )
19
+ }
@@ -0,0 +1,9 @@
1
+ import { SupportTicketsTable } from '~/components/support/support-tickets-table'
2
+
3
+ export default function AccountSupportPage() {
4
+ return (
5
+ <div className="mx-auto w-full max-w-5xl p-4 sm:p-6">
6
+ <SupportTicketsTable mode="account" basePath="/support" />
7
+ </div>
8
+ )
9
+ }
@@ -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,20 +13,36 @@ 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 { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
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: z.email()
31
+ email: emailValidation
21
32
  })
22
33
 
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
+ */
23
46
  export const EmailLoginForm = ({
24
47
  callbackUrl
25
48
  }: {
@@ -49,7 +72,7 @@ export const EmailLoginForm = ({
49
72
  })
50
73
  if (res?.error) throw res.error
51
74
  setIsSent(email)
52
- toastSuccessMessage('Magic link sent! Check your email.')
75
+ toastSuccessMessage('Magic link sent. Check your email.')
53
76
  } catch (err) {
54
77
  console.error('Error sending magic link:', err)
55
78
  if (
@@ -58,8 +81,8 @@ export const EmailLoginForm = ({
58
81
  'message' in err &&
59
82
  typeof err.message === 'string'
60
83
  )
61
- toastErrorMessage(err.message)
62
- else toastErrorMessage('Something went wrong. Please try again.')
84
+ toastActionError(err)
85
+ else toastErrorMessage('The magic link was not sent. Try again.')
63
86
  } finally {
64
87
  setIsLoading(false)
65
88
  }
@@ -69,7 +92,7 @@ export const EmailLoginForm = ({
69
92
  return (
70
93
  <div className="rounded-lg border p-4 text-center">
71
94
  <Mail className="text-primary mx-auto mb-2 size-6" />
72
- <h3 className="mb-1 text-sm font-medium">Check your email</h3>
95
+ <h3 className="mb-1 text-sm font-medium">Check Your Email</h3>
73
96
  <p className="text-muted-foreground text-xs">
74
97
  We&apos;ve sent a magic link to <strong>{isSent}</strong>
75
98
  </p>
@@ -84,25 +107,24 @@ export const EmailLoginForm = ({
84
107
  name="email"
85
108
  render={({ field, fieldState }) => (
86
109
  <Field data-invalid={fieldState.invalid}>
87
- <FieldLabel htmlFor="email">Email</FieldLabel>
88
- <FieldContent>
89
- <Input
90
- id="email"
91
- type="email"
92
- placeholder="name@example.com"
93
- autoCapitalize="off"
94
- autoComplete="email"
95
- autoFocus
96
- aria-invalid={fieldState.invalid}
97
- {...field}
98
- />
99
- <FieldError errors={[fieldState.error]} />
100
- </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
+ />
101
123
  </Field>
102
124
  )}
103
125
  />
104
126
  <Button type="submit" className="w-full" disabled={isLoading}>
105
- {isLoading ? 'Sending...' : 'Send Magic Link'}
127
+ {isLoading ? 'Sending…' : 'Send Magic Link'}
106
128
  </Button>
107
129
  </form>
108
130
  )
@@ -1,6 +1,21 @@
1
- import { Terminal } from 'lucide-react'
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,37 +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
- function sanitizeCallback(value: string | string[] | undefined) {
18
- if (typeof value !== 'string') return null
19
- if (!value.startsWith('/') || value.startsWith('//')) return null
20
- return value
21
- }
22
-
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
+ */
23
41
  export default async function LoginPage({
24
42
  searchParams
25
43
  }: {
26
44
  searchParams?: Promise<Record<string, string | string[] | undefined>>
27
45
  }) {
28
- const resolvedParams = (await searchParams) ?? {}
29
- const callbackUrl = sanitizeCallback(resolvedParams.callbackUrl)
30
-
31
46
  const authSession = await getAuthSession()
47
+ const resolvedParams = (await searchParams) ?? {}
48
+ const callbackUrl = sanitizeCallbackUrl(resolvedParams.callbackUrl)
32
49
 
33
- if (authSession) {
34
- return redirect(callbackUrl ?? (authSession.isAdmin ? '/admin' : '/app'))
50
+ if (authSession?.isAdmin) {
51
+ return redirect(callbackUrl ?? '/admin')
52
+ } else if (authSession) {
53
+ return redirect(callbackUrl ?? '/app')
35
54
  }
36
55
 
37
56
  return (
38
- <main className="flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
57
+ <div className="flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
39
58
  <Card className="w-full max-w-md">
40
59
  <CardHeader className="space-y-1 text-center">
41
- <div className="bg-primary/10 mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full">
42
- <Terminal className="text-primary size-6" />
43
- </div>
44
- <CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
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>
45
64
  <CardDescription className="text-muted-foreground text-sm">
46
65
  Sign in to your account to continue
47
66
  </CardDescription>
@@ -50,8 +69,7 @@ export default async function LoginPage({
50
69
  <div className="border-muted-foreground/50 bg-muted/50 rounded-lg border border-dashed p-4">
51
70
  <p className="text-muted-foreground text-sm">
52
71
  <strong>New to our site?</strong> No need to create a separate
53
- account. Simply use one of the options below to both sign up and
54
- log in.
72
+ account. Use either option below to sign up and log in.
55
73
  </p>
56
74
  </div>
57
75
 
@@ -77,11 +95,11 @@ export default async function LoginPage({
77
95
  href="/"
78
96
  className="text-primary underline-offset-4 hover:underline"
79
97
  >
80
- Return to home page
98
+ Return to Home Page
81
99
  </Link>
82
100
  </div>
83
101
  </CardFooter>
84
102
  </Card>
85
- </main>
103
+ </div>
86
104
  )
87
105
  }