create-tigra 1.0.7 → 2.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 (237) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +80 -87
  3. package/bin/create-tigra.js +242 -309
  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 +80 -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 -12
  202. package/template/server/README.md +0 -183
  203. package/template/server/SECURITY.md +0 -190
  204. package/template/server/Tigra-API.postman_collection.json +0 -733
  205. package/template/server/biome.json +0 -42
  206. package/template/server/scripts/setup-env.js +0 -50
  207. package/template/server/scripts/wait-for-db.js +0 -60
  208. package/template/server/src/hooks/request-timing.hook.ts +0 -26
  209. package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
  210. package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
  211. package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
  212. package/template/server/src/libs/db.ts +0 -76
  213. package/template/server/src/libs/error-handler.ts +0 -89
  214. package/template/server/src/libs/queue.ts +0 -79
  215. package/template/server/src/modules/admin/admin.controller.ts +0 -122
  216. package/template/server/src/modules/admin/admin.routes.ts +0 -62
  217. package/template/server/src/modules/admin/admin.schemas.ts +0 -35
  218. package/template/server/src/modules/admin/admin.service.ts +0 -167
  219. package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
  220. package/template/server/src/modules/auth/auth.service.test.ts +0 -119
  221. package/template/server/src/modules/auth/auth.types.ts +0 -97
  222. package/template/server/src/modules/resources/resources.controller.ts +0 -218
  223. package/template/server/src/modules/resources/resources.repo.ts +0 -253
  224. package/template/server/src/modules/resources/resources.routes.ts +0 -116
  225. package/template/server/src/modules/resources/resources.schemas.ts +0 -146
  226. package/template/server/src/modules/resources/resources.service.ts +0 -218
  227. package/template/server/src/modules/resources/resources.types.ts +0 -73
  228. package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
  229. package/template/server/src/plugins/security.plugin.ts +0 -21
  230. package/template/server/src/routes/health.routes.ts +0 -31
  231. package/template/server/src/types/fastify.d.ts +0 -36
  232. package/template/server/src/utils/errors.ts +0 -108
  233. package/template/server/src/utils/pagination.ts +0 -120
  234. package/template/server/src/utils/response.ts +0 -110
  235. package/template/server/src/workers/file.worker.ts +0 -106
  236. package/template/server/tsconfig.build.json +0 -30
  237. package/template/server/tsconfig.test.json +0 -22
@@ -1,60 +1,33 @@
1
- /**
2
- * Logger Configuration
3
- *
4
- * Pino logger with structured logging and sensitive data redaction.
5
- *
6
- * @see /mnt/project/08-observability.md
7
- */
8
-
9
- import pino from 'pino';
10
-
11
- /**
12
- * Create Pino logger instance
13
- *
14
- * Features:
15
- * - Structured JSON logging in production
16
- * - Pretty-printed logs in development
17
- * - Automatic redaction of sensitive data
18
- * - Configurable log levels
19
- */
20
- const logger = pino({
21
- // Log level from environment or default to 'info'
22
- level: process.env.LOG_LEVEL || 'info',
23
-
24
- // Pretty printing in development only
25
- transport:
26
- process.env.NODE_ENV === 'development'
27
- ? {
28
- target: 'pino-pretty',
29
- options: {
30
- colorize: true,
31
- translateTime: 'HH:MM:ss Z',
32
- ignore: 'pid,hostname',
33
- },
34
- }
35
- : undefined,
36
-
37
- // Redact sensitive data
38
- redact: {
39
- paths: [
40
- 'req.headers.authorization',
41
- '*.password',
42
- '*.token',
43
- 'req.body.password',
44
- 'req.body.refreshToken',
45
- 'req.body.accessToken',
46
- 'res.headers.authorization',
47
- ],
48
- censor: '[REDACTED]',
49
- },
50
-
51
- // Base configuration
52
- base: {
53
- env: process.env.NODE_ENV || 'development',
54
- },
55
-
56
- // Timestamp format
57
- timestamp: pino.stdTimeFunctions.isoTime,
58
- });
59
-
60
- export default logger;
1
+ import pino from 'pino';
2
+ import { env } from '@config/env.js';
3
+
4
+ export const logger = pino({
5
+ level: env.LOG_LEVEL,
6
+ ...(env.NODE_ENV !== 'production' && {
7
+ transport: {
8
+ target: 'pino-pretty',
9
+ options: {
10
+ colorize: true,
11
+ translateTime: 'HH:MM:ss.l',
12
+ ignore: 'pid,hostname',
13
+ messageFormat: '{msg}',
14
+ },
15
+ },
16
+ }),
17
+ serializers: {
18
+ req: (req) => ({
19
+ method: req.method,
20
+ url: req.url,
21
+ query: req.query,
22
+ params: req.params,
23
+ headers: {
24
+ host: req.headers.host,
25
+ 'user-agent': req.headers['user-agent'],
26
+ 'content-type': req.headers['content-type'],
27
+ },
28
+ }),
29
+ res: (res) => ({
30
+ statusCode: res.statusCode,
31
+ }),
32
+ },
33
+ });
@@ -0,0 +1,205 @@
1
+ import { prisma } from '@libs/prisma.js';
2
+ import { getRedis } from '@libs/redis.js';
3
+ import { env } from '@config/env.js';
4
+ import { logger } from '@libs/logger.js';
5
+
6
+ export interface HealthCheckResult {
7
+ status: 'healthy' | 'degraded' | 'unhealthy';
8
+ checks: {
9
+ database: CheckStatus;
10
+ redis: CheckStatus;
11
+ memory: CheckStatus;
12
+ uptime: CheckStatus;
13
+ };
14
+ timestamp: string;
15
+ version: string;
16
+ environment: string;
17
+ }
18
+
19
+ export interface CheckStatus {
20
+ status: 'up' | 'down' | 'degraded';
21
+ message?: string;
22
+ responseTime?: number;
23
+ details?: Record<string, unknown>;
24
+ }
25
+
26
+ const startTime = Date.now();
27
+
28
+ /**
29
+ * Check database connectivity and response time
30
+ */
31
+ async function checkDatabase(): Promise<CheckStatus> {
32
+ const start = Date.now();
33
+ try {
34
+ await prisma.$queryRaw`SELECT 1`;
35
+ const responseTime = Date.now() - start;
36
+
37
+ return {
38
+ status: responseTime < 100 ? 'up' : 'degraded',
39
+ message: responseTime < 100 ? 'Database is healthy' : 'Database responding slowly',
40
+ responseTime,
41
+ details: {
42
+ poolMin: env.DATABASE_POOL_MIN,
43
+ poolMax: env.DATABASE_POOL_MAX,
44
+ },
45
+ };
46
+ } catch (error) {
47
+ const responseTime = Date.now() - start;
48
+ logger.error({ error }, 'Database health check failed');
49
+ return {
50
+ status: 'down',
51
+ message: 'Database connection failed',
52
+ responseTime,
53
+ };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Check Redis connectivity and response time
59
+ */
60
+ async function checkRedis(): Promise<CheckStatus> {
61
+ const start = Date.now();
62
+ try {
63
+ const redis = getRedis();
64
+ await redis.ping();
65
+ const responseTime = Date.now() - start;
66
+
67
+ return {
68
+ status: responseTime < 50 ? 'up' : 'degraded',
69
+ message: responseTime < 50 ? 'Redis is healthy' : 'Redis responding slowly',
70
+ responseTime,
71
+ details: {
72
+ maxRetries: env.REDIS_MAX_RETRIES,
73
+ connectTimeout: env.REDIS_CONNECT_TIMEOUT,
74
+ },
75
+ };
76
+ } catch (error) {
77
+ const responseTime = Date.now() - start;
78
+ logger.warn({ error }, 'Redis health check failed');
79
+ return {
80
+ status: 'down',
81
+ message: 'Redis connection failed (non-fatal)',
82
+ responseTime,
83
+ };
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Check memory usage
89
+ */
90
+ function checkMemory(): CheckStatus {
91
+ const used = process.memoryUsage();
92
+ const heapUsedMB = Math.round(used.heapUsed / 1024 / 1024);
93
+ const heapTotalMB = Math.round(used.heapTotal / 1024 / 1024);
94
+ const rssMB = Math.round(used.rss / 1024 / 1024);
95
+ const heapUsagePercent = Math.round((used.heapUsed / used.heapTotal) * 100);
96
+
97
+ // Warn if heap usage > 80%
98
+ const status = heapUsagePercent > 80 ? 'degraded' : 'up';
99
+ const message =
100
+ status === 'degraded'
101
+ ? `High memory usage: ${heapUsagePercent}%`
102
+ : `Memory usage normal: ${heapUsagePercent}%`;
103
+
104
+ return {
105
+ status,
106
+ message,
107
+ details: {
108
+ heapUsedMB,
109
+ heapTotalMB,
110
+ rssMB,
111
+ heapUsagePercent,
112
+ external: Math.round(used.external / 1024 / 1024),
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Check server uptime
119
+ */
120
+ function checkUptime(): CheckStatus {
121
+ const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
122
+ const uptimeMinutes = Math.floor(uptimeSeconds / 60);
123
+ const uptimeHours = Math.floor(uptimeMinutes / 60);
124
+ const uptimeDays = Math.floor(uptimeHours / 24);
125
+
126
+ let uptimeFormatted: string;
127
+ if (uptimeDays > 0) {
128
+ uptimeFormatted = `${uptimeDays}d ${uptimeHours % 24}h`;
129
+ } else if (uptimeHours > 0) {
130
+ uptimeFormatted = `${uptimeHours}h ${uptimeMinutes % 60}m`;
131
+ } else if (uptimeMinutes > 0) {
132
+ uptimeFormatted = `${uptimeMinutes}m ${uptimeSeconds % 60}s`;
133
+ } else {
134
+ uptimeFormatted = `${uptimeSeconds}s`;
135
+ }
136
+
137
+ return {
138
+ status: 'up',
139
+ message: `Server running for ${uptimeFormatted}`,
140
+ details: {
141
+ uptimeSeconds,
142
+ startTime: new Date(startTime).toISOString(),
143
+ },
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Perform comprehensive health check
149
+ */
150
+ export async function performHealthCheck(): Promise<HealthCheckResult> {
151
+ const [database, redis, memory, uptime] = await Promise.all([
152
+ checkDatabase(),
153
+ checkRedis(),
154
+ checkMemory(),
155
+ checkUptime(),
156
+ ]);
157
+
158
+ // Determine overall status
159
+ let status: 'healthy' | 'degraded' | 'unhealthy';
160
+ if (database.status === 'down') {
161
+ status = 'unhealthy'; // Critical: DB is required
162
+ } else if (
163
+ database.status === 'degraded' ||
164
+ redis.status === 'degraded' ||
165
+ memory.status === 'degraded'
166
+ ) {
167
+ status = 'degraded';
168
+ } else {
169
+ status = 'healthy';
170
+ }
171
+
172
+ return {
173
+ status,
174
+ checks: {
175
+ database,
176
+ redis,
177
+ memory,
178
+ uptime,
179
+ },
180
+ timestamp: new Date().toISOString(),
181
+ version: '1.0.0',
182
+ environment: env.NODE_ENV,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Simple readiness check (for load balancers)
188
+ */
189
+ export async function checkReadiness(): Promise<boolean> {
190
+ try {
191
+ // Only check database - critical dependency
192
+ await prisma.$queryRaw`SELECT 1`;
193
+ return true;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Simple liveness check (for container orchestration)
201
+ */
202
+ export function checkLiveness(): boolean {
203
+ // Server is alive if this function executes
204
+ return true;
205
+ }
@@ -0,0 +1,38 @@
1
+ import argon2 from 'argon2';
2
+
3
+ // OWASP recommended argon2id configuration
4
+ const ARGON2_OPTIONS: argon2.Options = {
5
+ type: argon2.argon2id,
6
+ memoryCost: 65536, // 64 MB
7
+ timeCost: 3,
8
+ parallelism: 4,
9
+ };
10
+
11
+ /**
12
+ * Hash a password using argon2id
13
+ */
14
+ export async function hashPassword(password: string): Promise<string> {
15
+ return argon2.hash(password, ARGON2_OPTIONS);
16
+ }
17
+
18
+ /**
19
+ * Verify a password against a hash.
20
+ * Supports both argon2 and legacy bcrypt hashes.
21
+ * Returns { valid, needsRehash } so the caller can transparently upgrade bcrypt hashes.
22
+ */
23
+ export async function verifyPassword(
24
+ password: string,
25
+ hash: string,
26
+ ): Promise<{ valid: boolean; needsRehash: boolean }> {
27
+ const isBcryptHash = hash.startsWith('$2a$') || hash.startsWith('$2b$') || hash.startsWith('$2y$');
28
+
29
+ if (isBcryptHash) {
30
+ // Lazy-import bcryptjs only for legacy hash verification
31
+ const bcrypt = await import('bcryptjs');
32
+ const valid = await bcrypt.default.compare(password, hash);
33
+ return { valid, needsRehash: valid }; // rehash only if password is correct
34
+ }
35
+
36
+ const valid = await argon2.verify(hash, password);
37
+ return { valid, needsRehash: false };
38
+ }
@@ -0,0 +1,68 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { logger } from '@libs/logger.js';
3
+ import { env } from '@config/env.js';
4
+
5
+ const globalForPrisma = globalThis as unknown as {
6
+ prisma: PrismaClient | undefined;
7
+ };
8
+
9
+ export const prisma =
10
+ globalForPrisma.prisma ??
11
+ new PrismaClient({
12
+ // Note: Connection pool size is configured via DATABASE_URL query params
13
+ // Example: DATABASE_URL="mysql://...?connection_limit=50&pool_timeout=10"
14
+ log:
15
+ env.NODE_ENV === 'development'
16
+ ? [
17
+ { emit: 'event', level: 'query' },
18
+ { emit: 'stdout', level: 'warn' },
19
+ { emit: 'stdout', level: 'error' },
20
+ ]
21
+ : [
22
+ { emit: 'stdout', level: 'error' },
23
+ // Log slow queries in production (queries taking > 2000ms)
24
+ { emit: 'event', level: 'query' },
25
+ ],
26
+ // Note: Connection pool size is configured via DATABASE_URL query params
27
+ // Example: DATABASE_URL="mysql://...?connection_limit=50&pool_timeout=10"
28
+ });
29
+
30
+ // Log slow queries in production (queries taking > 2000ms)
31
+ if (env.NODE_ENV === 'production') {
32
+ prisma.$on('query' as never, (e: { duration: number; query: string }) => {
33
+ if (e.duration > 2000) {
34
+ logger.warn(
35
+ { duration: e.duration, query: e.query },
36
+ `Slow query detected: ${e.duration}ms`,
37
+ );
38
+ }
39
+ });
40
+ }
41
+
42
+ // Log all queries in development for debugging
43
+ if (env.NODE_ENV === 'development') {
44
+ prisma.$on('query' as never, (e: { duration: number; query: string }) => {
45
+ logger.debug({ duration: e.duration, query: e.query }, 'Database query');
46
+ });
47
+ }
48
+
49
+ if (env.NODE_ENV !== 'production') {
50
+ globalForPrisma.prisma = prisma;
51
+ }
52
+
53
+ export async function testDatabaseConnection(): Promise<boolean> {
54
+ try {
55
+ await prisma.$queryRaw`SELECT 1`;
56
+ logger.info('[DATABASE] Connection established');
57
+ return true;
58
+ } catch (error) {
59
+ logger.warn('[DATABASE] Connection failed — server will start without DB');
60
+ logger.debug(error);
61
+ return false;
62
+ }
63
+ }
64
+
65
+ export async function disconnectPrisma(): Promise<void> {
66
+ await prisma.$disconnect();
67
+ logger.info('[DATABASE] Disconnected');
68
+ }
@@ -1,79 +1,60 @@
1
- /**
2
- * Redis Connection
3
- *
4
- * Redis client for caching, sessions, and BullMQ queues.
5
- *
6
- * @see /mnt/project/10-background-jobs-v2.md
7
- */
8
-
9
- import Redis from 'ioredis';
10
- import logger from './logger';
11
-
12
- /**
13
- * Create Redis client instance
14
- *
15
- * Configuration:
16
- * - maxRetriesPerRequest: null (REQUIRED for BullMQ)
17
- * - Automatic reconnection
18
- * - Error handling
19
- */
20
- const redis = new Redis({
21
- host: process.env['REDIS_HOST'] || 'localhost',
22
- port: parseInt(process.env['REDIS_PORT'] || '6379'),
23
- password: process.env['REDIS_PASSWORD'] || undefined,
24
- db: parseInt(process.env['REDIS_DB'] || '0'),
25
-
26
- // CRITICAL: Required for BullMQ compatibility
27
- maxRetriesPerRequest: null,
28
-
29
- // Retry strategy
30
- retryStrategy: () => {
31
- // Stop retrying immediately if connection fails
32
- logger.error('Redis connection failed. Running without Redis.');
33
- return null;
34
- },
35
-
36
- // Connection timeout
37
- connectTimeout: 10000,
38
-
39
- // Enable offline queue
40
- enableOfflineQueue: true,
41
- });
42
-
43
- /**
44
- * Connection Event Handlers
45
- */
46
- redis.on('connect', () => {
47
- logger.info('Redis connected');
48
- });
49
-
50
- redis.on('ready', () => {
51
- logger.info('Redis ready to accept commands');
52
- });
53
-
54
- redis.on('error', (error: Error) => {
55
- // Suppress ECONNREFUSED logs (connection refused) to prevent noise when Redis is offline
56
- if (error.message.includes('ECONNREFUSED') || error.stack?.includes('ECONNREFUSED')) {
57
- return;
58
- }
59
-
60
- logger.error(
61
- {
62
- error: error.message,
63
- stack: error.stack,
64
- },
65
- 'Redis connection error'
66
- );
67
- });
68
-
69
- redis.on('close', () => {
70
- logger.warn('Redis connection closed');
71
- });
72
-
73
- redis.on('reconnecting', () => {
74
- logger.info('Redis reconnecting...');
75
- });
76
-
77
-
78
-
79
- export { redis };
1
+ import { Redis } from 'ioredis';
2
+ import { env } from '@config/env.js';
3
+ import { logger } from '@libs/logger.js';
4
+
5
+ let redis: Redis | null = null;
6
+
7
+ export function getRedis(): Redis {
8
+ if (!redis) {
9
+ redis = new Redis(env.REDIS_URL, {
10
+ maxRetriesPerRequest: env.REDIS_MAX_RETRIES,
11
+ connectTimeout: env.REDIS_CONNECT_TIMEOUT,
12
+ lazyConnect: true,
13
+ retryStrategy: (times: number) => {
14
+ // Exponential backoff with max delay of 3 seconds
15
+ if (times > env.REDIS_MAX_RETRIES) {
16
+ // Stop retrying after max retries
17
+ logger.error('[REDIS] Max retries reached, giving up');
18
+ return null;
19
+ }
20
+ const delay = Math.min(times * 50, 3000);
21
+ logger.debug(`[REDIS] Retry attempt ${times}, waiting ${delay}ms`);
22
+ return delay;
23
+ },
24
+ });
25
+
26
+ redis.on('connect', () => {
27
+ logger.info('[REDIS] Connection established');
28
+ });
29
+
30
+ redis.on('error', (error: Error) => {
31
+ logger.warn({ err: error }, '[REDIS] Connection error');
32
+ });
33
+
34
+ redis.on('reconnecting', (delay: number) => {
35
+ logger.info({ delay }, '[REDIS] Reconnecting');
36
+ });
37
+ }
38
+
39
+ return redis;
40
+ }
41
+
42
+ export async function connectRedis(): Promise<boolean> {
43
+ try {
44
+ const client = getRedis();
45
+ await client.connect();
46
+ return true;
47
+ } catch (error) {
48
+ logger.warn('[REDIS] Connection failed — server will start without Redis');
49
+ logger.debug(error);
50
+ return false;
51
+ }
52
+ }
53
+
54
+ export async function disconnectRedis(): Promise<void> {
55
+ if (redis) {
56
+ await redis.quit();
57
+ redis = null;
58
+ logger.info('[REDIS] Disconnected');
59
+ }
60
+ }
@@ -0,0 +1,66 @@
1
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2
+ import { logger } from '@libs/logger.js';
3
+
4
+ const colors = {
5
+ // Methods: blue=read, green=create, yellow=update, red=destroy, gray=meta
6
+ GET: '\x1b[34m',
7
+ POST: '\x1b[32m',
8
+ PUT: '\x1b[33m',
9
+ PATCH: '\x1b[33m',
10
+ DELETE: '\x1b[31m',
11
+ OPTIONS: '\x1b[90m',
12
+ HEAD: '\x1b[90m',
13
+
14
+ // Status: green=ok, cyan=redirect, yellow=client error, red=server error
15
+ success: '\x1b[32m',
16
+ redirect: '\x1b[36m',
17
+ clientError: '\x1b[33m',
18
+ serverError: '\x1b[31m',
19
+
20
+ dim: '\x1b[90m',
21
+ reset: '\x1b[0m',
22
+ };
23
+
24
+ function getStatusColor(statusCode: number): string {
25
+ if (statusCode >= 500) return colors.serverError;
26
+ if (statusCode >= 400) return colors.clientError;
27
+ if (statusCode >= 300) return colors.redirect;
28
+ return colors.success;
29
+ }
30
+
31
+ function formatDuration(ms: number): string {
32
+ if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`;
33
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
34
+ return `${(ms / 1000).toFixed(2)}s`;
35
+ }
36
+
37
+ /**
38
+ * Mark request start time (called in preHandler)
39
+ */
40
+ export function markRequestStart(request: FastifyRequest): void {
41
+ request.startTime = Date.now();
42
+ }
43
+
44
+ /**
45
+ * Log a single-line request/response summary (called in onResponse)
46
+ *
47
+ * Format: GET /api/v1/auth/me 200 OK (12ms)
48
+ */
49
+ export function logRequestLine(request: FastifyRequest, reply: FastifyReply): void {
50
+ const duration = request.startTime ? Date.now() - request.startTime : 0;
51
+ const statusCode = reply.statusCode;
52
+ const statusMessage = reply.raw.statusMessage || '';
53
+
54
+ const methodColor = colors[request.method as keyof typeof colors] || colors.reset;
55
+ const statusColor = getStatusColor(statusCode);
56
+
57
+ const line = `${methodColor}${request.method.padEnd(7)}${colors.reset}${request.url} ${statusColor}${statusCode} ${statusMessage}${colors.reset} ${colors.dim}(${formatDuration(duration)})${colors.reset}`;
58
+
59
+ if (statusCode >= 500) {
60
+ logger.error(line);
61
+ } else if (statusCode >= 400) {
62
+ logger.warn(line);
63
+ } else {
64
+ logger.info(line);
65
+ }
66
+ }