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,139 +1,58 @@
|
|
|
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
|
-
|
|
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
|
-
// Use console.log for shutdown messages to ensure they are printed
|
|
61
|
-
// even if the async logger is terminated early.
|
|
62
|
-
console.log(`\nReceived ${signal}. Starting graceful shutdown...`);
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
await app.close();
|
|
66
|
-
console.log('HTTP server closed');
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.error('Error closing HTTP server:', err);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
await prisma.$disconnect();
|
|
73
|
-
console.log('Database connection closed');
|
|
74
|
-
} catch (err) {
|
|
75
|
-
console.error('Error closing database connection:', err);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Close Redis-dependent services first, then Redis itself
|
|
79
|
-
// fileQueue uses the shared redis connection, so close it before redis
|
|
80
|
-
try {
|
|
81
|
-
await fileQueue.close();
|
|
82
|
-
console.log('File queue connection closed');
|
|
83
|
-
} catch (err) {
|
|
84
|
-
// Ignore - queue may already be disconnected if Redis failed
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Close Redis connection - use disconnect() for immediate close
|
|
88
|
-
// quit() can hang or reject if connection is already closed by BullMQ
|
|
89
|
-
try {
|
|
90
|
-
// Check if Redis is still connected before attempting to close
|
|
91
|
-
if (redis.status === 'ready' || redis.status === 'connect') {
|
|
92
|
-
await redis.quit();
|
|
93
|
-
console.log('Redis connection closed');
|
|
94
|
-
} else {
|
|
95
|
-
redis.disconnect();
|
|
96
|
-
console.log('Redis connection closed (was not connected)');
|
|
97
|
-
}
|
|
98
|
-
} catch (err) {
|
|
99
|
-
// Force disconnect if quit fails
|
|
100
|
-
try { redis.disconnect(); } catch (e) { /* ignore */ }
|
|
101
|
-
console.log('Redis connection closed (forced)');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
console.log('Graceful shutdown complete. Goodbye');
|
|
105
|
-
|
|
106
|
-
// Give a small grace period for any pending logs/events to flush
|
|
107
|
-
setTimeout(() => {
|
|
108
|
-
process.exit(0);
|
|
109
|
-
}, 500);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Listen for termination signals
|
|
113
|
-
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
114
|
-
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
115
|
-
|
|
116
|
-
} catch (error) {
|
|
117
|
-
logger.fatal({
|
|
118
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
119
|
-
stack: error instanceof Error ? error.stack : undefined
|
|
120
|
-
}, 'Failed to start server');
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Global Rejection Handler for Uncaught Promises
|
|
126
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
127
|
-
// During shutdown, BullMQ/ioredis may emit unhandled rejections
|
|
128
|
-
// as they try to perform operations on closed connections.
|
|
129
|
-
// These are expected and safe to ignore during graceful shutdown.
|
|
130
|
-
if (isShuttingDown) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
logger.fatal({ promise, reason }, 'Unhandled Rejection at Promise');
|
|
135
|
-
process.exit(1);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Run the server
|
|
139
|
-
startServer();
|
|
1
|
+
import { env } from '@config/env.js';
|
|
2
|
+
import { logger } from '@libs/logger.js';
|
|
3
|
+
import { testDatabaseConnection, disconnectPrisma } from '@libs/prisma.js';
|
|
4
|
+
import { connectRedis, disconnectRedis } from '@libs/redis.js';
|
|
5
|
+
import { buildApp } from './app.js';
|
|
6
|
+
|
|
7
|
+
async function start(): Promise<void> {
|
|
8
|
+
let app: Awaited<ReturnType<typeof buildApp>> | null = null;
|
|
9
|
+
let isShuttingDown = false;
|
|
10
|
+
|
|
11
|
+
// Graceful shutdown
|
|
12
|
+
const shutdown = async (signal: string): Promise<void> => {
|
|
13
|
+
if (isShuttingDown) return; // Prevent multiple shutdown attempts
|
|
14
|
+
isShuttingDown = true;
|
|
15
|
+
|
|
16
|
+
logger.info(`[SERVER] Received ${signal} — shutting down gracefully`);
|
|
17
|
+
try {
|
|
18
|
+
if (app) {
|
|
19
|
+
await app.close();
|
|
20
|
+
}
|
|
21
|
+
await disconnectRedis();
|
|
22
|
+
await disconnectPrisma();
|
|
23
|
+
logger.info('[SERVER] Shutdown complete');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error(error, '[SERVER] Error during shutdown');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Register signal handlers early
|
|
32
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
33
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
34
|
+
process.on('uncaughtException', (error) => {
|
|
35
|
+
logger.fatal(error, '[SERVER] Uncaught exception');
|
|
36
|
+
shutdown('uncaughtException');
|
|
37
|
+
});
|
|
38
|
+
process.on('unhandledRejection', (reason) => {
|
|
39
|
+
logger.fatal(reason, '[SERVER] Unhandled rejection');
|
|
40
|
+
shutdown('unhandledRejection');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Test external connections (non-fatal)
|
|
45
|
+
await testDatabaseConnection();
|
|
46
|
+
await connectRedis();
|
|
47
|
+
|
|
48
|
+
app = await buildApp();
|
|
49
|
+
|
|
50
|
+
await app.listen({ port: env.PORT, host: env.HOST });
|
|
51
|
+
logger.info(`[SERVER] Started on http://${env.HOST}:${env.PORT} [${env.NODE_ENV}]`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.fatal(error, '[SERVER] Failed to start');
|
|
54
|
+
await shutdown('startup-error');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
start();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class AppError extends Error {
|
|
2
|
+
public readonly isAppError = true;
|
|
3
|
+
public readonly code: string;
|
|
4
|
+
public readonly statusCode: number;
|
|
5
|
+
|
|
6
|
+
constructor(code: string, message: string, statusCode: number) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.name = this.constructor.name;
|
|
11
|
+
Error.captureStackTrace(this, this.constructor);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isAppError(error: unknown): error is AppError {
|
|
16
|
+
return (
|
|
17
|
+
error instanceof Error &&
|
|
18
|
+
'isAppError' in error &&
|
|
19
|
+
(error as AppError).isAppError === true
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AppError } from './AppError.js';
|
|
2
|
+
|
|
3
|
+
export class BadRequestError extends AppError {
|
|
4
|
+
constructor(message = 'Bad request', code = 'BAD_REQUEST') {
|
|
5
|
+
super(code, message, 400);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ValidationError extends AppError {
|
|
10
|
+
constructor(message = 'Validation failed', code = 'VALIDATION_FAILED') {
|
|
11
|
+
super(code, message, 422);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class UnauthorizedError extends AppError {
|
|
16
|
+
constructor(message = 'Unauthorized', code = 'UNAUTHORIZED') {
|
|
17
|
+
super(code, message, 401);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ForbiddenError extends AppError {
|
|
22
|
+
constructor(message = 'Forbidden', code = 'FORBIDDEN') {
|
|
23
|
+
super(code, message, 403);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NotFoundError extends AppError {
|
|
28
|
+
constructor(message = 'Resource not found', code = 'NOT_FOUND') {
|
|
29
|
+
super(code, message, 404);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class ConflictError extends AppError {
|
|
34
|
+
constructor(message = 'Resource already exists', code = 'CONFLICT') {
|
|
35
|
+
super(code, message, 409);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class InternalError extends AppError {
|
|
40
|
+
constructor(message = 'Internal server error', code = 'INTERNAL_ERROR') {
|
|
41
|
+
super(code, message, 500);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function paginatedResponse<T>(
|
|
2
|
+
message: string,
|
|
3
|
+
items: T[],
|
|
4
|
+
page: number,
|
|
5
|
+
limit: number,
|
|
6
|
+
totalItems: number,
|
|
7
|
+
): {
|
|
8
|
+
success: true;
|
|
9
|
+
message: string;
|
|
10
|
+
data: {
|
|
11
|
+
items: T[];
|
|
12
|
+
pagination: {
|
|
13
|
+
page: number;
|
|
14
|
+
limit: number;
|
|
15
|
+
totalItems: number;
|
|
16
|
+
totalPages: number;
|
|
17
|
+
hasNextPage: boolean;
|
|
18
|
+
hasPreviousPage: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
} {
|
|
22
|
+
const totalPages = Math.ceil(totalItems / limit);
|
|
23
|
+
return {
|
|
24
|
+
success: true as const,
|
|
25
|
+
message,
|
|
26
|
+
data: {
|
|
27
|
+
items,
|
|
28
|
+
pagination: {
|
|
29
|
+
page,
|
|
30
|
+
limit,
|
|
31
|
+
totalItems,
|
|
32
|
+
totalPages,
|
|
33
|
+
hasNextPage: page < totalPages,
|
|
34
|
+
hasPreviousPage: page > 1,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function successResponse<T>(message: string, data: T): {
|
|
2
|
+
success: true;
|
|
3
|
+
message: string;
|
|
4
|
+
data: T;
|
|
5
|
+
} {
|
|
6
|
+
return { success: true as const, message, data };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function errorResponse(code: string, message: string): {
|
|
10
|
+
success: false;
|
|
11
|
+
error: { code: string; message: string };
|
|
12
|
+
} {
|
|
13
|
+
return {
|
|
14
|
+
success: false as const,
|
|
15
|
+
error: { code, message },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const PaginationSchema = z.object({
|
|
4
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
5
|
+
limit: z.coerce.number().int().min(1).max(100).default(10),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type PaginationInput = z.infer<typeof PaginationSchema>;
|
|
9
|
+
|
|
10
|
+
export function calculateOffset(page: number, limit: number): number {
|
|
11
|
+
return (page - 1) * limit;
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
|
|
3
|
+
export type UserRole = 'USER' | 'ADMIN';
|
|
4
|
+
|
|
5
|
+
export interface JwtPayload {
|
|
6
|
+
userId: string;
|
|
7
|
+
role: UserRole;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module '@fastify/jwt' {
|
|
11
|
+
interface FastifyJWT {
|
|
12
|
+
payload: JwtPayload;
|
|
13
|
+
user: JwtPayload;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module 'fastify' {
|
|
18
|
+
interface FastifyRequest {
|
|
19
|
+
user: JwtPayload;
|
|
20
|
+
startTime?: number; // Added for duration calculation
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AuthenticatedRequest extends FastifyRequest {
|
|
25
|
+
user: JwtPayload;
|
|
26
|
+
}
|
|
@@ -1,38 +1,74 @@
|
|
|
1
|
-
import { vi
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
process.env
|
|
6
|
-
process.env
|
|
7
|
-
process.env
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock environment variables for testing
|
|
4
|
+
process.env.NODE_ENV = 'test';
|
|
5
|
+
process.env.DATABASE_URL = 'mysql://test:test@localhost:3306/test_db';
|
|
6
|
+
process.env.REDIS_URL = 'redis://localhost:6379';
|
|
7
|
+
process.env.JWT_SECRET = 'test-jwt-secret-must-be-at-least-32-characters-long';
|
|
8
|
+
process.env.JWT_ACCESS_EXPIRY = '15m';
|
|
9
|
+
process.env.JWT_REFRESH_EXPIRY = '7d';
|
|
10
|
+
process.env.PORT = '3000';
|
|
11
|
+
process.env.HOST = '0.0.0.0';
|
|
12
|
+
|
|
13
|
+
// Test user data
|
|
14
|
+
export const testUsers = {
|
|
15
|
+
validUser: {
|
|
16
|
+
id: 'test-user-id-1',
|
|
17
|
+
email: 'test@example.com',
|
|
18
|
+
password: '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYiIJU6u1Em', // hashed "Password123!"
|
|
19
|
+
firstName: 'Test',
|
|
20
|
+
lastName: 'User',
|
|
21
|
+
avatarUrl: null,
|
|
22
|
+
role: 'USER' as const,
|
|
23
|
+
isActive: true,
|
|
24
|
+
deletedAt: null,
|
|
25
|
+
failedLoginAttempts: 0,
|
|
26
|
+
lockedUntil: null,
|
|
27
|
+
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
28
|
+
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
|
29
|
+
},
|
|
30
|
+
inactiveUser: {
|
|
31
|
+
id: 'test-user-id-2',
|
|
32
|
+
email: 'inactive@example.com',
|
|
33
|
+
password: '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYiIJU6u1Em',
|
|
34
|
+
firstName: 'Inactive',
|
|
35
|
+
lastName: 'User',
|
|
36
|
+
avatarUrl: null,
|
|
37
|
+
role: 'USER' as const,
|
|
38
|
+
isActive: false,
|
|
39
|
+
deletedAt: null,
|
|
40
|
+
failedLoginAttempts: 0,
|
|
41
|
+
lockedUntil: null,
|
|
42
|
+
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
43
|
+
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
|
44
|
+
},
|
|
45
|
+
adminUser: {
|
|
46
|
+
id: 'test-user-id-3',
|
|
47
|
+
email: 'admin@example.com',
|
|
48
|
+
password: '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYiIJU6u1Em',
|
|
49
|
+
firstName: 'Admin',
|
|
50
|
+
lastName: 'User',
|
|
51
|
+
avatarUrl: null,
|
|
52
|
+
role: 'ADMIN' as const,
|
|
53
|
+
isActive: true,
|
|
54
|
+
deletedAt: null,
|
|
55
|
+
failedLoginAttempts: 0,
|
|
56
|
+
lockedUntil: null,
|
|
57
|
+
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
58
|
+
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Test refresh token data
|
|
63
|
+
export const testRefreshToken = {
|
|
64
|
+
id: 'test-token-id-1',
|
|
65
|
+
token: 'test-refresh-token-uuid',
|
|
66
|
+
userId: testUsers.validUser.id,
|
|
67
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
|
|
68
|
+
createdAt: new Date(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Reset all mocks before each test
|
|
72
|
+
export const resetMocks = (): void => {
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
};
|
|
@@ -1,89 +1,27 @@
|
|
|
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
|
-
"strictFunctionTypes": true,
|
|
29
|
-
"strictBindCallApply": true,
|
|
30
|
-
"strictPropertyInitialization": true,
|
|
31
|
-
"noImplicitThis": true,
|
|
32
|
-
"alwaysStrict": true,
|
|
33
|
-
"noUnusedLocals": true,
|
|
34
|
-
"noUnusedParameters": true,
|
|
35
|
-
"noImplicitReturns": true,
|
|
36
|
-
"noFallthroughCasesInSwitch": true,
|
|
37
|
-
"noUncheckedIndexedAccess": true,
|
|
38
|
-
"noImplicitOverride": true,
|
|
39
|
-
"noPropertyAccessFromIndexSignature": true,
|
|
40
|
-
/* Completeness */
|
|
41
|
-
"skipLibCheck": true,
|
|
42
|
-
"skipDefaultLibCheck": true,
|
|
43
|
-
/* Type Resolution */
|
|
44
|
-
"types": [],
|
|
45
|
-
/* Performance */
|
|
46
|
-
"incremental": true,
|
|
47
|
-
/* Path Mapping */
|
|
48
|
-
"baseUrl": ".",
|
|
49
|
-
"paths": {
|
|
50
|
-
"@/*": [
|
|
51
|
-
"src/*"
|
|
52
|
-
],
|
|
53
|
-
"@/config/*": [
|
|
54
|
-
"src/config/*"
|
|
55
|
-
],
|
|
56
|
-
"@/libs/*": [
|
|
57
|
-
"src/libs/*"
|
|
58
|
-
],
|
|
59
|
-
"@/modules/*": [
|
|
60
|
-
"src/modules/*"
|
|
61
|
-
],
|
|
62
|
-
"@/utils/*": [
|
|
63
|
-
"src/utils/*"
|
|
64
|
-
],
|
|
65
|
-
"@/types/*": [
|
|
66
|
-
"src/types/*"
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
/* Advanced */
|
|
70
|
-
"experimentalDecorators": true,
|
|
71
|
-
"emitDecoratorMetadata": true
|
|
72
|
-
},
|
|
73
|
-
"include": [
|
|
74
|
-
"src/**/*"
|
|
75
|
-
],
|
|
76
|
-
"exclude": [
|
|
77
|
-
"node_modules",
|
|
78
|
-
"dist",
|
|
79
|
-
"**/*.test.ts",
|
|
80
|
-
"**/*.spec.ts"
|
|
81
|
-
],
|
|
82
|
-
"ts-node": {
|
|
83
|
-
"require": [
|
|
84
|
-
"tsconfig-paths/register"
|
|
85
|
-
],
|
|
86
|
-
"transpileOnly": true,
|
|
87
|
-
"files": true
|
|
88
|
-
}
|
|
89
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "dist",
|
|
15
|
+
"rootDir": "src",
|
|
16
|
+
"baseUrl": ".",
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"],
|
|
19
|
+
"@modules/*": ["./src/modules/*"],
|
|
20
|
+
"@libs/*": ["./src/libs/*"],
|
|
21
|
+
"@config/*": ["./src/config/*"],
|
|
22
|
+
"@shared/*": ["./src/shared/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["src/**/*"],
|
|
26
|
+
"exclude": ["node_modules", "dist"]
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|