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.
Files changed (159) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/cli.js +224 -0
  5. package/index.js +12 -0
  6. package/package.json +74 -0
  7. package/src/constants.js +65 -0
  8. package/src/generator.js +271 -0
  9. package/src/index.js +13 -0
  10. package/src/moduleGenerator.js +586 -0
  11. package/src/postSetup.js +365 -0
  12. package/src/prompts.js +189 -0
  13. package/src/utils.js +112 -0
  14. package/templates/README.md +81 -0
  15. package/templates/base/eslint.config.mjs +34 -0
  16. package/templates/base/gitignore +78 -0
  17. package/templates/base/nest-cli.json +8 -0
  18. package/templates/base/src/common/constants/cookie.config.ts +20 -0
  19. package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
  20. package/templates/base/src/common/decorators/public.decorator.ts +4 -0
  21. package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
  22. package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
  23. package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
  24. package/templates/base/src/common/guards/auth.guard.ts +51 -0
  25. package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
  26. package/templates/base/src/common/guards/roles.guard.ts +45 -0
  27. package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
  28. package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
  29. package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
  30. package/templates/base/src/common/validators/password.validator.ts +37 -0
  31. package/templates/base/src/config/config.module.ts +14 -0
  32. package/templates/base/src/config/logger.config.ts +109 -0
  33. package/templates/base/src/main.ts +84 -0
  34. package/templates/base/src/modules/auth/auth.controller.ts +133 -0
  35. package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
  36. package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
  37. package/templates/base/src/modules/health/health.controller.ts +20 -0
  38. package/templates/base/src/modules/health/health.module.ts +7 -0
  39. package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
  40. package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
  41. package/templates/base/src/modules/users/users.controller.ts +65 -0
  42. package/templates/base/test/app.e2e-spec.ts +24 -0
  43. package/templates/base/test/jest-e2e.json +9 -0
  44. package/templates/base/tsconfig.build.json +4 -0
  45. package/templates/base/tsconfig.json +24 -0
  46. package/templates/base-crud/CRUD_README.md +385 -0
  47. package/templates/base-crud/src/common/base/base.controller.ts +321 -0
  48. package/templates/base-crud/src/common/base/base.service.ts +192 -0
  49. package/templates/base-crud/src/common/base/index.ts +20 -0
  50. package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
  51. package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
  52. package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
  53. package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
  54. package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
  55. package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
  56. package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
  57. package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
  58. package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
  59. package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
  60. package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
  61. package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
  62. package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
  63. package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
  64. package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
  65. package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
  66. package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
  67. package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
  68. package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
  69. package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
  70. package/templates/database/mongodb/.env.example +22 -0
  71. package/templates/database/mysql/.env.example +24 -0
  72. package/templates/database/mysql/drizzle.config.ts +13 -0
  73. package/templates/database/mysql/package.json +5 -0
  74. package/templates/database/mysql/prisma/schema.prisma +55 -0
  75. package/templates/database/mysql/src/database/drizzle.ts +13 -0
  76. package/templates/database/mysql/src/database/schema.ts +58 -0
  77. package/templates/database/postgres/.env.example +24 -0
  78. package/templates/database/postgres/drizzle.config.ts +13 -0
  79. package/templates/database/postgres/package.json +8 -0
  80. package/templates/database/postgres/prisma/schema.prisma +55 -0
  81. package/templates/database/sqlite/.env.example +24 -0
  82. package/templates/database/sqlite/drizzle.config.ts +13 -0
  83. package/templates/database/sqlite/package.json +8 -0
  84. package/templates/database/sqlite/prisma/schema.prisma +48 -0
  85. package/templates/database/sqlite/src/database/drizzle.ts +11 -0
  86. package/templates/database/sqlite/src/database/schema.ts +52 -0
  87. package/templates/orm/drizzle/drizzle.config.ts +13 -0
  88. package/templates/orm/drizzle/package.json +90 -0
  89. package/templates/orm/drizzle/src/app.module.ts +73 -0
  90. package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
  91. package/templates/orm/drizzle/src/database/database.module.ts +25 -0
  92. package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
  93. package/templates/orm/drizzle/src/database/schema.ts +60 -0
  94. package/templates/orm/drizzle/src/database/seed.ts +87 -0
  95. package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
  96. package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
  97. package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
  98. package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
  99. package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
  100. package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
  101. package/templates/orm/mongoose/.env.example +22 -0
  102. package/templates/orm/mongoose/package.json +86 -0
  103. package/templates/orm/mongoose/src/app.module.ts +73 -0
  104. package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
  105. package/templates/orm/mongoose/src/database/database.module.ts +19 -0
  106. package/templates/orm/mongoose/src/database/seed.ts +107 -0
  107. package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
  108. package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
  109. package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
  110. package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
  111. package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
  112. package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
  113. package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
  114. package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
  115. package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
  116. package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
  117. package/templates/orm/mongoose/src/schemas/index.ts +2 -0
  118. package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
  119. package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
  120. package/templates/orm/prisma/package.json +102 -0
  121. package/templates/orm/prisma/prisma/schema.prisma +60 -0
  122. package/templates/orm/prisma/prisma/seed.ts +69 -0
  123. package/templates/orm/prisma/src/app.module.ts +55 -0
  124. package/templates/orm/prisma/src/config/env.validation.ts +56 -0
  125. package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
  126. package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
  127. package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
  128. package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
  129. package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
  130. package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
  131. package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
  132. package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
  133. package/templates/orm/typeorm/package.json +100 -0
  134. package/templates/orm/typeorm/src/app.module.ts +55 -0
  135. package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
  136. package/templates/orm/typeorm/src/database/data-source.ts +19 -0
  137. package/templates/orm/typeorm/src/database/database.module.ts +27 -0
  138. package/templates/orm/typeorm/src/database/seed.ts +76 -0
  139. package/templates/orm/typeorm/src/entities/index.ts +2 -0
  140. package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
  141. package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
  142. package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
  143. package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
  144. package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
  145. package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
  146. package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
  147. package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
  148. package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
  149. package/templates/swagger/src/main.ts +116 -0
  150. package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
  151. package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
  152. package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
  153. package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
  154. package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
  155. package/templates/swagger/src/modules/users/users.controller.ts +214 -0
  156. package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
  157. package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
  158. package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
  159. 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,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
@@ -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,4 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+
3
+ export const isPublic = 'isPublic';
4
+ export const Public = () => SetMetadata(isPublic, true);
@@ -0,0 +1,4 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+
3
+ export const ROLES_KEY = 'roles';
4
+ export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
@@ -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 {}