create-tigra 1.1.0 → 2.0.1
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/LICENSE +21 -21
- package/README.md +80 -87
- package/bin/create-tigra.js +259 -308
- package/package.json +49 -41
- package/template/_claude/QUICK_REFERENCE.md +193 -0
- package/template/_claude/README.md +53 -0
- package/template/_claude/commands/create-client.md +881 -0
- package/template/_claude/commands/create-server.md +383 -0
- package/template/_claude/rules/client/01-project-structure.md +133 -0
- package/template/_claude/rules/client/02-components-and-types.md +146 -0
- package/template/_claude/rules/client/03-data-and-state.md +156 -0
- package/template/_claude/rules/client/04-design-system.md +185 -0
- package/template/_claude/rules/client/05-security.md +55 -0
- package/template/_claude/rules/client/06-ux-checklist.md +81 -0
- package/template/_claude/rules/client/core.md +42 -0
- package/template/_claude/rules/global/core.md +77 -0
- package/template/_claude/rules/server/core.md +50 -0
- package/template/_claude/rules/server/database.md +124 -0
- package/template/_claude/rules/server/project-conventions.md +150 -0
- package/template/_claude/rules/server/response-handling.md +144 -0
- package/template/client/.env.example +5 -0
- package/template/client/README.md +36 -0
- package/template/client/components.json +23 -0
- package/template/client/eslint.config.mjs +18 -0
- package/template/client/next.config.ts +34 -0
- package/template/client/package.json +44 -0
- package/template/client/postcss.config.mjs +7 -0
- package/template/client/src/app/(auth)/layout.tsx +18 -0
- package/template/client/src/app/(auth)/login/page.tsx +13 -0
- package/template/client/src/app/(auth)/register/page.tsx +13 -0
- package/template/client/src/app/(main)/dashboard/page.tsx +22 -0
- package/template/client/src/app/(main)/layout.tsx +11 -0
- package/template/client/src/app/error.tsx +27 -0
- package/template/client/src/app/favicon.ico +0 -0
- package/template/client/src/app/globals.css +145 -0
- package/template/client/src/app/layout.tsx +36 -0
- package/template/client/src/app/loading.tsx +11 -0
- package/template/client/src/app/not-found.tsx +23 -0
- package/template/client/src/app/page.tsx +45 -0
- package/template/client/src/app/providers.tsx +43 -0
- package/template/client/src/components/common/ConfirmDialog.tsx +56 -0
- package/template/client/src/components/common/EmptyState.tsx +31 -0
- package/template/client/src/components/common/LoadingSpinner.tsx +30 -0
- package/template/client/src/components/common/Pagination.tsx +55 -0
- package/template/client/src/components/layout/Footer.tsx +17 -0
- package/template/client/src/components/layout/Header.tsx +173 -0
- package/template/client/src/components/layout/MainLayout.tsx +18 -0
- package/template/client/src/components/ui/alert-dialog.tsx +196 -0
- package/template/client/src/components/ui/badge.tsx +48 -0
- package/template/client/src/components/ui/button.tsx +64 -0
- package/template/client/src/components/ui/card.tsx +92 -0
- package/template/client/src/components/ui/input.tsx +21 -0
- package/template/client/src/components/ui/label.tsx +24 -0
- package/template/client/src/components/ui/select.tsx +190 -0
- package/template/client/src/components/ui/skeleton.tsx +13 -0
- package/template/client/src/components/ui/table.tsx +116 -0
- package/template/client/src/features/auth/components/AuthInitializer.tsx +55 -0
- package/template/client/src/features/auth/components/LoginForm.tsx +107 -0
- package/template/client/src/features/auth/components/RegisterForm.tsx +178 -0
- package/template/client/src/features/auth/hooks/useAuth.ts +84 -0
- package/template/client/src/features/auth/services/auth.service.ts +52 -0
- package/template/client/src/features/auth/store/authSlice.ts +38 -0
- package/template/client/src/features/auth/types/auth.types.ts +32 -0
- package/template/client/src/hooks/useDebounce.ts +14 -0
- package/template/client/src/hooks/useLocalStorage.ts +55 -0
- package/template/client/src/hooks/useMediaQuery.ts +27 -0
- package/template/client/src/lib/api/api.types.ts +34 -0
- package/template/client/src/lib/api/axios.config.ts +98 -0
- package/template/client/src/lib/constants/api-endpoints.ts +18 -0
- package/template/client/src/lib/constants/app.constants.ts +12 -0
- package/template/client/src/lib/constants/routes.ts +9 -0
- package/template/client/src/lib/utils/error.ts +32 -0
- package/template/client/src/lib/utils/format.ts +37 -0
- package/template/client/src/lib/utils/security.ts +34 -0
- package/template/client/src/lib/utils.ts +6 -0
- package/template/client/src/middleware.ts +57 -0
- package/template/client/src/store/hooks.ts +7 -0
- package/template/client/src/store/index.ts +12 -0
- package/template/client/src/types/index.ts +3 -0
- package/template/client/tsconfig.json +34 -0
- package/template/gitignore +34 -0
- package/template/server/.dockerignore +66 -0
- package/template/server/.env.example +96 -69
- package/template/server/.env.production.example +90 -0
- package/template/server/Dockerfile +94 -0
- package/template/server/docker-compose.yml +82 -111
- package/template/server/docs/logging.md +62 -0
- package/template/server/eslint.config.mjs +17 -0
- package/template/server/package.json +68 -81
- package/template/server/phpmyadmin-config.php +26 -0
- package/template/server/postman_collection.json +666 -0
- package/template/server/prisma/schema.prisma +77 -93
- package/template/server/prisma/seed.ts +46 -142
- package/template/server/scripts/flush-redis.ts +41 -0
- package/template/server/src/app.ts +243 -71
- package/template/server/src/config/env.ts +67 -94
- package/template/server/src/libs/auth.ts +88 -0
- package/template/server/src/libs/cleanup.ts +35 -0
- package/template/server/src/libs/cookies.ts +46 -0
- package/template/server/src/libs/logger.ts +33 -60
- package/template/server/src/libs/monitoring.ts +205 -0
- package/template/server/src/libs/password.ts +38 -0
- package/template/server/src/libs/prisma.ts +68 -0
- package/template/server/src/libs/redis.ts +60 -79
- package/template/server/src/libs/requestLogger.ts +66 -0
- package/template/server/src/libs/storage/file-storage.service.ts +211 -0
- package/template/server/src/libs/storage/file-validator.ts +97 -0
- package/template/server/src/libs/storage/filename-sanitizer.ts +71 -0
- package/template/server/src/libs/storage/image-optimizer.service.ts +144 -0
- package/template/server/src/modules/auth/__tests__/auth.service.test.ts +365 -0
- package/template/server/src/modules/auth/auth.controller.ts +90 -141
- package/template/server/src/modules/auth/auth.repo.ts +120 -218
- package/template/server/src/modules/auth/auth.routes.ts +96 -83
- package/template/server/src/modules/auth/auth.schemas.ts +35 -137
- package/template/server/src/modules/auth/auth.service.ts +286 -329
- package/template/server/src/modules/auth/session.repo.ts +110 -0
- package/template/server/src/modules/users/users.controller.ts +120 -0
- package/template/server/src/modules/users/users.repo.ts +77 -0
- package/template/server/src/modules/users/users.routes.ts +89 -0
- package/template/server/src/modules/users/users.schemas.ts +21 -0
- package/template/server/src/modules/users/users.service.ts +169 -0
- package/template/server/src/server.ts +58 -139
- package/template/server/src/shared/errors/AppError.ts +21 -0
- package/template/server/src/shared/errors/errors.ts +43 -0
- package/template/server/src/shared/responses/paginatedResponse.ts +38 -0
- package/template/server/src/shared/responses/successResponse.ts +17 -0
- package/template/server/src/shared/schemas/pagination.schema.ts +12 -0
- package/template/server/src/shared/types/index.ts +26 -0
- package/template/server/src/test/setup.ts +74 -38
- package/template/server/tsconfig.json +27 -89
- package/template/server/uploads/avatars/.gitkeep +1 -0
- package/template/server/vitest.config.ts +43 -98
- package/template/.agent/rules/client/01-project-structure.md +0 -326
- package/template/.agent/rules/client/02-component-patterns.md +0 -249
- package/template/.agent/rules/client/03-typescript-rules.md +0 -226
- package/template/.agent/rules/client/04-state-management.md +0 -474
- package/template/.agent/rules/client/05-api-integration.md +0 -129
- package/template/.agent/rules/client/06-forms-validation.md +0 -129
- package/template/.agent/rules/client/07-common-patterns.md +0 -150
- package/template/.agent/rules/client/08-color-system.md +0 -93
- package/template/.agent/rules/client/09-security-rules.md +0 -97
- package/template/.agent/rules/client/10-testing-strategy.md +0 -370
- package/template/.agent/rules/global/ai-edit-safety.md +0 -38
- package/template/.agent/rules/server/01-db-and-migrations.md +0 -242
- package/template/.agent/rules/server/02-general-rules.md +0 -111
- package/template/.agent/rules/server/03-migrations.md +0 -20
- package/template/.agent/rules/server/04-pagination.md +0 -130
- package/template/.agent/rules/server/05-project-conventions.md +0 -71
- package/template/.agent/rules/server/06-response-handling.md +0 -173
- package/template/.agent/rules/server/07-testing-strategy.md +0 -506
- package/template/.agent/rules/server/08-observability.md +0 -180
- package/template/.agent/rules/server/10-background-jobs-v2.md +0 -185
- package/template/.agent/rules/server/11-rate-limiting-v2.md +0 -210
- package/template/.agent/rules/server/12-performance-optimization.md +0 -567
- package/template/.claude/rules/client-01-project-structure.md +0 -327
- package/template/.claude/rules/client-02-component-patterns.md +0 -250
- package/template/.claude/rules/client-03-typescript-rules.md +0 -227
- package/template/.claude/rules/client-04-state-management.md +0 -475
- package/template/.claude/rules/client-05-api-integration.md +0 -130
- package/template/.claude/rules/client-06-forms-validation.md +0 -130
- package/template/.claude/rules/client-07-common-patterns.md +0 -151
- package/template/.claude/rules/client-08-color-system.md +0 -94
- package/template/.claude/rules/client-09-security-rules.md +0 -98
- package/template/.claude/rules/client-10-testing-strategy.md +0 -371
- package/template/.claude/rules/global-ai-edit-safety.md +0 -39
- package/template/.claude/rules/server-01-db-and-migrations.md +0 -243
- package/template/.claude/rules/server-02-general-rules.md +0 -112
- package/template/.claude/rules/server-03-migrations.md +0 -21
- package/template/.claude/rules/server-04-pagination.md +0 -131
- package/template/.claude/rules/server-05-project-conventions.md +0 -72
- package/template/.claude/rules/server-06-response-handling.md +0 -174
- package/template/.claude/rules/server-07-testing-strategy.md +0 -507
- package/template/.claude/rules/server-08-observability.md +0 -181
- package/template/.claude/rules/server-10-background-jobs-v2.md +0 -186
- package/template/.claude/rules/server-11-rate-limiting-v2.md +0 -211
- package/template/.claude/rules/server-12-performance-optimization.md +0 -568
- package/template/.cursor/rules/client-01-project-structure.mdc +0 -327
- package/template/.cursor/rules/client-02-component-patterns.mdc +0 -250
- package/template/.cursor/rules/client-03-typescript-rules.mdc +0 -227
- package/template/.cursor/rules/client-04-state-management.mdc +0 -475
- package/template/.cursor/rules/client-05-api-integration.mdc +0 -130
- package/template/.cursor/rules/client-06-forms-validation.mdc +0 -130
- package/template/.cursor/rules/client-07-common-patterns.mdc +0 -151
- package/template/.cursor/rules/client-08-color-system.mdc +0 -94
- package/template/.cursor/rules/client-09-security-rules.mdc +0 -98
- package/template/.cursor/rules/client-10-testing-strategy.mdc +0 -371
- package/template/.cursor/rules/global-ai-edit-safety.mdc +0 -39
- package/template/.cursor/rules/server-01-db-and-migrations.mdc +0 -243
- package/template/.cursor/rules/server-02-general-rules.mdc +0 -112
- package/template/.cursor/rules/server-03-migrations.mdc +0 -21
- package/template/.cursor/rules/server-04-pagination.mdc +0 -131
- package/template/.cursor/rules/server-05-project-conventions.mdc +0 -72
- package/template/.cursor/rules/server-06-response-handling.mdc +0 -174
- package/template/.cursor/rules/server-07-testing-strategy.mdc +0 -507
- package/template/.cursor/rules/server-08-observability.mdc +0 -181
- package/template/.cursor/rules/server-09-api-documentation-v2.mdc +0 -169
- package/template/.cursor/rules/server-10-background-jobs-v2.mdc +0 -186
- package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +0 -211
- package/template/.cursor/rules/server-12-performance-optimization.mdc +0 -568
- package/template/CLAUDE.md +0 -207
- package/template/server/.tsc-aliasrc.json +0 -13
- package/template/server/IMPORT_FIX_CHECKLIST.md +0 -98
- package/template/server/IMPORT_FIX_COMPLETE.md +0 -89
- package/template/server/README.md +0 -183
- package/template/server/REMAINING_IMPORT_FIXES.md +0 -150
- package/template/server/SECURITY.md +0 -190
- package/template/server/Tigra-API.postman_collection.json +0 -733
- package/template/server/biome.json +0 -42
- package/template/server/scripts/fix-all-imports.ps1 +0 -52
- package/template/server/scripts/fix-imports-reference.ps1 +0 -16
- package/template/server/scripts/fix-imports.mjs +0 -55
- package/template/server/scripts/setup-env.js +0 -50
- package/template/server/scripts/wait-for-db.js +0 -60
- package/template/server/src/hooks/request-timing.hook.ts +0 -26
- package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
- package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
- package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
- package/template/server/src/libs/db.ts +0 -76
- package/template/server/src/libs/error-handler.ts +0 -89
- package/template/server/src/libs/queue.ts +0 -79
- package/template/server/src/modules/admin/admin.controller.ts +0 -122
- package/template/server/src/modules/admin/admin.routes.ts +0 -62
- package/template/server/src/modules/admin/admin.schemas.ts +0 -35
- package/template/server/src/modules/admin/admin.service.ts +0 -167
- package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
- package/template/server/src/modules/auth/auth.service.test.ts +0 -119
- package/template/server/src/modules/auth/auth.types.ts +0 -97
- package/template/server/src/modules/resources/resources.controller.ts +0 -218
- package/template/server/src/modules/resources/resources.repo.ts +0 -253
- package/template/server/src/modules/resources/resources.routes.ts +0 -116
- package/template/server/src/modules/resources/resources.schemas.ts +0 -146
- package/template/server/src/modules/resources/resources.service.ts +0 -218
- package/template/server/src/modules/resources/resources.types.ts +0 -73
- package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
- package/template/server/src/plugins/security.plugin.ts +0 -21
- package/template/server/src/routes/health.routes.ts +0 -31
- package/template/server/src/types/fastify.d.ts +0 -36
- package/template/server/src/utils/errors.ts +0 -108
- package/template/server/src/utils/pagination.ts +0 -120
- package/template/server/src/utils/response.ts +0 -110
- package/template/server/src/workers/file.worker.ts +0 -106
- package/template/server/tsconfig.build.json +0 -30
- package/template/server/tsconfig.test.json +0 -22
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import * as authService from './auth.service';
|
|
3
|
-
import * as authRepo from './auth.repo';
|
|
4
|
-
import bcrypt from 'bcryptjs';
|
|
5
|
-
import jwt from 'jsonwebtoken';
|
|
6
|
-
import { env } from '../../config/env.js';
|
|
7
|
-
import { ConflictError, UnauthorizedError } from '../../utils/errors.js';
|
|
8
|
-
|
|
9
|
-
// Mock dependencies
|
|
10
|
-
vi.mock('./auth.repo');
|
|
11
|
-
vi.mock('bcryptjs');
|
|
12
|
-
vi.mock('jsonwebtoken');
|
|
13
|
-
vi.mock('@/libs/logger', () => ({
|
|
14
|
-
default: {
|
|
15
|
-
info: vi.fn(),
|
|
16
|
-
error: vi.fn(),
|
|
17
|
-
warn: vi.fn(),
|
|
18
|
-
},
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
describe('Auth Service', () => {
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
vi.resetAllMocks();
|
|
24
|
-
// Setup default env mocks if needed
|
|
25
|
-
env.JWT_SECRET = 'test-secret';
|
|
26
|
-
env.JWT_ACCESS_EXPIRATION = '15m';
|
|
27
|
-
env.JWT_REFRESH_EXPIRATION = '7d';
|
|
28
|
-
env.JWT_ISSUER = 'test-issuer';
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('register', () => {
|
|
32
|
-
const registerData = {
|
|
33
|
-
email: 'test@example.com',
|
|
34
|
-
password: 'Password123!',
|
|
35
|
-
name: 'Test User',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
it('should successfully register a new user', async () => {
|
|
39
|
-
// Mocks
|
|
40
|
-
vi.mocked(authRepo.findUserByEmail).mockResolvedValue(null);
|
|
41
|
-
vi.mocked(bcrypt.hash).mockResolvedValue('hashed_password' as any);
|
|
42
|
-
vi.mocked(authRepo.createUser).mockResolvedValue({
|
|
43
|
-
id: 'user-123',
|
|
44
|
-
email: registerData.email,
|
|
45
|
-
name: registerData.name,
|
|
46
|
-
role: 'USER',
|
|
47
|
-
emailVerified: false,
|
|
48
|
-
createdAt: new Date(),
|
|
49
|
-
updatedAt: new Date(),
|
|
50
|
-
});
|
|
51
|
-
vi.mocked(jwt.sign).mockReturnValue('mock_token' as any);
|
|
52
|
-
vi.mocked(authRepo.createSession).mockResolvedValue({} as any);
|
|
53
|
-
|
|
54
|
-
// Execute
|
|
55
|
-
const result = await authService.register(registerData);
|
|
56
|
-
|
|
57
|
-
// Assert
|
|
58
|
-
expect(result).toBeDefined();
|
|
59
|
-
expect(result.user.email).toBe(registerData.email);
|
|
60
|
-
expect(result.tokens.accessToken).toBe('mock_token');
|
|
61
|
-
expect(authRepo.createUser).toHaveBeenCalled();
|
|
62
|
-
expect(authRepo.createSession).toHaveBeenCalled();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should throw ConflictError if email already exists', async () => {
|
|
66
|
-
// Mocks
|
|
67
|
-
vi.mocked(authRepo.findUserByEmail).mockResolvedValue({ id: 'existing' } as any);
|
|
68
|
-
|
|
69
|
-
// Execute & Assert
|
|
70
|
-
await expect(authService.register(registerData))
|
|
71
|
-
.rejects
|
|
72
|
-
.toThrow(ConflictError);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('login', () => {
|
|
77
|
-
const loginData = {
|
|
78
|
-
email: 'test@example.com',
|
|
79
|
-
password: 'Password123!',
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
it('should successfully login user', async () => {
|
|
83
|
-
// Mocks
|
|
84
|
-
vi.mocked(authRepo.findUserByEmailWithPassword).mockResolvedValue({
|
|
85
|
-
id: 'user-123',
|
|
86
|
-
email: loginData.email,
|
|
87
|
-
password: 'hashed_password', // Match what bcrypt.compare expects
|
|
88
|
-
role: 'USER',
|
|
89
|
-
} as any);
|
|
90
|
-
vi.mocked(bcrypt.compare).mockResolvedValue(true as any);
|
|
91
|
-
vi.mocked(authRepo.findUserById).mockResolvedValue({
|
|
92
|
-
id: 'user-123',
|
|
93
|
-
email: loginData.email,
|
|
94
|
-
role: 'USER',
|
|
95
|
-
} as any);
|
|
96
|
-
vi.mocked(jwt.sign).mockReturnValue('mock_token' as any);
|
|
97
|
-
vi.mocked(authRepo.createSession).mockResolvedValue({} as any);
|
|
98
|
-
|
|
99
|
-
const result = await authService.login(loginData);
|
|
100
|
-
|
|
101
|
-
expect(result).toBeDefined();
|
|
102
|
-
expect(result.user.id).toBe('user-123');
|
|
103
|
-
expect(result.tokens.accessToken).toBeDefined();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should throw UnauthorizedError on invalid password', async () => {
|
|
107
|
-
// Mocks
|
|
108
|
-
vi.mocked(authRepo.findUserByEmailWithPassword).mockResolvedValue({
|
|
109
|
-
id: 'user-123',
|
|
110
|
-
password: 'hashed_password',
|
|
111
|
-
} as any);
|
|
112
|
-
vi.mocked(bcrypt.compare).mockResolvedValue(false as any);
|
|
113
|
-
|
|
114
|
-
await expect(authService.login(loginData))
|
|
115
|
-
.rejects
|
|
116
|
-
.toThrow(UnauthorizedError);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authentication Types
|
|
3
|
-
*
|
|
4
|
-
* TypeScript types and interfaces for authentication module.
|
|
5
|
-
* These types are used throughout the auth flow.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* User Role Enum
|
|
10
|
-
*/
|
|
11
|
-
export type UserRole = 'USER' | 'ADMIN';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* User object (without sensitive fields like password)
|
|
15
|
-
*
|
|
16
|
-
* This is the safe user representation returned to clients.
|
|
17
|
-
*/
|
|
18
|
-
export interface User {
|
|
19
|
-
id: string;
|
|
20
|
-
email: string;
|
|
21
|
-
name: string | null;
|
|
22
|
-
role: UserRole;
|
|
23
|
-
emailVerified: boolean;
|
|
24
|
-
createdAt: Date;
|
|
25
|
-
updatedAt: Date;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Login Request Payload
|
|
30
|
-
*/
|
|
31
|
-
export interface LoginRequest {
|
|
32
|
-
email: string;
|
|
33
|
-
password: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Register Request Payload
|
|
38
|
-
*/
|
|
39
|
-
export interface RegisterRequest {
|
|
40
|
-
email: string;
|
|
41
|
-
password: string;
|
|
42
|
-
name: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Token Response
|
|
47
|
-
*
|
|
48
|
-
* Contains JWT tokens and expiration info.
|
|
49
|
-
*/
|
|
50
|
-
export interface TokenResponse {
|
|
51
|
-
accessToken: string;
|
|
52
|
-
refreshToken: string;
|
|
53
|
-
expiresIn: number; // Seconds until access token expires
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Authentication Response
|
|
58
|
-
*
|
|
59
|
-
* Complete response after successful login/register.
|
|
60
|
-
* Contains user info and tokens.
|
|
61
|
-
*/
|
|
62
|
-
export interface AuthResponse {
|
|
63
|
-
user: User;
|
|
64
|
-
tokens: TokenResponse;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Refresh Token Request Payload
|
|
69
|
-
*/
|
|
70
|
-
export interface RefreshTokenRequest {
|
|
71
|
-
refreshToken: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* JWT Payload
|
|
76
|
-
*
|
|
77
|
-
* Data stored inside the JWT access token.
|
|
78
|
-
*/
|
|
79
|
-
export interface JwtPayload {
|
|
80
|
-
userId: string;
|
|
81
|
-
email: string;
|
|
82
|
-
role: UserRole;
|
|
83
|
-
iat?: number; // Issued at
|
|
84
|
-
exp?: number; // Expiration
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* JWT Refresh Payload
|
|
89
|
-
*
|
|
90
|
-
* Data stored inside the JWT refresh token.
|
|
91
|
-
*/
|
|
92
|
-
export interface JwtRefreshPayload {
|
|
93
|
-
userId: string;
|
|
94
|
-
sessionId: string;
|
|
95
|
-
iat?: number;
|
|
96
|
-
exp?: number;
|
|
97
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resources Controller
|
|
3
|
-
*
|
|
4
|
-
* HTTP request handlers for resources endpoints.
|
|
5
|
-
* Controllers should ONLY handle HTTP concerns, no business logic.
|
|
6
|
-
*
|
|
7
|
-
* @see /mnt/project/02-general-rules.md
|
|
8
|
-
* @see /mnt/project/04-pagination.md
|
|
9
|
-
* @see /mnt/project/06-response-handling.md
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
13
|
-
import { successResponse } from '../../utils/response.js';
|
|
14
|
-
import { paginatedResponse } from '../../utils/pagination.js';
|
|
15
|
-
import * as resourceService from './resources.service.js';
|
|
16
|
-
import {
|
|
17
|
-
CreateResourceSchema,
|
|
18
|
-
UpdateResourceSchema,
|
|
19
|
-
ResourceFiltersSchema,
|
|
20
|
-
PaginationSchema,
|
|
21
|
-
} from './resources.schemas.js';
|
|
22
|
-
import type {
|
|
23
|
-
CreateResourceInput,
|
|
24
|
-
UpdateResourceInput,
|
|
25
|
-
ResourceFiltersInput,
|
|
26
|
-
PaginationInput,
|
|
27
|
-
} from './resources.schemas.js';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* List resources with filters and pagination
|
|
31
|
-
*
|
|
32
|
-
* @route GET /resources
|
|
33
|
-
* @access Public
|
|
34
|
-
*/
|
|
35
|
-
export async function listResources(
|
|
36
|
-
request: FastifyRequest<{
|
|
37
|
-
Querystring: ResourceFiltersInput & PaginationInput;
|
|
38
|
-
}>,
|
|
39
|
-
reply: FastifyReply
|
|
40
|
-
): Promise<FastifyReply> {
|
|
41
|
-
// Parse and validate query parameters
|
|
42
|
-
const filters = ResourceFiltersSchema.parse({
|
|
43
|
-
status: request.query.status,
|
|
44
|
-
minPrice: request.query.minPrice,
|
|
45
|
-
maxPrice: request.query.maxPrice,
|
|
46
|
-
ownerId: request.query.ownerId,
|
|
47
|
-
search: request.query.search,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const pagination = PaginationSchema.parse({
|
|
51
|
-
page: request.query.page,
|
|
52
|
-
limit: request.query.limit,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Call service
|
|
56
|
-
const { items, totalItems } = await resourceService.getResources(
|
|
57
|
-
filters,
|
|
58
|
-
pagination.page,
|
|
59
|
-
pagination.limit
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// Return paginated response (EXACT format from 04-pagination.md)
|
|
63
|
-
return reply.status(200).send(
|
|
64
|
-
paginatedResponse(
|
|
65
|
-
'Resources retrieved successfully',
|
|
66
|
-
items,
|
|
67
|
-
pagination.page,
|
|
68
|
-
pagination.limit,
|
|
69
|
-
totalItems
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get resource by ID
|
|
76
|
-
*
|
|
77
|
-
* @route GET /resources/:id
|
|
78
|
-
* @access Public
|
|
79
|
-
*/
|
|
80
|
-
export async function getResource(
|
|
81
|
-
request: FastifyRequest<{
|
|
82
|
-
Params: { id: string };
|
|
83
|
-
}>,
|
|
84
|
-
reply: FastifyReply
|
|
85
|
-
): Promise<FastifyReply> {
|
|
86
|
-
const { id } = request.params;
|
|
87
|
-
|
|
88
|
-
// Call service
|
|
89
|
-
const resource = await resourceService.getResource(id);
|
|
90
|
-
|
|
91
|
-
// Return success response
|
|
92
|
-
return reply.status(200).send(
|
|
93
|
-
successResponse('Resource retrieved successfully', resource)
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Create a new resource
|
|
99
|
-
*
|
|
100
|
-
* @route POST /resources
|
|
101
|
-
* @access Private (requires authentication)
|
|
102
|
-
*/
|
|
103
|
-
export async function createResource(
|
|
104
|
-
request: FastifyRequest<{
|
|
105
|
-
Body: CreateResourceInput;
|
|
106
|
-
}>,
|
|
107
|
-
reply: FastifyReply
|
|
108
|
-
): Promise<FastifyReply> {
|
|
109
|
-
// Validate request body
|
|
110
|
-
const body = CreateResourceSchema.parse(request.body);
|
|
111
|
-
|
|
112
|
-
// Get user ID from authenticated request
|
|
113
|
-
const userId = (request.user as any)?.userId;
|
|
114
|
-
|
|
115
|
-
// Call service
|
|
116
|
-
const resource = await resourceService.createResource(body, userId);
|
|
117
|
-
|
|
118
|
-
// Return success response
|
|
119
|
-
return reply.status(201).send(
|
|
120
|
-
successResponse('Resource created successfully', resource)
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Update a resource
|
|
126
|
-
*
|
|
127
|
-
* @route PATCH /resources/:id
|
|
128
|
-
* @access Private (requires authentication and ownership)
|
|
129
|
-
*/
|
|
130
|
-
export async function updateResource(
|
|
131
|
-
request: FastifyRequest<{
|
|
132
|
-
Params: { id: string };
|
|
133
|
-
Body: UpdateResourceInput;
|
|
134
|
-
}>,
|
|
135
|
-
reply: FastifyReply
|
|
136
|
-
): Promise<FastifyReply> {
|
|
137
|
-
const { id } = request.params;
|
|
138
|
-
|
|
139
|
-
// Validate request body
|
|
140
|
-
const body = UpdateResourceSchema.parse(request.body);
|
|
141
|
-
|
|
142
|
-
// Get user ID from authenticated request
|
|
143
|
-
const userId = (request.user as any)?.userId;
|
|
144
|
-
|
|
145
|
-
// Call service (includes ownership verification)
|
|
146
|
-
const resource = await resourceService.updateResource(id, userId, body);
|
|
147
|
-
|
|
148
|
-
// Return success response
|
|
149
|
-
return reply.status(200).send(
|
|
150
|
-
successResponse('Resource updated successfully', resource)
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Delete a resource
|
|
156
|
-
*
|
|
157
|
-
* @route DELETE /resources/:id
|
|
158
|
-
* @access Private (requires authentication and ownership)
|
|
159
|
-
*/
|
|
160
|
-
export async function deleteResource(
|
|
161
|
-
request: FastifyRequest<{
|
|
162
|
-
Params: { id: string };
|
|
163
|
-
}>,
|
|
164
|
-
reply: FastifyReply
|
|
165
|
-
): Promise<FastifyReply> {
|
|
166
|
-
const { id } = request.params;
|
|
167
|
-
|
|
168
|
-
// Get user ID from authenticated request
|
|
169
|
-
const userId = (request.user as any)?.userId;
|
|
170
|
-
|
|
171
|
-
// Call service (includes ownership verification)
|
|
172
|
-
const resource = await resourceService.deleteResource(id, userId);
|
|
173
|
-
|
|
174
|
-
// Return success response
|
|
175
|
-
return reply.status(200).send(
|
|
176
|
-
successResponse('Resource deleted successfully', resource)
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Get current user's resources
|
|
182
|
-
*
|
|
183
|
-
* @route GET /resources/my
|
|
184
|
-
* @access Private (requires authentication)
|
|
185
|
-
*/
|
|
186
|
-
export async function getMyResources(
|
|
187
|
-
request: FastifyRequest<{
|
|
188
|
-
Querystring: PaginationInput;
|
|
189
|
-
}>,
|
|
190
|
-
reply: FastifyReply
|
|
191
|
-
): Promise<FastifyReply> {
|
|
192
|
-
// Parse pagination parameters
|
|
193
|
-
const pagination = PaginationSchema.parse({
|
|
194
|
-
page: request.query.page,
|
|
195
|
-
limit: request.query.limit,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Get user ID from authenticated request
|
|
199
|
-
const userId = (request.user as any)?.userId;
|
|
200
|
-
|
|
201
|
-
// Call service
|
|
202
|
-
const { items, totalItems } = await resourceService.getMyResources(
|
|
203
|
-
userId,
|
|
204
|
-
pagination.page,
|
|
205
|
-
pagination.limit
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
// Return paginated response (EXACT format from 04-pagination.md)
|
|
209
|
-
return reply.status(200).send(
|
|
210
|
-
paginatedResponse(
|
|
211
|
-
'Your resources retrieved successfully',
|
|
212
|
-
items,
|
|
213
|
-
pagination.page,
|
|
214
|
-
pagination.limit,
|
|
215
|
-
totalItems
|
|
216
|
-
)
|
|
217
|
-
);
|
|
218
|
-
}
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resources Repository
|
|
3
|
-
*
|
|
4
|
-
* Database operations for resources module.
|
|
5
|
-
* Handles all Prisma queries related to resources.
|
|
6
|
-
*
|
|
7
|
-
* @see /mnt/project/02-general-rules.md
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { prisma } from '../../libs/db.js';
|
|
11
|
-
import type { Resource as PrismaResource } from '@prisma/client';
|
|
12
|
-
import type { ResourceFilters, Resource, ResourceWithOwner } from './resources.types.js';
|
|
13
|
-
import type { CreateResourceInput, UpdateResourceInput } from './resources.schemas.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Find many resources with filters and pagination
|
|
17
|
-
*
|
|
18
|
-
* @param filters - Filter criteria
|
|
19
|
-
* @param page - Page number (1-indexed)
|
|
20
|
-
* @param limit - Items per page
|
|
21
|
-
* @returns Array of resources
|
|
22
|
-
*/
|
|
23
|
-
export async function findMany(
|
|
24
|
-
filters: ResourceFilters,
|
|
25
|
-
page: number,
|
|
26
|
-
limit: number
|
|
27
|
-
): Promise<Resource[]> {
|
|
28
|
-
// Build where clause
|
|
29
|
-
const where: any = {};
|
|
30
|
-
|
|
31
|
-
// Status filter
|
|
32
|
-
if (filters.status) {
|
|
33
|
-
where.status = filters.status;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Price range filters
|
|
37
|
-
if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
|
|
38
|
-
where.price = {};
|
|
39
|
-
if (filters.minPrice !== undefined) {
|
|
40
|
-
where.price.gte = filters.minPrice;
|
|
41
|
-
}
|
|
42
|
-
if (filters.maxPrice !== undefined) {
|
|
43
|
-
where.price.lte = filters.maxPrice;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Owner filter
|
|
48
|
-
if (filters.ownerId) {
|
|
49
|
-
where.ownerId = filters.ownerId;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Search filter (title contains)
|
|
53
|
-
if (filters.search) {
|
|
54
|
-
where.title = {
|
|
55
|
-
contains: filters.search,
|
|
56
|
-
mode: 'insensitive', // Case-insensitive search
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Calculate offset
|
|
61
|
-
const skip = (page - 1) * limit;
|
|
62
|
-
|
|
63
|
-
// Execute query
|
|
64
|
-
const resources = await prisma.resource.findMany({
|
|
65
|
-
where,
|
|
66
|
-
skip,
|
|
67
|
-
take: limit,
|
|
68
|
-
orderBy: {
|
|
69
|
-
createdAt: 'desc',
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return resources;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Count resources with filters
|
|
78
|
-
*
|
|
79
|
-
* @param filters - Filter criteria
|
|
80
|
-
* @returns Total count of resources matching filters
|
|
81
|
-
*/
|
|
82
|
-
export async function count(filters: ResourceFilters): Promise<number> {
|
|
83
|
-
// Build where clause (same as findMany)
|
|
84
|
-
const where: any = {};
|
|
85
|
-
|
|
86
|
-
if (filters.status) {
|
|
87
|
-
where.status = filters.status;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
|
|
91
|
-
where.price = {};
|
|
92
|
-
if (filters.minPrice !== undefined) {
|
|
93
|
-
where.price.gte = filters.minPrice;
|
|
94
|
-
}
|
|
95
|
-
if (filters.maxPrice !== undefined) {
|
|
96
|
-
where.price.lte = filters.maxPrice;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (filters.ownerId) {
|
|
101
|
-
where.ownerId = filters.ownerId;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (filters.search) {
|
|
105
|
-
where.title = {
|
|
106
|
-
contains: filters.search,
|
|
107
|
-
mode: 'insensitive',
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Execute count query
|
|
112
|
-
const total = await prisma.resource.count({ where });
|
|
113
|
-
|
|
114
|
-
return total;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Find resource by ID
|
|
119
|
-
*
|
|
120
|
-
* @param id - Resource ID
|
|
121
|
-
* @returns Resource with owner relation, or null if not found
|
|
122
|
-
*/
|
|
123
|
-
export async function findById(id: string): Promise<ResourceWithOwner | null> {
|
|
124
|
-
const resource = await prisma.resource.findUnique({
|
|
125
|
-
where: { id },
|
|
126
|
-
include: {
|
|
127
|
-
owner: {
|
|
128
|
-
select: {
|
|
129
|
-
id: true,
|
|
130
|
-
email: true,
|
|
131
|
-
name: true,
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
return resource;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Create a new resource
|
|
142
|
-
*
|
|
143
|
-
* @param data - Resource creation data
|
|
144
|
-
* @param ownerId - Owner user ID
|
|
145
|
-
* @returns Created resource
|
|
146
|
-
*/
|
|
147
|
-
export async function create(
|
|
148
|
-
data: CreateResourceInput,
|
|
149
|
-
ownerId: string
|
|
150
|
-
): Promise<Resource> {
|
|
151
|
-
const resource = await prisma.resource.create({
|
|
152
|
-
data: {
|
|
153
|
-
title: data.title,
|
|
154
|
-
summary: data.summary || null,
|
|
155
|
-
price: data.price,
|
|
156
|
-
ownerId,
|
|
157
|
-
status: 'active',
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return resource;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Update a resource
|
|
166
|
-
*
|
|
167
|
-
* @param id - Resource ID
|
|
168
|
-
* @param data - Update data (partial)
|
|
169
|
-
* @returns Updated resource
|
|
170
|
-
*/
|
|
171
|
-
export async function update(
|
|
172
|
-
id: string,
|
|
173
|
-
data: UpdateResourceInput
|
|
174
|
-
): Promise<Resource> {
|
|
175
|
-
const resource = await prisma.resource.update({
|
|
176
|
-
where: { id },
|
|
177
|
-
data: {
|
|
178
|
-
...(data.title !== undefined && { title: data.title }),
|
|
179
|
-
...(data.summary !== undefined && { summary: data.summary }),
|
|
180
|
-
...(data.price !== undefined && { price: data.price }),
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
return resource;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Delete a resource (soft delete)
|
|
189
|
-
*
|
|
190
|
-
* Sets status to 'deleted' instead of removing from database.
|
|
191
|
-
*
|
|
192
|
-
* @param id - Resource ID
|
|
193
|
-
* @returns Deleted resource
|
|
194
|
-
*/
|
|
195
|
-
export async function deleteResource(id: string): Promise<Resource> {
|
|
196
|
-
const resource = await prisma.resource.update({
|
|
197
|
-
where: { id },
|
|
198
|
-
data: {
|
|
199
|
-
status: 'deleted',
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return resource;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Hard delete a resource
|
|
208
|
-
*
|
|
209
|
-
* Permanently removes resource from database.
|
|
210
|
-
* Use with caution - prefer soft delete.
|
|
211
|
-
*
|
|
212
|
-
* @param id - Resource ID
|
|
213
|
-
*/
|
|
214
|
-
export async function hardDelete(id: string): Promise<void> {
|
|
215
|
-
await prisma.resource.delete({
|
|
216
|
-
where: { id },
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Check if resource exists
|
|
222
|
-
*
|
|
223
|
-
* @param id - Resource ID
|
|
224
|
-
* @returns True if resource exists, false otherwise
|
|
225
|
-
*/
|
|
226
|
-
export async function exists(id: string): Promise<boolean> {
|
|
227
|
-
const count = await prisma.resource.count({
|
|
228
|
-
where: { id },
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
return count > 0;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Check if user owns resource
|
|
236
|
-
*
|
|
237
|
-
* @param resourceId - Resource ID
|
|
238
|
-
* @param userId - User ID
|
|
239
|
-
* @returns True if user owns resource, false otherwise
|
|
240
|
-
*/
|
|
241
|
-
export async function isOwner(
|
|
242
|
-
resourceId: string,
|
|
243
|
-
userId: string
|
|
244
|
-
): Promise<boolean> {
|
|
245
|
-
const count = await prisma.resource.count({
|
|
246
|
-
where: {
|
|
247
|
-
id: resourceId,
|
|
248
|
-
ownerId: userId,
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
return count > 0;
|
|
253
|
-
}
|