create-ej-app 26.0.0 → 27.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +46 -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 +49 -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,62 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { sql } from 'drizzle-orm'
3
+ import { db } from '~/db'
4
+
5
+ /**
6
+ * Postgres does not index foreign key columns automatically. Without an index,
7
+ * every cascade or SET NULL from the parent side sequentially scans the child
8
+ * table while holding row locks, so deleting one user can stall the
9
+ * platform.
10
+ *
11
+ * This finds every FK whose leading column is not the leading column of any
12
+ * index, which is exactly the shape that degrades.
13
+ */
14
+ const UNINDEXED_FOREIGN_KEYS = sql`
15
+ SELECT
16
+ c.conrelid::regclass::text AS table_name,
17
+ a.attname::text AS column_name
18
+ FROM pg_constraint c
19
+ JOIN pg_attribute a
20
+ ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1]
21
+ WHERE c.contype = 'f'
22
+ AND NOT EXISTS (
23
+ SELECT 1
24
+ FROM pg_index i
25
+ WHERE i.indrelid = c.conrelid
26
+ AND i.indkey[0] = c.conkey[1]
27
+ )
28
+ ORDER BY 1, 2
29
+ `
30
+
31
+ /** postgres-js returns a row array; PGlite returns `{ rows }`. */
32
+ const toRows = <T>(result: unknown): T[] =>
33
+ Array.isArray(result)
34
+ ? (result as T[])
35
+ : ((result as { rows: T[] }).rows ?? [])
36
+
37
+ describe('foreign key indexing', () => {
38
+ test('every foreign key column leads an index', async () => {
39
+ const rows = toRows<{ table_name: string; column_name: string }>(
40
+ await db.execute(UNINDEXED_FOREIGN_KEYS)
41
+ )
42
+
43
+ expect(rows.map(r => `${r.table_name}.${r.column_name}`)).toEqual([])
44
+ })
45
+
46
+ test('every foreign key declares an explicit delete rule', async () => {
47
+ const rows = toRows<{ table_name: string; constraint_name: string }>(
48
+ await db.execute(sql`
49
+ SELECT
50
+ c.conrelid::regclass::text AS table_name,
51
+ c.conname::text AS constraint_name
52
+ FROM pg_constraint c
53
+ WHERE c.contype = 'f' AND c.confdeltype = 'a'
54
+ ORDER BY 1, 2
55
+ `)
56
+ )
57
+
58
+ // confdeltype 'a' is NO ACTION, which is what an omitted onDelete leaves
59
+ // behind. Every FK should say cascade, set null, or restrict on purpose.
60
+ expect(rows.map(r => `${r.table_name}.${r.constraint_name}`)).toEqual([])
61
+ })
62
+ })
@@ -0,0 +1,49 @@
1
+ import { PGlite } from '@electric-sql/pglite'
2
+ import { describe, expect, test } from 'bun:test'
3
+ import { pushSchema } from 'drizzle-kit/api'
4
+ import { drizzle } from 'drizzle-orm/pglite'
5
+ import * as relations from '~/db/relations'
6
+ import * as schema from '~/db/schema'
7
+ import { seedDatabase } from '~/scripts/seed'
8
+
9
+ /**
10
+ * The seed writes straight through the schema, around Zod, so it exercises
11
+ * constraints and foreign keys against realistic data.
12
+ *
13
+ * It runs on its own PGlite instance: seeded rows would otherwise leak into
14
+ * every other suite in the same worker and break their counts.
15
+ */
16
+ describe('seed script', () => {
17
+ test('applies cleanly once, then skips', async () => {
18
+ // Same Emscripten proc_exit side effect as tests/setup/pglite-db.ts.
19
+ const exitCodeBeforePglite = process.exitCode
20
+
21
+ const client = new PGlite()
22
+ const db = drizzle({ client, schema: { ...schema, ...relations } })
23
+
24
+ const { apply, hasDataLoss } = await pushSchema(
25
+ schema,
26
+ db as unknown as Parameters<typeof pushSchema>[1]
27
+ )
28
+ expect(hasDataLoss).toBe(false)
29
+ await apply()
30
+
31
+ const seed = () =>
32
+ seedDatabase(db as unknown as Parameters<typeof seedDatabase>[0])
33
+
34
+ expect((await seed()).skipped).toBe(false)
35
+
36
+ const users = await db.select().from(schema.usersTable)
37
+ expect(users.map(user => user.role).sort()).toEqual(['superadmin', 'user'])
38
+
39
+ expect((await seed()).skipped).toBe(true)
40
+ expect(await db.select().from(schema.usersTable)).toHaveLength(2)
41
+
42
+ if (process.exitCode === 99) {
43
+ process.exitCode =
44
+ exitCodeBeforePglite !== undefined && exitCodeBeforePglite !== 99
45
+ ? exitCodeBeforePglite
46
+ : 0
47
+ }
48
+ }, 120_000)
49
+ })
@@ -0,0 +1,71 @@
1
+ import { expect } from 'bun:test'
2
+ import { UNEXPECTED_ERROR_MESSAGE } from '~/lib/safe-action'
3
+
4
+ type ActionResult = {
5
+ data?: unknown
6
+ serverError?: string
7
+ validationErrors?: unknown
8
+ }
9
+
10
+ const MASKED = UNEXPECTED_ERROR_MESSAGE
11
+
12
+ export function expectData<T>(
13
+ result: ActionResult | undefined
14
+ ): asserts result is { data: T } {
15
+ if (!result) throw new Error('Expected action result, got undefined')
16
+ if (result.serverError) {
17
+ throw new Error(`Expected success, got serverError: ${result.serverError}`)
18
+ }
19
+ if (result.validationErrors) {
20
+ throw new Error(
21
+ `Expected success, got validationErrors: ${JSON.stringify(result.validationErrors)}`
22
+ )
23
+ }
24
+ if (result.data === undefined) {
25
+ throw new Error('Expected action data, got undefined')
26
+ }
27
+ }
28
+
29
+ /** Action returned null/undefined data (some actions swallow errors). */
30
+ export function expectNoData(result: ActionResult | undefined) {
31
+ expect(result?.data ?? null).toBeNull()
32
+ }
33
+
34
+ /** Action was rejected by its input schema, before any handler ran. */
35
+ export function expectValidationError(result: ActionResult | undefined) {
36
+ if (!result?.validationErrors) {
37
+ throw new Error(
38
+ `Expected validationErrors, got: ${JSON.stringify(result ?? null)}`
39
+ )
40
+ }
41
+ return result.validationErrors
42
+ }
43
+
44
+ /**
45
+ * Action failed with a serverError.
46
+ * next-safe-action can mask thrown Error messages as a generic string.
47
+ * In that case, this helper only asserts that a failure occurred.
48
+ */
49
+ export function expectServerError(
50
+ result: ActionResult | undefined,
51
+ messageIncludes?: string | RegExp
52
+ ) {
53
+ if (!result?.serverError) {
54
+ throw new Error(
55
+ `Expected serverError, got: ${JSON.stringify(result ?? null)}`
56
+ )
57
+ }
58
+ if (
59
+ messageIncludes &&
60
+ result.serverError.toLowerCase() !== MASKED.toLowerCase()
61
+ ) {
62
+ if (typeof messageIncludes === 'string') {
63
+ expect(result.serverError.toLowerCase()).toContain(
64
+ messageIncludes.toLowerCase()
65
+ )
66
+ } else {
67
+ expect(result.serverError).toMatch(messageIncludes)
68
+ }
69
+ }
70
+ return result.serverError
71
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Throwaway DB fixtures and auth helpers for Bun tests.
3
+ *
4
+ * const fx = new Fixtures()
5
+ * afterEach(() => fx.cleanup())
6
+ * const admin = await fx.platformUser('superadmin')
7
+ * await asUser(admin, () => someAction(...))
8
+ */
9
+ import { createId } from '@paralleldrive/cuid2'
10
+ import { inArray } from 'drizzle-orm'
11
+ import { db } from '~/db'
12
+ import { usersTable } from '~/db/schema'
13
+ import { runWithAuthSession } from '../setup/preload'
14
+
15
+ type User = typeof usersTable.$inferSelect
16
+
17
+ const uid = () =>
18
+ `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
19
+
20
+ export class Fixtures {
21
+ private userIds: string[] = []
22
+
23
+ async user(
24
+ overrides: { name?: string; email?: string; role?: string | null } = {}
25
+ ): Promise<User> {
26
+ const s = uid()
27
+ const [row] = await db
28
+ .insert(usersTable)
29
+ .values({
30
+ id: `user_${createId()}`,
31
+ name: overrides.name ?? `Test User ${s}`,
32
+ email: overrides.email ?? `test+${s}@example.com`,
33
+ emailVerified: true,
34
+ role: overrides.role ?? 'user'
35
+ })
36
+ .returning()
37
+ if (!row) throw new Error('Failed to create test user')
38
+ this.userIds.push(row.id)
39
+ return row
40
+ }
41
+
42
+ /** Platform user for admin-panel tests. */
43
+ platformUser(role: 'admin' | 'superadmin') {
44
+ return this.user({ name: `Platform ${role}`, role })
45
+ }
46
+
47
+ async cleanup() {
48
+ if (this.userIds.length) {
49
+ await db.delete(usersTable).where(inArray(usersTable.id, this.userIds))
50
+ this.userIds = []
51
+ }
52
+ }
53
+ }
54
+
55
+ /** Runs `fn` with `getAuthSession()` returning this user. */
56
+ export function asUser<T>(user: User, fn: () => Promise<T>): Promise<T> {
57
+ const isSuperAdmin = user.role === 'superadmin'
58
+ const isAdmin = user.role === 'admin' || isSuperAdmin
59
+ return runWithAuthSession(
60
+ {
61
+ user: {
62
+ id: user.id,
63
+ email: user.email,
64
+ name: user.name,
65
+ role: user.role
66
+ },
67
+ session: { id: `test_session_${user.id}`, userId: user.id },
68
+ isAdmin,
69
+ isSuperAdmin
70
+ },
71
+ fn
72
+ )
73
+ }
74
+
75
+ /** Runs `fn` signed out. */
76
+ export function asAnonymous<T>(fn: () => Promise<T>): Promise<T> {
77
+ return runWithAuthSession(null, fn)
78
+ }
@@ -0,0 +1,2 @@
1
+ export * from './action'
2
+ export * from './fixtures'
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { sanitizeCallbackUrl } from '~/lib/callback-url'
3
+
4
+ // Everything sanitizeCallbackUrl rejects must be something a browser would
5
+ // resolve off-origin, so each case is checked against URL resolution too.
6
+ const APP_ORIGIN = 'https://app.example.com'
7
+
8
+ const resolvesOffOrigin = (value: string) =>
9
+ new URL(value, APP_ORIGIN).origin !== APP_ORIGIN
10
+
11
+ describe('sanitizeCallbackUrl', () => {
12
+ test('keeps same-origin paths, including query and hash', () => {
13
+ expect(sanitizeCallbackUrl('/app')).toBe('/app')
14
+ expect(sanitizeCallbackUrl('/accept-invitation/inv_abc123')).toBe(
15
+ '/accept-invitation/inv_abc123'
16
+ )
17
+ expect(sanitizeCallbackUrl('/app/orders?status=open#top')).toBe(
18
+ '/app/orders?status=open#top'
19
+ )
20
+ })
21
+
22
+ test('rejects values that are not a path', () => {
23
+ expect(sanitizeCallbackUrl(undefined)).toBeNull()
24
+ expect(sanitizeCallbackUrl(['/app', '/admin'])).toBeNull()
25
+ expect(sanitizeCallbackUrl('app')).toBeNull()
26
+ expect(sanitizeCallbackUrl('https://evil.com')).toBeNull()
27
+ })
28
+
29
+ test('rejects the backslash and double-slash escapes out of the origin', () => {
30
+ for (const value of ['//evil.com', '/\\evil.com', '/\\\\evil.com']) {
31
+ expect(resolvesOffOrigin(value)).toBe(true)
32
+ expect(sanitizeCallbackUrl(value)).toBeNull()
33
+ }
34
+ })
35
+
36
+ test('rejects tabs and newlines, which URL parsing strips before resolving', () => {
37
+ for (const value of ['/\t/evil.com', '/\n/evil.com', '/\r/evil.com']) {
38
+ expect(resolvesOffOrigin(value)).toBe(true)
39
+ expect(sanitizeCallbackUrl(value)).toBeNull()
40
+ }
41
+ })
42
+
43
+ test('leaves lookalike hosts on the app origin instead of rejecting them', () => {
44
+ // These stay same-origin, so they are safe to honour.
45
+ expect(sanitizeCallbackUrl('/@evil.com')).toBe('/@evil.com')
46
+ expect(sanitizeCallbackUrl('/%5Cevil.com')).toBe('/%5Cevil.com')
47
+ })
48
+ })
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Guard: `'use client'` modules must not import server runtime, secrets, or
3
+ * the database directly. Transitive imports leak too, but this test only reads
4
+ * each client file's own imports, so review what a new import pulls in.
5
+ */
6
+ import { describe, expect, test } from 'bun:test'
7
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
8
+ import path from 'node:path'
9
+
10
+ const SRC = path.resolve(import.meta.dir, '../../src')
11
+
12
+ /**
13
+ * Import paths that must never appear in a `'use client'` module. Add every
14
+ * new server-only lib here.
15
+ */
16
+ const FORBIDDEN_IMPORT_RE =
17
+ /from\s+['"](~\/db(?:\/(?:schema|enums|relations|common|index))?|~\/env|~\/lib\/(?:auth|safe-action|email-service|nodemailer)|server-only|drizzle-orm(?:\/[^'"]*)?)['"]/
18
+
19
+ function walk(dir: string, out: string[] = []): string[] {
20
+ for (const name of readdirSync(dir)) {
21
+ if (name === 'node_modules' || name.startsWith('.')) continue
22
+ const full = path.join(dir, name)
23
+ if (statSync(full).isDirectory()) walk(full, out)
24
+ else if (/\.(ts|tsx)$/.test(name)) out.push(full)
25
+ }
26
+ return out
27
+ }
28
+
29
+ describe('client / server boundary', () => {
30
+ test('no use-client file imports server runtime or db modules', () => {
31
+ const violations: string[] = []
32
+
33
+ for (const file of walk(SRC)) {
34
+ const source = readFileSync(file, 'utf8')
35
+ if (
36
+ !source.includes("'use client'") &&
37
+ !source.includes('"use client"')
38
+ ) {
39
+ continue
40
+ }
41
+
42
+ for (const [index, line] of source.split('\n').entries()) {
43
+ // Type-only imports are erased, but they are banned too so a later
44
+ // value import is harder to slip in.
45
+ if (!FORBIDDEN_IMPORT_RE.test(line)) continue
46
+ // Explicit client-safe siblings
47
+ if (line.includes('auth-client')) continue
48
+ if (line.includes('safe-action-client')) continue
49
+
50
+ const rel = path.relative(SRC, file)
51
+ violations.push(`${rel}:${index + 1}: ${line.trim()}`)
52
+ }
53
+ }
54
+
55
+ expect(violations).toEqual([])
56
+ })
57
+ })
@@ -0,0 +1,10 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { isUniqueViolation } from '~/lib/errors'
3
+
4
+ describe('postgres error codes', () => {
5
+ test('an error whose cause points back at itself is not a match and does not hang', () => {
6
+ const error: { message: string; cause?: unknown } = { message: 'loop' }
7
+ error.cause = error
8
+ expect(isUniqueViolation(error)).toBe(false)
9
+ })
10
+ })
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { escapeLikePattern, likeContains } from '~/lib/search'
3
+
4
+ describe('escapeLikePattern', () => {
5
+ test('escapes LIKE wildcards and the escape character', () => {
6
+ expect(escapeLikePattern('%')).toBe('\\%')
7
+ expect(escapeLikePattern('_')).toBe('\\_')
8
+ expect(escapeLikePattern('\\')).toBe('\\\\')
9
+ expect(escapeLikePattern('100%_off\\sale')).toBe('100\\%\\_off\\\\sale')
10
+ })
11
+
12
+ test('leaves ordinary search text unchanged', () => {
13
+ expect(escapeLikePattern('Quarterly report')).toBe('Quarterly report')
14
+ })
15
+ })
16
+
17
+ describe('likeContains', () => {
18
+ test('wraps an escaped pattern for contains matching', () => {
19
+ expect(likeContains('report')).toBe('%report%')
20
+ expect(likeContains('%')).toBe('%\\%%')
21
+ expect(likeContains('_')).toBe('%\\_%')
22
+ })
23
+ })
@@ -0,0 +1,63 @@
1
+ /**
2
+ * In-memory Postgres for Bun tests via Electric PGlite (WASM).
3
+ *
4
+ * One instance per Bun worker process:
5
+ * 1. Start PGlite
6
+ * 2. pushSchema from Drizzle table definitions
7
+ */
8
+ import { PGlite } from '@electric-sql/pglite'
9
+ import { pushSchema } from 'drizzle-kit/api'
10
+ import { drizzle, type PgliteDatabase } from 'drizzle-orm/pglite'
11
+ import * as relations from '~/db/relations'
12
+ import * as schema from '~/db/schema'
13
+
14
+ const fullSchema = { ...schema, ...relations }
15
+
16
+ type TestDatabase = PgliteDatabase<typeof fullSchema>
17
+
18
+ let initPromise: Promise<TestDatabase> | null = null
19
+
20
+ async function bootstrap(): Promise<TestDatabase> {
21
+ // PGlite's Emscripten runtime sets process.exitCode to 99 via proc_exit on
22
+ // the first WASM query. bun test then exits with that code even when every
23
+ // assertion passed. Snapshot first, restore only the spurious 99, and leave
24
+ // any real non-zero code alone.
25
+ const exitCodeBeforePglite = process.exitCode
26
+
27
+ const client = new PGlite()
28
+ const db = drizzle({ client, schema: fullSchema })
29
+
30
+ const { apply, warnings, hasDataLoss } = await pushSchema(
31
+ schema,
32
+ db as unknown as Parameters<typeof pushSchema>[1]
33
+ )
34
+ if (warnings.length) {
35
+ console.warn('[pglite] schema push warnings:', warnings)
36
+ }
37
+ if (hasDataLoss) {
38
+ throw new Error(
39
+ '[pglite] schema push reported data loss on a fresh database. Aborting.'
40
+ )
41
+ }
42
+ await apply()
43
+
44
+ if (process.exitCode === 99) {
45
+ process.exitCode =
46
+ exitCodeBeforePglite !== undefined && exitCodeBeforePglite !== 99
47
+ ? exitCodeBeforePglite
48
+ : 0
49
+ }
50
+
51
+ return db
52
+ }
53
+
54
+ /**
55
+ * Idempotent: first call boots PGlite and the schema; later calls return the same db.
56
+ * Safe to await from preload (once per worker).
57
+ */
58
+ export function initPgliteDatabase(): Promise<TestDatabase> {
59
+ if (!initPromise) {
60
+ initPromise = bootstrap()
61
+ }
62
+ return initPromise
63
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Bun test preload (runs before every test file and worker).
3
+ *
4
+ * - In-memory PGlite Postgres (WASM) through a `~/db` mock
5
+ * - `react.cache` becomes identity
6
+ * - `getAuthSession` reads an AsyncLocalStorage (parallel-safe)
7
+ *
8
+ * Set `TEST_DATABASE=postgres` to use the real `DATABASE_URL` instead.
9
+ */
10
+ import { mock } from 'bun:test'
11
+ import { AsyncLocalStorage } from 'node:async_hooks'
12
+ import { createRequire } from 'node:module'
13
+ import path from 'node:path'
14
+
15
+ // Bun does not set the `react-server` export condition, so the real
16
+ // `server-only` package throws. Next.js client builds still fail on it.
17
+ mock.module('server-only', () => ({}))
18
+
19
+ // `start()` needs the Workflow runtime. Tests only need fire-and-forget
20
+ // side effects such as emails to no-op.
21
+ mock.module('workflow/api', () => ({
22
+ start: async () => ({ runId: 'test-workflow-run' })
23
+ }))
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Database: PGlite (default) or real Postgres
27
+ // ---------------------------------------------------------------------------
28
+
29
+ process.env.TEST_DATABASE ??= 'pglite'
30
+
31
+ if (process.env.TEST_DATABASE === 'pglite') {
32
+ // ~/env requires a URL even though PGlite never connects to it.
33
+ process.env.DATABASE_URL ??= 'postgres://pglite.local/test'
34
+
35
+ const { initPgliteDatabase } = await import('./pglite-db')
36
+ const dbModule = { db: await initPgliteDatabase() }
37
+
38
+ mock.module('~/db', () => dbModule)
39
+ mock.module(
40
+ path.resolve(import.meta.dir, '../../src/db/index.ts'),
41
+ () => dbModule
42
+ )
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Auth and request headers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ type MockAuthSession = {
50
+ user: { id: string; email: string; name: string; role?: string | null }
51
+ session: { id: string; userId: string; impersonatedBy?: string | null }
52
+ isAdmin: boolean
53
+ isSuperAdmin: boolean
54
+ }
55
+
56
+ const authAls = new AsyncLocalStorage<MockAuthSession | null>()
57
+
58
+ /** Runs `fn` with `getAuthSession()` returning `session`. */
59
+ export function runWithAuthSession<T>(
60
+ session: MockAuthSession | null,
61
+ fn: () => Promise<T>
62
+ ): Promise<T> {
63
+ return authAls.run(session, fn)
64
+ }
65
+
66
+ const require = createRequire(import.meta.url)
67
+ const React = require('react') as typeof import('react')
68
+
69
+ mock.module('react', () => ({
70
+ ...React,
71
+ default: React,
72
+ cache: <T extends (...args: never[]) => unknown>(fn: T) => fn
73
+ }))
74
+
75
+ const authModuleMock = {
76
+ getAuthSession: async () => authAls.getStore() ?? null,
77
+ auth: null as unknown
78
+ }
79
+
80
+ mock.module('~/lib/auth', () => authModuleMock)
81
+ mock.module(
82
+ path.resolve(import.meta.dir, '../../src/lib/auth.ts'),
83
+ () => authModuleMock
84
+ )
85
+
86
+ mock.module('next/headers', () => ({
87
+ headers: async () => new Headers(),
88
+ cookies: async () => ({
89
+ get: () => undefined,
90
+ getAll: () => [],
91
+ has: () => false,
92
+ set: () => {},
93
+ delete: () => {}
94
+ })
95
+ }))
@@ -2,6 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2017",
4
4
  "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
+ "types": ["bun"],
5
6
  "allowJs": true,
6
7
  "checkJs": true,
7
8
  "skipLibCheck": true,
@@ -1,3 +1,4 @@
1
+ .git
1
2
  node_modules
2
3
  .next
3
4
  out
@@ -1,23 +1,23 @@
1
- APP_ENV="production"
1
+ APP_ENV="development"
2
2
  DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
3
3
  BETTER_AUTH_URL="http://localhost:3000"
4
- BETTER_AUTH_SECRET="secret"
5
- BETTER_AUTH_GITHUB_ID="github_id"
6
- BETTER_AUTH_GITHUB_SECRET="github_secret"
7
- BETTER_AUTH_GOOGLE_ID="google_id"
8
- BETTER_AUTH_GOOGLE_SECRET="google_secret"
9
- EMAIL_SERVER_USER="username"
10
- EMAIL_SERVER_PASSWORD="password"
4
+ BETTER_AUTH_SECRET="your_better_auth_secret_here"
5
+ BETTER_AUTH_GITHUB_ID="your_github_client_id_here"
6
+ BETTER_AUTH_GITHUB_SECRET="your_github_client_secret_here"
7
+ BETTER_AUTH_GOOGLE_ID="your_google_client_id_here"
8
+ BETTER_AUTH_GOOGLE_SECRET="your_google_client_secret_here"
9
+ EMAIL_SERVER_USER="your_smtp_username_here"
10
+ EMAIL_SERVER_PASSWORD="your_smtp_password_here"
11
11
  EMAIL_SERVER_HOST="smtp.example.com"
12
12
  EMAIL_SERVER_PORT="465"
13
13
  EMAIL_FROM="no-reply@example.com"
14
- STRIPE_SECRET_KEY="sk_test_xxxxxxxxxx"
15
- STRIPE_WEBHOOK_SECRET="whsec_xxxxxxxxxx"
16
- STRIPE_PRICE_PRO_MONTHLY="price_xxxxxxxxxx"
17
- STRIPE_PRICE_PRO_ANNUAL="price_xxxxxxxxxx"
18
- STRIPE_PRICE_ULTIMATE_MONTHLY="price_xxxxxxxxxx"
19
- STRIPE_PRICE_ULTIMATE_ANNUAL="price_xxxxxxxxxx"
20
- STRIPE_PORTAL_CONFIGURATION_ID="bpc_xxxxxxxxxx"
14
+ STRIPE_SECRET_KEY="your_stripe_secret_key_here"
15
+ STRIPE_WEBHOOK_SECRET="your_stripe_webhook_secret_here"
16
+ STRIPE_PRICE_PRO_MONTHLY="your_pro_monthly_price_id_here"
17
+ STRIPE_PRICE_PRO_ANNUAL="your_pro_annual_price_id_here"
18
+ STRIPE_PRICE_ULTIMATE_MONTHLY="your_ultimate_monthly_price_id_here"
19
+ STRIPE_PRICE_ULTIMATE_ANNUAL="your_ultimate_annual_price_id_here"
20
+ STRIPE_PORTAL_CONFIGURATION_ID="your_portal_configuration_id_here"
21
21
  R2_ACCOUNT_ID=""
22
22
  R2_ACCESS_KEY_ID=""
23
23
  R2_SECRET_ACCESS_KEY=""