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.
- package/LICENSE +21 -21
- package/README.md +80 -87
- package/bin/create-tigra.js +242 -309
- package/package.json +49 -41
- package/template/_claude/QUICK_REFERENCE.md +193 -0
- package/template/_claude/README.md +53 -0
- package/template/_claude/commands/create-client.md +881 -0
- package/template/_claude/commands/create-server.md +383 -0
- package/template/_claude/rules/client/01-project-structure.md +133 -0
- package/template/_claude/rules/client/02-components-and-types.md +146 -0
- package/template/_claude/rules/client/03-data-and-state.md +156 -0
- package/template/_claude/rules/client/04-design-system.md +185 -0
- package/template/_claude/rules/client/05-security.md +55 -0
- package/template/_claude/rules/client/06-ux-checklist.md +81 -0
- package/template/_claude/rules/client/core.md +42 -0
- package/template/_claude/rules/global/core.md +77 -0
- package/template/_claude/rules/server/core.md +50 -0
- package/template/_claude/rules/server/database.md +124 -0
- package/template/_claude/rules/server/project-conventions.md +150 -0
- package/template/_claude/rules/server/response-handling.md +144 -0
- package/template/client/.env.example +5 -0
- package/template/client/README.md +36 -0
- package/template/client/components.json +23 -0
- package/template/client/eslint.config.mjs +18 -0
- package/template/client/next.config.ts +34 -0
- package/template/client/package.json +44 -0
- package/template/client/postcss.config.mjs +7 -0
- package/template/client/src/app/(auth)/layout.tsx +18 -0
- package/template/client/src/app/(auth)/login/page.tsx +13 -0
- package/template/client/src/app/(auth)/register/page.tsx +13 -0
- package/template/client/src/app/(main)/dashboard/page.tsx +22 -0
- package/template/client/src/app/(main)/layout.tsx +11 -0
- package/template/client/src/app/error.tsx +27 -0
- package/template/client/src/app/favicon.ico +0 -0
- package/template/client/src/app/globals.css +145 -0
- package/template/client/src/app/layout.tsx +36 -0
- package/template/client/src/app/loading.tsx +11 -0
- package/template/client/src/app/not-found.tsx +23 -0
- package/template/client/src/app/page.tsx +45 -0
- package/template/client/src/app/providers.tsx +43 -0
- package/template/client/src/components/common/ConfirmDialog.tsx +56 -0
- package/template/client/src/components/common/EmptyState.tsx +31 -0
- package/template/client/src/components/common/LoadingSpinner.tsx +30 -0
- package/template/client/src/components/common/Pagination.tsx +55 -0
- package/template/client/src/components/layout/Footer.tsx +17 -0
- package/template/client/src/components/layout/Header.tsx +173 -0
- package/template/client/src/components/layout/MainLayout.tsx +18 -0
- package/template/client/src/components/ui/alert-dialog.tsx +196 -0
- package/template/client/src/components/ui/badge.tsx +48 -0
- package/template/client/src/components/ui/button.tsx +64 -0
- package/template/client/src/components/ui/card.tsx +92 -0
- package/template/client/src/components/ui/input.tsx +21 -0
- package/template/client/src/components/ui/label.tsx +24 -0
- package/template/client/src/components/ui/select.tsx +190 -0
- package/template/client/src/components/ui/skeleton.tsx +13 -0
- package/template/client/src/components/ui/table.tsx +116 -0
- package/template/client/src/features/auth/components/AuthInitializer.tsx +55 -0
- package/template/client/src/features/auth/components/LoginForm.tsx +107 -0
- package/template/client/src/features/auth/components/RegisterForm.tsx +178 -0
- package/template/client/src/features/auth/hooks/useAuth.ts +84 -0
- package/template/client/src/features/auth/services/auth.service.ts +52 -0
- package/template/client/src/features/auth/store/authSlice.ts +38 -0
- package/template/client/src/features/auth/types/auth.types.ts +32 -0
- package/template/client/src/hooks/useDebounce.ts +14 -0
- package/template/client/src/hooks/useLocalStorage.ts +55 -0
- package/template/client/src/hooks/useMediaQuery.ts +27 -0
- package/template/client/src/lib/api/api.types.ts +34 -0
- package/template/client/src/lib/api/axios.config.ts +98 -0
- package/template/client/src/lib/constants/api-endpoints.ts +18 -0
- package/template/client/src/lib/constants/app.constants.ts +12 -0
- package/template/client/src/lib/constants/routes.ts +9 -0
- package/template/client/src/lib/utils/error.ts +32 -0
- package/template/client/src/lib/utils/format.ts +37 -0
- package/template/client/src/lib/utils/security.ts +34 -0
- package/template/client/src/lib/utils.ts +6 -0
- package/template/client/src/middleware.ts +57 -0
- package/template/client/src/store/hooks.ts +7 -0
- package/template/client/src/store/index.ts +12 -0
- package/template/client/src/types/index.ts +3 -0
- package/template/client/tsconfig.json +34 -0
- package/template/gitignore +34 -0
- package/template/server/.dockerignore +66 -0
- package/template/server/.env.example +96 -69
- package/template/server/.env.production.example +90 -0
- package/template/server/Dockerfile +94 -0
- package/template/server/docker-compose.yml +80 -111
- package/template/server/docs/logging.md +62 -0
- package/template/server/eslint.config.mjs +17 -0
- package/template/server/package.json +68 -81
- package/template/server/phpmyadmin-config.php +26 -0
- package/template/server/postman_collection.json +666 -0
- package/template/server/prisma/schema.prisma +77 -93
- package/template/server/prisma/seed.ts +46 -142
- package/template/server/scripts/flush-redis.ts +41 -0
- package/template/server/src/app.ts +243 -71
- package/template/server/src/config/env.ts +67 -94
- package/template/server/src/libs/auth.ts +88 -0
- package/template/server/src/libs/cleanup.ts +35 -0
- package/template/server/src/libs/cookies.ts +46 -0
- package/template/server/src/libs/logger.ts +33 -60
- package/template/server/src/libs/monitoring.ts +205 -0
- package/template/server/src/libs/password.ts +38 -0
- package/template/server/src/libs/prisma.ts +68 -0
- package/template/server/src/libs/redis.ts +60 -79
- package/template/server/src/libs/requestLogger.ts +66 -0
- package/template/server/src/libs/storage/file-storage.service.ts +211 -0
- package/template/server/src/libs/storage/file-validator.ts +97 -0
- package/template/server/src/libs/storage/filename-sanitizer.ts +71 -0
- package/template/server/src/libs/storage/image-optimizer.service.ts +144 -0
- package/template/server/src/modules/auth/__tests__/auth.service.test.ts +365 -0
- package/template/server/src/modules/auth/auth.controller.ts +90 -141
- package/template/server/src/modules/auth/auth.repo.ts +120 -218
- package/template/server/src/modules/auth/auth.routes.ts +96 -83
- package/template/server/src/modules/auth/auth.schemas.ts +35 -137
- package/template/server/src/modules/auth/auth.service.ts +286 -329
- package/template/server/src/modules/auth/session.repo.ts +110 -0
- package/template/server/src/modules/users/users.controller.ts +120 -0
- package/template/server/src/modules/users/users.repo.ts +77 -0
- package/template/server/src/modules/users/users.routes.ts +89 -0
- package/template/server/src/modules/users/users.schemas.ts +21 -0
- package/template/server/src/modules/users/users.service.ts +169 -0
- package/template/server/src/server.ts +58 -139
- package/template/server/src/shared/errors/AppError.ts +21 -0
- package/template/server/src/shared/errors/errors.ts +43 -0
- package/template/server/src/shared/responses/paginatedResponse.ts +38 -0
- package/template/server/src/shared/responses/successResponse.ts +17 -0
- package/template/server/src/shared/schemas/pagination.schema.ts +12 -0
- package/template/server/src/shared/types/index.ts +26 -0
- package/template/server/src/test/setup.ts +74 -38
- package/template/server/tsconfig.json +27 -89
- package/template/server/uploads/avatars/.gitkeep +1 -0
- package/template/server/vitest.config.ts +43 -98
- package/template/.agent/rules/client/01-project-structure.md +0 -326
- package/template/.agent/rules/client/02-component-patterns.md +0 -249
- package/template/.agent/rules/client/03-typescript-rules.md +0 -226
- package/template/.agent/rules/client/04-state-management.md +0 -474
- package/template/.agent/rules/client/05-api-integration.md +0 -129
- package/template/.agent/rules/client/06-forms-validation.md +0 -129
- package/template/.agent/rules/client/07-common-patterns.md +0 -150
- package/template/.agent/rules/client/08-color-system.md +0 -93
- package/template/.agent/rules/client/09-security-rules.md +0 -97
- package/template/.agent/rules/client/10-testing-strategy.md +0 -370
- package/template/.agent/rules/global/ai-edit-safety.md +0 -38
- package/template/.agent/rules/server/01-db-and-migrations.md +0 -242
- package/template/.agent/rules/server/02-general-rules.md +0 -111
- package/template/.agent/rules/server/03-migrations.md +0 -20
- package/template/.agent/rules/server/04-pagination.md +0 -130
- package/template/.agent/rules/server/05-project-conventions.md +0 -71
- package/template/.agent/rules/server/06-response-handling.md +0 -173
- package/template/.agent/rules/server/07-testing-strategy.md +0 -506
- package/template/.agent/rules/server/08-observability.md +0 -180
- package/template/.agent/rules/server/10-background-jobs-v2.md +0 -185
- package/template/.agent/rules/server/11-rate-limiting-v2.md +0 -210
- package/template/.agent/rules/server/12-performance-optimization.md +0 -567
- package/template/.claude/rules/client-01-project-structure.md +0 -327
- package/template/.claude/rules/client-02-component-patterns.md +0 -250
- package/template/.claude/rules/client-03-typescript-rules.md +0 -227
- package/template/.claude/rules/client-04-state-management.md +0 -475
- package/template/.claude/rules/client-05-api-integration.md +0 -130
- package/template/.claude/rules/client-06-forms-validation.md +0 -130
- package/template/.claude/rules/client-07-common-patterns.md +0 -151
- package/template/.claude/rules/client-08-color-system.md +0 -94
- package/template/.claude/rules/client-09-security-rules.md +0 -98
- package/template/.claude/rules/client-10-testing-strategy.md +0 -371
- package/template/.claude/rules/global-ai-edit-safety.md +0 -39
- package/template/.claude/rules/server-01-db-and-migrations.md +0 -243
- package/template/.claude/rules/server-02-general-rules.md +0 -112
- package/template/.claude/rules/server-03-migrations.md +0 -21
- package/template/.claude/rules/server-04-pagination.md +0 -131
- package/template/.claude/rules/server-05-project-conventions.md +0 -72
- package/template/.claude/rules/server-06-response-handling.md +0 -174
- package/template/.claude/rules/server-07-testing-strategy.md +0 -507
- package/template/.claude/rules/server-08-observability.md +0 -181
- package/template/.claude/rules/server-10-background-jobs-v2.md +0 -186
- package/template/.claude/rules/server-11-rate-limiting-v2.md +0 -211
- package/template/.claude/rules/server-12-performance-optimization.md +0 -568
- package/template/.cursor/rules/client-01-project-structure.mdc +0 -327
- package/template/.cursor/rules/client-02-component-patterns.mdc +0 -250
- package/template/.cursor/rules/client-03-typescript-rules.mdc +0 -227
- package/template/.cursor/rules/client-04-state-management.mdc +0 -475
- package/template/.cursor/rules/client-05-api-integration.mdc +0 -130
- package/template/.cursor/rules/client-06-forms-validation.mdc +0 -130
- package/template/.cursor/rules/client-07-common-patterns.mdc +0 -151
- package/template/.cursor/rules/client-08-color-system.mdc +0 -94
- package/template/.cursor/rules/client-09-security-rules.mdc +0 -98
- package/template/.cursor/rules/client-10-testing-strategy.mdc +0 -371
- package/template/.cursor/rules/global-ai-edit-safety.mdc +0 -39
- package/template/.cursor/rules/server-01-db-and-migrations.mdc +0 -243
- package/template/.cursor/rules/server-02-general-rules.mdc +0 -112
- package/template/.cursor/rules/server-03-migrations.mdc +0 -21
- package/template/.cursor/rules/server-04-pagination.mdc +0 -131
- package/template/.cursor/rules/server-05-project-conventions.mdc +0 -72
- package/template/.cursor/rules/server-06-response-handling.mdc +0 -174
- package/template/.cursor/rules/server-07-testing-strategy.mdc +0 -507
- package/template/.cursor/rules/server-08-observability.mdc +0 -181
- package/template/.cursor/rules/server-09-api-documentation-v2.mdc +0 -169
- package/template/.cursor/rules/server-10-background-jobs-v2.mdc +0 -186
- package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +0 -211
- package/template/.cursor/rules/server-12-performance-optimization.mdc +0 -568
- package/template/CLAUDE.md +0 -207
- package/template/server/.tsc-aliasrc.json +0 -12
- package/template/server/README.md +0 -183
- package/template/server/SECURITY.md +0 -190
- package/template/server/Tigra-API.postman_collection.json +0 -733
- package/template/server/biome.json +0 -42
- package/template/server/scripts/setup-env.js +0 -50
- package/template/server/scripts/wait-for-db.js +0 -60
- package/template/server/src/hooks/request-timing.hook.ts +0 -26
- package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
- package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
- package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
- package/template/server/src/libs/db.ts +0 -76
- package/template/server/src/libs/error-handler.ts +0 -89
- package/template/server/src/libs/queue.ts +0 -79
- package/template/server/src/modules/admin/admin.controller.ts +0 -122
- package/template/server/src/modules/admin/admin.routes.ts +0 -62
- package/template/server/src/modules/admin/admin.schemas.ts +0 -35
- package/template/server/src/modules/admin/admin.service.ts +0 -167
- package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
- package/template/server/src/modules/auth/auth.service.test.ts +0 -119
- package/template/server/src/modules/auth/auth.types.ts +0 -97
- package/template/server/src/modules/resources/resources.controller.ts +0 -218
- package/template/server/src/modules/resources/resources.repo.ts +0 -253
- package/template/server/src/modules/resources/resources.routes.ts +0 -116
- package/template/server/src/modules/resources/resources.schemas.ts +0 -146
- package/template/server/src/modules/resources/resources.service.ts +0 -218
- package/template/server/src/modules/resources/resources.types.ts +0 -73
- package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
- package/template/server/src/plugins/security.plugin.ts +0 -21
- package/template/server/src/routes/health.routes.ts +0 -31
- package/template/server/src/types/fastify.d.ts +0 -36
- package/template/server/src/utils/errors.ts +0 -108
- package/template/server/src/utils/pagination.ts +0 -120
- package/template/server/src/utils/response.ts +0 -110
- package/template/server/src/workers/file.worker.ts +0 -106
- package/template/server/tsconfig.build.json +0 -30
- package/template/server/tsconfig.test.json +0 -22
|
@@ -1,60 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|