create-tigra 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/bin/create-tigra.js +292 -0
  4. package/package.json +41 -0
  5. package/template/.agent/rules/client/01-project-structure.md +326 -0
  6. package/template/.agent/rules/client/02-component-patterns.md +249 -0
  7. package/template/.agent/rules/client/03-typescript-rules.md +226 -0
  8. package/template/.agent/rules/client/04-state-management.md +474 -0
  9. package/template/.agent/rules/client/05-api-integration.md +129 -0
  10. package/template/.agent/rules/client/06-forms-validation.md +129 -0
  11. package/template/.agent/rules/client/07-common-patterns.md +150 -0
  12. package/template/.agent/rules/client/08-color-system.md +93 -0
  13. package/template/.agent/rules/client/09-security-rules.md +97 -0
  14. package/template/.agent/rules/client/10-testing-strategy.md +370 -0
  15. package/template/.agent/rules/global/ai-edit-safety.md +38 -0
  16. package/template/.agent/rules/server/01-db-and-migrations.md +242 -0
  17. package/template/.agent/rules/server/02-general-rules.md +111 -0
  18. package/template/.agent/rules/server/03-migrations.md +20 -0
  19. package/template/.agent/rules/server/04-pagination.md +130 -0
  20. package/template/.agent/rules/server/05-project-conventions.md +71 -0
  21. package/template/.agent/rules/server/06-response-handling.md +173 -0
  22. package/template/.agent/rules/server/07-testing-strategy.md +506 -0
  23. package/template/.agent/rules/server/08-observability.md +180 -0
  24. package/template/.agent/rules/server/09-api-documentation-v2.md +168 -0
  25. package/template/.agent/rules/server/10-background-jobs-v2.md +185 -0
  26. package/template/.agent/rules/server/11-rate-limiting-v2.md +210 -0
  27. package/template/.agent/rules/server/12-performance-optimization.md +567 -0
  28. package/template/.claude/rules/client-01-project-structure.md +327 -0
  29. package/template/.claude/rules/client-02-component-patterns.md +250 -0
  30. package/template/.claude/rules/client-03-typescript-rules.md +227 -0
  31. package/template/.claude/rules/client-04-state-management.md +475 -0
  32. package/template/.claude/rules/client-05-api-integration.md +130 -0
  33. package/template/.claude/rules/client-06-forms-validation.md +130 -0
  34. package/template/.claude/rules/client-07-common-patterns.md +151 -0
  35. package/template/.claude/rules/client-08-color-system.md +94 -0
  36. package/template/.claude/rules/client-09-security-rules.md +98 -0
  37. package/template/.claude/rules/client-10-testing-strategy.md +371 -0
  38. package/template/.claude/rules/global-ai-edit-safety.md +39 -0
  39. package/template/.claude/rules/server-01-db-and-migrations.md +243 -0
  40. package/template/.claude/rules/server-02-general-rules.md +112 -0
  41. package/template/.claude/rules/server-03-migrations.md +21 -0
  42. package/template/.claude/rules/server-04-pagination.md +131 -0
  43. package/template/.claude/rules/server-05-project-conventions.md +72 -0
  44. package/template/.claude/rules/server-06-response-handling.md +174 -0
  45. package/template/.claude/rules/server-07-testing-strategy.md +507 -0
  46. package/template/.claude/rules/server-08-observability.md +181 -0
  47. package/template/.claude/rules/server-09-api-documentation-v2.md +169 -0
  48. package/template/.claude/rules/server-10-background-jobs-v2.md +186 -0
  49. package/template/.claude/rules/server-11-rate-limiting-v2.md +211 -0
  50. package/template/.claude/rules/server-12-performance-optimization.md +568 -0
  51. package/template/.cursor/rules/client-01-project-structure.mdc +327 -0
  52. package/template/.cursor/rules/client-02-component-patterns.mdc +250 -0
  53. package/template/.cursor/rules/client-03-typescript-rules.mdc +227 -0
  54. package/template/.cursor/rules/client-04-state-management.mdc +475 -0
  55. package/template/.cursor/rules/client-05-api-integration.mdc +130 -0
  56. package/template/.cursor/rules/client-06-forms-validation.mdc +130 -0
  57. package/template/.cursor/rules/client-07-common-patterns.mdc +151 -0
  58. package/template/.cursor/rules/client-08-color-system.mdc +94 -0
  59. package/template/.cursor/rules/client-09-security-rules.mdc +98 -0
  60. package/template/.cursor/rules/client-10-testing-strategy.mdc +371 -0
  61. package/template/.cursor/rules/global-ai-edit-safety.mdc +39 -0
  62. package/template/.cursor/rules/server-01-db-and-migrations.mdc +243 -0
  63. package/template/.cursor/rules/server-02-general-rules.mdc +112 -0
  64. package/template/.cursor/rules/server-03-migrations.mdc +21 -0
  65. package/template/.cursor/rules/server-04-pagination.mdc +131 -0
  66. package/template/.cursor/rules/server-05-project-conventions.mdc +72 -0
  67. package/template/.cursor/rules/server-06-response-handling.mdc +174 -0
  68. package/template/.cursor/rules/server-07-testing-strategy.mdc +507 -0
  69. package/template/.cursor/rules/server-08-observability.mdc +181 -0
  70. package/template/.cursor/rules/server-09-api-documentation-v2.mdc +169 -0
  71. package/template/.cursor/rules/server-10-background-jobs-v2.mdc +186 -0
  72. package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +211 -0
  73. package/template/.cursor/rules/server-12-performance-optimization.mdc +568 -0
  74. package/template/CLAUDE.md +207 -0
  75. package/template/server/.env.example +148 -0
  76. package/template/server/.tsc-aliasrc.json +12 -0
  77. package/template/server/README.md +175 -0
  78. package/template/server/SECURITY.md +190 -0
  79. package/template/server/biome.json +42 -0
  80. package/template/server/docker-compose.yml +111 -0
  81. package/template/server/package.json +83 -0
  82. package/template/server/postman_collection.json +733 -0
  83. package/template/server/prisma/schema.prisma +92 -0
  84. package/template/server/prisma/seed.ts +142 -0
  85. package/template/server/scripts/wait-for-db.js +60 -0
  86. package/template/server/src/app.ts +74 -0
  87. package/template/server/src/config/env.ts +101 -0
  88. package/template/server/src/hooks/request-timing.hook.ts +26 -0
  89. package/template/server/src/libs/auth/authenticate.middleware.ts +22 -0
  90. package/template/server/src/libs/auth/rbac.middleware.test.ts +134 -0
  91. package/template/server/src/libs/auth/rbac.middleware.ts +147 -0
  92. package/template/server/src/libs/db.ts +76 -0
  93. package/template/server/src/libs/error-handler.ts +89 -0
  94. package/template/server/src/libs/logger.ts +60 -0
  95. package/template/server/src/libs/queue.ts +79 -0
  96. package/template/server/src/libs/redis.ts +79 -0
  97. package/template/server/src/libs/swagger-schemas.ts +16 -0
  98. package/template/server/src/modules/admin/admin.controller.ts +122 -0
  99. package/template/server/src/modules/admin/admin.routes.ts +100 -0
  100. package/template/server/src/modules/admin/admin.schemas.ts +35 -0
  101. package/template/server/src/modules/admin/admin.service.ts +167 -0
  102. package/template/server/src/modules/auth/auth.controller.ts +141 -0
  103. package/template/server/src/modules/auth/auth.integration.test.ts +150 -0
  104. package/template/server/src/modules/auth/auth.repo.ts +218 -0
  105. package/template/server/src/modules/auth/auth.routes.ts +204 -0
  106. package/template/server/src/modules/auth/auth.schemas.ts +137 -0
  107. package/template/server/src/modules/auth/auth.service.test.ts +119 -0
  108. package/template/server/src/modules/auth/auth.service.ts +329 -0
  109. package/template/server/src/modules/auth/auth.types.ts +97 -0
  110. package/template/server/src/modules/resources/resources.controller.ts +218 -0
  111. package/template/server/src/modules/resources/resources.repo.ts +253 -0
  112. package/template/server/src/modules/resources/resources.routes.ts +355 -0
  113. package/template/server/src/modules/resources/resources.schemas.ts +146 -0
  114. package/template/server/src/modules/resources/resources.service.ts +218 -0
  115. package/template/server/src/modules/resources/resources.types.ts +73 -0
  116. package/template/server/src/plugins/rate-limit.plugin.ts +21 -0
  117. package/template/server/src/plugins/security.plugin.ts +21 -0
  118. package/template/server/src/plugins/swagger.plugin.ts +41 -0
  119. package/template/server/src/routes/health.routes.ts +31 -0
  120. package/template/server/src/server.ts +142 -0
  121. package/template/server/src/test/setup.ts +38 -0
  122. package/template/server/src/types/fastify.d.ts +36 -0
  123. package/template/server/src/utils/errors.ts +108 -0
  124. package/template/server/src/utils/pagination.ts +120 -0
  125. package/template/server/src/utils/response.ts +110 -0
  126. package/template/server/src/workers/file.worker.ts +106 -0
  127. package/template/server/tsconfig.build.json +30 -0
  128. package/template/server/tsconfig.build.tsbuildinfo +1 -0
  129. package/template/server/tsconfig.json +89 -0
  130. package/template/server/tsconfig.test.json +22 -0
  131. package/template/server/vitest.config.ts +98 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Authentication Service
3
+ *
4
+ * Business logic for authentication operations.
5
+ * Handles registration, login, token refresh, and logout.
6
+ *
7
+ * @see /mnt/project/02-general-rules.md
8
+ */
9
+
10
+ import bcrypt from 'bcryptjs';
11
+ import jwt from 'jsonwebtoken';
12
+ import { env } from '@/config/env';
13
+ import logger from '@/libs/logger';
14
+ import {
15
+ ConflictError,
16
+ UnauthorizedError,
17
+ } from '@/utils/errors';
18
+ import * as authRepo from './auth.repo';
19
+ import type {
20
+ AuthResponse,
21
+ TokenResponse,
22
+ JwtPayload,
23
+ JwtRefreshPayload,
24
+ } from './auth.types';
25
+ import type { RegisterInput, LoginInput } from './auth.schemas';
26
+
27
+ /**
28
+ * Password hashing rounds
29
+ */
30
+ const BCRYPT_ROUNDS = 10;
31
+
32
+ /**
33
+ * Parse JWT expiration string to seconds
34
+ *
35
+ * @param expiration - Expiration string (e.g., '15m', '7d')
36
+ * @returns Expiration in seconds
37
+ */
38
+ function parseExpiration(expiration: string): number {
39
+ const match = expiration.match(/^(\d+)([smhd])$/);
40
+ if (!match) {
41
+ throw new Error(`Invalid expiration format: ${expiration}`);
42
+ }
43
+
44
+ const value = parseInt(match[1]!);
45
+ const unit = match[2]!;
46
+
47
+ const multipliers: Record<string, number> = {
48
+ s: 1,
49
+ m: 60,
50
+ h: 3600,
51
+ d: 86400,
52
+ };
53
+
54
+ return value * multipliers[unit]!;
55
+ }
56
+
57
+ /**
58
+ * Generate access token
59
+ *
60
+ * @param userId - User ID
61
+ * @param email - User email
62
+ * @param role - User role
63
+ * @returns JWT access token
64
+ */
65
+ function generateAccessToken(
66
+ userId: string,
67
+ email: string,
68
+ role: string
69
+ ): string {
70
+ const payload: JwtPayload = {
71
+ userId,
72
+ email,
73
+ role: role as 'USER' | 'ADMIN',
74
+ };
75
+
76
+ return jwt.sign(payload, env.JWT_SECRET, {
77
+ expiresIn: env.JWT_ACCESS_EXPIRATION,
78
+ issuer: env.JWT_ISSUER,
79
+ } as jwt.SignOptions);
80
+ }
81
+
82
+ /**
83
+ * Generate refresh token
84
+ *
85
+ * @param userId - User ID
86
+ * @param sessionId - Session ID
87
+ * @returns JWT refresh token
88
+ */
89
+ function generateRefreshToken(userId: string, sessionId: string): string {
90
+ const payload: JwtRefreshPayload = {
91
+ userId,
92
+ sessionId,
93
+ };
94
+
95
+ return jwt.sign(payload, env.JWT_SECRET, {
96
+ expiresIn: env.JWT_REFRESH_EXPIRATION,
97
+ issuer: env.JWT_ISSUER,
98
+ } as jwt.SignOptions);
99
+ }
100
+
101
+ /**
102
+ * Register a new user
103
+ *
104
+ * @param data - Registration data
105
+ * @returns User and tokens
106
+ * @throws ConflictError if email already exists
107
+ */
108
+ export async function register(data: RegisterInput): Promise<AuthResponse> {
109
+ try {
110
+ // Check if user already exists
111
+ const existingUser = await authRepo.findUserByEmail(data.email);
112
+ if (existingUser) {
113
+ throw new ConflictError('Email already registered');
114
+ }
115
+
116
+ // Hash password
117
+ const hashedPassword = await bcrypt.hash(data.password, BCRYPT_ROUNDS);
118
+
119
+ // Create user
120
+ const user = await authRepo.createUser({
121
+ email: data.email,
122
+ password: hashedPassword,
123
+ name: data.name,
124
+ });
125
+
126
+ // Generate tokens
127
+ const accessToken = generateAccessToken(user.id, user.email, user.role);
128
+ const sessionId = `session_${Date.now()}_${user.id}`;
129
+ const refreshToken = generateRefreshToken(user.id, sessionId);
130
+
131
+ // Calculate refresh token expiration
132
+ const expiresInSeconds = parseExpiration(env.JWT_REFRESH_EXPIRATION);
133
+ const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
134
+
135
+ // Create session
136
+ await authRepo.createSession({
137
+ userId: user.id,
138
+ refreshToken,
139
+ expiresAt,
140
+ });
141
+
142
+ logger.info({ userId: user.id, email: user.email }, 'User registered');
143
+
144
+ return {
145
+ user,
146
+ tokens: {
147
+ accessToken,
148
+ refreshToken,
149
+ expiresIn: parseExpiration(env.JWT_ACCESS_EXPIRATION),
150
+ },
151
+ };
152
+ } catch (error) {
153
+ logger.error({ error, email: data.email }, 'Registration failed');
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Login user
160
+ *
161
+ * @param data - Login credentials
162
+ * @returns User and tokens
163
+ * @throws UnauthorizedError if credentials are invalid
164
+ */
165
+ export async function login(data: LoginInput): Promise<AuthResponse> {
166
+ try {
167
+ // Find user with password
168
+ const userWithPassword = await authRepo.findUserByEmailWithPassword(
169
+ data.email
170
+ );
171
+
172
+ if (!userWithPassword) {
173
+ throw new UnauthorizedError('Invalid email or password');
174
+ }
175
+
176
+ // Verify password
177
+ const isPasswordValid = await bcrypt.compare(
178
+ data.password,
179
+ userWithPassword.password
180
+ );
181
+
182
+ if (!isPasswordValid) {
183
+ throw new UnauthorizedError('Invalid email or password');
184
+ }
185
+
186
+ // Get user without password
187
+ const user = await authRepo.findUserById(userWithPassword.id);
188
+ if (!user) {
189
+ throw new UnauthorizedError('User not found');
190
+ }
191
+
192
+ // Generate tokens
193
+ const accessToken = generateAccessToken(user.id, user.email, user.role);
194
+ const sessionId = `session_${Date.now()}_${user.id}`;
195
+ const refreshToken = generateRefreshToken(user.id, sessionId);
196
+
197
+ // Calculate refresh token expiration
198
+ const expiresInSeconds = parseExpiration(env.JWT_REFRESH_EXPIRATION);
199
+ const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
200
+
201
+ // Create session
202
+ await authRepo.createSession({
203
+ userId: user.id,
204
+ refreshToken,
205
+ expiresAt,
206
+ });
207
+
208
+ logger.info({ userId: user.id, email: user.email }, 'User logged in');
209
+
210
+ return {
211
+ user,
212
+ tokens: {
213
+ accessToken,
214
+ refreshToken,
215
+ expiresIn: parseExpiration(env.JWT_ACCESS_EXPIRATION),
216
+ },
217
+ };
218
+ } catch (error) {
219
+ logger.error({ error, email: data.email }, 'Login failed');
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Refresh access token
226
+ *
227
+ * @param refreshToken - Refresh token
228
+ * @returns New access and refresh tokens
229
+ * @throws UnauthorizedError if token is invalid or expired
230
+ */
231
+ export async function refreshTokens(
232
+ refreshToken: string
233
+ ): Promise<TokenResponse> {
234
+ try {
235
+ // Find session
236
+ const session = await authRepo.findSessionByToken(refreshToken);
237
+ if (!session) {
238
+ throw new UnauthorizedError('Invalid refresh token');
239
+ }
240
+
241
+ // Check if session expired
242
+ if (session.expiresAt < new Date()) {
243
+ await authRepo.deleteSession(refreshToken);
244
+ throw new UnauthorizedError('Refresh token expired');
245
+ }
246
+
247
+ // Verify JWT token
248
+ let payload: JwtRefreshPayload;
249
+ try {
250
+ payload = jwt.verify(refreshToken, env.JWT_SECRET, {
251
+ issuer: env.JWT_ISSUER,
252
+ }) as JwtRefreshPayload;
253
+ } catch (error) {
254
+ await authRepo.deleteSession(refreshToken);
255
+ throw new UnauthorizedError('Invalid refresh token');
256
+ }
257
+
258
+ // Generate new tokens
259
+ const newAccessToken = generateAccessToken(
260
+ session.user.id,
261
+ session.user.email,
262
+ session.user.role
263
+ );
264
+ const sessionId = `session_${Date.now()}_${session.user.id}`;
265
+ const newRefreshToken = generateRefreshToken(session.user.id, sessionId);
266
+
267
+ // Calculate new expiration
268
+ const expiresInSeconds = parseExpiration(env.JWT_REFRESH_EXPIRATION);
269
+ const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
270
+
271
+ // Update session with new refresh token
272
+ await authRepo.updateSession(refreshToken, newRefreshToken, expiresAt);
273
+
274
+ logger.info({ userId: session.user.id }, 'Tokens refreshed');
275
+
276
+ return {
277
+ accessToken: newAccessToken,
278
+ refreshToken: newRefreshToken,
279
+ expiresIn: parseExpiration(env.JWT_ACCESS_EXPIRATION),
280
+ };
281
+ } catch (error) {
282
+ logger.error({ error }, 'Token refresh failed');
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Logout user
289
+ *
290
+ * Deletes the session associated with the refresh token.
291
+ *
292
+ * @param refreshToken - Refresh token
293
+ */
294
+ export async function logout(refreshToken: string): Promise<void> {
295
+ try {
296
+ await authRepo.deleteSession(refreshToken);
297
+ logger.info('User logged out');
298
+ } catch (error) {
299
+ // Ignore errors if session doesn't exist
300
+ logger.warn({ error }, 'Logout failed - session may not exist');
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Verify access token
306
+ *
307
+ * @param token - Access token
308
+ * @returns JWT payload
309
+ * @throws UnauthorizedError if token is invalid
310
+ */
311
+ export async function verifyAccessToken(token: string): Promise<JwtPayload> {
312
+ try {
313
+ const payload = jwt.verify(token, env.JWT_SECRET, {
314
+ issuer: env.JWT_ISSUER,
315
+ }) as JwtPayload;
316
+
317
+ return payload;
318
+ } catch (error) {
319
+ if (error instanceof jwt.TokenExpiredError) {
320
+ throw new UnauthorizedError('Access token expired');
321
+ }
322
+ if (error instanceof jwt.JsonWebTokenError) {
323
+ throw new UnauthorizedError('Invalid access token');
324
+ }
325
+ throw new UnauthorizedError('Token verification failed');
326
+ }
327
+ }
328
+
329
+
@@ -0,0 +1,97 @@
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
+ }
@@ -0,0 +1,218 @@
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';
14
+ import { paginatedResponse } from '@/utils/pagination';
15
+ import * as resourceService from './resources.service';
16
+ import {
17
+ CreateResourceSchema,
18
+ UpdateResourceSchema,
19
+ ResourceFiltersSchema,
20
+ PaginationSchema,
21
+ } from './resources.schemas';
22
+ import type {
23
+ CreateResourceInput,
24
+ UpdateResourceInput,
25
+ ResourceFiltersInput,
26
+ PaginationInput,
27
+ } from './resources.schemas';
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
+ }