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.
Files changed (243) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +80 -87
  3. package/bin/create-tigra.js +259 -308
  4. package/package.json +49 -41
  5. package/template/_claude/QUICK_REFERENCE.md +193 -0
  6. package/template/_claude/README.md +53 -0
  7. package/template/_claude/commands/create-client.md +881 -0
  8. package/template/_claude/commands/create-server.md +383 -0
  9. package/template/_claude/rules/client/01-project-structure.md +133 -0
  10. package/template/_claude/rules/client/02-components-and-types.md +146 -0
  11. package/template/_claude/rules/client/03-data-and-state.md +156 -0
  12. package/template/_claude/rules/client/04-design-system.md +185 -0
  13. package/template/_claude/rules/client/05-security.md +55 -0
  14. package/template/_claude/rules/client/06-ux-checklist.md +81 -0
  15. package/template/_claude/rules/client/core.md +42 -0
  16. package/template/_claude/rules/global/core.md +77 -0
  17. package/template/_claude/rules/server/core.md +50 -0
  18. package/template/_claude/rules/server/database.md +124 -0
  19. package/template/_claude/rules/server/project-conventions.md +150 -0
  20. package/template/_claude/rules/server/response-handling.md +144 -0
  21. package/template/client/.env.example +5 -0
  22. package/template/client/README.md +36 -0
  23. package/template/client/components.json +23 -0
  24. package/template/client/eslint.config.mjs +18 -0
  25. package/template/client/next.config.ts +34 -0
  26. package/template/client/package.json +44 -0
  27. package/template/client/postcss.config.mjs +7 -0
  28. package/template/client/src/app/(auth)/layout.tsx +18 -0
  29. package/template/client/src/app/(auth)/login/page.tsx +13 -0
  30. package/template/client/src/app/(auth)/register/page.tsx +13 -0
  31. package/template/client/src/app/(main)/dashboard/page.tsx +22 -0
  32. package/template/client/src/app/(main)/layout.tsx +11 -0
  33. package/template/client/src/app/error.tsx +27 -0
  34. package/template/client/src/app/favicon.ico +0 -0
  35. package/template/client/src/app/globals.css +145 -0
  36. package/template/client/src/app/layout.tsx +36 -0
  37. package/template/client/src/app/loading.tsx +11 -0
  38. package/template/client/src/app/not-found.tsx +23 -0
  39. package/template/client/src/app/page.tsx +45 -0
  40. package/template/client/src/app/providers.tsx +43 -0
  41. package/template/client/src/components/common/ConfirmDialog.tsx +56 -0
  42. package/template/client/src/components/common/EmptyState.tsx +31 -0
  43. package/template/client/src/components/common/LoadingSpinner.tsx +30 -0
  44. package/template/client/src/components/common/Pagination.tsx +55 -0
  45. package/template/client/src/components/layout/Footer.tsx +17 -0
  46. package/template/client/src/components/layout/Header.tsx +173 -0
  47. package/template/client/src/components/layout/MainLayout.tsx +18 -0
  48. package/template/client/src/components/ui/alert-dialog.tsx +196 -0
  49. package/template/client/src/components/ui/badge.tsx +48 -0
  50. package/template/client/src/components/ui/button.tsx +64 -0
  51. package/template/client/src/components/ui/card.tsx +92 -0
  52. package/template/client/src/components/ui/input.tsx +21 -0
  53. package/template/client/src/components/ui/label.tsx +24 -0
  54. package/template/client/src/components/ui/select.tsx +190 -0
  55. package/template/client/src/components/ui/skeleton.tsx +13 -0
  56. package/template/client/src/components/ui/table.tsx +116 -0
  57. package/template/client/src/features/auth/components/AuthInitializer.tsx +55 -0
  58. package/template/client/src/features/auth/components/LoginForm.tsx +107 -0
  59. package/template/client/src/features/auth/components/RegisterForm.tsx +178 -0
  60. package/template/client/src/features/auth/hooks/useAuth.ts +84 -0
  61. package/template/client/src/features/auth/services/auth.service.ts +52 -0
  62. package/template/client/src/features/auth/store/authSlice.ts +38 -0
  63. package/template/client/src/features/auth/types/auth.types.ts +32 -0
  64. package/template/client/src/hooks/useDebounce.ts +14 -0
  65. package/template/client/src/hooks/useLocalStorage.ts +55 -0
  66. package/template/client/src/hooks/useMediaQuery.ts +27 -0
  67. package/template/client/src/lib/api/api.types.ts +34 -0
  68. package/template/client/src/lib/api/axios.config.ts +98 -0
  69. package/template/client/src/lib/constants/api-endpoints.ts +18 -0
  70. package/template/client/src/lib/constants/app.constants.ts +12 -0
  71. package/template/client/src/lib/constants/routes.ts +9 -0
  72. package/template/client/src/lib/utils/error.ts +32 -0
  73. package/template/client/src/lib/utils/format.ts +37 -0
  74. package/template/client/src/lib/utils/security.ts +34 -0
  75. package/template/client/src/lib/utils.ts +6 -0
  76. package/template/client/src/middleware.ts +57 -0
  77. package/template/client/src/store/hooks.ts +7 -0
  78. package/template/client/src/store/index.ts +12 -0
  79. package/template/client/src/types/index.ts +3 -0
  80. package/template/client/tsconfig.json +34 -0
  81. package/template/gitignore +34 -0
  82. package/template/server/.dockerignore +66 -0
  83. package/template/server/.env.example +96 -69
  84. package/template/server/.env.production.example +90 -0
  85. package/template/server/Dockerfile +94 -0
  86. package/template/server/docker-compose.yml +82 -111
  87. package/template/server/docs/logging.md +62 -0
  88. package/template/server/eslint.config.mjs +17 -0
  89. package/template/server/package.json +68 -81
  90. package/template/server/phpmyadmin-config.php +26 -0
  91. package/template/server/postman_collection.json +666 -0
  92. package/template/server/prisma/schema.prisma +77 -93
  93. package/template/server/prisma/seed.ts +46 -142
  94. package/template/server/scripts/flush-redis.ts +41 -0
  95. package/template/server/src/app.ts +243 -71
  96. package/template/server/src/config/env.ts +67 -94
  97. package/template/server/src/libs/auth.ts +88 -0
  98. package/template/server/src/libs/cleanup.ts +35 -0
  99. package/template/server/src/libs/cookies.ts +46 -0
  100. package/template/server/src/libs/logger.ts +33 -60
  101. package/template/server/src/libs/monitoring.ts +205 -0
  102. package/template/server/src/libs/password.ts +38 -0
  103. package/template/server/src/libs/prisma.ts +68 -0
  104. package/template/server/src/libs/redis.ts +60 -79
  105. package/template/server/src/libs/requestLogger.ts +66 -0
  106. package/template/server/src/libs/storage/file-storage.service.ts +211 -0
  107. package/template/server/src/libs/storage/file-validator.ts +97 -0
  108. package/template/server/src/libs/storage/filename-sanitizer.ts +71 -0
  109. package/template/server/src/libs/storage/image-optimizer.service.ts +144 -0
  110. package/template/server/src/modules/auth/__tests__/auth.service.test.ts +365 -0
  111. package/template/server/src/modules/auth/auth.controller.ts +90 -141
  112. package/template/server/src/modules/auth/auth.repo.ts +120 -218
  113. package/template/server/src/modules/auth/auth.routes.ts +96 -83
  114. package/template/server/src/modules/auth/auth.schemas.ts +35 -137
  115. package/template/server/src/modules/auth/auth.service.ts +286 -329
  116. package/template/server/src/modules/auth/session.repo.ts +110 -0
  117. package/template/server/src/modules/users/users.controller.ts +120 -0
  118. package/template/server/src/modules/users/users.repo.ts +77 -0
  119. package/template/server/src/modules/users/users.routes.ts +89 -0
  120. package/template/server/src/modules/users/users.schemas.ts +21 -0
  121. package/template/server/src/modules/users/users.service.ts +169 -0
  122. package/template/server/src/server.ts +58 -139
  123. package/template/server/src/shared/errors/AppError.ts +21 -0
  124. package/template/server/src/shared/errors/errors.ts +43 -0
  125. package/template/server/src/shared/responses/paginatedResponse.ts +38 -0
  126. package/template/server/src/shared/responses/successResponse.ts +17 -0
  127. package/template/server/src/shared/schemas/pagination.schema.ts +12 -0
  128. package/template/server/src/shared/types/index.ts +26 -0
  129. package/template/server/src/test/setup.ts +74 -38
  130. package/template/server/tsconfig.json +27 -89
  131. package/template/server/uploads/avatars/.gitkeep +1 -0
  132. package/template/server/vitest.config.ts +43 -98
  133. package/template/.agent/rules/client/01-project-structure.md +0 -326
  134. package/template/.agent/rules/client/02-component-patterns.md +0 -249
  135. package/template/.agent/rules/client/03-typescript-rules.md +0 -226
  136. package/template/.agent/rules/client/04-state-management.md +0 -474
  137. package/template/.agent/rules/client/05-api-integration.md +0 -129
  138. package/template/.agent/rules/client/06-forms-validation.md +0 -129
  139. package/template/.agent/rules/client/07-common-patterns.md +0 -150
  140. package/template/.agent/rules/client/08-color-system.md +0 -93
  141. package/template/.agent/rules/client/09-security-rules.md +0 -97
  142. package/template/.agent/rules/client/10-testing-strategy.md +0 -370
  143. package/template/.agent/rules/global/ai-edit-safety.md +0 -38
  144. package/template/.agent/rules/server/01-db-and-migrations.md +0 -242
  145. package/template/.agent/rules/server/02-general-rules.md +0 -111
  146. package/template/.agent/rules/server/03-migrations.md +0 -20
  147. package/template/.agent/rules/server/04-pagination.md +0 -130
  148. package/template/.agent/rules/server/05-project-conventions.md +0 -71
  149. package/template/.agent/rules/server/06-response-handling.md +0 -173
  150. package/template/.agent/rules/server/07-testing-strategy.md +0 -506
  151. package/template/.agent/rules/server/08-observability.md +0 -180
  152. package/template/.agent/rules/server/10-background-jobs-v2.md +0 -185
  153. package/template/.agent/rules/server/11-rate-limiting-v2.md +0 -210
  154. package/template/.agent/rules/server/12-performance-optimization.md +0 -567
  155. package/template/.claude/rules/client-01-project-structure.md +0 -327
  156. package/template/.claude/rules/client-02-component-patterns.md +0 -250
  157. package/template/.claude/rules/client-03-typescript-rules.md +0 -227
  158. package/template/.claude/rules/client-04-state-management.md +0 -475
  159. package/template/.claude/rules/client-05-api-integration.md +0 -130
  160. package/template/.claude/rules/client-06-forms-validation.md +0 -130
  161. package/template/.claude/rules/client-07-common-patterns.md +0 -151
  162. package/template/.claude/rules/client-08-color-system.md +0 -94
  163. package/template/.claude/rules/client-09-security-rules.md +0 -98
  164. package/template/.claude/rules/client-10-testing-strategy.md +0 -371
  165. package/template/.claude/rules/global-ai-edit-safety.md +0 -39
  166. package/template/.claude/rules/server-01-db-and-migrations.md +0 -243
  167. package/template/.claude/rules/server-02-general-rules.md +0 -112
  168. package/template/.claude/rules/server-03-migrations.md +0 -21
  169. package/template/.claude/rules/server-04-pagination.md +0 -131
  170. package/template/.claude/rules/server-05-project-conventions.md +0 -72
  171. package/template/.claude/rules/server-06-response-handling.md +0 -174
  172. package/template/.claude/rules/server-07-testing-strategy.md +0 -507
  173. package/template/.claude/rules/server-08-observability.md +0 -181
  174. package/template/.claude/rules/server-10-background-jobs-v2.md +0 -186
  175. package/template/.claude/rules/server-11-rate-limiting-v2.md +0 -211
  176. package/template/.claude/rules/server-12-performance-optimization.md +0 -568
  177. package/template/.cursor/rules/client-01-project-structure.mdc +0 -327
  178. package/template/.cursor/rules/client-02-component-patterns.mdc +0 -250
  179. package/template/.cursor/rules/client-03-typescript-rules.mdc +0 -227
  180. package/template/.cursor/rules/client-04-state-management.mdc +0 -475
  181. package/template/.cursor/rules/client-05-api-integration.mdc +0 -130
  182. package/template/.cursor/rules/client-06-forms-validation.mdc +0 -130
  183. package/template/.cursor/rules/client-07-common-patterns.mdc +0 -151
  184. package/template/.cursor/rules/client-08-color-system.mdc +0 -94
  185. package/template/.cursor/rules/client-09-security-rules.mdc +0 -98
  186. package/template/.cursor/rules/client-10-testing-strategy.mdc +0 -371
  187. package/template/.cursor/rules/global-ai-edit-safety.mdc +0 -39
  188. package/template/.cursor/rules/server-01-db-and-migrations.mdc +0 -243
  189. package/template/.cursor/rules/server-02-general-rules.mdc +0 -112
  190. package/template/.cursor/rules/server-03-migrations.mdc +0 -21
  191. package/template/.cursor/rules/server-04-pagination.mdc +0 -131
  192. package/template/.cursor/rules/server-05-project-conventions.mdc +0 -72
  193. package/template/.cursor/rules/server-06-response-handling.mdc +0 -174
  194. package/template/.cursor/rules/server-07-testing-strategy.mdc +0 -507
  195. package/template/.cursor/rules/server-08-observability.mdc +0 -181
  196. package/template/.cursor/rules/server-09-api-documentation-v2.mdc +0 -169
  197. package/template/.cursor/rules/server-10-background-jobs-v2.mdc +0 -186
  198. package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +0 -211
  199. package/template/.cursor/rules/server-12-performance-optimization.mdc +0 -568
  200. package/template/CLAUDE.md +0 -207
  201. package/template/server/.tsc-aliasrc.json +0 -13
  202. package/template/server/IMPORT_FIX_CHECKLIST.md +0 -98
  203. package/template/server/IMPORT_FIX_COMPLETE.md +0 -89
  204. package/template/server/README.md +0 -183
  205. package/template/server/REMAINING_IMPORT_FIXES.md +0 -150
  206. package/template/server/SECURITY.md +0 -190
  207. package/template/server/Tigra-API.postman_collection.json +0 -733
  208. package/template/server/biome.json +0 -42
  209. package/template/server/scripts/fix-all-imports.ps1 +0 -52
  210. package/template/server/scripts/fix-imports-reference.ps1 +0 -16
  211. package/template/server/scripts/fix-imports.mjs +0 -55
  212. package/template/server/scripts/setup-env.js +0 -50
  213. package/template/server/scripts/wait-for-db.js +0 -60
  214. package/template/server/src/hooks/request-timing.hook.ts +0 -26
  215. package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
  216. package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
  217. package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
  218. package/template/server/src/libs/db.ts +0 -76
  219. package/template/server/src/libs/error-handler.ts +0 -89
  220. package/template/server/src/libs/queue.ts +0 -79
  221. package/template/server/src/modules/admin/admin.controller.ts +0 -122
  222. package/template/server/src/modules/admin/admin.routes.ts +0 -62
  223. package/template/server/src/modules/admin/admin.schemas.ts +0 -35
  224. package/template/server/src/modules/admin/admin.service.ts +0 -167
  225. package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
  226. package/template/server/src/modules/auth/auth.service.test.ts +0 -119
  227. package/template/server/src/modules/auth/auth.types.ts +0 -97
  228. package/template/server/src/modules/resources/resources.controller.ts +0 -218
  229. package/template/server/src/modules/resources/resources.repo.ts +0 -253
  230. package/template/server/src/modules/resources/resources.routes.ts +0 -116
  231. package/template/server/src/modules/resources/resources.schemas.ts +0 -146
  232. package/template/server/src/modules/resources/resources.service.ts +0 -218
  233. package/template/server/src/modules/resources/resources.types.ts +0 -73
  234. package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
  235. package/template/server/src/plugins/security.plugin.ts +0 -21
  236. package/template/server/src/routes/health.routes.ts +0 -31
  237. package/template/server/src/types/fastify.d.ts +0 -36
  238. package/template/server/src/utils/errors.ts +0 -108
  239. package/template/server/src/utils/pagination.ts +0 -120
  240. package/template/server/src/utils/response.ts +0 -110
  241. package/template/server/src/workers/file.worker.ts +0 -106
  242. package/template/server/tsconfig.build.json +0 -30
  243. package/template/server/tsconfig.test.json +0 -22
@@ -1,71 +1,243 @@
1
- /**
2
- * Fastify Application Configuration
3
- *
4
- * Main entry point for configuring the Fastify instance, plugins,
5
- * middleware, hooks, and global error handling.
6
- *
7
- * @see /mnt/project/02-general-rules.md
8
- * @see /mnt/project/06-response-handling.md
9
- * @see /mnt/project/08-observability.md
10
- * @see /mnt/project/11-rate-limiting-v2.md
11
- */
12
-
13
- import fastify from 'fastify';
14
- import { env } from './config/env.js';
15
- import logger from './libs/logger.js';
16
-
17
- // Extracted Modules
18
- import { setupErrorHandler } from './libs/error-handler.js';
19
- import { registerSecurityPlugins } from './plugins/security.plugin.js';
20
- import { registerRateLimit } from './plugins/rate-limit.plugin.js';
21
- import { registerRequestHooks } from './hooks/request-timing.hook.js';
22
- import { authenticateMiddleware } from './libs/auth/authenticate.middleware.js';
23
- import { healthRoutes } from './routes/health.routes.js';
24
-
25
- // Routes & RBAC
26
- import { authRoutes } from './modules/auth/auth.routes.js';
27
- import { resourceRoutes } from './modules/resources/resources.routes.js';
28
- import { adminRoutes } from './modules/admin/admin.routes.js';
29
- import {
30
- requireRole,
31
- requireAdmin,
32
- requireUser,
33
- requireAny,
34
- } from './libs/auth/rbac.middleware.js';
35
-
36
- /**
37
- * Configure and build the Fastify application
38
- */
39
- const buildApp = async () => {
40
- const app = fastify({
41
- loggerInstance: logger,
42
- disableRequestLogging: true, // Custom logging handled by hooks
43
- });
44
-
45
- // 1. Plugins
46
- await registerSecurityPlugins(app);
47
- await registerRateLimit(app);
48
-
49
- // 2. Decorators
50
- app.decorate('authenticate', authenticateMiddleware);
51
- app.decorate('requireRole', requireRole);
52
- app.decorate('requireAdmin', requireAdmin);
53
- app.decorate('requireUser', requireUser);
54
- app.decorate('requireAny', requireAny);
55
-
56
- // 3. Hooks
57
- registerRequestHooks(app);
58
-
59
- // 4. Error Handler
60
- app.setErrorHandler(setupErrorHandler);
61
-
62
- // 5. Routes
63
- await app.register(healthRoutes);
64
- await app.register(authRoutes, { prefix: `${env.API_PREFIX}/auth` });
65
- await app.register(resourceRoutes, { prefix: `${env.API_PREFIX}/resources` });
66
- await app.register(adminRoutes, { prefix: `${env.API_PREFIX}/admin` });
67
-
68
- return app;
69
- };
70
-
71
- export default buildApp;
1
+ import Fastify, { type FastifyError } from 'fastify';
2
+ import cors from '@fastify/cors';
3
+ import helmet from '@fastify/helmet';
4
+ import rateLimit from '@fastify/rate-limit';
5
+ import cookie from '@fastify/cookie';
6
+ import jwt from '@fastify/jwt';
7
+ import compress from '@fastify/compress';
8
+ import multipart from '@fastify/multipart';
9
+ import fastifyStatic from '@fastify/static';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { env } from '@config/env.js';
13
+ import { logger } from '@libs/logger.js';
14
+ import { markRequestStart, logRequestLine } from '@libs/requestLogger.js';
15
+ import { initAuth } from '@libs/auth.js';
16
+ import { isAppError } from '@shared/errors/AppError.js';
17
+ import { successResponse, errorResponse } from '@shared/responses/successResponse.js';
18
+ import { authRoutes } from '@modules/auth/auth.routes.js';
19
+ import { usersRoutes } from '@modules/users/users.routes.js';
20
+ import { fileStorageService } from '@libs/storage/file-storage.service.js';
21
+ import { registerCleanupJob } from '@libs/cleanup.js';
22
+ import {
23
+ serializerCompiler,
24
+ validatorCompiler,
25
+ type ZodTypeProvider,
26
+ } from 'fastify-type-provider-zod';
27
+
28
+ // Import types to register Fastify augmentations
29
+ import type {} from '@shared/types/index.js';
30
+
31
+ export async function buildApp() {
32
+ const app = Fastify({
33
+ logger: false,
34
+ // Trust proxy headers (X-Forwarded-For) for accurate client IP behind Nginx/load balancer
35
+ trustProxy: env.NODE_ENV === 'production',
36
+ // Graceful shutdown configuration
37
+ forceCloseConnections: true, // Force close idle connections on shutdown
38
+ requestTimeout: 30000, // 30s request timeout
39
+ connectionTimeout: 60000, // 60s connection timeout
40
+ keepAliveTimeout: 5000, // 5s keep-alive timeout
41
+ // Request body size limits (prevent DoS attacks)
42
+ bodyLimit: 1048576, // 1MB default limit (1024 * 1024)
43
+ }).withTypeProvider<ZodTypeProvider>();
44
+
45
+ // Set Zod validator and serializer
46
+ app.setValidatorCompiler(validatorCompiler);
47
+ app.setSerializerCompiler(serializerCompiler);
48
+
49
+ // --- Plugins ---
50
+ // CORS: Allow all origins in development, specific origin(s) in production
51
+ const corsOrigin = env.NODE_ENV === 'development'
52
+ ? true
53
+ : env.CORS_ORIGIN?.includes(',')
54
+ ? env.CORS_ORIGIN.split(',').map((o) => o.trim())
55
+ : env.CORS_ORIGIN;
56
+
57
+ await app.register(cors, {
58
+ origin: corsOrigin,
59
+ credentials: true,
60
+ });
61
+
62
+ // Enhanced security headers for production
63
+ await app.register(helmet, {
64
+ global: true,
65
+ });
66
+
67
+ // Response compression (gzip/brotli) for performance
68
+ await app.register(compress, {
69
+ global: true,
70
+ threshold: 1024, // Only compress responses > 1KB
71
+ encodings: ['gzip', 'deflate'],
72
+ });
73
+
74
+ await app.register(rateLimit, {
75
+ global: false,
76
+ max: 100,
77
+ timeWindow: '1 minute',
78
+ });
79
+
80
+ await app.register(cookie, {
81
+ secret: env.COOKIE_SECRET || env.JWT_SECRET,
82
+ });
83
+
84
+ await app.register(jwt, {
85
+ secret: env.JWT_SECRET,
86
+ cookie: {
87
+ cookieName: 'access_token',
88
+ signed: false,
89
+ },
90
+ });
91
+
92
+ // Initialize auth helpers after JWT plugin is registered
93
+ initAuth(app);
94
+
95
+ // File upload handling (multipart/form-data)
96
+ await app.register(multipart, {
97
+ limits: {
98
+ fileSize: 5 * 1024 * 1024, // 5MB max file size
99
+ files: 1, // Only one file per request
100
+ },
101
+ });
102
+
103
+ // Static file serving for uploads
104
+ // Get __dirname equivalent in ES modules
105
+ const __filename = fileURLToPath(import.meta.url);
106
+ const __dirname = path.dirname(__filename);
107
+
108
+ await app.register(fastifyStatic, {
109
+ root: path.join(__dirname, '..', 'uploads'),
110
+ prefix: '/uploads/',
111
+ });
112
+
113
+ // Initialize file storage (create directories)
114
+ await fileStorageService.initialize();
115
+
116
+ // --- Request/Response Logging ---
117
+ const skipLogPaths = new Set(['/api/v1/health', '/api/v1/ready', '/api/v1/live']);
118
+
119
+ app.addHook('preHandler', async (request) => {
120
+ const pathname = request.url.split('?')[0];
121
+ if (!skipLogPaths.has(pathname)) {
122
+ markRequestStart(request);
123
+ }
124
+ });
125
+
126
+ app.addHook('onResponse', async (request, reply) => {
127
+ const pathname = request.url.split('?')[0];
128
+ if (!skipLogPaths.has(pathname)) {
129
+ logRequestLine(request, reply);
130
+ }
131
+ });
132
+
133
+ // --- Global Error Handler (must be set before routes) ---
134
+ app.setErrorHandler((error: FastifyError, request, reply) => {
135
+ // AppError — our typed errors (use duck-type check to avoid instanceof issues)
136
+ if (isAppError(error)) {
137
+ return reply.status(error.statusCode).send(errorResponse(error.code, error.message));
138
+ }
139
+
140
+ // Zod validation error
141
+ if (error.name === 'ZodError') {
142
+ return reply.status(422).send(errorResponse('VALIDATION_FAILED', 'Validation failed'));
143
+ }
144
+
145
+ // Fastify validation error
146
+ if (error.validation) {
147
+ return reply.status(400).send(
148
+ errorResponse(
149
+ 'BAD_REQUEST',
150
+ error.message || 'Invalid request',
151
+ ),
152
+ );
153
+ }
154
+
155
+ // Fastify plugin errors (file size, rate limiting, etc.)
156
+ // These have statusCode properties but aren't AppError instances
157
+ if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
158
+ // Map common Fastify error codes to user-friendly messages
159
+ const errorCodeMap: Record<number, { code: string; message: string }> = {
160
+ 413: { code: 'FILE_TOO_LARGE', message: 'File size exceeds the maximum allowed limit' },
161
+ 429: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests. Please try again later' },
162
+ };
163
+
164
+ const errorInfo = errorCodeMap[error.statusCode] || {
165
+ code: 'BAD_REQUEST',
166
+ message: error.message || 'Bad request',
167
+ };
168
+
169
+ return reply.status(error.statusCode).send(errorResponse(errorInfo.code, errorInfo.message));
170
+ }
171
+
172
+ // Unexpected error — log and return generic 500
173
+ const requestId = request.id || 'unknown';
174
+ logger.error(
175
+ {
176
+ err: error,
177
+ requestId,
178
+ url: request.url,
179
+ method: request.method,
180
+ stack: error.stack,
181
+ },
182
+ `Unhandled error [${requestId}]: ${error.message}`,
183
+ );
184
+
185
+ return reply.status(500).send(errorResponse('INTERNAL_ERROR', 'Internal server error'));
186
+ });
187
+
188
+ // --- Monitoring & Health Checks ---
189
+ const { performHealthCheck, checkReadiness, checkLiveness } = await import('@libs/monitoring.js');
190
+
191
+ // Comprehensive health check (DB + Redis + Memory + Uptime)
192
+ app.get('/api/v1/health', async (_request, reply) => {
193
+ const health = await performHealthCheck();
194
+
195
+ const statusCode = health.status === 'healthy' ? 200 : health.status === 'degraded' ? 200 : 503;
196
+
197
+ return reply.status(statusCode).send(
198
+ successResponse(
199
+ health.status === 'healthy'
200
+ ? 'All systems operational'
201
+ : health.status === 'degraded'
202
+ ? 'Some systems degraded'
203
+ : 'System unhealthy',
204
+ health,
205
+ ),
206
+ );
207
+ });
208
+
209
+ // Readiness probe (for load balancers / K8s)
210
+ app.get('/api/v1/ready', async (_request, reply) => {
211
+ const ready = await checkReadiness();
212
+ const statusCode = ready ? 200 : 503;
213
+ return reply.status(statusCode).send(
214
+ successResponse(ready ? 'Service is ready' : 'Service not ready', {
215
+ ready,
216
+ timestamp: new Date().toISOString(),
217
+ })
218
+ );
219
+ });
220
+
221
+ // Liveness probe (for container orchestration)
222
+ app.get('/api/v1/live', (_request, reply) => {
223
+ const alive = checkLiveness();
224
+ const statusCode = alive ? 200 : 503;
225
+ return reply.status(statusCode).send(
226
+ successResponse(alive ? 'Service is alive' : 'Service not alive', {
227
+ alive,
228
+ timestamp: new Date().toISOString(),
229
+ })
230
+ );
231
+ });
232
+
233
+ // --- Routes ---
234
+ await app.register(authRoutes, { prefix: '/api/v1' });
235
+ await app.register(usersRoutes, { prefix: '/api/v1' });
236
+
237
+ // --- Cleanup Jobs ---
238
+ await registerCleanupJob(app);
239
+
240
+ return app;
241
+ }
242
+
243
+ export default buildApp;
@@ -1,94 +1,67 @@
1
- /**
2
- * Environment Configuration
3
- *
4
- * Validates and exports typed environment variables using Zod.
5
- * Ensures all required variables are present at startup.
6
- */
7
-
8
- import { z } from 'zod';
9
-
10
- /**
11
- * Environment variable schema
12
- *
13
- * All required variables from .env.example
14
- */
15
- const envSchema = z.object({
16
- // Server Configuration
17
- NODE_ENV: z
18
- .enum(['development', 'production', 'test'])
19
- .default('development'),
20
- PORT: z.coerce.number().int().min(1).max(65535).default(3000),
21
- API_PREFIX: z.string().default('/api/v1'),
22
- LOG_LEVEL: z
23
- .enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal'])
24
- .default('info'),
25
-
26
- // Database Configuration
27
- DATABASE_URL: z.string().url().min(1),
28
-
29
- // Redis Configuration
30
- REDIS_HOST: z.string().default('localhost'),
31
- REDIS_PORT: z.coerce.number().int().min(1).max(65535).default(6379),
32
- REDIS_PASSWORD: z.string().optional(),
33
- REDIS_DB: z.coerce.number().int().min(0).max(15).default(0),
34
-
35
- // JWT Configuration
36
- JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
37
- JWT_ACCESS_EXPIRATION: z.string().default('15m'),
38
- JWT_REFRESH_EXPIRATION: z.string().default('7d'),
39
- JWT_ISSUER: z.string().default('{{PROJECT_NAME}}'),
40
-
41
- // CORS Configuration
42
- CORS_ALLOWED_ORIGINS: z.string().default('http://localhost:5173'),
43
-
44
-
45
-
46
- // File Upload Configuration
47
- MAX_FILE_SIZE: z.coerce.number().int().positive().default(10485760), // 10MB
48
- ALLOWED_FILE_TYPES: z
49
- .string()
50
- .default('image/jpeg,image/png,image/webp,application/pdf'),
51
- UPLOAD_DIR: z.string().default('./uploads'),
52
-
53
- // Rate Limiting Configuration
54
- RATE_LIMIT_MAX: z.coerce.number().int().positive().default(100),
55
- RATE_LIMIT_WINDOW: z.coerce.number().int().positive().default(900000), // 15 minutes
56
-
57
- // Development Tools
58
- PRISMA_QUERY_LOG: z
59
- .string()
60
- .transform((val) => val === 'true')
61
- .optional(),
62
- SLOW_QUERY_THRESHOLD: z.coerce.number().int().positive().default(1000),
63
- });
64
-
65
- /**
66
- * Validate environment variables
67
- *
68
- * Throws error if validation fails, preventing server startup
69
- * with invalid configuration.
70
- */
71
- const parseEnv = () => {
72
- try {
73
- return envSchema.parse(process.env);
74
- } catch (error) {
75
- if (error instanceof z.ZodError) {
76
- console.error('Environment validation failed:');
77
- console.error(JSON.stringify(error.errors, null, 2));
78
- process.exit(1);
79
- }
80
- throw error;
81
- }
82
- };
83
-
84
- /**
85
- * Typed environment configuration
86
- *
87
- * Use this instead of process.env for type safety
88
- */
89
- export const env = parseEnv();
90
-
91
- /**
92
- * Type for environment variables
93
- */
94
- export type Env = z.infer<typeof envSchema>;
1
+ import { z } from 'zod';
2
+ import dotenv from 'dotenv';
3
+
4
+ // Suppress dotenv's informational logs
5
+ process.env.DOTENV_CONFIG_QUIET = 'true';
6
+ dotenv.config();
7
+
8
+ const envSchema = z.object({
9
+ // --- Application ---
10
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
11
+ PORT: z.coerce.number().int().min(1).max(65535).default(8000),
12
+ HOST: z.string().default('0.0.0.0'),
13
+
14
+ // --- Database (MySQL 8.0+) ---
15
+ DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
16
+ DATABASE_POOL_MIN: z.coerce.number().int().min(1).default(2),
17
+ DATABASE_POOL_MAX: z.coerce.number().int().min(1).max(1000).default(10),
18
+
19
+ // --- Redis ---
20
+ REDIS_URL: z.string().default('redis://localhost:6379'),
21
+ REDIS_MAX_RETRIES: z.coerce.number().int().min(0).default(3),
22
+ REDIS_CONNECT_TIMEOUT: z.coerce.number().int().min(1000).default(10000), // ms
23
+
24
+ // --- JWT Authentication ---
25
+ JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
26
+ JWT_ACCESS_EXPIRY: z.string().default('15m'),
27
+ JWT_REFRESH_EXPIRY: z.string().default('7d'),
28
+
29
+ // --- Cookie ---
30
+ // Separate secret for cookie signing (defaults to JWT_SECRET if not set)
31
+ COOKIE_SECRET: z.string().min(32, 'COOKIE_SECRET must be at least 32 characters').optional(),
32
+
33
+ // --- CORS ---
34
+ // In development: CORS_ORIGIN is optional (allows all origins)
35
+ // In production: REQUIRED for security
36
+ // Supports comma-separated multiple origins: "https://a.com,https://b.com"
37
+ CORS_ORIGIN: z.string().optional(),
38
+
39
+ // --- Logging ---
40
+ LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
41
+
42
+ // --- Error Tracking (Optional) ---
43
+ SENTRY_DSN: z.string().url().optional(),
44
+ });
45
+
46
+ const parsed = envSchema.safeParse(process.env);
47
+
48
+ if (!parsed.success) {
49
+ const formatted = parsed.error.issues
50
+ .map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
51
+ .join('\n');
52
+
53
+ // eslint-disable-next-line no-console
54
+ console.error(`\nEnvironment validation failed:\n${formatted}\n`);
55
+ process.exit(1);
56
+ }
57
+
58
+ // Validate CORS_ORIGIN in production
59
+ if (parsed.data.NODE_ENV === 'production' && !parsed.data.CORS_ORIGIN) {
60
+ // eslint-disable-next-line no-console
61
+ console.error('\nCORS_ORIGIN is required in production for security\n');
62
+ process.exit(1);
63
+ }
64
+
65
+ export const env = parsed.data;
66
+
67
+ export type Env = z.infer<typeof envSchema>;
@@ -0,0 +1,88 @@
1
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { env } from '@config/env.js';
4
+ import { UnauthorizedError, ForbiddenError } from '@shared/errors/errors.js';
5
+ import type { JwtPayload, UserRole } from '@shared/types/index.js';
6
+
7
+ let app: FastifyInstance | null = null;
8
+
9
+ export function initAuth(fastify: FastifyInstance): void {
10
+ app = fastify;
11
+ }
12
+
13
+ function getApp(): FastifyInstance {
14
+ if (!app) {
15
+ throw new Error('Auth not initialized. Call initAuth(fastify) before using auth helpers.');
16
+ }
17
+ return app;
18
+ }
19
+
20
+ export function signAccessToken(payload: JwtPayload): string {
21
+ return getApp().jwt.sign(
22
+ { userId: payload.userId, role: payload.role },
23
+ { expiresIn: env.JWT_ACCESS_EXPIRY },
24
+ );
25
+ }
26
+
27
+ export function generateRefreshToken(): string {
28
+ return uuidv4();
29
+ }
30
+
31
+ export function parseDurationMs(duration: string, fallbackMs: number): number {
32
+ const match = duration.match(/^(\d+)([smhd])$/);
33
+ if (!match) return fallbackMs;
34
+
35
+ const value = parseInt(match[1], 10);
36
+ const unit = match[2];
37
+ const multipliers: Record<string, number> = {
38
+ s: 1000,
39
+ m: 60 * 1000,
40
+ h: 60 * 60 * 1000,
41
+ d: 24 * 60 * 60 * 1000,
42
+ };
43
+
44
+ return value * multipliers[unit];
45
+ }
46
+
47
+ export function getRefreshTokenExpiresAt(): Date {
48
+ const ms = parseDurationMs(env.JWT_REFRESH_EXPIRY, 7 * 24 * 60 * 60 * 1000);
49
+ return new Date(Date.now() + ms);
50
+ }
51
+
52
+ export async function authenticate(
53
+ request: FastifyRequest,
54
+ _reply: FastifyReply,
55
+ ): Promise<void> {
56
+ try {
57
+ await request.jwtVerify();
58
+ } catch {
59
+ throw new UnauthorizedError('Invalid or expired token');
60
+ }
61
+ }
62
+
63
+ export async function optionalAuth(
64
+ request: FastifyRequest,
65
+ _reply: FastifyReply,
66
+ ): Promise<void> {
67
+ try {
68
+ await request.jwtVerify();
69
+ } catch {
70
+ // Not authenticated — that's okay for optional auth
71
+ }
72
+ }
73
+
74
+ export function authorize(...roles: UserRole[]) {
75
+ return async function authorizeHandler(
76
+ request: FastifyRequest,
77
+ _reply: FastifyReply,
78
+ ): Promise<void> {
79
+ if (!request.user) {
80
+ throw new UnauthorizedError('Authentication required');
81
+ }
82
+
83
+ if (!roles.includes(request.user.role)) {
84
+ throw new ForbiddenError('Insufficient permissions');
85
+ }
86
+ };
87
+ }
88
+
@@ -0,0 +1,35 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { prisma } from '@libs/prisma.js';
3
+ import { logger } from '@libs/logger.js';
4
+
5
+ const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
6
+
7
+ export async function registerCleanupJob(app: FastifyInstance): Promise<void> {
8
+ const intervalId = setInterval(async () => {
9
+ try {
10
+ const now = new Date();
11
+
12
+ const deletedTokens = await prisma.refreshToken.deleteMany({
13
+ where: { expiresAt: { lt: now } },
14
+ });
15
+
16
+ const deletedSessions = await prisma.session.deleteMany({
17
+ where: { expiresAt: { lt: now } },
18
+ });
19
+
20
+ if (deletedTokens.count > 0 || deletedSessions.count > 0) {
21
+ logger.info(
22
+ { deletedTokens: deletedTokens.count, deletedSessions: deletedSessions.count },
23
+ 'Expired auth records cleaned up',
24
+ );
25
+ }
26
+ } catch (error) {
27
+ logger.error({ err: error }, 'Failed to clean up expired auth records');
28
+ }
29
+ }, CLEANUP_INTERVAL_MS);
30
+
31
+ // Clear interval on server shutdown
32
+ app.addHook('onClose', async () => {
33
+ clearInterval(intervalId);
34
+ });
35
+ }
@@ -0,0 +1,46 @@
1
+ import type { FastifyReply } from 'fastify';
2
+ import { env } from '@config/env.js';
3
+ import { parseDurationMs } from '@libs/auth.js';
4
+
5
+ const isProduction = env.NODE_ENV === 'production';
6
+
7
+ const ACCESS_TOKEN_MAX_AGE_MS = parseDurationMs(env.JWT_ACCESS_EXPIRY, 15 * 60 * 1000);
8
+ const REFRESH_TOKEN_MAX_AGE_MS = parseDurationMs(env.JWT_REFRESH_EXPIRY, 7 * 24 * 60 * 60 * 1000);
9
+
10
+ export function setAuthCookies(
11
+ reply: FastifyReply,
12
+ accessToken: string,
13
+ refreshToken: string,
14
+ ): void {
15
+ reply.setCookie('access_token', accessToken, {
16
+ httpOnly: true,
17
+ secure: isProduction,
18
+ sameSite: 'strict',
19
+ path: '/',
20
+ maxAge: Math.floor(ACCESS_TOKEN_MAX_AGE_MS / 1000), // setCookie expects seconds
21
+ });
22
+
23
+ reply.setCookie('refresh_token', refreshToken, {
24
+ httpOnly: true,
25
+ secure: isProduction,
26
+ sameSite: 'strict',
27
+ path: '/api/v1/auth',
28
+ maxAge: Math.floor(REFRESH_TOKEN_MAX_AGE_MS / 1000),
29
+ });
30
+ }
31
+
32
+ export function clearAuthCookies(reply: FastifyReply): void {
33
+ reply.clearCookie('access_token', {
34
+ httpOnly: true,
35
+ secure: isProduction,
36
+ sameSite: 'strict',
37
+ path: '/',
38
+ });
39
+
40
+ reply.clearCookie('refresh_token', {
41
+ httpOnly: true,
42
+ secure: isProduction,
43
+ sameSite: 'strict',
44
+ path: '/api/v1/auth',
45
+ });
46
+ }