speedrun-cli 2.6.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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/cli.js +224 -0
- package/index.js +12 -0
- package/package.json +74 -0
- package/src/constants.js +65 -0
- package/src/generator.js +271 -0
- package/src/index.js +13 -0
- package/src/moduleGenerator.js +586 -0
- package/src/postSetup.js +365 -0
- package/src/prompts.js +189 -0
- package/src/utils.js +112 -0
- package/templates/README.md +81 -0
- package/templates/base/eslint.config.mjs +34 -0
- package/templates/base/gitignore +78 -0
- package/templates/base/nest-cli.json +8 -0
- package/templates/base/src/common/constants/cookie.config.ts +20 -0
- package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
- package/templates/base/src/common/decorators/public.decorator.ts +4 -0
- package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
- package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
- package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
- package/templates/base/src/common/guards/auth.guard.ts +51 -0
- package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
- package/templates/base/src/common/guards/roles.guard.ts +45 -0
- package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
- package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
- package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
- package/templates/base/src/common/validators/password.validator.ts +37 -0
- package/templates/base/src/config/config.module.ts +14 -0
- package/templates/base/src/config/logger.config.ts +109 -0
- package/templates/base/src/main.ts +84 -0
- package/templates/base/src/modules/auth/auth.controller.ts +133 -0
- package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
- package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/base/src/modules/health/health.controller.ts +20 -0
- package/templates/base/src/modules/health/health.module.ts +7 -0
- package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
- package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/base/src/modules/users/users.controller.ts +65 -0
- package/templates/base/test/app.e2e-spec.ts +24 -0
- package/templates/base/test/jest-e2e.json +9 -0
- package/templates/base/tsconfig.build.json +4 -0
- package/templates/base/tsconfig.json +24 -0
- package/templates/base-crud/CRUD_README.md +385 -0
- package/templates/base-crud/src/common/base/base.controller.ts +321 -0
- package/templates/base-crud/src/common/base/base.service.ts +192 -0
- package/templates/base-crud/src/common/base/index.ts +20 -0
- package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
- package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
- package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
- package/templates/database/mongodb/.env.example +22 -0
- package/templates/database/mysql/.env.example +24 -0
- package/templates/database/mysql/drizzle.config.ts +13 -0
- package/templates/database/mysql/package.json +5 -0
- package/templates/database/mysql/prisma/schema.prisma +55 -0
- package/templates/database/mysql/src/database/drizzle.ts +13 -0
- package/templates/database/mysql/src/database/schema.ts +58 -0
- package/templates/database/postgres/.env.example +24 -0
- package/templates/database/postgres/drizzle.config.ts +13 -0
- package/templates/database/postgres/package.json +8 -0
- package/templates/database/postgres/prisma/schema.prisma +55 -0
- package/templates/database/sqlite/.env.example +24 -0
- package/templates/database/sqlite/drizzle.config.ts +13 -0
- package/templates/database/sqlite/package.json +8 -0
- package/templates/database/sqlite/prisma/schema.prisma +48 -0
- package/templates/database/sqlite/src/database/drizzle.ts +11 -0
- package/templates/database/sqlite/src/database/schema.ts +52 -0
- package/templates/orm/drizzle/drizzle.config.ts +13 -0
- package/templates/orm/drizzle/package.json +90 -0
- package/templates/orm/drizzle/src/app.module.ts +73 -0
- package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
- package/templates/orm/drizzle/src/database/database.module.ts +25 -0
- package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
- package/templates/orm/drizzle/src/database/schema.ts +60 -0
- package/templates/orm/drizzle/src/database/seed.ts +87 -0
- package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
- package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
- package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
- package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
- package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
- package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
- package/templates/orm/mongoose/.env.example +22 -0
- package/templates/orm/mongoose/package.json +86 -0
- package/templates/orm/mongoose/src/app.module.ts +73 -0
- package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
- package/templates/orm/mongoose/src/database/database.module.ts +19 -0
- package/templates/orm/mongoose/src/database/seed.ts +107 -0
- package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
- package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
- package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
- package/templates/orm/mongoose/src/schemas/index.ts +2 -0
- package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
- package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
- package/templates/orm/prisma/package.json +102 -0
- package/templates/orm/prisma/prisma/schema.prisma +60 -0
- package/templates/orm/prisma/prisma/seed.ts +69 -0
- package/templates/orm/prisma/src/app.module.ts +55 -0
- package/templates/orm/prisma/src/config/env.validation.ts +56 -0
- package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
- package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
- package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
- package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
- package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
- package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
- package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
- package/templates/orm/typeorm/package.json +100 -0
- package/templates/orm/typeorm/src/app.module.ts +55 -0
- package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
- package/templates/orm/typeorm/src/database/data-source.ts +19 -0
- package/templates/orm/typeorm/src/database/database.module.ts +27 -0
- package/templates/orm/typeorm/src/database/seed.ts +76 -0
- package/templates/orm/typeorm/src/entities/index.ts +2 -0
- package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
- package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
- package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
- package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
- package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
- package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
- package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
- package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
- package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
- package/templates/swagger/src/main.ts +116 -0
- package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
- package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
- package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
- package/templates/swagger/src/modules/users/users.controller.ts +214 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-user.dto.ts +23 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Templates Structure
|
|
2
|
+
|
|
3
|
+
This directory contains modular templates for generating NestJS authentication projects.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
templates/
|
|
9
|
+
├── base/ # Shared, ORM-agnostic code (80% of the project)
|
|
10
|
+
│ ├── src/
|
|
11
|
+
│ │ ├── common/ # Guards, decorators, filters, interceptors
|
|
12
|
+
│ │ ├── config/ # Configuration module, logger
|
|
13
|
+
│ │ ├── modules/
|
|
14
|
+
│ │ │ ├── auth/ # Auth controller & DTOs (no service - ORM-specific)
|
|
15
|
+
│ │ │ └── users/ # Users controller & DTOs (no service - ORM-specific)
|
|
16
|
+
│ │ └── main.ts # Application entry point
|
|
17
|
+
│ ├── test/ # E2E tests
|
|
18
|
+
│ ├── tsconfig.json
|
|
19
|
+
│ ├── tsconfig.build.json
|
|
20
|
+
│ ├── nest-cli.json
|
|
21
|
+
│ └── eslint.config.mjs
|
|
22
|
+
│
|
|
23
|
+
├── orm/ # ORM-specific implementations
|
|
24
|
+
│ ├── prisma/ # Prisma adapter
|
|
25
|
+
│ │ ├── prisma/ # Schema & seed files
|
|
26
|
+
│ │ ├── src/
|
|
27
|
+
│ │ │ ├── prisma/ # PrismaService & PrismaModule
|
|
28
|
+
│ │ │ ├── modules/
|
|
29
|
+
│ │ │ │ ├── auth/ # Auth service & module
|
|
30
|
+
│ │ │ │ ├── users/ # Users service & module
|
|
31
|
+
│ │ │ │ └── health/ # Health controller (DB-aware)
|
|
32
|
+
│ │ │ ├── config/ # env.validation.ts
|
|
33
|
+
│ │ │ └── app.module.ts
|
|
34
|
+
│ │ └── package.json # ORM-specific dependencies
|
|
35
|
+
│ │
|
|
36
|
+
│ ├── typeorm/ # (Future) TypeORM adapter
|
|
37
|
+
│ ├── mongoose/ # (Future) Mongoose adapter
|
|
38
|
+
│ └── drizzle/ # (Future) Drizzle adapter
|
|
39
|
+
│
|
|
40
|
+
└── database/ # Database-specific configurations
|
|
41
|
+
├── postgres/
|
|
42
|
+
│ ├── .env.example
|
|
43
|
+
│ └── prisma/schema.prisma # (if different from default)
|
|
44
|
+
├── mysql/
|
|
45
|
+
│ ├── .env.example
|
|
46
|
+
│ └── prisma/schema.prisma
|
|
47
|
+
├── sqlite/
|
|
48
|
+
│ ├── .env.example
|
|
49
|
+
│ └── prisma/schema.prisma
|
|
50
|
+
└── mongodb/
|
|
51
|
+
├── .env.example
|
|
52
|
+
└── prisma/schema.prisma
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## How Generation Works
|
|
56
|
+
|
|
57
|
+
1. **Copy base template** → All shared code (guards, decorators, main.ts, etc.)
|
|
58
|
+
2. **Copy ORM adapter** → Overwrites/adds ORM-specific files (services, modules)
|
|
59
|
+
3. **Copy database config** → Overwrites database-specific files (schema, .env)
|
|
60
|
+
|
|
61
|
+
## Adding a New ORM
|
|
62
|
+
|
|
63
|
+
1. Create `templates/orm/<orm-name>/` directory
|
|
64
|
+
2. Add ORM-specific files:
|
|
65
|
+
- `src/<orm>/` - ORM service & module
|
|
66
|
+
- `src/modules/auth/auth.service.ts` - Auth service using the ORM
|
|
67
|
+
- `src/modules/auth/auth.module.ts` - Auth module with ORM imports
|
|
68
|
+
- `src/modules/users/users.service.ts` - Users service
|
|
69
|
+
- `src/modules/users/users.module.ts` - Users module
|
|
70
|
+
- `src/modules/health/` - Health check with DB indicator
|
|
71
|
+
- `src/app.module.ts` - Root module with ORM imports
|
|
72
|
+
- `package.json` - ORM-specific dependencies
|
|
73
|
+
3. Update `index.js` to add the ORM to `ORM_OPTIONS`
|
|
74
|
+
|
|
75
|
+
## Adding a New Database
|
|
76
|
+
|
|
77
|
+
1. Create `templates/database/<db-name>/` directory
|
|
78
|
+
2. Add database-specific files:
|
|
79
|
+
- `.env.example` - With correct DATABASE_URL format
|
|
80
|
+
- `prisma/schema.prisma` - (If using Prisma) DB-specific schema
|
|
81
|
+
3. Update `index.js` to add the database to `DATABASE_OPTIONS`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import eslint from '@eslint/js';
|
|
3
|
+
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
|
4
|
+
import globals from 'globals';
|
|
5
|
+
import tseslint from 'typescript-eslint';
|
|
6
|
+
|
|
7
|
+
export default tseslint.config(
|
|
8
|
+
{
|
|
9
|
+
ignores: ['eslint.config.mjs'],
|
|
10
|
+
},
|
|
11
|
+
eslint.configs.recommended,
|
|
12
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
13
|
+
eslintPluginPrettierRecommended,
|
|
14
|
+
{
|
|
15
|
+
languageOptions: {
|
|
16
|
+
globals: {
|
|
17
|
+
...globals.node,
|
|
18
|
+
...globals.jest,
|
|
19
|
+
},
|
|
20
|
+
sourceType: 'commonjs',
|
|
21
|
+
parserOptions: {
|
|
22
|
+
projectService: true,
|
|
23
|
+
tsconfigRootDir: import.meta.dirname,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
rules: {
|
|
29
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
30
|
+
'@typescript-eslint/no-floating-promises': 'warn',
|
|
31
|
+
'@typescript-eslint/no-unsafe-argument': 'warn'
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
/.pnp
|
|
4
|
+
.pnp.js
|
|
5
|
+
|
|
6
|
+
# Build output
|
|
7
|
+
/dist
|
|
8
|
+
/build
|
|
9
|
+
|
|
10
|
+
# Environment files
|
|
11
|
+
.env
|
|
12
|
+
.env.local
|
|
13
|
+
.env.development.local
|
|
14
|
+
.env.test.local
|
|
15
|
+
.env.production.local
|
|
16
|
+
*.env
|
|
17
|
+
|
|
18
|
+
# Keep example env file
|
|
19
|
+
!.env.example
|
|
20
|
+
|
|
21
|
+
# Logs
|
|
22
|
+
logs
|
|
23
|
+
*.log
|
|
24
|
+
npm-debug.log*
|
|
25
|
+
pnpm-debug.log*
|
|
26
|
+
yarn-debug.log*
|
|
27
|
+
yarn-error.log*
|
|
28
|
+
lerna-debug.log*
|
|
29
|
+
|
|
30
|
+
# OS files
|
|
31
|
+
.DS_Store
|
|
32
|
+
.DS_Store?
|
|
33
|
+
._*
|
|
34
|
+
.Spotlight-V100
|
|
35
|
+
.Trashes
|
|
36
|
+
ehthumbs.db
|
|
37
|
+
Thumbs.db
|
|
38
|
+
|
|
39
|
+
# IDE
|
|
40
|
+
.idea/
|
|
41
|
+
.vscode/
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
*.sublime-workspace
|
|
45
|
+
*.sublime-project
|
|
46
|
+
|
|
47
|
+
# Testing
|
|
48
|
+
/coverage
|
|
49
|
+
/.nyc_output
|
|
50
|
+
|
|
51
|
+
# Prisma
|
|
52
|
+
prisma/*.db
|
|
53
|
+
prisma/*.db-journal
|
|
54
|
+
|
|
55
|
+
# SQLite
|
|
56
|
+
*.db
|
|
57
|
+
*.db-journal
|
|
58
|
+
*.sqlite
|
|
59
|
+
*.sqlite3
|
|
60
|
+
|
|
61
|
+
# Drizzle
|
|
62
|
+
drizzle/meta/
|
|
63
|
+
|
|
64
|
+
# TypeORM
|
|
65
|
+
/migrations/*.js
|
|
66
|
+
/migrations/*.js.map
|
|
67
|
+
|
|
68
|
+
# Temporary files
|
|
69
|
+
tmp/
|
|
70
|
+
temp/
|
|
71
|
+
*.tmp
|
|
72
|
+
*.temp
|
|
73
|
+
|
|
74
|
+
# Package manager lock files (optional - uncomment if you want to ignore)
|
|
75
|
+
# package-lock.json
|
|
76
|
+
# yarn.lock
|
|
77
|
+
# pnpm-lock.yaml
|
|
78
|
+
# bun.lockb
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const COOKIE_CONFIG = {
|
|
2
|
+
ACCESS_TOKEN: {
|
|
3
|
+
name: 'accessToken',
|
|
4
|
+
options: {
|
|
5
|
+
httpOnly: true,
|
|
6
|
+
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
|
|
7
|
+
secure: process.env.NODE_ENV === 'production',
|
|
8
|
+
maxAge: 60 * 60 * 1000, // 1 hour
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
REFRESH_TOKEN: {
|
|
12
|
+
name: 'refreshToken',
|
|
13
|
+
options: {
|
|
14
|
+
httpOnly: true,
|
|
15
|
+
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
|
|
16
|
+
secure: process.env.NODE_ENV === 'production',
|
|
17
|
+
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
} as const;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
interface RequestWithUser {
|
|
4
|
+
user?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const GetUser = createParamDecorator(
|
|
8
|
+
(data: string | undefined, ctx: ExecutionContext) => {
|
|
9
|
+
const request = ctx.switchToHttp().getRequest<RequestWithUser>();
|
|
10
|
+
const user = request.user;
|
|
11
|
+
return data && user ? user[data] : user;
|
|
12
|
+
},
|
|
13
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IsOptional, IsInt, Min, Max } from 'class-validator';
|
|
2
|
+
import { Type } from 'class-transformer';
|
|
3
|
+
|
|
4
|
+
export class PaginationDto {
|
|
5
|
+
@IsOptional()
|
|
6
|
+
@Type(() => Number)
|
|
7
|
+
@IsInt()
|
|
8
|
+
@Min(1)
|
|
9
|
+
page?: number = 1;
|
|
10
|
+
|
|
11
|
+
@IsOptional()
|
|
12
|
+
@Type(() => Number)
|
|
13
|
+
@IsInt()
|
|
14
|
+
@Min(1)
|
|
15
|
+
@Max(100)
|
|
16
|
+
limit?: number = 10;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PaginatedResponse<T> {
|
|
20
|
+
data: T[];
|
|
21
|
+
meta: {
|
|
22
|
+
total: number;
|
|
23
|
+
page: number;
|
|
24
|
+
limit: number;
|
|
25
|
+
totalPages: number;
|
|
26
|
+
hasNext: boolean;
|
|
27
|
+
hasPrevious: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExceptionFilter,
|
|
3
|
+
Catch,
|
|
4
|
+
ArgumentsHost,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
Inject,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
import { Request, Response } from 'express';
|
|
10
|
+
import { ApiResponse } from '../interfaces/api-response.interface';
|
|
11
|
+
import { Logger } from 'nestjs-pino';
|
|
12
|
+
|
|
13
|
+
@Catch()
|
|
14
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
15
|
+
constructor(@Inject(Logger) private readonly logger: Logger) {}
|
|
16
|
+
|
|
17
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
18
|
+
const ctx = host.switchToHttp();
|
|
19
|
+
const request = ctx.getRequest<Request>();
|
|
20
|
+
const response = ctx.getResponse<Response>();
|
|
21
|
+
const correlationId = request['correlationId'] as string;
|
|
22
|
+
|
|
23
|
+
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
24
|
+
let message = 'Internal server error';
|
|
25
|
+
let code = 'INTERNAL_SERVER_ERROR';
|
|
26
|
+
let details: unknown = undefined;
|
|
27
|
+
|
|
28
|
+
if (exception instanceof HttpException) {
|
|
29
|
+
status = exception.getStatus();
|
|
30
|
+
const exceptionResponse = exception.getResponse();
|
|
31
|
+
|
|
32
|
+
if (typeof exceptionResponse === 'string') {
|
|
33
|
+
message = exceptionResponse;
|
|
34
|
+
} else if (
|
|
35
|
+
typeof exceptionResponse === 'object' &&
|
|
36
|
+
exceptionResponse !== null
|
|
37
|
+
) {
|
|
38
|
+
const responseObj = exceptionResponse as Record<string, unknown>;
|
|
39
|
+
message =
|
|
40
|
+
(typeof responseObj.message === 'string'
|
|
41
|
+
? responseObj.message
|
|
42
|
+
: undefined) || message;
|
|
43
|
+
details = responseObj.error || responseObj.details;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Generate error code from status
|
|
47
|
+
code = this.getErrorCode(status, message);
|
|
48
|
+
} else if (exception instanceof Error) {
|
|
49
|
+
message = exception.message;
|
|
50
|
+
code = 'INTERNAL_SERVER_ERROR';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Log the error with correlation ID
|
|
54
|
+
this.logger.error(
|
|
55
|
+
{
|
|
56
|
+
correlationId,
|
|
57
|
+
statusCode: status,
|
|
58
|
+
errorCode: code,
|
|
59
|
+
message,
|
|
60
|
+
details,
|
|
61
|
+
path: request.url,
|
|
62
|
+
method: request.method,
|
|
63
|
+
stack: exception instanceof Error ? exception.stack : undefined,
|
|
64
|
+
},
|
|
65
|
+
`Error occurred: ${message}`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const errorResponse: ApiResponse = {
|
|
69
|
+
success: false,
|
|
70
|
+
error: {
|
|
71
|
+
message,
|
|
72
|
+
code,
|
|
73
|
+
...(details !== undefined && { details }),
|
|
74
|
+
},
|
|
75
|
+
meta: {
|
|
76
|
+
correlationId,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
response.status(status).json(errorResponse);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private getErrorCode(status: number, message: string): string {
|
|
84
|
+
// Convert message to error code format
|
|
85
|
+
const messageCode = message
|
|
86
|
+
.toUpperCase()
|
|
87
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
88
|
+
.replace(/^_|_$/g, '');
|
|
89
|
+
|
|
90
|
+
// Map common HTTP status codes
|
|
91
|
+
const statusCodeMap: { [key: number]: string } = {
|
|
92
|
+
400: 'BAD_REQUEST',
|
|
93
|
+
401: 'UNAUTHORIZED',
|
|
94
|
+
403: 'FORBIDDEN',
|
|
95
|
+
404: 'NOT_FOUND',
|
|
96
|
+
409: 'CONFLICT',
|
|
97
|
+
422: 'UNPROCESSABLE_ENTITY',
|
|
98
|
+
500: 'INTERNAL_SERVER_ERROR',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// If we have a specific message code, use it, otherwise use status code
|
|
102
|
+
if (messageCode && messageCode !== statusCodeMap[status]) {
|
|
103
|
+
return messageCode;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return statusCodeMap[status] || 'INTERNAL_SERVER_ERROR';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Injectable,
|
|
5
|
+
UnauthorizedException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { ConfigService } from '@nestjs/config';
|
|
8
|
+
import { Reflector } from '@nestjs/core';
|
|
9
|
+
import { JwtService } from '@nestjs/jwt';
|
|
10
|
+
import { COOKIE_CONFIG } from '../constants/cookie.config';
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class AuthGuard implements CanActivate {
|
|
14
|
+
constructor(
|
|
15
|
+
private jwtService: JwtService,
|
|
16
|
+
private config: ConfigService,
|
|
17
|
+
private reflector: Reflector,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
21
|
+
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
|
22
|
+
context.getHandler(),
|
|
23
|
+
context.getClass(),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
if (isPublic) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const request = context.switchToHttp().getRequest<{
|
|
31
|
+
cookies: Record<string, string>;
|
|
32
|
+
user?: unknown;
|
|
33
|
+
}>();
|
|
34
|
+
const token = request.cookies[COOKIE_CONFIG.ACCESS_TOKEN.name];
|
|
35
|
+
|
|
36
|
+
if (!token || typeof token !== 'string') {
|
|
37
|
+
throw new UnauthorizedException('Access Token missing or malformed');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const payload: unknown = await this.jwtService.verifyAsync(token, {
|
|
42
|
+
secret: this.config.get<string>('JWT_ACCESS_SECRET'),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
request.user = payload;
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
throw new UnauthorizedException('Invalid or expired Access Token');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Injectable,
|
|
5
|
+
UnauthorizedException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { ConfigService } from '@nestjs/config';
|
|
8
|
+
import { JwtService } from '@nestjs/jwt';
|
|
9
|
+
import { COOKIE_CONFIG } from '../constants/cookie.config';
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class RefreshTokenGuard implements CanActivate {
|
|
13
|
+
constructor(
|
|
14
|
+
private jwtService: JwtService,
|
|
15
|
+
private config: ConfigService,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
19
|
+
const request = context.switchToHttp().getRequest<{
|
|
20
|
+
cookies: Record<string, string>;
|
|
21
|
+
user?: unknown;
|
|
22
|
+
}>();
|
|
23
|
+
const token = request.cookies[COOKIE_CONFIG.REFRESH_TOKEN.name];
|
|
24
|
+
|
|
25
|
+
if (!token || typeof token !== 'string') {
|
|
26
|
+
throw new UnauthorizedException('Refresh Token missing or malformed');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const payload: unknown = await this.jwtService.verifyAsync(token, {
|
|
31
|
+
secret: this.config.get<string>('JWT_REFRESH_SECRET'),
|
|
32
|
+
});
|
|
33
|
+
request.user = payload;
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
throw new UnauthorizedException('Invalid or expired Refresh Token');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
ForbiddenException,
|
|
5
|
+
Injectable,
|
|
6
|
+
UnauthorizedException,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Reflector } from '@nestjs/core';
|
|
9
|
+
import { ROLES_KEY } from '../decorators/roles.decorator';
|
|
10
|
+
|
|
11
|
+
// ORM-agnostic UserRole enum - define your own or import from your ORM
|
|
12
|
+
export enum UserRole {
|
|
13
|
+
USER = 'USER',
|
|
14
|
+
ADMIN = 'ADMIN',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class RolesGuard implements CanActivate {
|
|
19
|
+
constructor(private reflector: Reflector) {}
|
|
20
|
+
|
|
21
|
+
canActivate(context: ExecutionContext): boolean {
|
|
22
|
+
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
|
|
23
|
+
ROLES_KEY,
|
|
24
|
+
[context.getHandler(), context.getClass()],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!requiredRoles) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const request = context.switchToHttp().getRequest<{
|
|
32
|
+
user?: { role: UserRole };
|
|
33
|
+
}>();
|
|
34
|
+
const { user } = request;
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw new UnauthorizedException('User not found in request');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!requiredRoles.includes(user.role)) {
|
|
40
|
+
throw new ForbiddenException('Insufficient role to access this resource');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
NestInterceptor,
|
|
4
|
+
ExecutionContext,
|
|
5
|
+
CallHandler,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { Observable } from 'rxjs';
|
|
8
|
+
import { map } from 'rxjs/operators';
|
|
9
|
+
import { ApiResponse } from '../interfaces/api-response.interface';
|
|
10
|
+
import { Request } from 'express';
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class ResponseInterceptor<T>
|
|
14
|
+
implements NestInterceptor<T, ApiResponse<T>>
|
|
15
|
+
{
|
|
16
|
+
intercept(
|
|
17
|
+
context: ExecutionContext,
|
|
18
|
+
next: CallHandler,
|
|
19
|
+
): Observable<ApiResponse<T>> {
|
|
20
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
21
|
+
const correlationId = request['correlationId'] as string;
|
|
22
|
+
|
|
23
|
+
return next.handle().pipe(
|
|
24
|
+
map((data: T | ApiResponse<T>) => {
|
|
25
|
+
// If the response is already in the correct format, return it
|
|
26
|
+
if (
|
|
27
|
+
data &&
|
|
28
|
+
typeof data === 'object' &&
|
|
29
|
+
'success' in data &&
|
|
30
|
+
'meta' in data
|
|
31
|
+
) {
|
|
32
|
+
const existingMeta = data.meta || {};
|
|
33
|
+
return {
|
|
34
|
+
...data,
|
|
35
|
+
meta: {
|
|
36
|
+
...existingMeta,
|
|
37
|
+
correlationId,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
},
|
|
40
|
+
} as ApiResponse<T>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Otherwise, wrap it in the standard format
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
data: data as T | undefined,
|
|
47
|
+
meta: {
|
|
48
|
+
correlationId,
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
},
|
|
51
|
+
} as ApiResponse<T>;
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ApiResponse<T = unknown> {
|
|
2
|
+
success: boolean;
|
|
3
|
+
data?: T;
|
|
4
|
+
error?: ApiError;
|
|
5
|
+
meta?: ApiMeta;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ApiError {
|
|
9
|
+
message: string;
|
|
10
|
+
code: string;
|
|
11
|
+
details?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ApiMeta {
|
|
15
|
+
timestamp?: string;
|
|
16
|
+
requestId?: string;
|
|
17
|
+
pagination?: PaginationMeta;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PaginationMeta {
|
|
22
|
+
page: number;
|
|
23
|
+
limit: number;
|
|
24
|
+
total: number;
|
|
25
|
+
totalPages: number;
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { Request, Response, NextFunction } from 'express';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class CorrelationIdMiddleware implements NestMiddleware {
|
|
7
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
8
|
+
// Get correlation ID from header or generate new one
|
|
9
|
+
const correlationId =
|
|
10
|
+
(req.headers['x-correlation-id'] as string) || randomUUID();
|
|
11
|
+
|
|
12
|
+
// Attach to request for later use
|
|
13
|
+
req['correlationId'] = correlationId;
|
|
14
|
+
|
|
15
|
+
// Set response header
|
|
16
|
+
res.setHeader('X-Correlation-ID', correlationId);
|
|
17
|
+
|
|
18
|
+
next();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerDecorator, ValidationOptions } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export function IsStrongPassword(validationOptions?: ValidationOptions) {
|
|
4
|
+
return function (object: object, propertyName: string) {
|
|
5
|
+
registerDecorator({
|
|
6
|
+
name: 'isStrongPassword',
|
|
7
|
+
target: object.constructor,
|
|
8
|
+
propertyName: propertyName,
|
|
9
|
+
options: validationOptions,
|
|
10
|
+
validator: {
|
|
11
|
+
validate(value: unknown) {
|
|
12
|
+
if (typeof value !== 'string') return false;
|
|
13
|
+
|
|
14
|
+
// At least 8 characters
|
|
15
|
+
if (value.length < 8) return false;
|
|
16
|
+
|
|
17
|
+
// At least one uppercase letter
|
|
18
|
+
if (!/[A-Z]/.test(value)) return false;
|
|
19
|
+
|
|
20
|
+
// At least one lowercase letter
|
|
21
|
+
if (!/[a-z]/.test(value)) return false;
|
|
22
|
+
|
|
23
|
+
// At least one number
|
|
24
|
+
if (!/[0-9]/.test(value)) return false;
|
|
25
|
+
|
|
26
|
+
// At least one special character
|
|
27
|
+
if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(value)) return false;
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
defaultMessage() {
|
|
32
|
+
return 'Password must contain at least 8 characters, including one uppercase letter, one lowercase letter, one number, and one special character';
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { validate } from './env.validation';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
imports: [
|
|
7
|
+
ConfigModule.forRoot({
|
|
8
|
+
isGlobal: true,
|
|
9
|
+
envFilePath: '.env',
|
|
10
|
+
validate,
|
|
11
|
+
}),
|
|
12
|
+
],
|
|
13
|
+
})
|
|
14
|
+
export class AppConfigModule {}
|