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
@@ -0,0 +1,116 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { cn } from '~/lib/cn'
6
+
7
+ function Table({ className, ...props }: React.ComponentProps<'table'>) {
8
+ return (
9
+ <div
10
+ data-slot="table-container"
11
+ className="relative w-full overflow-x-auto"
12
+ >
13
+ <table
14
+ data-slot="table"
15
+ className={cn('w-full caption-bottom text-sm', className)}
16
+ {...props}
17
+ />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
23
+ return (
24
+ <thead
25
+ data-slot="table-header"
26
+ className={cn('[&_tr]:border-b', className)}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
33
+ return (
34
+ <tbody
35
+ data-slot="table-body"
36
+ className={cn('[&_tr:last-child]:border-0', className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
43
+ return (
44
+ <tfoot
45
+ data-slot="table-footer"
46
+ className={cn(
47
+ 'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
56
+ return (
57
+ <tr
58
+ data-slot="table-row"
59
+ className={cn(
60
+ 'hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
69
+ return (
70
+ <th
71
+ data-slot="table-head"
72
+ className={cn(
73
+ 'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0',
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
82
+ return (
83
+ <td
84
+ data-slot="table-cell"
85
+ className={cn(
86
+ 'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function TableCaption({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<'caption'>) {
98
+ return (
99
+ <caption
100
+ data-slot="table-caption"
101
+ className={cn('text-muted-foreground mt-4 text-sm', className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export {
108
+ Table,
109
+ TableBody,
110
+ TableCaption,
111
+ TableCell,
112
+ TableFooter,
113
+ TableHead,
114
+ TableHeader,
115
+ TableRow
116
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '~/lib/cn'
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ 'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Textarea }
@@ -0,0 +1,57 @@
1
+ 'use client'
2
+
3
+ import { Tooltip as TooltipPrimitive } from 'radix-ui'
4
+ import * as React from 'react'
5
+
6
+ import { cn } from '~/lib/cn'
7
+
8
+ function TooltipProvider({
9
+ delayDuration = 0,
10
+ ...props
11
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
+ return (
13
+ <TooltipPrimitive.Provider
14
+ data-slot="tooltip-provider"
15
+ delayDuration={delayDuration}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function Tooltip({
22
+ ...props
23
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
25
+ }
26
+
27
+ function TooltipTrigger({
28
+ ...props
29
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
30
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
31
+ }
32
+
33
+ function TooltipContent({
34
+ className,
35
+ sideOffset = 0,
36
+ children,
37
+ ...props
38
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
39
+ return (
40
+ <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Content
42
+ data-slot="tooltip-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ 'bg-foreground text-background data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm',
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
52
+ </TooltipPrimitive.Content>
53
+ </TooltipPrimitive.Portal>
54
+ )
55
+ }
56
+
57
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
@@ -0,0 +1,206 @@
1
+ 'use client'
2
+
3
+ import {
4
+ House,
5
+ LayoutGrid,
6
+ LogOut,
7
+ ShieldCheck,
8
+ UserCog,
9
+ UserX
10
+ } from 'lucide-react'
11
+ import Link from 'next/link'
12
+ import { usePathname, useRouter } from 'next/navigation'
13
+ import { useState } from 'react'
14
+ import { ThemeToggle } from '~/components/theme-toggle'
15
+ import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'
16
+ import { Button } from '~/components/ui/button'
17
+ import {
18
+ DropdownMenu,
19
+ DropdownMenuContent,
20
+ DropdownMenuGroup,
21
+ DropdownMenuItem,
22
+ DropdownMenuLabel,
23
+ DropdownMenuSeparator,
24
+ DropdownMenuTrigger
25
+ } from '~/components/ui/dropdown-menu'
26
+ import { admin, signOut } from '~/lib/auth-client'
27
+ import { cn } from '~/lib/cn'
28
+
29
+ type UserMenuProps = {
30
+ user: {
31
+ name?: string | null
32
+ email?: string | null
33
+ image?: string | null
34
+ }
35
+ fallbackName?: string
36
+ fallbackInitials?: string
37
+ wrapperClassName?: string
38
+ /** Superadmins work in both panels, so they get a way to cross over. */
39
+ isSuperAdmin?: boolean
40
+ /** This session is an admin acting as someone else. */
41
+ isImpersonating?: boolean
42
+ }
43
+
44
+ export function UserMenu({
45
+ user,
46
+ fallbackName = 'Unknown',
47
+ fallbackInitials = 'NA',
48
+ wrapperClassName = 'flex items-center gap-2',
49
+ isSuperAdmin = false,
50
+ isImpersonating = false
51
+ }: UserMenuProps) {
52
+ const router = useRouter()
53
+ const pathname = usePathname()
54
+ const [isStopping, setIsStopping] = useState(false)
55
+
56
+ // One entry, pointing at the panel you are not in. Account pages sit outside
57
+ // both, and lead back to the admin panel.
58
+ const inAdminPanel = pathname?.startsWith('/admin') ?? false
59
+ const switchTo = inAdminPanel
60
+ ? { href: '/app', label: 'Switch to App', Icon: LayoutGrid }
61
+ : { href: '/admin', label: 'Switch to Admin', Icon: ShieldCheck }
62
+
63
+ const displayName = user.name ?? fallbackName
64
+ const initials =
65
+ user.name
66
+ ?.split(' ')
67
+ .slice(0, 2)
68
+ .map(part => part[0])
69
+ .join('') || fallbackInitials
70
+
71
+ async function stopImpersonating() {
72
+ setIsStopping(true)
73
+ try {
74
+ await admin.stopImpersonating()
75
+ // Back to the admin panel: the restored session may have no access to the
76
+ // page the impersonated user was looking at.
77
+ router.push('/admin')
78
+ router.refresh()
79
+ } catch {
80
+ setIsStopping(false)
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div className={wrapperClassName}>
86
+ <ThemeToggle />
87
+ <DropdownMenu>
88
+ <DropdownMenuTrigger asChild>
89
+ <Button
90
+ variant="ghost"
91
+ className="relative size-8 rounded-full"
92
+ aria-label={
93
+ isImpersonating
94
+ ? `Account menu, impersonating ${displayName}`
95
+ : 'Account menu'
96
+ }
97
+ >
98
+ <Avatar
99
+ className={cn(
100
+ 'size-8',
101
+ isImpersonating &&
102
+ 'ring-warning ring-offset-background ring-2 ring-offset-2'
103
+ )}
104
+ >
105
+ <AvatarFallback className="text-foreground">
106
+ {initials}
107
+ </AvatarFallback>
108
+ {user.image ? (
109
+ <AvatarImage src={user.image} alt={displayName} />
110
+ ) : null}
111
+ </Avatar>
112
+ {isImpersonating ? (
113
+ <span className="bg-warning text-warning-foreground ring-background absolute -right-0.5 -bottom-0.5 flex size-3.5 items-center justify-center rounded-full ring-2">
114
+ <UserX className="size-2.5" aria-hidden="true" />
115
+ </span>
116
+ ) : null}
117
+ </Button>
118
+ </DropdownMenuTrigger>
119
+ <DropdownMenuContent className="w-60" align="end" forceMount>
120
+ <DropdownMenuLabel className="font-normal">
121
+ <div className="flex flex-col space-y-1">
122
+ <p className="text-foreground text-sm leading-none font-medium">
123
+ {displayName}
124
+ </p>
125
+ <p className="text-muted-foreground text-xs leading-none">
126
+ {user.email ?? ''}
127
+ </p>
128
+ </div>
129
+ </DropdownMenuLabel>
130
+
131
+ {isImpersonating ? (
132
+ <>
133
+ <DropdownMenuSeparator />
134
+ <div className="border-warning/40 bg-warning/10 mx-1 mb-1 flex items-start gap-2 rounded-md border px-2 py-1.5">
135
+ <UserX
136
+ className="text-warning mt-0.5 size-3.5 shrink-0"
137
+ aria-hidden="true"
138
+ />
139
+ <p className="text-foreground text-xs leading-snug">
140
+ You are signed in as this user. Anything you do is recorded
141
+ against their account.
142
+ </p>
143
+ </div>
144
+ <DropdownMenuGroup>
145
+ <DropdownMenuItem
146
+ variant="destructive"
147
+ disabled={isStopping}
148
+ onSelect={event => {
149
+ // Hold the menu open while the session swaps back.
150
+ event.preventDefault()
151
+ void stopImpersonating()
152
+ }}
153
+ >
154
+ <UserX className="mr-2 size-4" />
155
+ <span>{isStopping ? 'Stopping…' : 'Stop Impersonating'}</span>
156
+ </DropdownMenuItem>
157
+ </DropdownMenuGroup>
158
+ </>
159
+ ) : null}
160
+
161
+ {isSuperAdmin ? (
162
+ <>
163
+ <DropdownMenuSeparator />
164
+ <DropdownMenuGroup>
165
+ <DropdownMenuItem asChild>
166
+ <Link href={switchTo.href}>
167
+ <switchTo.Icon className="mr-2 size-4" />
168
+ <span>{switchTo.label}</span>
169
+ </Link>
170
+ </DropdownMenuItem>
171
+ </DropdownMenuGroup>
172
+ </>
173
+ ) : null}
174
+
175
+ <DropdownMenuSeparator />
176
+ <DropdownMenuGroup>
177
+ <DropdownMenuItem asChild>
178
+ <Link href="/">
179
+ <House className="mr-2 size-4" />
180
+ <span>Home</span>
181
+ </Link>
182
+ </DropdownMenuItem>
183
+ <DropdownMenuItem asChild>
184
+ <Link href="/profile">
185
+ <UserCog className="mr-2 size-4" />
186
+ <span>Profile</span>
187
+ </Link>
188
+ </DropdownMenuItem>
189
+ </DropdownMenuGroup>
190
+ <DropdownMenuSeparator />
191
+ <DropdownMenuGroup>
192
+ <DropdownMenuItem
193
+ onClick={async () => {
194
+ await signOut()
195
+ router.push('/login')
196
+ }}
197
+ >
198
+ <LogOut className="mr-2 size-4" />
199
+ <span>Log Out</span>
200
+ </DropdownMenuItem>
201
+ </DropdownMenuGroup>
202
+ </DropdownMenuContent>
203
+ </DropdownMenu>
204
+ </div>
205
+ )
206
+ }
@@ -1,17 +1,26 @@
1
+ /**
2
+ * @fileoverview Reusable Drizzle column definitions shared across tables.
3
+ *
4
+ * Centralises common patterns (prefixed CUID primary keys, timestamps) so
5
+ * every table stays consistent. Add CHECK helpers here as tables need them.
6
+ */
1
7
  import { createId } from '@paralleldrive/cuid2'
2
8
  import { sql } from 'drizzle-orm'
3
9
  import { boolean, timestamp, varchar } from 'drizzle-orm/pg-core'
4
10
 
5
11
  export const commonFieldDefs = {
12
+ /** Generates a prefixed CUID2 primary key, such as `user_clx1234567890`. */
6
13
  id: (prefix: string) =>
7
14
  varchar('id')
8
15
  .primaryKey()
9
16
  .$defaultFn(() => prefix + '_' + createId()),
17
+ /** Nullable timezone-aware timestamp column. */
10
18
  date: (name: string) =>
11
19
  timestamp(name, {
12
20
  mode: 'date',
13
21
  withTimezone: true
14
22
  }),
23
+ /** Auto-populated `created_at` and `updated_at` timestamp pair. */
15
24
  dates: {
16
25
  createdAt: timestamp('created_at', {
17
26
  mode: 'date',
@@ -27,5 +36,6 @@ export const commonFieldDefs = {
27
36
  .$onUpdate(() => sql`NOW()`)
28
37
  .defaultNow()
29
38
  },
39
+ /** Boolean `is_active` flag, defaults to `true`. */
30
40
  isActive: boolean('is_active').notNull().default(true)
31
41
  }
@@ -1,6 +1,9 @@
1
+ import 'server-only'
2
+
1
3
  import { drizzle } from 'drizzle-orm/postgres-js'
2
4
  import postgres from 'postgres'
3
5
  import { env } from '~/env'
6
+ import * as relations from './relations'
4
7
  import * as schema from './schema'
5
8
 
6
9
  // In development, Next.js hot-reloading can re-evaluate modules frequently.
@@ -11,14 +14,21 @@ const globalForDb = globalThis as unknown as {
11
14
  }
12
15
 
13
16
  // Prefer the cached client (dev) and fall back to creating a new one.
14
- // `max: 1` limits the pool size, and `prepare: false` disables prepared
15
- // statements for compatibility with environments where prepared statements
16
- // can be problematic.
17
+ // `prepare: false` disables prepared statements, which transaction-mode
18
+ // poolers such as PgBouncer and Neon's pooler reject.
17
19
  const client =
18
20
  globalForDb.client ??
19
21
  postgres(env.DATABASE_URL, {
20
- max: 1,
21
- prepare: false
22
+ prepare: false,
23
+ // For neon.tech postgres db, as its serverless
24
+ ...(env.DATABASE_URL.includes('neon.tech')
25
+ ? {
26
+ max: 5,
27
+ idle_timeout: 30, // seconds an idle connection is kept before closing
28
+ max_lifetime: 60 * 5, // recycle a connection after 5 minutes
29
+ connect_timeout: 60 // seconds to wait for a connection (Neon cold start)
30
+ }
31
+ : {})
22
32
  })
23
33
 
24
34
  // Only cache the client outside production to avoid unexpected cross-request
@@ -43,7 +53,7 @@ if (env.APP_ENV !== 'production') globalForDb.client = client
43
53
  * })
44
54
  */
45
55
  export const db = drizzle(client, {
46
- schema: { ...schema }
56
+ schema: { ...schema, ...relations }
47
57
  })
48
58
 
49
59
  /** The transaction client type passed into `db.transaction(async (tx) => ...)`. */
@@ -0,0 +1,21 @@
1
+ import { relations } from 'drizzle-orm'
2
+ import { accountsTable, sessionsTable, usersTable } from './schema'
3
+
4
+ export const userRelations = relations(usersTable, ({ many }) => ({
5
+ sessions: many(sessionsTable),
6
+ accounts: many(accountsTable)
7
+ }))
8
+
9
+ export const sessionRelations = relations(sessionsTable, ({ one }) => ({
10
+ user: one(usersTable, {
11
+ fields: [sessionsTable.userId],
12
+ references: [usersTable.id]
13
+ })
14
+ }))
15
+
16
+ export const accountRelations = relations(accountsTable, ({ one }) => ({
17
+ user: one(usersTable, {
18
+ fields: [accountsTable.userId],
19
+ references: [usersTable.id]
20
+ })
21
+ }))
@@ -1,53 +1,92 @@
1
- import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
1
+ /**
2
+ * @fileoverview Drizzle table definitions.
3
+ *
4
+ * Reusable column helpers live in `./common.ts`. Put Postgres enums in a
5
+ * sibling `./enums.ts` and re-export them from here.
6
+ * Every foreign key leads an index and declares an explicit `onDelete`;
7
+ * `tests/db/fk-indexes.test.ts` enforces both.
8
+ */
9
+ import { boolean, index, pgTable, text, varchar } from 'drizzle-orm/pg-core'
2
10
  import { commonFieldDefs } from './common'
3
11
 
4
- export const usersTable = pgTable('users', {
5
- id: commonFieldDefs.id('user'),
6
- email: text('email').notNull().unique(),
7
- name: text('name').notNull(),
8
- emailVerified: boolean('email_verified').default(false).notNull(),
9
- image: text('image'),
10
- role: text('role'),
11
- banned: boolean('banned').default(false),
12
- banReason: text('ban_reason'),
13
- banExpires: commonFieldDefs.date('ban_expires'),
14
- ...commonFieldDefs.dates
15
- })
12
+ // ─── Auth (Better Auth) ─────────────────────────────────────────
16
13
 
17
- export const sessionsTable = pgTable('sessions', {
18
- id: commonFieldDefs.id('session'),
19
- expiresAt: commonFieldDefs.date('expires_at').notNull(),
20
- token: text('token').notNull().unique(),
21
- ipAddress: text('ip_address'),
22
- userAgent: text('user_agent'),
23
- userId: text('user_id')
24
- .notNull()
25
- .references(() => usersTable.id, { onDelete: 'cascade' }),
26
- impersonatedBy: text('impersonated_by'),
27
- ...commonFieldDefs.dates
28
- })
14
+ export const usersTable = pgTable(
15
+ 'users',
16
+ {
17
+ id: commonFieldDefs.id('user'),
18
+ name: text('name').notNull(),
19
+ email: text('email').notNull().unique(),
20
+ emailVerified: boolean('email_verified').default(false).notNull(),
21
+ image: text('image'),
22
+ role: text('role'),
23
+ banned: boolean('banned').default(false),
24
+ banReason: text('ban_reason'),
25
+ banExpires: commonFieldDefs.date('ban_expires'),
26
+ ...commonFieldDefs.dates
27
+ },
28
+ table => [index().on(table.createdAt), index().on(table.role)]
29
+ )
29
30
 
30
- export const accountsTable = pgTable('accounts', {
31
- id: commonFieldDefs.id('account'),
32
- accountId: text('account_id').notNull(),
33
- providerId: text('provider_id').notNull(),
34
- userId: text('user_id')
35
- .notNull()
36
- .references(() => usersTable.id, { onDelete: 'cascade' }),
37
- accessToken: text('access_token'),
38
- refreshToken: text('refresh_token'),
39
- idToken: text('id_token'),
40
- accessTokenExpiresAt: commonFieldDefs.date('access_token_expires_at'),
41
- refreshTokenExpiresAt: commonFieldDefs.date('refresh_token_expires_at'),
42
- scope: text('scope'),
43
- password: text('password'),
44
- ...commonFieldDefs.dates
45
- })
31
+ export const sessionsTable = pgTable(
32
+ 'sessions',
33
+ {
34
+ id: commonFieldDefs.id('session'),
35
+ expiresAt: commonFieldDefs.date('expires_at').notNull(),
36
+ token: text('token').notNull().unique(),
37
+ ipAddress: text('ip_address'),
38
+ userAgent: text('user_agent'),
39
+ userId: text('user_id')
40
+ .notNull()
41
+ .references(() => usersTable.id, { onDelete: 'cascade' }),
42
+ impersonatedBy: text('impersonated_by'),
43
+ ...commonFieldDefs.dates
44
+ },
45
+ table => [index().on(table.userId)]
46
+ )
47
+
48
+ export const accountsTable = pgTable(
49
+ 'accounts',
50
+ {
51
+ id: commonFieldDefs.id('account'),
52
+ accountId: text('account_id').notNull(),
53
+ providerId: text('provider_id').notNull(),
54
+ userId: text('user_id')
55
+ .notNull()
56
+ .references(() => usersTable.id, { onDelete: 'cascade' }),
57
+ accessToken: text('access_token'),
58
+ refreshToken: text('refresh_token'),
59
+ idToken: text('id_token'),
60
+ accessTokenExpiresAt: commonFieldDefs.date('access_token_expires_at'),
61
+ refreshTokenExpiresAt: commonFieldDefs.date('refresh_token_expires_at'),
62
+ scope: text('scope'),
63
+ password: text('password'),
64
+ ...commonFieldDefs.dates
65
+ },
66
+ table => [index().on(table.userId)]
67
+ )
68
+
69
+ export const verificationsTable = pgTable(
70
+ 'verifications',
71
+ {
72
+ id: commonFieldDefs.id('verification'),
73
+ identifier: text('identifier').notNull(),
74
+ value: text('value').notNull(),
75
+ expiresAt: commonFieldDefs.date('expires_at').notNull(),
76
+ ...commonFieldDefs.dates
77
+ },
78
+ table => [index().on(table.identifier)]
79
+ )
80
+
81
+ // ─── Seed ───────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Singleton marker for a successful `seedDatabase` run. While
85
+ * {@link SEED_RUN_MARKER_ID} exists, the seed script does nothing.
86
+ */
87
+ export const SEED_RUN_MARKER_ID = 'seed_run_default'
46
88
 
47
- export const verificationsTable = pgTable('verifications', {
48
- id: commonFieldDefs.id('verification'),
49
- identifier: text('identifier').notNull(),
50
- value: text('value').notNull(),
51
- expiresAt: commonFieldDefs.date('expires_at').notNull(),
89
+ export const seedRunsTable = pgTable('seed_runs', {
90
+ id: varchar('id').primaryKey(),
52
91
  ...commonFieldDefs.dates
53
92
  })