create-blitzpack 0.1.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 (259) hide show
  1. package/dist/index.js +452 -0
  2. package/package.json +57 -0
  3. package/template/.dockerignore +59 -0
  4. package/template/.github/workflows/ci.yml +157 -0
  5. package/template/.husky/pre-commit +1 -0
  6. package/template/.husky/pre-push +1 -0
  7. package/template/.lintstagedrc.cjs +4 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/.prettierrc +9 -0
  10. package/template/.vscode/settings.json +13 -0
  11. package/template/CLAUDE.md +175 -0
  12. package/template/CONTRIBUTING.md +32 -0
  13. package/template/Dockerfile +90 -0
  14. package/template/GETTING_STARTED.md +35 -0
  15. package/template/LICENSE +21 -0
  16. package/template/README.md +116 -0
  17. package/template/apps/api/.dockerignore +51 -0
  18. package/template/apps/api/.env.local.example +62 -0
  19. package/template/apps/api/emails/account-deleted-email.tsx +69 -0
  20. package/template/apps/api/emails/components/email-layout.tsx +154 -0
  21. package/template/apps/api/emails/config.ts +22 -0
  22. package/template/apps/api/emails/password-changed-email.tsx +88 -0
  23. package/template/apps/api/emails/password-reset-email.tsx +86 -0
  24. package/template/apps/api/emails/verification-email.tsx +85 -0
  25. package/template/apps/api/emails/welcome-email.tsx +70 -0
  26. package/template/apps/api/package.json +84 -0
  27. package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
  28. package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
  29. package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
  30. package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
  31. package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
  32. package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
  33. package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
  34. package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
  35. package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
  36. package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
  37. package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +116 -0
  40. package/template/apps/api/prisma/seed.ts +159 -0
  41. package/template/apps/api/prisma.config.ts +14 -0
  42. package/template/apps/api/src/app.ts +377 -0
  43. package/template/apps/api/src/common/logger.service.ts +227 -0
  44. package/template/apps/api/src/config/env.ts +60 -0
  45. package/template/apps/api/src/config/rate-limit.ts +29 -0
  46. package/template/apps/api/src/hooks/auth.ts +122 -0
  47. package/template/apps/api/src/plugins/auth.ts +198 -0
  48. package/template/apps/api/src/plugins/database.ts +45 -0
  49. package/template/apps/api/src/plugins/logger.ts +33 -0
  50. package/template/apps/api/src/plugins/multipart.ts +16 -0
  51. package/template/apps/api/src/plugins/scalar.ts +20 -0
  52. package/template/apps/api/src/plugins/schedule.ts +52 -0
  53. package/template/apps/api/src/plugins/services.ts +66 -0
  54. package/template/apps/api/src/plugins/swagger.ts +56 -0
  55. package/template/apps/api/src/routes/accounts.ts +91 -0
  56. package/template/apps/api/src/routes/admin-sessions.ts +92 -0
  57. package/template/apps/api/src/routes/metrics.ts +71 -0
  58. package/template/apps/api/src/routes/password.ts +46 -0
  59. package/template/apps/api/src/routes/sessions.ts +53 -0
  60. package/template/apps/api/src/routes/stats.ts +38 -0
  61. package/template/apps/api/src/routes/uploads-serve.ts +27 -0
  62. package/template/apps/api/src/routes/uploads.ts +154 -0
  63. package/template/apps/api/src/routes/users.ts +114 -0
  64. package/template/apps/api/src/routes/verification.ts +90 -0
  65. package/template/apps/api/src/server.ts +34 -0
  66. package/template/apps/api/src/services/accounts.service.ts +125 -0
  67. package/template/apps/api/src/services/authorization.service.ts +162 -0
  68. package/template/apps/api/src/services/email.service.ts +170 -0
  69. package/template/apps/api/src/services/file-storage.service.ts +267 -0
  70. package/template/apps/api/src/services/metrics.service.ts +175 -0
  71. package/template/apps/api/src/services/password.service.ts +56 -0
  72. package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
  73. package/template/apps/api/src/services/sessions.service.ts +276 -0
  74. package/template/apps/api/src/services/stats.service.ts +273 -0
  75. package/template/apps/api/src/services/uploads.service.ts +163 -0
  76. package/template/apps/api/src/services/users.service.spec.ts +249 -0
  77. package/template/apps/api/src/services/users.service.ts +198 -0
  78. package/template/apps/api/src/utils/file-validation.ts +108 -0
  79. package/template/apps/api/start.sh +33 -0
  80. package/template/apps/api/test/helpers/fastify-app.ts +24 -0
  81. package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
  82. package/template/apps/api/test/helpers/mock-logger.ts +28 -0
  83. package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
  84. package/template/apps/api/test/helpers/test-db.ts +125 -0
  85. package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
  86. package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
  87. package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
  88. package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
  89. package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
  90. package/template/apps/api/test/setup.ts +31 -0
  91. package/template/apps/api/tsconfig.json +26 -0
  92. package/template/apps/api/vitest.config.ts +35 -0
  93. package/template/apps/web/.env.local.example +11 -0
  94. package/template/apps/web/components.json +24 -0
  95. package/template/apps/web/next.config.ts +22 -0
  96. package/template/apps/web/package.json +56 -0
  97. package/template/apps/web/postcss.config.js +5 -0
  98. package/template/apps/web/public/apple-icon.png +0 -0
  99. package/template/apps/web/public/icon.png +0 -0
  100. package/template/apps/web/public/robots.txt +3 -0
  101. package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
  102. package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
  103. package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
  104. package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
  105. package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
  106. package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
  107. package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
  108. package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
  109. package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
  110. package/template/apps/web/src/app/dashboard/page.tsx +296 -0
  111. package/template/apps/web/src/app/error.tsx +32 -0
  112. package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
  113. package/template/apps/web/src/app/favicon.ico +0 -0
  114. package/template/apps/web/src/app/global-error.tsx +96 -0
  115. package/template/apps/web/src/app/globals.css +22 -0
  116. package/template/apps/web/src/app/icon.png +0 -0
  117. package/template/apps/web/src/app/layout.tsx +34 -0
  118. package/template/apps/web/src/app/not-found.tsx +28 -0
  119. package/template/apps/web/src/app/page.tsx +192 -0
  120. package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
  121. package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
  122. package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
  123. package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
  124. package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
  125. package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
  126. package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
  127. package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
  128. package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
  129. package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
  130. package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
  131. package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
  132. package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
  133. package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
  134. package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
  135. package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
  136. package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
  137. package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
  138. package/template/apps/web/src/components/auth/github-button.tsx +40 -0
  139. package/template/apps/web/src/components/auth/google-button.tsx +54 -0
  140. package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
  141. package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
  142. package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
  143. package/template/apps/web/src/components/error/error-card.tsx +47 -0
  144. package/template/apps/web/src/components/error/forbidden.tsx +25 -0
  145. package/template/apps/web/src/components/landing/command-block.tsx +64 -0
  146. package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
  147. package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
  148. package/template/apps/web/src/components/landing/logo.tsx +41 -0
  149. package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
  150. package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
  151. package/template/apps/web/src/components/layout/footer.tsx +3 -0
  152. package/template/apps/web/src/config/landing-data.ts +152 -0
  153. package/template/apps/web/src/config/site.ts +5 -0
  154. package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
  155. package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
  156. package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
  157. package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
  158. package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
  159. package/template/apps/web/src/hooks/api/use-users.ts +149 -0
  160. package/template/apps/web/src/hooks/use-mobile.ts +21 -0
  161. package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
  162. package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
  163. package/template/apps/web/src/lib/api.ts +151 -0
  164. package/template/apps/web/src/lib/auth.ts +13 -0
  165. package/template/apps/web/src/lib/env.ts +52 -0
  166. package/template/apps/web/src/lib/form-utils.ts +11 -0
  167. package/template/apps/web/src/lib/utils.ts +1 -0
  168. package/template/apps/web/src/providers.tsx +34 -0
  169. package/template/apps/web/src/store/atoms.ts +15 -0
  170. package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
  171. package/template/apps/web/src/test/setup.ts +8 -0
  172. package/template/apps/web/tailwind.config.ts +5 -0
  173. package/template/apps/web/tsconfig.json +26 -0
  174. package/template/apps/web/vitest.config.ts +32 -0
  175. package/template/assets/logo-512.png +0 -0
  176. package/template/assets/logo.svg +4 -0
  177. package/template/docker-compose.prod.yml +66 -0
  178. package/template/docker-compose.yml +36 -0
  179. package/template/eslint.config.ts +119 -0
  180. package/template/package.json +77 -0
  181. package/template/packages/tailwind-config/package.json +9 -0
  182. package/template/packages/tailwind-config/theme.css +179 -0
  183. package/template/packages/types/package.json +29 -0
  184. package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
  185. package/template/packages/types/src/api-response.ts +53 -0
  186. package/template/packages/types/src/health-check.ts +11 -0
  187. package/template/packages/types/src/pagination.ts +41 -0
  188. package/template/packages/types/src/role.ts +5 -0
  189. package/template/packages/types/src/session.ts +48 -0
  190. package/template/packages/types/src/stats.ts +113 -0
  191. package/template/packages/types/src/upload.ts +51 -0
  192. package/template/packages/types/src/user.ts +36 -0
  193. package/template/packages/types/tsconfig.json +5 -0
  194. package/template/packages/types/vitest.config.ts +21 -0
  195. package/template/packages/ui/components.json +21 -0
  196. package/template/packages/ui/package.json +108 -0
  197. package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
  198. package/template/packages/ui/src/alert-dialog.tsx +141 -0
  199. package/template/packages/ui/src/alert.tsx +66 -0
  200. package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
  201. package/template/packages/ui/src/avatar.tsx +53 -0
  202. package/template/packages/ui/src/badge.tsx +36 -0
  203. package/template/packages/ui/src/button.tsx +84 -0
  204. package/template/packages/ui/src/card.tsx +92 -0
  205. package/template/packages/ui/src/checkbox.tsx +32 -0
  206. package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
  207. package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
  208. package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
  209. package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
  210. package/template/packages/ui/src/data-table/data-table.tsx +167 -0
  211. package/template/packages/ui/src/dialog.tsx +143 -0
  212. package/template/packages/ui/src/dropdown-menu.tsx +257 -0
  213. package/template/packages/ui/src/empty-state.tsx +52 -0
  214. package/template/packages/ui/src/file-upload-input.tsx +202 -0
  215. package/template/packages/ui/src/form.tsx +168 -0
  216. package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
  217. package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
  218. package/template/packages/ui/src/input.tsx +21 -0
  219. package/template/packages/ui/src/label.tsx +24 -0
  220. package/template/packages/ui/src/lib/utils.ts +6 -0
  221. package/template/packages/ui/src/password-input.tsx +102 -0
  222. package/template/packages/ui/src/popover.tsx +48 -0
  223. package/template/packages/ui/src/radio-group.tsx +45 -0
  224. package/template/packages/ui/src/scroll-area.tsx +58 -0
  225. package/template/packages/ui/src/select.tsx +187 -0
  226. package/template/packages/ui/src/separator.tsx +28 -0
  227. package/template/packages/ui/src/sheet.tsx +139 -0
  228. package/template/packages/ui/src/sidebar.tsx +726 -0
  229. package/template/packages/ui/src/skeleton-variants.tsx +87 -0
  230. package/template/packages/ui/src/skeleton.tsx +13 -0
  231. package/template/packages/ui/src/slider.tsx +63 -0
  232. package/template/packages/ui/src/sonner.tsx +25 -0
  233. package/template/packages/ui/src/spinner.tsx +16 -0
  234. package/template/packages/ui/src/switch.tsx +31 -0
  235. package/template/packages/ui/src/table.tsx +116 -0
  236. package/template/packages/ui/src/tabs.tsx +66 -0
  237. package/template/packages/ui/src/textarea.tsx +18 -0
  238. package/template/packages/ui/src/tooltip.tsx +61 -0
  239. package/template/packages/ui/src/user-avatar.tsx +97 -0
  240. package/template/packages/ui/test-config.js +3 -0
  241. package/template/packages/ui/tsconfig.json +12 -0
  242. package/template/packages/ui/turbo.json +18 -0
  243. package/template/packages/ui/vitest.config.ts +17 -0
  244. package/template/packages/ui/vitest.setup.ts +1 -0
  245. package/template/packages/utils/package.json +23 -0
  246. package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
  247. package/template/packages/utils/src/array.ts +18 -0
  248. package/template/packages/utils/src/async.ts +3 -0
  249. package/template/packages/utils/src/date.ts +77 -0
  250. package/template/packages/utils/src/errors.ts +73 -0
  251. package/template/packages/utils/src/number.ts +11 -0
  252. package/template/packages/utils/src/string.ts +13 -0
  253. package/template/packages/utils/tsconfig.json +5 -0
  254. package/template/packages/utils/vitest.config.ts +21 -0
  255. package/template/pnpm-workspace.yaml +4 -0
  256. package/template/tsconfig.base.json +32 -0
  257. package/template/turbo.json +133 -0
  258. package/template/vitest.shared.ts +26 -0
  259. package/template/vitest.workspace.ts +9 -0
@@ -0,0 +1,125 @@
1
+ import 'dotenv-flow/config';
2
+
3
+ import { PrismaPg } from '@prisma/adapter-pg';
4
+ import { execSync } from 'child_process';
5
+ import { Pool } from 'pg';
6
+
7
+ import { PrismaClient } from '@/generated/client/client.js';
8
+
9
+ let prisma: PrismaClient | null = null;
10
+ let pool: Pool | null = null;
11
+
12
+ /**
13
+ * Get test database URL
14
+ * Uses DATABASE_URL from env but appends _test suffix to database name
15
+ */
16
+ function getTestDatabaseUrl(): string {
17
+ const databaseUrl =
18
+ process.env.DATABASE_URL ||
19
+ 'postgresql://postgres:postgres_dev_password@localhost:5432/app_dev';
20
+
21
+ const url = new URL(databaseUrl);
22
+ const dbName = url.pathname.slice(1);
23
+ url.pathname = `/${dbName}_test`;
24
+
25
+ return url.toString();
26
+ }
27
+
28
+ /**
29
+ * Setup test database before running tests
30
+ * - Sets up test database URL
31
+ * - Runs migrations
32
+ * - Creates Prisma client instance
33
+ */
34
+ export async function setupTestDatabase(): Promise<void> {
35
+ const testDatabaseUrl = getTestDatabaseUrl();
36
+
37
+ process.env.DATABASE_URL = testDatabaseUrl;
38
+
39
+ try {
40
+ // Run migrations on test database
41
+ console.log('⏳ Running test database migrations...');
42
+ execSync('npx prisma migrate deploy', {
43
+ stdio: 'inherit',
44
+ env: { ...process.env, DATABASE_URL: testDatabaseUrl },
45
+ });
46
+
47
+ pool = new Pool({ connectionString: testDatabaseUrl });
48
+ const adapter = new PrismaPg(pool);
49
+ prisma = new PrismaClient({ adapter });
50
+
51
+ await prisma.$connect();
52
+ console.log('✅ Test database ready');
53
+ } catch (error) {
54
+ console.error('❌ Failed to setup test database:', error);
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Cleanup test database after tests complete
61
+ * - Disconnects Prisma client
62
+ * - Optionally drops test database (set KEEP_TEST_DB=true to preserve)
63
+ */
64
+ export async function cleanupTestDatabase(): Promise<void> {
65
+ if (prisma) {
66
+ await prisma.$disconnect();
67
+ prisma = null;
68
+ }
69
+
70
+ if (pool) {
71
+ await pool.end();
72
+ pool = null;
73
+ }
74
+
75
+ if (process.env.KEEP_TEST_DB !== 'true') {
76
+ try {
77
+ const testDatabaseUrl = getTestDatabaseUrl();
78
+ const url = new URL(testDatabaseUrl);
79
+ const dbName = url.pathname.slice(1);
80
+
81
+ url.pathname = '/postgres';
82
+ const adminUrl = url.toString();
83
+
84
+ console.log('⏳ Cleaning up test database...');
85
+
86
+ const adminPool = new Pool({ connectionString: adminUrl });
87
+ const adminAdapter = new PrismaPg(adminPool);
88
+ const adminPrisma = new PrismaClient({ adapter: adminAdapter });
89
+
90
+ await adminPrisma.$connect();
91
+ await adminPrisma.$executeRawUnsafe(
92
+ `DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE);`
93
+ );
94
+ await adminPrisma.$disconnect();
95
+ await adminPool.end();
96
+
97
+ console.log('✅ Test database cleaned up');
98
+ } catch (error) {
99
+ console.warn('⚠️ Test database cleanup failed (usually okay)');
100
+ }
101
+ }
102
+ }
103
+
104
+ export function getTestPrisma(): PrismaClient {
105
+ if (!prisma) {
106
+ throw new Error(
107
+ 'Test database not initialized. Make sure setupTestDatabase() was called.'
108
+ );
109
+ }
110
+ return prisma;
111
+ }
112
+
113
+ /**
114
+ * Reset test database between tests
115
+ * Truncates all tables while preserving schema
116
+ */
117
+ export async function resetTestDatabase(): Promise<void> {
118
+ const client = getTestPrisma();
119
+
120
+ // Disable foreign key checks and truncate all tables at once
121
+ // This is more efficient and avoids deadlocks
122
+ await client.$executeRaw`
123
+ TRUNCATE TABLE "verifications", "sessions", "accounts", "users" RESTART IDENTITY CASCADE;
124
+ `;
125
+ }
@@ -0,0 +1,449 @@
1
+ import { getTestPrisma, resetTestDatabase } from '@test/helpers/test-db';
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ describe('Authentication Flow Integration Tests', () => {
5
+ beforeEach(async () => {
6
+ await resetTestDatabase();
7
+ });
8
+
9
+ afterEach(async () => {
10
+ // Additional cleanup if needed
11
+ });
12
+
13
+ describe('Signup and Login Flow', () => {
14
+ it('should allow user to signup and login with email/password', async () => {
15
+ const prisma = getTestPrisma();
16
+
17
+ const email = 'newuser@test.com';
18
+ const password = 'SecurePass123!';
19
+ const name = 'New User';
20
+
21
+ // Simulate signup by creating user directly in database
22
+ // (Better Auth handles signup via API endpoint)
23
+ const user = await prisma.user.create({
24
+ data: {
25
+ email,
26
+ name,
27
+ emailVerified: false,
28
+ },
29
+ });
30
+
31
+ // Create credential account for password auth
32
+ const bcrypt = await import('bcryptjs');
33
+ const hashedPassword = await bcrypt.hash(password, 10);
34
+
35
+ await prisma.account.create({
36
+ data: {
37
+ userId: user.id,
38
+ accountId: email, // Use email as accountId for credential provider
39
+ providerId: 'credential',
40
+ password: hashedPassword,
41
+ },
42
+ });
43
+
44
+ // Verify user was created
45
+ const createdUser = await prisma.user.findUnique({
46
+ where: { email },
47
+ });
48
+
49
+ expect(createdUser).toBeDefined();
50
+ expect(createdUser?.email).toBe(email);
51
+ expect(createdUser?.name).toBe(name);
52
+ expect(createdUser?.emailVerified).toBe(false);
53
+ });
54
+
55
+ it('should create session on successful login', async () => {
56
+ const prisma = getTestPrisma();
57
+
58
+ const user = await prisma.user.create({
59
+ data: {
60
+ email: 'session@test.com',
61
+ name: 'Session User',
62
+ emailVerified: true,
63
+ },
64
+ });
65
+
66
+ const bcrypt = await import('bcryptjs');
67
+ const hashedPassword = await bcrypt.hash('password123', 10);
68
+
69
+ await prisma.account.create({
70
+ data: {
71
+ userId: user.id,
72
+ accountId: 'session@test.com', // Use email as accountId for credential provider
73
+ providerId: 'credential',
74
+ password: hashedPassword,
75
+ },
76
+ });
77
+
78
+ // Create session manually (Better Auth would do this on login)
79
+ const session = await prisma.session.create({
80
+ data: {
81
+ userId: user.id,
82
+ token: 'test-session-token',
83
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
84
+ ipAddress: '127.0.0.1',
85
+ userAgent: 'Test Browser',
86
+ },
87
+ });
88
+
89
+ expect(session).toBeDefined();
90
+ expect(session.userId).toBe(user.id);
91
+ expect(session.expiresAt.getTime()).toBeGreaterThan(Date.now());
92
+ });
93
+
94
+ it('should not allow duplicate email signups', async () => {
95
+ const prisma = getTestPrisma();
96
+
97
+ const email = 'duplicate@test.com';
98
+
99
+ // Create first user
100
+ await prisma.user.create({
101
+ data: {
102
+ email,
103
+ name: 'First User',
104
+ },
105
+ });
106
+
107
+ // Attempt to create second user with same email
108
+ await expect(
109
+ prisma.user.create({
110
+ data: {
111
+ email,
112
+ name: 'Second User',
113
+ },
114
+ })
115
+ ).rejects.toThrow();
116
+ });
117
+ });
118
+
119
+ describe('Session Management', () => {
120
+ it('should allow multiple sessions for same user', async () => {
121
+ const prisma = getTestPrisma();
122
+
123
+ const user = await prisma.user.create({
124
+ data: {
125
+ email: 'multisession@test.com',
126
+ name: 'Multi Session User',
127
+ },
128
+ });
129
+
130
+ // Create multiple sessions (different devices)
131
+ const session1 = await prisma.session.create({
132
+ data: {
133
+ userId: user.id,
134
+ token: 'session-1-token',
135
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
136
+ ipAddress: '192.168.1.1',
137
+ userAgent: 'Desktop Browser',
138
+ },
139
+ });
140
+
141
+ const session2 = await prisma.session.create({
142
+ data: {
143
+ userId: user.id,
144
+ token: 'session-2-token',
145
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
146
+ ipAddress: '192.168.1.2',
147
+ userAgent: 'Mobile Browser',
148
+ },
149
+ });
150
+
151
+ const sessions = await prisma.session.findMany({
152
+ where: { userId: user.id },
153
+ });
154
+
155
+ expect(sessions).toHaveLength(2);
156
+ expect(sessions[0].id).toBe(session1.id);
157
+ expect(sessions[1].id).toBe(session2.id);
158
+ });
159
+
160
+ it('should filter out expired sessions', async () => {
161
+ const prisma = getTestPrisma();
162
+
163
+ const user = await prisma.user.create({
164
+ data: {
165
+ email: 'expired@test.com',
166
+ name: 'Expired Session User',
167
+ },
168
+ });
169
+
170
+ // Create active session
171
+ await prisma.session.create({
172
+ data: {
173
+ userId: user.id,
174
+ token: 'active-session',
175
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
176
+ },
177
+ });
178
+
179
+ // Create expired session
180
+ await prisma.session.create({
181
+ data: {
182
+ userId: user.id,
183
+ token: 'expired-session',
184
+ expiresAt: new Date(Date.now() - 1000), // Already expired
185
+ },
186
+ });
187
+
188
+ // Query only active sessions
189
+ const activeSessions = await prisma.session.findMany({
190
+ where: {
191
+ userId: user.id,
192
+ expiresAt: {
193
+ gt: new Date(),
194
+ },
195
+ },
196
+ });
197
+
198
+ expect(activeSessions).toHaveLength(1);
199
+ expect(activeSessions[0].token).toBe('active-session');
200
+ });
201
+
202
+ it('should delete session on logout', async () => {
203
+ const prisma = getTestPrisma();
204
+
205
+ const user = await prisma.user.create({
206
+ data: {
207
+ email: 'logout@test.com',
208
+ name: 'Logout User',
209
+ },
210
+ });
211
+
212
+ const session = await prisma.session.create({
213
+ data: {
214
+ userId: user.id,
215
+ token: 'logout-session',
216
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
217
+ },
218
+ });
219
+
220
+ // Delete session (logout)
221
+ await prisma.session.delete({
222
+ where: { id: session.id },
223
+ });
224
+
225
+ const deletedSession = await prisma.session.findUnique({
226
+ where: { id: session.id },
227
+ });
228
+
229
+ expect(deletedSession).toBeNull();
230
+ });
231
+
232
+ it('should revoke all user sessions', async () => {
233
+ const prisma = getTestPrisma();
234
+
235
+ const user = await prisma.user.create({
236
+ data: {
237
+ email: 'revoke-all@test.com',
238
+ name: 'Revoke All User',
239
+ },
240
+ });
241
+
242
+ // Create multiple sessions
243
+ await prisma.session.createMany({
244
+ data: [
245
+ {
246
+ userId: user.id,
247
+ token: 'session-1',
248
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
249
+ },
250
+ {
251
+ userId: user.id,
252
+ token: 'session-2',
253
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
254
+ },
255
+ {
256
+ userId: user.id,
257
+ token: 'session-3',
258
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
259
+ },
260
+ ],
261
+ });
262
+
263
+ // Revoke all sessions
264
+ await prisma.session.deleteMany({
265
+ where: { userId: user.id },
266
+ });
267
+
268
+ const remainingSessions = await prisma.session.findMany({
269
+ where: { userId: user.id },
270
+ });
271
+
272
+ expect(remainingSessions).toHaveLength(0);
273
+ });
274
+ });
275
+
276
+ describe('Email Verification Flow', () => {
277
+ it('should create verification record on signup', async () => {
278
+ const prisma = getTestPrisma();
279
+
280
+ const user = await prisma.user.create({
281
+ data: {
282
+ email: 'verify@test.com',
283
+ name: 'Verify User',
284
+ emailVerified: false,
285
+ },
286
+ });
287
+
288
+ // Create verification token
289
+ const verification = await prisma.verification.create({
290
+ data: {
291
+ identifier: user.email,
292
+ value: 'verification-token-12345',
293
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
294
+ },
295
+ });
296
+
297
+ expect(verification).toBeDefined();
298
+ expect(verification.identifier).toBe(user.email);
299
+ expect(verification.expiresAt.getTime()).toBeGreaterThan(Date.now());
300
+ });
301
+
302
+ it('should mark user as verified after email verification', async () => {
303
+ const prisma = getTestPrisma();
304
+
305
+ const user = await prisma.user.create({
306
+ data: {
307
+ email: 'verify-success@test.com',
308
+ name: 'Verify Success User',
309
+ emailVerified: false,
310
+ },
311
+ });
312
+
313
+ // Simulate verification
314
+ const updatedUser = await prisma.user.update({
315
+ where: { id: user.id },
316
+ data: { emailVerified: true },
317
+ });
318
+
319
+ expect(updatedUser.emailVerified).toBe(true);
320
+ });
321
+
322
+ it('should delete verification token after successful verification', async () => {
323
+ const prisma = getTestPrisma();
324
+
325
+ const verification = await prisma.verification.create({
326
+ data: {
327
+ identifier: 'delete-token@test.com',
328
+ value: 'token-to-delete',
329
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
330
+ },
331
+ });
332
+
333
+ // Delete verification token after use
334
+ await prisma.verification.delete({
335
+ where: { id: verification.id },
336
+ });
337
+
338
+ const deletedVerification = await prisma.verification.findUnique({
339
+ where: { id: verification.id },
340
+ });
341
+
342
+ expect(deletedVerification).toBeNull();
343
+ });
344
+ });
345
+
346
+ describe('Password Reset Flow', () => {
347
+ it('should create password reset token', async () => {
348
+ const prisma = getTestPrisma();
349
+
350
+ const user = await prisma.user.create({
351
+ data: {
352
+ email: 'reset@test.com',
353
+ name: 'Reset User',
354
+ },
355
+ });
356
+
357
+ // Create password reset verification
358
+ const resetToken = await prisma.verification.create({
359
+ data: {
360
+ identifier: user.email,
361
+ value: 'reset-token-12345',
362
+ expiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour
363
+ },
364
+ });
365
+
366
+ expect(resetToken).toBeDefined();
367
+ expect(resetToken.identifier).toBe(user.email);
368
+ });
369
+
370
+ it('should update password and invalidate reset token', async () => {
371
+ const prisma = getTestPrisma();
372
+
373
+ const user = await prisma.user.create({
374
+ data: {
375
+ email: 'password-change@test.com',
376
+ name: 'Password Change User',
377
+ },
378
+ });
379
+
380
+ // Create account with old password
381
+ const bcrypt = await import('bcryptjs');
382
+ const oldPassword = await bcrypt.hash('oldPassword123', 10);
383
+
384
+ const account = await prisma.account.create({
385
+ data: {
386
+ userId: user.id,
387
+ accountId: user.id,
388
+ providerId: 'credential',
389
+ password: oldPassword,
390
+ },
391
+ });
392
+
393
+ // Update password
394
+ const newPassword = await bcrypt.hash('newPassword456', 10);
395
+ const updatedAccount = await prisma.account.update({
396
+ where: { id: account.id },
397
+ data: { password: newPassword },
398
+ });
399
+
400
+ // Verify password was changed
401
+ const isOldPasswordValid = await bcrypt.compare(
402
+ 'oldPassword123',
403
+ updatedAccount.password!
404
+ );
405
+ const isNewPasswordValid = await bcrypt.compare(
406
+ 'newPassword456',
407
+ updatedAccount.password!
408
+ );
409
+
410
+ expect(isOldPasswordValid).toBe(false);
411
+ expect(isNewPasswordValid).toBe(true);
412
+ });
413
+ });
414
+
415
+ describe('User Role Management', () => {
416
+ it('should assign default role on user creation', async () => {
417
+ const prisma = getTestPrisma();
418
+
419
+ const user = await prisma.user.create({
420
+ data: {
421
+ email: 'default-role@test.com',
422
+ name: 'Default Role User',
423
+ },
424
+ });
425
+
426
+ // Better Auth admin plugin uses 'role' field
427
+ // Default role should be 'user' (as configured in auth plugin)
428
+ const userWithRole = user as typeof user & { role?: string };
429
+ expect(userWithRole.role || 'user').toBe('user');
430
+ });
431
+
432
+ it('should allow admin role assignment', async () => {
433
+ const prisma = getTestPrisma();
434
+
435
+ // Create user with admin role
436
+ const adminUser = await prisma.user.create({
437
+ data: {
438
+ email: 'admin@test.com',
439
+ name: 'Admin User',
440
+ // Note: Better Auth admin plugin manages roles via user.role field
441
+ // The exact implementation depends on your Prisma schema
442
+ },
443
+ });
444
+
445
+ expect(adminUser).toBeDefined();
446
+ expect(adminUser.email).toBe('admin@test.com');
447
+ });
448
+ });
449
+ });