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,427 @@
|
|
|
1
|
+
import { UnauthorizedError } from '@repo/packages-utils/errors';
|
|
2
|
+
import { createMockLogger } from '@test/helpers/mock-logger';
|
|
3
|
+
import { getTestPrisma, resetTestDatabase } from '@test/helpers/test-db';
|
|
4
|
+
import * as bcrypt from 'bcryptjs';
|
|
5
|
+
import { ValidationError } from 'better-auth/client';
|
|
6
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import type { LoggerService } from '@/common/logger.service';
|
|
9
|
+
import { PasswordService } from '@/services/password.service';
|
|
10
|
+
import { SessionsService } from '@/services/sessions.service';
|
|
11
|
+
|
|
12
|
+
describe('Password Service Integration Tests', () => {
|
|
13
|
+
let passwordService: PasswordService;
|
|
14
|
+
let sessionsService: SessionsService;
|
|
15
|
+
let logger: LoggerService;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await resetTestDatabase();
|
|
19
|
+
|
|
20
|
+
logger = createMockLogger();
|
|
21
|
+
const prisma = getTestPrisma();
|
|
22
|
+
sessionsService = new SessionsService(prisma);
|
|
23
|
+
passwordService = new PasswordService(prisma, sessionsService);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('changePassword', () => {
|
|
27
|
+
it('should successfully change password with correct current password', async () => {
|
|
28
|
+
const prisma = getTestPrisma();
|
|
29
|
+
|
|
30
|
+
// Create user
|
|
31
|
+
const user = await prisma.user.create({
|
|
32
|
+
data: {
|
|
33
|
+
email: 'change@test.com',
|
|
34
|
+
name: 'Change Password User',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Create credential account
|
|
39
|
+
const oldPassword = 'OldPassword123!';
|
|
40
|
+
const hashedOldPassword = await bcrypt.hash(oldPassword, 10);
|
|
41
|
+
|
|
42
|
+
await prisma.account.create({
|
|
43
|
+
data: {
|
|
44
|
+
userId: user.id,
|
|
45
|
+
accountId: user.email,
|
|
46
|
+
providerId: 'credential',
|
|
47
|
+
password: hashedOldPassword,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Change password
|
|
52
|
+
const newPassword = 'NewPassword456!';
|
|
53
|
+
await passwordService.changePassword(user.id, oldPassword, newPassword);
|
|
54
|
+
|
|
55
|
+
// Verify new password is set
|
|
56
|
+
const account = await prisma.account.findFirst({
|
|
57
|
+
where: {
|
|
58
|
+
userId: user.id,
|
|
59
|
+
providerId: 'credential',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(account).toBeDefined();
|
|
64
|
+
expect(account?.password).toBeDefined();
|
|
65
|
+
|
|
66
|
+
const isNewPasswordValid = await bcrypt.compare(
|
|
67
|
+
newPassword,
|
|
68
|
+
account!.password!
|
|
69
|
+
);
|
|
70
|
+
const isOldPasswordValid = await bcrypt.compare(
|
|
71
|
+
oldPassword,
|
|
72
|
+
account!.password!
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(isNewPasswordValid).toBe(true);
|
|
76
|
+
expect(isOldPasswordValid).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should revoke all sessions after password change', async () => {
|
|
80
|
+
const prisma = getTestPrisma();
|
|
81
|
+
|
|
82
|
+
const user = await prisma.user.create({
|
|
83
|
+
data: {
|
|
84
|
+
email: 'revoke-sessions@test.com',
|
|
85
|
+
name: 'Revoke Sessions User',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const password = 'CurrentPassword123!';
|
|
90
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
91
|
+
|
|
92
|
+
await prisma.account.create({
|
|
93
|
+
data: {
|
|
94
|
+
userId: user.id,
|
|
95
|
+
accountId: user.email,
|
|
96
|
+
providerId: 'credential',
|
|
97
|
+
password: hashedPassword,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Create multiple sessions
|
|
102
|
+
await prisma.session.createMany({
|
|
103
|
+
data: [
|
|
104
|
+
{
|
|
105
|
+
userId: user.id,
|
|
106
|
+
token: 'session-1',
|
|
107
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
userId: user.id,
|
|
111
|
+
token: 'session-2',
|
|
112
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Change password
|
|
118
|
+
await passwordService.changePassword(
|
|
119
|
+
user.id,
|
|
120
|
+
password,
|
|
121
|
+
'NewPassword456!'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Verify all sessions are revoked
|
|
125
|
+
const sessions = await prisma.session.findMany({
|
|
126
|
+
where: { userId: user.id },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(sessions).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should throw UnauthorizedError with incorrect current password', async () => {
|
|
133
|
+
const prisma = getTestPrisma();
|
|
134
|
+
|
|
135
|
+
const user = await prisma.user.create({
|
|
136
|
+
data: {
|
|
137
|
+
email: 'wrong-password@test.com',
|
|
138
|
+
name: 'Wrong Password User',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const correctPassword = 'CorrectPassword123!';
|
|
143
|
+
const hashedPassword = await bcrypt.hash(correctPassword, 10);
|
|
144
|
+
|
|
145
|
+
await prisma.account.create({
|
|
146
|
+
data: {
|
|
147
|
+
userId: user.id,
|
|
148
|
+
accountId: user.email,
|
|
149
|
+
providerId: 'credential',
|
|
150
|
+
password: hashedPassword,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
passwordService.changePassword(
|
|
156
|
+
user.id,
|
|
157
|
+
'WrongPassword123!',
|
|
158
|
+
'NewPassword456!'
|
|
159
|
+
)
|
|
160
|
+
).rejects.toThrow(UnauthorizedError);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should throw ValidationError when user has no password account', async () => {
|
|
164
|
+
const prisma = getTestPrisma();
|
|
165
|
+
|
|
166
|
+
// Create user without credential account (e.g., OAuth user)
|
|
167
|
+
const user = await prisma.user.create({
|
|
168
|
+
data: {
|
|
169
|
+
email: 'oauth@test.com',
|
|
170
|
+
name: 'OAuth User',
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await expect(
|
|
175
|
+
passwordService.changePassword(
|
|
176
|
+
user.id,
|
|
177
|
+
'OldPassword123!',
|
|
178
|
+
'NewPassword456!'
|
|
179
|
+
)
|
|
180
|
+
).rejects.toThrow(ValidationError);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should throw ValidationError when account has no password', async () => {
|
|
184
|
+
const prisma = getTestPrisma();
|
|
185
|
+
|
|
186
|
+
const user = await prisma.user.create({
|
|
187
|
+
data: {
|
|
188
|
+
email: 'no-password@test.com',
|
|
189
|
+
name: 'No Password User',
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Create credential account without password
|
|
194
|
+
await prisma.account.create({
|
|
195
|
+
data: {
|
|
196
|
+
userId: user.id,
|
|
197
|
+
accountId: user.email,
|
|
198
|
+
providerId: 'credential',
|
|
199
|
+
password: null,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
passwordService.changePassword(
|
|
205
|
+
user.id,
|
|
206
|
+
'OldPassword123!',
|
|
207
|
+
'NewPassword456!'
|
|
208
|
+
)
|
|
209
|
+
).rejects.toThrow(ValidationError);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle bcrypt password hashing correctly', async () => {
|
|
213
|
+
const prisma = getTestPrisma();
|
|
214
|
+
|
|
215
|
+
const user = await prisma.user.create({
|
|
216
|
+
data: {
|
|
217
|
+
email: 'bcrypt@test.com',
|
|
218
|
+
name: 'Bcrypt User',
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const password = 'TestPassword123!';
|
|
223
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
224
|
+
|
|
225
|
+
await prisma.account.create({
|
|
226
|
+
data: {
|
|
227
|
+
userId: user.id,
|
|
228
|
+
accountId: user.email,
|
|
229
|
+
providerId: 'credential',
|
|
230
|
+
password: hashedPassword,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const newPassword = 'NewSecurePassword456!';
|
|
235
|
+
await passwordService.changePassword(user.id, password, newPassword);
|
|
236
|
+
|
|
237
|
+
const account = await prisma.account.findFirst({
|
|
238
|
+
where: {
|
|
239
|
+
userId: user.id,
|
|
240
|
+
providerId: 'credential',
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Verify password is hashed (bcrypt hashes start with $2a$ or $2b$)
|
|
245
|
+
expect(account?.password).toMatch(/^\$2[ab]\$/);
|
|
246
|
+
|
|
247
|
+
// Verify hash is unique (different from old hash)
|
|
248
|
+
expect(account?.password).not.toBe(hashedPassword);
|
|
249
|
+
|
|
250
|
+
// Verify new password works
|
|
251
|
+
const isValid = await bcrypt.compare(newPassword, account!.password!);
|
|
252
|
+
expect(isValid).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should work with special characters in password', async () => {
|
|
256
|
+
const prisma = getTestPrisma();
|
|
257
|
+
|
|
258
|
+
const user = await prisma.user.create({
|
|
259
|
+
data: {
|
|
260
|
+
email: 'special@test.com',
|
|
261
|
+
name: 'Special Chars User',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const oldPassword = 'Old!@#$%^&*()_+{}|:"<>?Pass123';
|
|
266
|
+
const hashedOldPassword = await bcrypt.hash(oldPassword, 10);
|
|
267
|
+
|
|
268
|
+
await prisma.account.create({
|
|
269
|
+
data: {
|
|
270
|
+
userId: user.id,
|
|
271
|
+
accountId: user.email,
|
|
272
|
+
providerId: 'credential',
|
|
273
|
+
password: hashedOldPassword,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const newPassword = 'New!@#$%^&*()_+{}|:"<>?Pass456';
|
|
278
|
+
await passwordService.changePassword(user.id, oldPassword, newPassword);
|
|
279
|
+
|
|
280
|
+
const account = await prisma.account.findFirst({
|
|
281
|
+
where: {
|
|
282
|
+
userId: user.id,
|
|
283
|
+
providerId: 'credential',
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const isValid = await bcrypt.compare(newPassword, account!.password!);
|
|
288
|
+
expect(isValid).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should maintain data consistency on concurrent password changes', async () => {
|
|
292
|
+
const prisma = getTestPrisma();
|
|
293
|
+
|
|
294
|
+
const user = await prisma.user.create({
|
|
295
|
+
data: {
|
|
296
|
+
email: 'concurrent@test.com',
|
|
297
|
+
name: 'Concurrent User',
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const password = 'OriginalPassword123!';
|
|
302
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
303
|
+
|
|
304
|
+
await prisma.account.create({
|
|
305
|
+
data: {
|
|
306
|
+
userId: user.id,
|
|
307
|
+
accountId: user.email,
|
|
308
|
+
providerId: 'credential',
|
|
309
|
+
password: hashedPassword,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// First password change
|
|
314
|
+
await passwordService.changePassword(
|
|
315
|
+
user.id,
|
|
316
|
+
password,
|
|
317
|
+
'FirstChange456!'
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Second password change (using new password)
|
|
321
|
+
await passwordService.changePassword(
|
|
322
|
+
user.id,
|
|
323
|
+
'FirstChange456!',
|
|
324
|
+
'SecondChange789!'
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const account = await prisma.account.findFirst({
|
|
328
|
+
where: {
|
|
329
|
+
userId: user.id,
|
|
330
|
+
providerId: 'credential',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Only the final password should work
|
|
335
|
+
const isOriginalValid = await bcrypt.compare(
|
|
336
|
+
password,
|
|
337
|
+
account!.password!
|
|
338
|
+
);
|
|
339
|
+
const isFirstChangeValid = await bcrypt.compare(
|
|
340
|
+
'FirstChange456!',
|
|
341
|
+
account!.password!
|
|
342
|
+
);
|
|
343
|
+
const isSecondChangeValid = await bcrypt.compare(
|
|
344
|
+
'SecondChange789!',
|
|
345
|
+
account!.password!
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(isOriginalValid).toBe(false);
|
|
349
|
+
expect(isFirstChangeValid).toBe(false);
|
|
350
|
+
expect(isSecondChangeValid).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('Database Constraints and Edge Cases', () => {
|
|
355
|
+
it('should handle non-existent user gracefully', async () => {
|
|
356
|
+
await expect(
|
|
357
|
+
passwordService.changePassword(
|
|
358
|
+
'non-existent-user-id',
|
|
359
|
+
'OldPassword123!',
|
|
360
|
+
'NewPassword456!'
|
|
361
|
+
)
|
|
362
|
+
).rejects.toThrow(ValidationError);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should handle user with multiple accounts correctly', async () => {
|
|
366
|
+
const prisma = getTestPrisma();
|
|
367
|
+
|
|
368
|
+
const user = await prisma.user.create({
|
|
369
|
+
data: {
|
|
370
|
+
email: 'multiple-accounts@test.com',
|
|
371
|
+
name: 'Multiple Accounts User',
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Create credential account
|
|
376
|
+
const password = 'Password123!';
|
|
377
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
378
|
+
|
|
379
|
+
await prisma.account.create({
|
|
380
|
+
data: {
|
|
381
|
+
userId: user.id,
|
|
382
|
+
accountId: user.email,
|
|
383
|
+
providerId: 'credential',
|
|
384
|
+
password: hashedPassword,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Create OAuth account (github)
|
|
389
|
+
await prisma.account.create({
|
|
390
|
+
data: {
|
|
391
|
+
userId: user.id,
|
|
392
|
+
accountId: 'github-123',
|
|
393
|
+
providerId: 'github',
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Should only update the credential account
|
|
398
|
+
await passwordService.changePassword(
|
|
399
|
+
user.id,
|
|
400
|
+
password,
|
|
401
|
+
'NewPassword456!'
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const credentialAccount = await prisma.account.findFirst({
|
|
405
|
+
where: {
|
|
406
|
+
userId: user.id,
|
|
407
|
+
providerId: 'credential',
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const githubAccount = await prisma.account.findFirst({
|
|
412
|
+
where: {
|
|
413
|
+
userId: user.id,
|
|
414
|
+
providerId: 'github',
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const isNewPasswordValid = await bcrypt.compare(
|
|
419
|
+
'NewPassword456!',
|
|
420
|
+
credentialAccount!.password!
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(isNewPasswordValid).toBe(true);
|
|
424
|
+
expect(githubAccount?.password).toBeNull();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getTestPrisma, resetTestDatabase } from '@test/helpers/test-db';
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { RATE_LIMIT_CONFIG } from '@/config/rate-limit';
|
|
5
|
+
|
|
6
|
+
describe('Rate Limiting Integration Tests', () => {
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
await resetTestDatabase();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should enforce anonymous rate limits', async () => {
|
|
12
|
+
const prisma = getTestPrisma();
|
|
13
|
+
|
|
14
|
+
// Anonymous rate limit is 30 req/min
|
|
15
|
+
expect(RATE_LIMIT_CONFIG.anonymous.max).toBe(30);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should enforce user rate limits', async () => {
|
|
19
|
+
const prisma = getTestPrisma();
|
|
20
|
+
|
|
21
|
+
// User rate limit is 60 req/min
|
|
22
|
+
expect(RATE_LIMIT_CONFIG.user.max).toBe(60);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should enforce admin rate limits', async () => {
|
|
26
|
+
const prisma = getTestPrisma();
|
|
27
|
+
|
|
28
|
+
// Admin rate limit is 200 req/min
|
|
29
|
+
expect(RATE_LIMIT_CONFIG.admin.max).toBe(200);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should have stricter auth route limits', async () => {
|
|
33
|
+
// Auth endpoints should have stricter limits (10 req/min)
|
|
34
|
+
expect(RATE_LIMIT_CONFIG.routes.auth.max).toBe(10);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have stricter upload route limits', async () => {
|
|
38
|
+
// Upload endpoints should have limits (20 req/min)
|
|
39
|
+
expect(RATE_LIMIT_CONFIG.routes.uploads.max).toBe(20);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should have consistent time windows', () => {
|
|
43
|
+
const oneMinute = 60 * 1000;
|
|
44
|
+
|
|
45
|
+
expect(RATE_LIMIT_CONFIG.anonymous.timeWindow).toBe(oneMinute);
|
|
46
|
+
expect(RATE_LIMIT_CONFIG.user.timeWindow).toBe(oneMinute);
|
|
47
|
+
expect(RATE_LIMIT_CONFIG.admin.timeWindow).toBe(oneMinute);
|
|
48
|
+
expect(RATE_LIMIT_CONFIG.routes.auth.timeWindow).toBe(oneMinute);
|
|
49
|
+
expect(RATE_LIMIT_CONFIG.routes.uploads.timeWindow).toBe(oneMinute);
|
|
50
|
+
});
|
|
51
|
+
});
|