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,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
|
3
|
-
"organizeImports": {
|
|
4
|
-
"enabled": true
|
|
5
|
-
},
|
|
6
|
-
"linter": {
|
|
7
|
-
"enabled": true,
|
|
8
|
-
"rules": {
|
|
9
|
-
"recommended": true,
|
|
10
|
-
"complexity": {
|
|
11
|
-
"noForEach": "off"
|
|
12
|
-
},
|
|
13
|
-
"suspicious": {
|
|
14
|
-
"noExplicitAny": "warn"
|
|
15
|
-
},
|
|
16
|
-
"style": {
|
|
17
|
-
"noNonNullAssertion": "off"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"formatter": {
|
|
22
|
-
"enabled": true,
|
|
23
|
-
"indentStyle": "space",
|
|
24
|
-
"indentWidth": 4,
|
|
25
|
-
"lineWidth": 100
|
|
26
|
-
},
|
|
27
|
-
"javascript": {
|
|
28
|
-
"formatter": {
|
|
29
|
-
"quoteStyle": "single",
|
|
30
|
-
"trailingCommas": "es5",
|
|
31
|
-
"semicolons": "always"
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"files": {
|
|
35
|
-
"ignore": [
|
|
36
|
-
"node_modules",
|
|
37
|
-
"dist",
|
|
38
|
-
"coverage",
|
|
39
|
-
"*.json"
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Setup environment file
|
|
5
|
-
* Copies .env.example to .env if .env doesn't exist
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import fs from 'fs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import { dirname } from 'path';
|
|
12
|
-
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = dirname(__filename);
|
|
15
|
-
|
|
16
|
-
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
17
|
-
const ENV_FILE = path.join(ROOT_DIR, '.env');
|
|
18
|
-
const EXAMPLE_ENV_FILE = path.join(ROOT_DIR, '.env.example');
|
|
19
|
-
|
|
20
|
-
function setupEnv() {
|
|
21
|
-
// Check if .env already exists
|
|
22
|
-
if (fs.existsSync(ENV_FILE)) {
|
|
23
|
-
console.log('✓ .env file already exists');
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Check if .env.example exists
|
|
28
|
-
if (!fs.existsSync(EXAMPLE_ENV_FILE)) {
|
|
29
|
-
console.error('✗ .env.example file not found!');
|
|
30
|
-
console.error(' Please create a .env.example file first');
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Copy .env.example to .env
|
|
35
|
-
try {
|
|
36
|
-
fs.copyFileSync(EXAMPLE_ENV_FILE, ENV_FILE);
|
|
37
|
-
console.log('✓ Created .env file from .env.example');
|
|
38
|
-
console.log('');
|
|
39
|
-
console.log('⚠ IMPORTANT: Please update the following values in .env:');
|
|
40
|
-
console.log(' - DATABASE_URL (replace {{DATABASE_PASSWORD}} and {{DATABASE_NAME}})');
|
|
41
|
-
console.log(' - JWT_SECRET (use a strong secret in production)');
|
|
42
|
-
console.log(' - ADMIN_EMAIL and ADMIN_PASSWORD');
|
|
43
|
-
console.log('');
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.error('✗ Failed to create .env file:', error.message);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
setupEnv();
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Wait for database to be ready before running migrations
|
|
5
|
-
* This script polls the database connection until it's available
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { createConnection } from 'mysql2/promise';
|
|
9
|
-
import { config } from 'dotenv';
|
|
10
|
-
import { resolve } from 'path';
|
|
11
|
-
|
|
12
|
-
// Load environment variables
|
|
13
|
-
config({ path: resolve(process.cwd(), '.env') });
|
|
14
|
-
|
|
15
|
-
const MAX_ATTEMPTS = 30; // 30 attempts
|
|
16
|
-
const RETRY_DELAY = 2000; // 2 seconds between attempts
|
|
17
|
-
|
|
18
|
-
async function waitForDatabase() {
|
|
19
|
-
const dbConfig = {
|
|
20
|
-
host: process.env.DB_HOST || 'localhost',
|
|
21
|
-
port: parseInt(process.env.DB_PORT || '3306'),
|
|
22
|
-
user: process.env.DB_USER || 'root',
|
|
23
|
-
password: process.env.DB_PASSWORD || 'password',
|
|
24
|
-
database: process.env.DB_NAME,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
console.log('Waiting for database to be ready...');
|
|
28
|
-
console.log(` Host: ${dbConfig.host}:${dbConfig.port}`);
|
|
29
|
-
console.log(` Database: ${dbConfig.database}`);
|
|
30
|
-
console.log();
|
|
31
|
-
|
|
32
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
33
|
-
try {
|
|
34
|
-
const connection = await createConnection(dbConfig);
|
|
35
|
-
await connection.ping();
|
|
36
|
-
await connection.end();
|
|
37
|
-
|
|
38
|
-
console.log('Database is ready!');
|
|
39
|
-
console.log();
|
|
40
|
-
process.exit(0);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
if (attempt === MAX_ATTEMPTS) {
|
|
43
|
-
console.error('Database failed to become ready after maximum attempts');
|
|
44
|
-
console.error(` Error: ${error.message}`);
|
|
45
|
-
console.error();
|
|
46
|
-
console.error('Troubleshooting:');
|
|
47
|
-
console.error(' 1. Ensure Docker is running: docker ps');
|
|
48
|
-
console.error(' 2. Check if containers are healthy: docker-compose ps');
|
|
49
|
-
console.error(' 3. View MySQL logs: docker-compose logs mysql');
|
|
50
|
-
console.error(' 4. Restart containers: docker-compose restart');
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
process.stdout.write(` Attempt ${attempt}/${MAX_ATTEMPTS} - Database not ready yet, retrying in ${RETRY_DELAY / 1000}s...\r`);
|
|
55
|
-
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
waitForDatabase();
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance } from 'fastify';
|
|
2
|
-
import { env } from '@/config/env';
|
|
3
|
-
|
|
4
|
-
export function registerRequestHooks(app: FastifyInstance) {
|
|
5
|
-
app.addHook('onRequest', async (request) => {
|
|
6
|
-
request.startTime = Date.now();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
app.addHook('onResponse', async (request, reply) => {
|
|
10
|
-
const duration = Date.now() - request.startTime;
|
|
11
|
-
const logData = {
|
|
12
|
-
method: request.method,
|
|
13
|
-
url: request.url,
|
|
14
|
-
statusCode: reply.statusCode,
|
|
15
|
-
duration: `${duration}ms`,
|
|
16
|
-
ip: request.ip,
|
|
17
|
-
requestId: request.id,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
if (duration > env.SLOW_QUERY_THRESHOLD) {
|
|
21
|
-
app.log.warn(logData, 'Slow response detected');
|
|
22
|
-
} else {
|
|
23
|
-
app.log.info(logData, 'Request completed');
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
-
import { verifyAccessToken } from '@/modules/auth/auth.service';
|
|
3
|
-
import { UnauthorizedError, AppError } from '@/utils/errors';
|
|
4
|
-
|
|
5
|
-
export async function authenticateMiddleware(request: FastifyRequest, reply: FastifyReply) {
|
|
6
|
-
try {
|
|
7
|
-
const authHeader = request.headers.authorization;
|
|
8
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
9
|
-
throw new UnauthorizedError('Authentication token missing');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const token = authHeader.split(' ')[1];
|
|
13
|
-
if (!token) {
|
|
14
|
-
throw new UnauthorizedError('Malformed authentication token');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const payload = await verifyAccessToken(token);
|
|
18
|
-
request.user = payload;
|
|
19
|
-
} catch (error) {
|
|
20
|
-
throw error instanceof AppError ? error : new UnauthorizedError('Invalid or expired token');
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RBAC Middleware Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests for role-based access control middleware functions.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
-
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
9
|
-
import { requireRole, requireAdmin, requireUser, hasRole, isAdmin } from './rbac.middleware';
|
|
10
|
-
import { ForbiddenError, UnauthorizedError } from '../../utils/errors';
|
|
11
|
-
|
|
12
|
-
describe('RBAC Middleware', () => {
|
|
13
|
-
let mockRequest: any;
|
|
14
|
-
let mockReply: any;
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
mockRequest = {
|
|
18
|
-
user: undefined,
|
|
19
|
-
};
|
|
20
|
-
mockReply = {};
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('requireRole', () => {
|
|
24
|
-
it('should allow access when user has required role', async () => {
|
|
25
|
-
mockRequest.user = { userId: '123', email: 'admin@test.com', role: 'ADMIN' };
|
|
26
|
-
const middleware = requireRole('ADMIN');
|
|
27
|
-
|
|
28
|
-
await expect(
|
|
29
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
30
|
-
).resolves.not.toThrow();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should allow access when user has one of multiple allowed roles', async () => {
|
|
34
|
-
mockRequest.user = { userId: '123', email: 'user@test.com', role: 'USER' };
|
|
35
|
-
const middleware = requireRole('USER', 'ADMIN');
|
|
36
|
-
|
|
37
|
-
await expect(
|
|
38
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
39
|
-
).resolves.not.toThrow();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should throw UnauthorizedError when user is not authenticated', async () => {
|
|
43
|
-
mockRequest.user = undefined;
|
|
44
|
-
const middleware = requireRole('ADMIN');
|
|
45
|
-
|
|
46
|
-
await expect(
|
|
47
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
48
|
-
).rejects.toThrow(UnauthorizedError);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should throw ForbiddenError when user lacks required role', async () => {
|
|
52
|
-
mockRequest.user = { userId: '123', email: 'user@test.com', role: 'USER' };
|
|
53
|
-
const middleware = requireRole('ADMIN');
|
|
54
|
-
|
|
55
|
-
await expect(
|
|
56
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
57
|
-
).rejects.toThrow(ForbiddenError);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('requireAdmin', () => {
|
|
62
|
-
it('should allow access for ADMIN users', async () => {
|
|
63
|
-
mockRequest.user = { userId: '123', email: 'admin@test.com', role: 'ADMIN' };
|
|
64
|
-
const middleware = requireAdmin();
|
|
65
|
-
|
|
66
|
-
await expect(
|
|
67
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
68
|
-
).resolves.not.toThrow();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should deny access for USER role', async () => {
|
|
72
|
-
mockRequest.user = { userId: '123', email: 'user@test.com', role: 'USER' };
|
|
73
|
-
const middleware = requireAdmin();
|
|
74
|
-
|
|
75
|
-
await expect(
|
|
76
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
77
|
-
).rejects.toThrow(ForbiddenError);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('requireUser', () => {
|
|
82
|
-
it('should allow access for USER role', async () => {
|
|
83
|
-
mockRequest.user = { userId: '123', email: 'user@test.com', role: 'USER' };
|
|
84
|
-
const middleware = requireUser();
|
|
85
|
-
|
|
86
|
-
await expect(
|
|
87
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
88
|
-
).resolves.not.toThrow();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should deny access for ADMIN role', async () => {
|
|
92
|
-
mockRequest.user = { userId: '123', email: 'admin@test.com', role: 'ADMIN' };
|
|
93
|
-
const middleware = requireUser();
|
|
94
|
-
|
|
95
|
-
await expect(
|
|
96
|
-
middleware(mockRequest as FastifyRequest, mockReply as FastifyReply)
|
|
97
|
-
).rejects.toThrow(ForbiddenError);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('hasRole utility', () => {
|
|
102
|
-
it('should return true when user has the role', () => {
|
|
103
|
-
mockRequest.user = { userId: '123', email: 'admin@test.com', role: 'ADMIN' };
|
|
104
|
-
|
|
105
|
-
expect(hasRole(mockRequest as FastifyRequest, 'ADMIN')).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should return false when user does not have the role', () => {
|
|
109
|
-
mockRequest.user = { userId: '123', email: 'user@test.com', role: 'USER' };
|
|
110
|
-
|
|
111
|
-
expect(hasRole(mockRequest as FastifyRequest, 'ADMIN')).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should return false when user is not authenticated', () => {
|
|
115
|
-
mockRequest.user = undefined;
|
|
116
|
-
|
|
117
|
-
expect(hasRole(mockRequest as FastifyRequest, 'ADMIN')).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('isAdmin utility', () => {
|
|
122
|
-
it('should return true for ADMIN users', () => {
|
|
123
|
-
mockRequest.user = { userId: '123', email: 'admin@test.com', role: 'ADMIN' };
|
|
124
|
-
|
|
125
|
-
expect(isAdmin(mockRequest as FastifyRequest)).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should return false for non-ADMIN users', () => {
|
|
129
|
-
mockRequest.user = { userId: '123', email: 'user@test.com', role: 'USER' };
|
|
130
|
-
|
|
131
|
-
expect(isAdmin(mockRequest as FastifyRequest)).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Role-Based Access Control (RBAC) Middleware
|
|
3
|
-
*
|
|
4
|
-
* Provides middleware functions to enforce role-based permissions on routes.
|
|
5
|
-
* Must be used AFTER the authenticate middleware.
|
|
6
|
-
*
|
|
7
|
-
* @see /mnt/project/02-general-rules.md
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
11
|
-
import { ForbiddenError, UnauthorizedError } from '@/utils/errors';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* User Role Type
|
|
15
|
-
*
|
|
16
|
-
* Must match the roles defined in Prisma schema and JWT payload
|
|
17
|
-
*/
|
|
18
|
-
export type UserRole = 'USER' | 'ADMIN';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* RBAC Middleware Factory
|
|
22
|
-
*
|
|
23
|
-
* Creates a preHandler middleware that checks if the authenticated user
|
|
24
|
-
* has one of the required roles.
|
|
25
|
-
*
|
|
26
|
-
* @param allowedRoles - Array of roles that are allowed to access the route
|
|
27
|
-
* @returns Fastify preHandler function
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* // Single role
|
|
31
|
-
* app.get('/admin/users', {
|
|
32
|
-
* preHandler: [app.authenticate, requireRole('ADMIN')],
|
|
33
|
-
* handler: adminController.listUsers
|
|
34
|
-
* });
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* // Multiple roles
|
|
38
|
-
* app.post('/resources', {
|
|
39
|
-
* preHandler: [app.authenticate, requireRole('USER', 'ADMIN')],
|
|
40
|
-
* handler: resourceController.create
|
|
41
|
-
* });
|
|
42
|
-
*/
|
|
43
|
-
export function requireRole(...allowedRoles: UserRole[]) {
|
|
44
|
-
return async (request: FastifyRequest, _reply: FastifyReply): Promise<void> => {
|
|
45
|
-
// Ensure user is authenticated (should be set by app.authenticate)
|
|
46
|
-
if (!request.user) {
|
|
47
|
-
throw new UnauthorizedError('Authentication required');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const userRole = request.user.role as UserRole;
|
|
51
|
-
|
|
52
|
-
// Validate that user's role is in the allowed roles list
|
|
53
|
-
if (!allowedRoles.includes(userRole)) {
|
|
54
|
-
throw new ForbiddenError(
|
|
55
|
-
`Access denied. Required role: ${allowedRoles.join(' or ')}`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// User has required role, allow request to proceed
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Require ADMIN role
|
|
65
|
-
*
|
|
66
|
-
* Shorthand helper for routes that require admin access only.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* app.delete('/users/:id', {
|
|
70
|
-
* preHandler: [app.authenticate, requireAdmin()],
|
|
71
|
-
* handler: userController.deleteUser
|
|
72
|
-
* });
|
|
73
|
-
*/
|
|
74
|
-
export const requireAdmin = () => requireRole('ADMIN');
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Require USER role
|
|
78
|
-
*
|
|
79
|
-
* Shorthand helper for routes that require standard user access.
|
|
80
|
-
* Note: This excludes ADMIN users. Use requireAny() to allow both.
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* app.get('/profile', {
|
|
84
|
-
* preHandler: [app.authenticate, requireUser()],
|
|
85
|
-
* handler: userController.getProfile
|
|
86
|
-
* });
|
|
87
|
-
*/
|
|
88
|
-
export const requireUser = () => requireRole('USER');
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Require any authenticated user (USER or ADMIN)
|
|
92
|
-
*
|
|
93
|
-
* Shorthand helper for routes that require authentication but accept any role.
|
|
94
|
-
* This is equivalent to just using app.authenticate without role checking.
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* app.get('/resources', {
|
|
98
|
-
* preHandler: [app.authenticate, requireAny()],
|
|
99
|
-
* handler: resourceController.list
|
|
100
|
-
* });
|
|
101
|
-
*/
|
|
102
|
-
export const requireAny = () => requireRole('USER', 'ADMIN');
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Check if user has specific role (utility function)
|
|
106
|
-
*
|
|
107
|
-
* Can be used within route handlers for conditional logic.
|
|
108
|
-
*
|
|
109
|
-
* @param request - Fastify request object
|
|
110
|
-
* @param role - Role to check
|
|
111
|
-
* @returns true if user has the role, false otherwise
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* export async function getResources(request: FastifyRequest) {
|
|
115
|
-
* const resources = await resourceRepo.findAll();
|
|
116
|
-
*
|
|
117
|
-
* // Filter sensitive data for non-admin users
|
|
118
|
-
* if (!hasRole(request, 'ADMIN')) {
|
|
119
|
-
* return resources.map(r => omit(r, 'internalNotes'));
|
|
120
|
-
* }
|
|
121
|
-
*
|
|
122
|
-
* return resources;
|
|
123
|
-
* }
|
|
124
|
-
*/
|
|
125
|
-
export function hasRole(request: FastifyRequest, role: UserRole): boolean {
|
|
126
|
-
return request.user?.role === role;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Check if user is admin (utility function)
|
|
131
|
-
*
|
|
132
|
-
* @param request - Fastify request object
|
|
133
|
-
* @returns true if user is admin, false otherwise
|
|
134
|
-
*/
|
|
135
|
-
export function isAdmin(request: FastifyRequest): boolean {
|
|
136
|
-
return hasRole(request, 'ADMIN');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Check if user is regular user (utility function)
|
|
141
|
-
*
|
|
142
|
-
* @param request - Fastify request object
|
|
143
|
-
* @returns true if user is regular user, false otherwise
|
|
144
|
-
*/
|
|
145
|
-
export function isUser(request: FastifyRequest): boolean {
|
|
146
|
-
return hasRole(request, 'USER');
|
|
147
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database Connection
|
|
3
|
-
*
|
|
4
|
-
* Prisma Client instance with query logging and error handling.
|
|
5
|
-
*
|
|
6
|
-
* @see /mnt/project/01-db-and-migrations.md
|
|
7
|
-
* @see /mnt/project/08-observability.md
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { PrismaClient, Prisma } from '@prisma/client';
|
|
11
|
-
import logger from './logger';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Create Prisma Client instance
|
|
15
|
-
*
|
|
16
|
-
* Features:
|
|
17
|
-
* - Slow query logging (> 1000ms)
|
|
18
|
-
* - Error logging
|
|
19
|
-
* - Query logging in development (optional)
|
|
20
|
-
*/
|
|
21
|
-
const prisma = new PrismaClient({
|
|
22
|
-
log:
|
|
23
|
-
process.env['PRISMA_QUERY_LOG'] === 'true'
|
|
24
|
-
? [
|
|
25
|
-
{ level: 'query', emit: 'event' },
|
|
26
|
-
{ level: 'info', emit: 'stdout' },
|
|
27
|
-
{ level: 'warn', emit: 'stdout' },
|
|
28
|
-
{ level: 'error', emit: 'event' },
|
|
29
|
-
]
|
|
30
|
-
: [
|
|
31
|
-
{ level: 'warn', emit: 'stdout' },
|
|
32
|
-
{ level: 'error', emit: 'event' },
|
|
33
|
-
{ level: 'query', emit: 'event' },
|
|
34
|
-
],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Slow Query Logging
|
|
39
|
-
*
|
|
40
|
-
* Logs a warning when a query takes longer than the threshold.
|
|
41
|
-
* Helps identify performance bottlenecks.
|
|
42
|
-
*/
|
|
43
|
-
prisma.$on('query', (e: Prisma.QueryEvent) => {
|
|
44
|
-
const threshold = parseInt(process.env['SLOW_QUERY_THRESHOLD'] || '1000');
|
|
45
|
-
|
|
46
|
-
if (e.duration > threshold) {
|
|
47
|
-
logger.warn(
|
|
48
|
-
{
|
|
49
|
-
query: e.query,
|
|
50
|
-
params: e.params,
|
|
51
|
-
duration: e.duration,
|
|
52
|
-
target: e.target,
|
|
53
|
-
},
|
|
54
|
-
`Slow query detected (${e.duration}ms)`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Error Logging
|
|
61
|
-
*
|
|
62
|
-
* Logs database errors for monitoring and debugging.
|
|
63
|
-
*/
|
|
64
|
-
prisma.$on('error', (e: Prisma.LogEvent) => {
|
|
65
|
-
logger.error(
|
|
66
|
-
{
|
|
67
|
-
message: e.message,
|
|
68
|
-
target: e.target,
|
|
69
|
-
},
|
|
70
|
-
'Database error occurred'
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
export { prisma };
|
|
@@ -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';
|
|
6
|
-
import { AppError, InternalError } from '@/utils/errors';
|
|
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
|
-
}
|