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.
- package/dist/index.js +452 -0
- package/package.json +57 -0
- package/template/.dockerignore +59 -0
- package/template/.github/workflows/ci.yml +157 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.husky/pre-push +1 -0
- package/template/.lintstagedrc.cjs +4 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierrc +9 -0
- package/template/.vscode/settings.json +13 -0
- package/template/CLAUDE.md +175 -0
- package/template/CONTRIBUTING.md +32 -0
- package/template/Dockerfile +90 -0
- package/template/GETTING_STARTED.md +35 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/apps/api/.dockerignore +51 -0
- package/template/apps/api/.env.local.example +62 -0
- package/template/apps/api/emails/account-deleted-email.tsx +69 -0
- package/template/apps/api/emails/components/email-layout.tsx +154 -0
- package/template/apps/api/emails/config.ts +22 -0
- package/template/apps/api/emails/password-changed-email.tsx +88 -0
- package/template/apps/api/emails/password-reset-email.tsx +86 -0
- package/template/apps/api/emails/verification-email.tsx +85 -0
- package/template/apps/api/emails/welcome-email.tsx +70 -0
- package/template/apps/api/package.json +84 -0
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +116 -0
- package/template/apps/api/prisma/seed.ts +159 -0
- package/template/apps/api/prisma.config.ts +14 -0
- package/template/apps/api/src/app.ts +377 -0
- package/template/apps/api/src/common/logger.service.ts +227 -0
- package/template/apps/api/src/config/env.ts +60 -0
- package/template/apps/api/src/config/rate-limit.ts +29 -0
- package/template/apps/api/src/hooks/auth.ts +122 -0
- package/template/apps/api/src/plugins/auth.ts +198 -0
- package/template/apps/api/src/plugins/database.ts +45 -0
- package/template/apps/api/src/plugins/logger.ts +33 -0
- package/template/apps/api/src/plugins/multipart.ts +16 -0
- package/template/apps/api/src/plugins/scalar.ts +20 -0
- package/template/apps/api/src/plugins/schedule.ts +52 -0
- package/template/apps/api/src/plugins/services.ts +66 -0
- package/template/apps/api/src/plugins/swagger.ts +56 -0
- package/template/apps/api/src/routes/accounts.ts +91 -0
- package/template/apps/api/src/routes/admin-sessions.ts +92 -0
- package/template/apps/api/src/routes/metrics.ts +71 -0
- package/template/apps/api/src/routes/password.ts +46 -0
- package/template/apps/api/src/routes/sessions.ts +53 -0
- package/template/apps/api/src/routes/stats.ts +38 -0
- package/template/apps/api/src/routes/uploads-serve.ts +27 -0
- package/template/apps/api/src/routes/uploads.ts +154 -0
- package/template/apps/api/src/routes/users.ts +114 -0
- package/template/apps/api/src/routes/verification.ts +90 -0
- package/template/apps/api/src/server.ts +34 -0
- package/template/apps/api/src/services/accounts.service.ts +125 -0
- package/template/apps/api/src/services/authorization.service.ts +162 -0
- package/template/apps/api/src/services/email.service.ts +170 -0
- package/template/apps/api/src/services/file-storage.service.ts +267 -0
- package/template/apps/api/src/services/metrics.service.ts +175 -0
- package/template/apps/api/src/services/password.service.ts +56 -0
- package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
- package/template/apps/api/src/services/sessions.service.ts +276 -0
- package/template/apps/api/src/services/stats.service.ts +273 -0
- package/template/apps/api/src/services/uploads.service.ts +163 -0
- package/template/apps/api/src/services/users.service.spec.ts +249 -0
- package/template/apps/api/src/services/users.service.ts +198 -0
- package/template/apps/api/src/utils/file-validation.ts +108 -0
- package/template/apps/api/start.sh +33 -0
- package/template/apps/api/test/helpers/fastify-app.ts +24 -0
- package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
- package/template/apps/api/test/helpers/mock-logger.ts +28 -0
- package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
- package/template/apps/api/test/helpers/test-db.ts +125 -0
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
- package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
- package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
- package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
- package/template/apps/api/test/setup.ts +31 -0
- package/template/apps/api/tsconfig.json +26 -0
- package/template/apps/api/vitest.config.ts +35 -0
- package/template/apps/web/.env.local.example +11 -0
- package/template/apps/web/components.json +24 -0
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +56 -0
- package/template/apps/web/postcss.config.js +5 -0
- package/template/apps/web/public/apple-icon.png +0 -0
- package/template/apps/web/public/icon.png +0 -0
- package/template/apps/web/public/robots.txt +3 -0
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
- package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
- package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
- package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
- package/template/apps/web/src/app/dashboard/page.tsx +296 -0
- package/template/apps/web/src/app/error.tsx +32 -0
- package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +96 -0
- package/template/apps/web/src/app/globals.css +22 -0
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +34 -0
- package/template/apps/web/src/app/not-found.tsx +28 -0
- package/template/apps/web/src/app/page.tsx +192 -0
- package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
- package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
- package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
- package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
- package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
- package/template/apps/web/src/components/auth/github-button.tsx +40 -0
- package/template/apps/web/src/components/auth/google-button.tsx +54 -0
- package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
- package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
- package/template/apps/web/src/components/error/error-card.tsx +47 -0
- package/template/apps/web/src/components/error/forbidden.tsx +25 -0
- package/template/apps/web/src/components/landing/command-block.tsx +64 -0
- package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
- package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
- package/template/apps/web/src/components/landing/logo.tsx +41 -0
- package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
- package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
- package/template/apps/web/src/components/layout/footer.tsx +3 -0
- package/template/apps/web/src/config/landing-data.ts +152 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
- package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
- package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
- package/template/apps/web/src/hooks/api/use-users.ts +149 -0
- package/template/apps/web/src/hooks/use-mobile.ts +21 -0
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
- package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
- package/template/apps/web/src/lib/api.ts +151 -0
- package/template/apps/web/src/lib/auth.ts +13 -0
- package/template/apps/web/src/lib/env.ts +52 -0
- package/template/apps/web/src/lib/form-utils.ts +11 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/providers.tsx +34 -0
- package/template/apps/web/src/store/atoms.ts +15 -0
- package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
- package/template/apps/web/src/test/setup.ts +8 -0
- package/template/apps/web/tailwind.config.ts +5 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/vitest.config.ts +32 -0
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +4 -0
- package/template/docker-compose.prod.yml +66 -0
- package/template/docker-compose.yml +36 -0
- package/template/eslint.config.ts +119 -0
- package/template/package.json +77 -0
- package/template/packages/tailwind-config/package.json +9 -0
- package/template/packages/tailwind-config/theme.css +179 -0
- package/template/packages/types/package.json +29 -0
- package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
- package/template/packages/types/src/api-response.ts +53 -0
- package/template/packages/types/src/health-check.ts +11 -0
- package/template/packages/types/src/pagination.ts +41 -0
- package/template/packages/types/src/role.ts +5 -0
- package/template/packages/types/src/session.ts +48 -0
- package/template/packages/types/src/stats.ts +113 -0
- package/template/packages/types/src/upload.ts +51 -0
- package/template/packages/types/src/user.ts +36 -0
- package/template/packages/types/tsconfig.json +5 -0
- package/template/packages/types/vitest.config.ts +21 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +108 -0
- package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
- package/template/packages/ui/src/alert-dialog.tsx +141 -0
- package/template/packages/ui/src/alert.tsx +66 -0
- package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
- package/template/packages/ui/src/avatar.tsx +53 -0
- package/template/packages/ui/src/badge.tsx +36 -0
- package/template/packages/ui/src/button.tsx +84 -0
- package/template/packages/ui/src/card.tsx +92 -0
- package/template/packages/ui/src/checkbox.tsx +32 -0
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
- package/template/packages/ui/src/data-table/data-table.tsx +167 -0
- package/template/packages/ui/src/dialog.tsx +143 -0
- package/template/packages/ui/src/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/empty-state.tsx +52 -0
- package/template/packages/ui/src/file-upload-input.tsx +202 -0
- package/template/packages/ui/src/form.tsx +168 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
- package/template/packages/ui/src/input.tsx +21 -0
- package/template/packages/ui/src/label.tsx +24 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/password-input.tsx +102 -0
- package/template/packages/ui/src/popover.tsx +48 -0
- package/template/packages/ui/src/radio-group.tsx +45 -0
- package/template/packages/ui/src/scroll-area.tsx +58 -0
- package/template/packages/ui/src/select.tsx +187 -0
- package/template/packages/ui/src/separator.tsx +28 -0
- package/template/packages/ui/src/sheet.tsx +139 -0
- package/template/packages/ui/src/sidebar.tsx +726 -0
- package/template/packages/ui/src/skeleton-variants.tsx +87 -0
- package/template/packages/ui/src/skeleton.tsx +13 -0
- package/template/packages/ui/src/slider.tsx +63 -0
- package/template/packages/ui/src/sonner.tsx +25 -0
- package/template/packages/ui/src/spinner.tsx +16 -0
- package/template/packages/ui/src/switch.tsx +31 -0
- package/template/packages/ui/src/table.tsx +116 -0
- package/template/packages/ui/src/tabs.tsx +66 -0
- package/template/packages/ui/src/textarea.tsx +18 -0
- package/template/packages/ui/src/tooltip.tsx +61 -0
- package/template/packages/ui/src/user-avatar.tsx +97 -0
- package/template/packages/ui/test-config.js +3 -0
- package/template/packages/ui/tsconfig.json +12 -0
- package/template/packages/ui/turbo.json +18 -0
- package/template/packages/ui/vitest.config.ts +17 -0
- package/template/packages/ui/vitest.setup.ts +1 -0
- package/template/packages/utils/package.json +23 -0
- package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
- package/template/packages/utils/src/array.ts +18 -0
- package/template/packages/utils/src/async.ts +3 -0
- package/template/packages/utils/src/date.ts +77 -0
- package/template/packages/utils/src/errors.ts +73 -0
- package/template/packages/utils/src/number.ts +11 -0
- package/template/packages/utils/src/string.ts +13 -0
- package/template/packages/utils/tsconfig.json +5 -0
- package/template/packages/utils/vitest.config.ts +21 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.base.json +32 -0
- package/template/turbo.json +133 -0
- package/template/vitest.shared.ts +26 -0
- 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
|
+
});
|