create-tigra 1.1.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +80 -87
- package/bin/create-tigra.js +259 -308
- 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 +82 -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 -13
- package/template/server/IMPORT_FIX_CHECKLIST.md +0 -98
- package/template/server/IMPORT_FIX_COMPLETE.md +0 -89
- package/template/server/README.md +0 -183
- package/template/server/REMAINING_IMPORT_FIXES.md +0 -150
- 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/fix-all-imports.ps1 +0 -52
- package/template/server/scripts/fix-imports-reference.ps1 +0 -16
- package/template/server/scripts/fix-imports.mjs +0 -55
- 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,89 +0,0 @@
|
|
|
1
|
-
import { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
-
import { ZodError } from 'zod';
|
|
3
|
-
import { Prisma } from '@prisma/client';
|
|
4
|
-
import jsonwebtoken from 'jsonwebtoken';
|
|
5
|
-
import { env } from '../config/env.js';
|
|
6
|
-
import { AppError, InternalError } from '../utils/errors.js';
|
|
7
|
-
|
|
8
|
-
export function setupErrorHandler(error: FastifyError, request: FastifyRequest, reply: FastifyReply) {
|
|
9
|
-
// 1. Log the error internally
|
|
10
|
-
request.log.error({
|
|
11
|
-
err: error,
|
|
12
|
-
requestId: request.id,
|
|
13
|
-
userId: request.user?.userId,
|
|
14
|
-
path: request.url,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// 2. Handle known AppErrors
|
|
18
|
-
if (error instanceof AppError) {
|
|
19
|
-
return reply.status(error.statusCode).send({
|
|
20
|
-
success: false,
|
|
21
|
-
error: {
|
|
22
|
-
code: error.code,
|
|
23
|
-
message: error.message,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 3. Handle Zod Validation Errors
|
|
29
|
-
if (error instanceof ZodError) {
|
|
30
|
-
return reply.status(400).send({
|
|
31
|
-
success: false,
|
|
32
|
-
error: {
|
|
33
|
-
code: 'VALIDATION_ERROR',
|
|
34
|
-
message: error.errors[0]?.message || 'Invalid input data',
|
|
35
|
-
details: error.errors,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 4. Handle Prisma Database Errors
|
|
41
|
-
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
42
|
-
// P2002: Unique constraint failed
|
|
43
|
-
if (error.code === 'P2002') {
|
|
44
|
-
return reply.status(409).send({
|
|
45
|
-
success: false,
|
|
46
|
-
error: { code: 'CONFLICT', message: 'Resource already exists' },
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
// P2025: Record not found
|
|
50
|
-
if (error.code === 'P2025') {
|
|
51
|
-
return reply.status(404).send({
|
|
52
|
-
success: false,
|
|
53
|
-
error: { code: 'NOT_FOUND', message: 'Resource not found' },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 5. Handle JWT Errors explicitly
|
|
59
|
-
if (error instanceof jsonwebtoken.TokenExpiredError) {
|
|
60
|
-
return reply.status(401).send({
|
|
61
|
-
success: false,
|
|
62
|
-
error: { code: 'UNAUTHORIZED', message: 'Token expired' },
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (error instanceof jsonwebtoken.JsonWebTokenError) {
|
|
67
|
-
return reply.status(401).send({
|
|
68
|
-
success: false,
|
|
69
|
-
error: { code: 'UNAUTHORIZED', message: 'Invalid token' },
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 6. Handle Rate Limit Errors
|
|
74
|
-
if (reply.statusCode === 429) {
|
|
75
|
-
return reply.send(error); // Keep custom error from rate-limit plugin
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 7. Fallback: Internal Server Error
|
|
79
|
-
const internalError = new InternalError();
|
|
80
|
-
return reply.status(500).send({
|
|
81
|
-
success: false,
|
|
82
|
-
error: {
|
|
83
|
-
code: internalError.code,
|
|
84
|
-
message: env.NODE_ENV === 'production'
|
|
85
|
-
? 'An unexpected error occurred'
|
|
86
|
-
: error.message,
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Background Job Queues
|
|
3
|
-
*
|
|
4
|
-
* BullMQ queue configuration for asynchronous task processing.
|
|
5
|
-
*
|
|
6
|
-
* @see /mnt/project/10-background-jobs-v2.md
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Queue } from 'bullmq';
|
|
10
|
-
import { redis } from './redis.js';
|
|
11
|
-
import logger from './logger.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* File Processing Queue
|
|
15
|
-
*
|
|
16
|
-
* Handles file processing jobs (thumbnails, compression, format conversion, etc.)
|
|
17
|
-
*
|
|
18
|
-
* Job Types:
|
|
19
|
-
* - generate-thumbnail
|
|
20
|
-
* - compress-image
|
|
21
|
-
* - convert-format
|
|
22
|
-
* - process-upload
|
|
23
|
-
*/
|
|
24
|
-
export const fileQueue = new Queue('files', {
|
|
25
|
-
connection: redis as any,
|
|
26
|
-
|
|
27
|
-
defaultJobOptions: {
|
|
28
|
-
// Retry configuration
|
|
29
|
-
attempts: 3,
|
|
30
|
-
backoff: {
|
|
31
|
-
type: 'exponential',
|
|
32
|
-
delay: 2000, // Start with 2 seconds, then 4s, 8s
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
// Job cleanup
|
|
36
|
-
removeOnComplete: {
|
|
37
|
-
age: 3600, // Remove completed jobs after 1 hour
|
|
38
|
-
count: 100, // Keep max 100 completed jobs
|
|
39
|
-
},
|
|
40
|
-
removeOnFail: {
|
|
41
|
-
age: 86400, // Keep failed jobs for 24 hours
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Queue Event Handlers
|
|
50
|
-
*/
|
|
51
|
-
fileQueue.on('error', (error: Error) => {
|
|
52
|
-
// Suppress ECONNREFUSED logs during reconnection attempts to reduce noise
|
|
53
|
-
if (error.message.includes('ECONNREFUSED') || error.stack?.includes('ECONNREFUSED')) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
logger.error(
|
|
58
|
-
{
|
|
59
|
-
queue: 'files',
|
|
60
|
-
error: error.message,
|
|
61
|
-
stack: error.stack,
|
|
62
|
-
},
|
|
63
|
-
'File queue error'
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
fileQueue.on('waiting', (job) => {
|
|
68
|
-
logger.debug(
|
|
69
|
-
{
|
|
70
|
-
queue: 'files',
|
|
71
|
-
jobId: job.id,
|
|
72
|
-
},
|
|
73
|
-
'Job waiting in queue'
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
logger.info('File queue initialized');
|
|
78
|
-
|
|
79
|
-
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Controller
|
|
3
|
-
* HTTP request handlers for admin operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
7
|
-
import { successResponse, paginatedResponse } from '../../utils/response.js';
|
|
8
|
-
import { BadRequestError } from '../../utils/errors.js';
|
|
9
|
-
import * as adminService from './admin.service.js';
|
|
10
|
-
import type { ListUsersInput, UserIdInput, ChangeRoleInput } from './admin.schemas.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* GET /admin/users
|
|
14
|
-
* List all users with pagination
|
|
15
|
-
*/
|
|
16
|
-
export async function listAllUsers(
|
|
17
|
-
request: FastifyRequest<{ Querystring: ListUsersInput }>,
|
|
18
|
-
reply: FastifyReply
|
|
19
|
-
): Promise<FastifyReply> {
|
|
20
|
-
const { page, limit } = request.query;
|
|
21
|
-
|
|
22
|
-
const { users, totalCount } = await adminService.listAllUsers(page, limit);
|
|
23
|
-
|
|
24
|
-
return reply.status(200).send(
|
|
25
|
-
paginatedResponse('Users retrieved successfully', users, page, limit, totalCount)
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* GET /admin/users/:id
|
|
31
|
-
* Get user by ID
|
|
32
|
-
*/
|
|
33
|
-
export async function getUserById(
|
|
34
|
-
request: FastifyRequest<{ Params: UserIdInput }>,
|
|
35
|
-
reply: FastifyReply
|
|
36
|
-
): Promise<FastifyReply> {
|
|
37
|
-
const { id } = request.params;
|
|
38
|
-
|
|
39
|
-
const user = await adminService.getUserById(id);
|
|
40
|
-
|
|
41
|
-
return reply.status(200).send(
|
|
42
|
-
successResponse('User retrieved successfully', user)
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* DELETE /admin/users/:id
|
|
48
|
-
* Delete user
|
|
49
|
-
*/
|
|
50
|
-
export async function deleteUser(
|
|
51
|
-
request: FastifyRequest<{ Params: UserIdInput }>,
|
|
52
|
-
reply: FastifyReply
|
|
53
|
-
): Promise<FastifyReply> {
|
|
54
|
-
const { id } = request.params;
|
|
55
|
-
|
|
56
|
-
if (request.user?.userId === id) {
|
|
57
|
-
throw new BadRequestError('Cannot delete your own account');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
await adminService.deleteUser(id);
|
|
61
|
-
|
|
62
|
-
return reply.status(200).send(
|
|
63
|
-
successResponse('User deleted successfully', null)
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* POST /admin/users/:id/change-role
|
|
69
|
-
* Change user role
|
|
70
|
-
*/
|
|
71
|
-
export async function changeUserRole(
|
|
72
|
-
request: FastifyRequest<{
|
|
73
|
-
Params: UserIdInput;
|
|
74
|
-
Body: ChangeRoleInput;
|
|
75
|
-
}>,
|
|
76
|
-
reply: FastifyReply
|
|
77
|
-
): Promise<FastifyReply> {
|
|
78
|
-
const { id } = request.params;
|
|
79
|
-
const { role } = request.body;
|
|
80
|
-
|
|
81
|
-
if (request.user?.userId === id) {
|
|
82
|
-
throw new BadRequestError('Cannot change your own role');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const updated = await adminService.changeUserRole(id, role);
|
|
86
|
-
|
|
87
|
-
return reply.status(200).send(
|
|
88
|
-
successResponse('User role updated successfully', updated)
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* POST /admin/users/:id/verify-email
|
|
94
|
-
* Manually verify user email
|
|
95
|
-
*/
|
|
96
|
-
export async function verifyUserEmail(
|
|
97
|
-
request: FastifyRequest<{ Params: UserIdInput }>,
|
|
98
|
-
reply: FastifyReply
|
|
99
|
-
): Promise<FastifyReply> {
|
|
100
|
-
const { id } = request.params;
|
|
101
|
-
|
|
102
|
-
await adminService.verifyUserEmail(id);
|
|
103
|
-
|
|
104
|
-
return reply.status(200).send(
|
|
105
|
-
successResponse('User email verified successfully', null)
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* GET /admin/stats
|
|
111
|
-
* Get system statistics
|
|
112
|
-
*/
|
|
113
|
-
export async function getSystemStats(
|
|
114
|
-
request: FastifyRequest,
|
|
115
|
-
reply: FastifyReply
|
|
116
|
-
): Promise<FastifyReply> {
|
|
117
|
-
const stats = await adminService.getSystemStats();
|
|
118
|
-
|
|
119
|
-
return reply.status(200).send(
|
|
120
|
-
successResponse('System statistics retrieved successfully', stats)
|
|
121
|
-
);
|
|
122
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance } from 'fastify';
|
|
2
|
-
import * as adminController from './admin.controller.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Admin Routes
|
|
6
|
-
* All routes require ADMIN role
|
|
7
|
-
*/
|
|
8
|
-
export async function adminRoutes(app: FastifyInstance) {
|
|
9
|
-
/**
|
|
10
|
-
* GET /admin/users
|
|
11
|
-
* List all users with pagination
|
|
12
|
-
*/
|
|
13
|
-
app.get('/users', {
|
|
14
|
-
preHandler: [app.authenticate, app.requireAdmin()],
|
|
15
|
-
handler: adminController.listAllUsers,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* GET /admin/users/:id
|
|
20
|
-
* Get user details
|
|
21
|
-
*/
|
|
22
|
-
app.get('/users/:id', {
|
|
23
|
-
preHandler: [app.authenticate, app.requireAdmin()],
|
|
24
|
-
handler: adminController.getUserById,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* DELETE /admin/users/:id
|
|
29
|
-
* Delete user
|
|
30
|
-
*/
|
|
31
|
-
app.delete('/users/:id', {
|
|
32
|
-
preHandler: [app.authenticate, app.requireAdmin()],
|
|
33
|
-
handler: adminController.deleteUser,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* POST /admin/users/:id/change-role
|
|
38
|
-
* Change user role
|
|
39
|
-
*/
|
|
40
|
-
app.post('/users/:id/change-role', {
|
|
41
|
-
preHandler: [app.authenticate, app.requireAdmin()],
|
|
42
|
-
handler: adminController.changeUserRole,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* POST /admin/users/:id/verify-email
|
|
47
|
-
* Manually verify user email
|
|
48
|
-
*/
|
|
49
|
-
app.post('/users/:id/verify-email', {
|
|
50
|
-
preHandler: [app.authenticate, app.requireAdmin()],
|
|
51
|
-
handler: adminController.verifyUserEmail,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* GET /admin/stats
|
|
56
|
-
* Get system statistics
|
|
57
|
-
*/
|
|
58
|
-
app.get('/stats', {
|
|
59
|
-
preHandler: [app.authenticate, app.requireAdmin()],
|
|
60
|
-
handler: adminController.getSystemStats,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Module Validation Schemas
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* List users query parameters
|
|
9
|
-
*/
|
|
10
|
-
export const ListUsersSchema = z.object({
|
|
11
|
-
page: z.coerce.number().int().min(1).default(1),
|
|
12
|
-
limit: z.coerce.number().int().min(1).max(100).default(10),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export type ListUsersInput = z.infer<typeof ListUsersSchema>;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* User ID parameter
|
|
19
|
-
*/
|
|
20
|
-
export const UserIdSchema = z.object({
|
|
21
|
-
id: z.string().uuid('Invalid user ID format'),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export type UserIdInput = z.infer<typeof UserIdSchema>;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Change role request body
|
|
28
|
-
*/
|
|
29
|
-
export const ChangeRoleSchema = z.object({
|
|
30
|
-
role: z.enum(['USER', 'ADMIN'], {
|
|
31
|
-
errorMap: () => ({ message: 'Role must be USER or ADMIN' }),
|
|
32
|
-
}),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export type ChangeRoleInput = z.infer<typeof ChangeRoleSchema>;
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Service
|
|
3
|
-
* Business logic for admin operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { prisma } from '../../libs/db.js';
|
|
7
|
-
import { NotFoundError, BadRequestError } from '../../utils/errors.js';
|
|
8
|
-
import logger from '../../libs/logger.js';
|
|
9
|
-
import type { UserRole } from '@prisma/client';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* List all users with pagination
|
|
13
|
-
*/
|
|
14
|
-
export async function listAllUsers(page: number = 1, limit: number = 10) {
|
|
15
|
-
const skip = (page - 1) * limit;
|
|
16
|
-
|
|
17
|
-
const [users, totalCount] = await Promise.all([
|
|
18
|
-
prisma.user.findMany({
|
|
19
|
-
select: {
|
|
20
|
-
id: true,
|
|
21
|
-
email: true,
|
|
22
|
-
name: true,
|
|
23
|
-
role: true,
|
|
24
|
-
emailVerified: true,
|
|
25
|
-
createdAt: true,
|
|
26
|
-
updatedAt: true,
|
|
27
|
-
},
|
|
28
|
-
skip,
|
|
29
|
-
take: limit,
|
|
30
|
-
orderBy: { createdAt: 'desc' },
|
|
31
|
-
}),
|
|
32
|
-
prisma.user.count(),
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
return { users, totalCount };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get user by ID
|
|
40
|
-
*/
|
|
41
|
-
export async function getUserById(userId: string) {
|
|
42
|
-
const user = await prisma.user.findUnique({
|
|
43
|
-
where: { id: userId },
|
|
44
|
-
select: {
|
|
45
|
-
id: true,
|
|
46
|
-
email: true,
|
|
47
|
-
name: true,
|
|
48
|
-
role: true,
|
|
49
|
-
emailVerified: true,
|
|
50
|
-
createdAt: true,
|
|
51
|
-
updatedAt: true,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
if (!user) {
|
|
56
|
-
throw new NotFoundError('User not found');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return user;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Delete user
|
|
64
|
-
*/
|
|
65
|
-
export async function deleteUser(userId: string) {
|
|
66
|
-
const user = await prisma.user.findUnique({
|
|
67
|
-
where: { id: userId },
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
if (!user) {
|
|
71
|
-
throw new NotFoundError('User not found');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
await prisma.user.delete({
|
|
75
|
-
where: { id: userId },
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
logger.info({ userId }, 'User deleted by admin');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Change user role
|
|
83
|
-
*/
|
|
84
|
-
export async function changeUserRole(userId: string, newRole: UserRole) {
|
|
85
|
-
const user = await prisma.user.findUnique({
|
|
86
|
-
where: { id: userId },
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (!user) {
|
|
90
|
-
throw new NotFoundError('User not found');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const updated = await prisma.user.update({
|
|
94
|
-
where: { id: userId },
|
|
95
|
-
data: { role: newRole },
|
|
96
|
-
select: {
|
|
97
|
-
id: true,
|
|
98
|
-
email: true,
|
|
99
|
-
role: true,
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
logger.info({ userId, oldRole: user.role, newRole }, 'User role changed by admin');
|
|
104
|
-
|
|
105
|
-
return updated;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Manually verify user email
|
|
110
|
-
*/
|
|
111
|
-
export async function verifyUserEmail(userId: string) {
|
|
112
|
-
const user = await prisma.user.findUnique({
|
|
113
|
-
where: { id: userId },
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!user) {
|
|
117
|
-
throw new NotFoundError('User not found');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (user.emailVerified) {
|
|
121
|
-
throw new BadRequestError('Email already verified');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
await prisma.user.update({
|
|
125
|
-
where: { id: userId },
|
|
126
|
-
data: { emailVerified: true },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
logger.info({ userId }, 'User email verified by admin');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Get system statistics
|
|
134
|
-
*/
|
|
135
|
-
export async function getSystemStats() {
|
|
136
|
-
const [
|
|
137
|
-
totalUsers,
|
|
138
|
-
totalAdmins,
|
|
139
|
-
totalResources,
|
|
140
|
-
verifiedUsers,
|
|
141
|
-
usersToday,
|
|
142
|
-
] = await Promise.all([
|
|
143
|
-
prisma.user.count(),
|
|
144
|
-
prisma.user.count({ where: { role: 'ADMIN' } }),
|
|
145
|
-
prisma.resource.count(),
|
|
146
|
-
prisma.user.count({ where: { emailVerified: true } }),
|
|
147
|
-
prisma.user.count({
|
|
148
|
-
where: {
|
|
149
|
-
createdAt: {
|
|
150
|
-
gte: new Date(new Date().setHours(0, 0, 0, 0)),
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
}),
|
|
154
|
-
]);
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
users: {
|
|
158
|
-
total: totalUsers,
|
|
159
|
-
admins: totalAdmins,
|
|
160
|
-
verified: verifiedUsers,
|
|
161
|
-
registeredToday: usersToday,
|
|
162
|
-
},
|
|
163
|
-
resources: {
|
|
164
|
-
total: totalResources,
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import buildApp from '../../app';
|
|
3
|
-
import * as authService from './auth.service';
|
|
4
|
-
import type { FastifyInstance } from 'fastify';
|
|
5
|
-
|
|
6
|
-
// Mock auth service to avoid real DB calls
|
|
7
|
-
vi.mock('./auth.service');
|
|
8
|
-
|
|
9
|
-
// Mock db and redis to prevent connection attempts during integration tests
|
|
10
|
-
vi.mock('@/libs/db', () => ({
|
|
11
|
-
prisma: {
|
|
12
|
-
$connect: vi.fn(),
|
|
13
|
-
$disconnect: vi.fn(),
|
|
14
|
-
$on: vi.fn(),
|
|
15
|
-
user: { findUnique: vi.fn() },
|
|
16
|
-
$queryRaw: vi.fn(),
|
|
17
|
-
},
|
|
18
|
-
}));
|
|
19
|
-
vi.mock('@/libs/redis', () => ({
|
|
20
|
-
redis: {
|
|
21
|
-
get: vi.fn(),
|
|
22
|
-
set: vi.fn(),
|
|
23
|
-
setex: vi.fn(),
|
|
24
|
-
del: vi.fn(),
|
|
25
|
-
ping: vi.fn().mockResolvedValue('PONG'),
|
|
26
|
-
call: vi.fn(),
|
|
27
|
-
on: vi.fn(),
|
|
28
|
-
off: vi.fn(),
|
|
29
|
-
quit: vi.fn(),
|
|
30
|
-
incr: vi.fn().mockResolvedValue(1),
|
|
31
|
-
expire: vi.fn(),
|
|
32
|
-
ttl: vi.fn().mockResolvedValue(-1),
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
vi.mock('@/libs/logger', () => ({
|
|
37
|
-
default: {
|
|
38
|
-
info: vi.fn(),
|
|
39
|
-
error: vi.fn(),
|
|
40
|
-
warn: vi.fn(),
|
|
41
|
-
debug: vi.fn(),
|
|
42
|
-
fatal: vi.fn(),
|
|
43
|
-
child: vi.fn().mockReturnThis(),
|
|
44
|
-
},
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Integration tests are skipped because they require full Fastify app initialization
|
|
49
|
-
* with all plugins (rate-limit, etc.) which need complex mocking.
|
|
50
|
-
*
|
|
51
|
-
* For proper integration testing, use a test database and real dependencies.
|
|
52
|
-
* Unit tests (auth.service.test.ts, rbac.middleware.test.ts) provide adequate coverage.
|
|
53
|
-
*/
|
|
54
|
-
describe.skip('Auth Integration', () => {
|
|
55
|
-
let app: FastifyInstance;
|
|
56
|
-
|
|
57
|
-
beforeAll(async () => {
|
|
58
|
-
try {
|
|
59
|
-
app = await buildApp();
|
|
60
|
-
await app.ready();
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('Failed to build app:', error);
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
afterAll(async () => {
|
|
68
|
-
if (app) {
|
|
69
|
-
await app.close();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('POST /auth/register', () => {
|
|
74
|
-
it('should register a user', async () => {
|
|
75
|
-
const payload = {
|
|
76
|
-
email: 'newuser@example.com',
|
|
77
|
-
password: 'Password123!',
|
|
78
|
-
name: 'New User',
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const mockResponse = {
|
|
82
|
-
user: {
|
|
83
|
-
id: '123',
|
|
84
|
-
email: payload.email,
|
|
85
|
-
name: payload.name,
|
|
86
|
-
role: 'USER',
|
|
87
|
-
emailVerified: false,
|
|
88
|
-
createdAt: new Date(),
|
|
89
|
-
updatedAt: new Date(),
|
|
90
|
-
},
|
|
91
|
-
tokens: {
|
|
92
|
-
accessToken: 'access-token',
|
|
93
|
-
refreshToken: 'refresh-token',
|
|
94
|
-
expiresIn: 3600,
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
vi.mocked(authService.register).mockResolvedValue(mockResponse as any);
|
|
99
|
-
|
|
100
|
-
const response = await app.inject({
|
|
101
|
-
method: 'POST',
|
|
102
|
-
url: '/api/v1/auth/register',
|
|
103
|
-
payload,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
expect(response.statusCode).toBe(201);
|
|
107
|
-
expect(response.json().success).toBe(true);
|
|
108
|
-
expect(response.json().data.user.email).toBe(payload.email);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should validate input', async () => {
|
|
112
|
-
const response = await app.inject({
|
|
113
|
-
method: 'POST',
|
|
114
|
-
url: '/api/v1/auth/register',
|
|
115
|
-
payload: {
|
|
116
|
-
email: 'invalid-email',
|
|
117
|
-
password: 'short',
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
expect(response.statusCode).toBe(400);
|
|
122
|
-
expect(response.json().success).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('POST /auth/login', () => {
|
|
127
|
-
it('should login a user', async () => {
|
|
128
|
-
const payload = {
|
|
129
|
-
email: 'user@example.com',
|
|
130
|
-
password: 'Password123!',
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const mockResponse = {
|
|
134
|
-
user: { id: '123', email: payload.email, role: 'USER' },
|
|
135
|
-
tokens: { accessToken: 'token', refreshToken: 'refresh' },
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
vi.mocked(authService.login).mockResolvedValue(mockResponse as any);
|
|
139
|
-
|
|
140
|
-
const response = await app.inject({
|
|
141
|
-
method: 'POST',
|
|
142
|
-
url: '/api/v1/auth/login',
|
|
143
|
-
payload,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(response.statusCode).toBe(200);
|
|
147
|
-
expect(response.json().success).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
});
|