nodejs-quickstart-structure 2.0.0 → 2.1.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +44 -40
  3. package/bin/index.js +6 -3
  4. package/lib/generator.js +10 -4
  5. package/lib/modules/app-setup.js +76 -6
  6. package/lib/modules/auth-setup.js +143 -0
  7. package/lib/modules/caching-setup.js +8 -1
  8. package/lib/modules/config-files.js +10 -0
  9. package/lib/modules/database-setup.js +2 -1
  10. package/lib/modules/project-setup.js +1 -0
  11. package/lib/prompts.js +40 -1
  12. package/package.json +5 -4
  13. package/templates/clean-architecture/js/src/domain/models/User.js +3 -1
  14. package/templates/clean-architecture/js/src/index.js.ejs +2 -0
  15. package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -3
  16. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +25 -2
  17. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +27 -0
  18. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +3 -0
  19. package/templates/clean-architecture/js/src/infrastructure/webserver/server.spec.js.ejs +49 -0
  20. package/templates/clean-architecture/js/src/infrastructure/webserver/swagger.spec.js.ejs +14 -0
  21. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +41 -4
  22. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +69 -4
  23. package/templates/clean-architecture/js/src/interfaces/graphql/context.js.ejs +13 -6
  24. package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +38 -21
  25. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +10 -5
  26. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +32 -10
  27. package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +1 -1
  28. package/templates/clean-architecture/js/src/interfaces/routes/api.js.ejs +15 -0
  29. package/templates/clean-architecture/js/src/interfaces/routes/api.spec.js.ejs +4 -0
  30. package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +34 -0
  31. package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +3 -2
  32. package/templates/clean-architecture/js/src/usecases/DeleteUser.js.ejs +27 -0
  33. package/templates/clean-architecture/js/src/usecases/GetAllUsers.js.ejs +36 -0
  34. package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +14 -0
  35. package/templates/clean-architecture/js/src/usecases/GetUserById.js.ejs +36 -0
  36. package/templates/clean-architecture/js/src/usecases/GetUserById.spec.js.ejs +48 -0
  37. package/templates/clean-architecture/js/src/usecases/UpdateUser.js.ejs +28 -0
  38. package/templates/clean-architecture/js/src/utils/errorMessages.js +1 -0
  39. package/templates/clean-architecture/js/src/utils/httpCodes.js +2 -0
  40. package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -3
  41. package/templates/clean-architecture/ts/src/domain/user.ts +3 -1
  42. package/templates/clean-architecture/ts/src/index.ts.ejs +4 -0
  43. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +55 -9
  44. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +32 -3
  45. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +26 -6
  46. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +57 -15
  47. package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +38 -23
  48. package/templates/clean-architecture/ts/src/interfaces/graphql/context.ts.ejs +14 -8
  49. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +33 -10
  50. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +15 -5
  51. package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +1 -1
  52. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +9 -1
  53. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts.ejs +16 -0
  54. package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +3 -2
  55. package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +35 -0
  56. package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +1 -0
  57. package/templates/clean-architecture/ts/src/usecases/deleteUser.ts.ejs +24 -0
  58. package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts.ejs +21 -0
  59. package/templates/clean-architecture/ts/src/usecases/getUserById.spec.ts.ejs +47 -0
  60. package/templates/clean-architecture/ts/src/usecases/getUserById.ts.ejs +23 -0
  61. package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +1 -0
  62. package/templates/clean-architecture/ts/src/usecases/updateUser.ts.ejs +25 -0
  63. package/templates/clean-architecture/ts/src/utils/errorMessages.ts +1 -0
  64. package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
  65. package/templates/common/.cursorrules.ejs +9 -0
  66. package/templates/common/.env.example.ejs +17 -10
  67. package/templates/common/.gitlab-ci.yml.ejs +3 -1
  68. package/templates/common/Jenkinsfile.ejs +10 -1
  69. package/templates/common/README.md.ejs +64 -19
  70. package/templates/common/_circleci/config.yml.ejs +96 -0
  71. package/templates/common/_github/workflows/ci.yml.ejs +1 -1
  72. package/templates/common/auth/js/controllers/authController.js.ejs +168 -0
  73. package/templates/common/auth/js/controllers/authController.spec.js.ejs +148 -0
  74. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +58 -0
  75. package/templates/common/auth/js/middleware/authMiddleware.spec.js.ejs +108 -0
  76. package/templates/common/auth/js/routes/authRoutes.js.ejs +16 -0
  77. package/templates/common/auth/js/services/jwtService.js.ejs +54 -0
  78. package/templates/common/auth/js/services/jwtService.spec.js.ejs +84 -0
  79. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +161 -0
  80. package/templates/common/auth/ts/controllers/authController.ts.ejs +165 -0
  81. package/templates/common/auth/ts/middleware/authMiddleware.spec.ts.ejs +128 -0
  82. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +59 -0
  83. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +20 -0
  84. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +89 -0
  85. package/templates/common/auth/ts/services/jwtService.ts.ejs +60 -0
  86. package/templates/common/bitbucket-pipelines.yml.ejs +60 -0
  87. package/templates/common/caching/clean/js/CreateUser.js.ejs +14 -5
  88. package/templates/common/caching/clean/js/DeleteUser.js.ejs +2 -1
  89. package/templates/common/caching/clean/js/GetUserById.js.ejs +39 -0
  90. package/templates/common/caching/clean/js/UpdateUser.js.ejs +2 -1
  91. package/templates/common/caching/clean/ts/createUser.ts.ejs +14 -6
  92. package/templates/common/caching/clean/ts/deleteUser.ts.ejs +2 -1
  93. package/templates/common/caching/clean/ts/getUserById.ts.ejs +32 -0
  94. package/templates/common/caching/clean/ts/updateUser.ts.ejs +2 -2
  95. package/templates/common/database/js/models/User.js.ejs +14 -1
  96. package/templates/common/database/js/models/User.js.mongoose.ejs +7 -0
  97. package/templates/common/database/js/models/User.spec.js.ejs +12 -0
  98. package/templates/common/database/ts/models/User.spec.ts.ejs +10 -0
  99. package/templates/common/database/ts/models/User.ts.ejs +17 -0
  100. package/templates/common/database/ts/models/User.ts.mongoose.ejs +8 -0
  101. package/templates/common/docker-compose.yml.ejs +14 -0
  102. package/templates/common/ecosystem.config.js.ejs +9 -3
  103. package/templates/common/eslint.config.mjs.ejs +3 -0
  104. package/templates/common/jest.config.js.ejs +11 -9
  105. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +1 -1
  106. package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
  107. package/templates/common/migrations/init.js.ejs +5 -4
  108. package/templates/common/package.json.ejs +10 -2
  109. package/templates/common/prompts/project-context.md.ejs +8 -1
  110. package/templates/common/scripts/run-e2e.js.ejs +26 -10
  111. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +149 -107
  112. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +88 -47
  113. package/templates/common/swagger.yml.ejs +148 -0
  114. package/templates/common/tsconfig.eslint.json +15 -0
  115. package/templates/common/tsconfig.json +2 -1
  116. package/templates/common/views/ejs/index.ejs +264 -30
  117. package/templates/common/views/ejs/login.ejs.ejs +244 -0
  118. package/templates/common/views/ejs/signup.ejs.ejs +282 -0
  119. package/templates/common/views/pug/index.pug +269 -38
  120. package/templates/common/views/pug/login.pug.ejs +195 -0
  121. package/templates/common/views/pug/signup.pug.ejs +241 -0
  122. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +6 -0
  123. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +6 -0
  124. package/templates/mvc/js/src/config/env.js.ejs +12 -3
  125. package/templates/mvc/js/src/controllers/userController.js.ejs +29 -5
  126. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +27 -12
  127. package/templates/mvc/js/src/graphql/context.js.ejs +14 -3
  128. package/templates/mvc/js/src/graphql/context.spec.js.ejs +36 -21
  129. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +10 -5
  130. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +32 -10
  131. package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +1 -1
  132. package/templates/mvc/js/src/index.js.ejs +16 -3
  133. package/templates/mvc/js/src/routes/api.js.ejs +14 -0
  134. package/templates/mvc/js/src/routes/api.spec.js.ejs +3 -0
  135. package/templates/mvc/js/src/utils/errorMessages.js +1 -0
  136. package/templates/mvc/js/src/utils/httpCodes.js +1 -0
  137. package/templates/mvc/ts/src/config/env.ts.ejs +12 -3
  138. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +95 -7
  139. package/templates/mvc/ts/src/controllers/userController.ts.ejs +68 -11
  140. package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +36 -23
  141. package/templates/mvc/ts/src/graphql/context.ts.ejs +15 -6
  142. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +32 -10
  143. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +15 -5
  144. package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +1 -1
  145. package/templates/mvc/ts/src/index.ts.ejs +15 -3
  146. package/templates/mvc/ts/src/routes/api.spec.ts.ejs +6 -0
  147. package/templates/mvc/ts/src/routes/api.ts.ejs +15 -0
  148. package/templates/mvc/ts/src/utils/errorMessages.ts +1 -0
  149. package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
  150. package/templates/clean-architecture/js/src/interfaces/routes/api.js +0 -12
  151. package/templates/clean-architecture/js/src/usecases/CreateUser.js +0 -14
  152. package/templates/clean-architecture/js/src/usecases/DeleteUser.js +0 -11
  153. package/templates/clean-architecture/js/src/usecases/GetAllUsers.js +0 -12
  154. package/templates/clean-architecture/js/src/usecases/UpdateUser.js +0 -11
  155. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +0 -13
  156. package/templates/clean-architecture/ts/src/usecases/createUser.ts +0 -13
  157. package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +0 -9
  158. package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts +0 -10
  159. package/templates/clean-architecture/ts/src/usecases/updateUser.ts +0 -9
  160. package/templates/mvc/js/src/routes/api.js +0 -10
  161. package/templates/mvc/ts/src/routes/api.ts +0 -12
@@ -0,0 +1,23 @@
1
+ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
2
+ <%_ if (caching !== 'None') { _%>
3
+ import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
4
+ <%_ } _%>
5
+
6
+ export default class GetUserById {
7
+ constructor(private userRepository: UserRepository) {}
8
+
9
+ async execute(id: string | number) {
10
+ <%_ if (caching !== 'None') { -%>
11
+ const cacheKey = `user:${id}`;
12
+ const cachedUser = await cacheService.get(cacheKey);
13
+ if (cachedUser) return cachedUser;
14
+ <%_ } -%>
15
+ const user = await this.userRepository.findById(id);
16
+ <%_ if (caching !== 'None') { -%>
17
+ if (user) {
18
+ await cacheService.set(`user:${id}`, user, 3600); // Cache for 1 hour
19
+ }<%_ } -%>
20
+
21
+ return user;
22
+ }
23
+ }
@@ -36,6 +36,7 @@ describe('UpdateUser UseCase', () => {
36
36
  expect(result).toEqual(expectedUser);
37
37
  <%_ if (caching !== 'None') { -%>
38
38
  expect(cacheService.del).toHaveBeenCalledWith('users:all');
39
+ expect(cacheService.del).toHaveBeenCalledWith(`user:${id}`);
39
40
  <%_ } %>
40
41
  });
41
42
 
@@ -0,0 +1,25 @@
1
+ import { User } from '@/domain/user';
2
+ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
3
+ <%_ if (caching !== 'None') { _%>
4
+ import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
5
+ import logger from '@/infrastructure/log/logger';
6
+ <%_ } _%>
7
+
8
+ export default class UpdateUser {
9
+ constructor(private userRepository: UserRepository) {}
10
+
11
+ async execute(id: number | string, data: Partial<User>) {
12
+ const user = await this.userRepository.update(id, data);
13
+ <%_ if (caching !== 'None') { -%>
14
+ if (user) {
15
+ try {
16
+ await cacheService.del('users:all');
17
+ await cacheService.del(`user:${id}`);
18
+ } catch (error) {
19
+ logger.warn('Failed to clear cache after user update:', error);
20
+ }
21
+ }<%_ } -%>
22
+
23
+ return user;
24
+ }
25
+ }
@@ -5,6 +5,7 @@ export const ERROR_MESSAGES = {
5
5
  INTERNAL_SERVER_ERROR: 'Internal Server Error',
6
6
  BAD_REQUEST: 'Bad Request',
7
7
  FETCH_USERS_ERROR: 'Error fetching users',
8
+ FETCH_USER_ERROR: 'Error fetching user',
8
9
  CREATE_USER_ERROR: 'Error creating user',
9
10
  UPDATE_USER_ERROR: 'Error updating user',
10
11
  DELETE_USER_ERROR: 'Error deleting user',
@@ -2,6 +2,7 @@ export const HTTP_STATUS = {
2
2
  OK: 200,
3
3
  CREATED: 201,
4
4
  BAD_REQUEST: 400,
5
+ UNAUTHORIZED: 401,
5
6
  NOT_FOUND: 404,
6
7
  INTERNAL_SERVER_ERROR: 500
7
8
  } as const;
@@ -58,3 +58,12 @@ When indexing or searching the workspace, ignore the following paths to prevent
58
58
  - `controllers`: Request handlers and business logic.
59
59
  - `routes`: Define endpoints routing to controllers.
60
60
  <% } -%>
61
+
62
+ ### 5. Security & Authentication
63
+ <% if (auth.includes('JWT')) { -%>
64
+ - **Protected Routes**: When adding new routes, consider if they need `authMiddleware`.
65
+ - **User Identity**: Extract user info from `req.user` (populated by middleware).
66
+ - **Sensitive Data**: Never return passwords in API responses (ensure `.toJSON()` or DTOs handle this).
67
+ <% } -%>
68
+ - **Input Validation**: Always validate user input using schemas or controller-level checks.
69
+ - **Sanitization**: Ensure inputs are sanitized before being used in DB queries (though our ORMs like Sequelize/Mongoose handle much of this).
@@ -1,8 +1,8 @@
1
1
  # Application
2
2
  PORT=3000
3
3
  NODE_ENV=development
4
-
5
4
  <%_ if (database !== 'None') { -%>
5
+
6
6
  # Database
7
7
  <%_ if (database === 'MySQL') { -%>
8
8
  DB_HOST=localhost
@@ -10,32 +10,39 @@ DB_PORT=3306
10
10
  DB_USER=root
11
11
  DB_PASSWORD=root
12
12
  DB_NAME=<%= dbName %>
13
- <%_ } -%>
13
+ <% } -%>
14
14
  <%_ if (database === 'PostgreSQL') { -%>
15
15
  DB_HOST=localhost
16
16
  DB_PORT=5432
17
17
  DB_USER=postgres
18
18
  DB_PASSWORD=root
19
19
  DB_NAME=<%= dbName %>
20
- <%_ } -%>
20
+ <% } -%>
21
21
  <%_ if (database === 'MongoDB') { -%>
22
22
  DB_HOST=localhost
23
23
  DB_PORT=27017
24
24
  DB_NAME=<%= dbName %>
25
- <%_ } -%>
26
- <%_ } -%>
27
-
28
-
25
+ <% } -%>
26
+ <% } -%>
29
27
  <%_ if (communication === 'Kafka') { -%>
28
+
30
29
  # Communication
31
30
  KAFKA_BROKER=localhost:9093
32
31
  KAFKA_CLIENT_ID=<%= projectName %>
33
32
  KAFKA_GROUP_ID=<%= projectName %>-group
34
- <%_ } -%>
35
-
33
+ <% } -%>
36
34
  <%_ if (caching === 'Redis') { -%>
35
+
37
36
  # Caching
38
37
  REDIS_HOST=localhost
39
38
  REDIS_PORT=6379
40
39
  REDIS_PASSWORD=
41
- <%_ } -%>
40
+ <% } -%>
41
+ <%_ if (auth.includes('JWT')) { -%>
42
+
43
+ # Authentication
44
+ JWT_SECRET=your_jwt_secret_key_here_change_it
45
+ JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here_change_it
46
+ JWT_EXPIRES_IN=15m
47
+ JWT_REFRESH_EXPIRES_IN=7d
48
+ <% } -%>
@@ -1,5 +1,7 @@
1
1
  variables:
2
2
  NODE_ENV: 'test'
3
+ WAIT_ON_HOST: docker
4
+ TEST_URL: http://docker:3001
3
5
 
4
6
  stages:
5
7
  - lint
@@ -30,7 +32,7 @@ run_unit_tests:
30
32
  stage: test
31
33
  image: node:22-slim
32
34
  script:
33
- - npm run test:coverage
35
+ - npm run test:coverage -- --maxWorkers=2
34
36
 
35
37
  run_e2e_tests:
36
38
  stage: test
@@ -3,6 +3,15 @@ pipeline {
3
3
 
4
4
  environment {
5
5
  CI = 'true'
6
+ DOCKER_BUILDKIT = '0'
7
+ PORT = '3001'
8
+ DB_PORT = '3307'
9
+ WAIT_ON_HOST = 'host.docker.internal'
10
+ TEST_URL = 'http://host.docker.internal:3001'
11
+ }
12
+
13
+ tools {
14
+ nodejs 'nodejs'
6
15
  }
7
16
 
8
17
  stages {
@@ -21,7 +30,7 @@ pipeline {
21
30
 
22
31
  stage('Unit Test') {
23
32
  steps {
24
- sh 'npm run test:coverage'
33
+ sh 'npm run test:coverage -- --maxWorkers=2'
25
34
  }
26
35
  }
27
36
 
@@ -29,9 +29,10 @@ This project follows a strict **7-Step Production-Ready Process** to ensure qual
29
29
 
30
30
  - **Architecture**: <%= architecture %> (<% if (architecture === 'Clean Architecture') { %>Domain, UseCases, Infrastructure<% } else { %>MVC Pattern<% } %>).
31
31
  - **Database**: <%= database %> <% if (database !== 'None') { %>(via <%= database === 'MongoDB' ? 'Mongoose' : 'Sequelize' %>)<% } %>.
32
+ <% if (auth.includes('JWT')) { %>- **Authentication**: JWT-based Auth (Sign Up, Login, Protected Routes).<% } %>
32
33
  - **Security**: Helmet, CORS, Rate Limiting, HPP, Snyk SCA.
33
34
  - **Quality**: 80%+ Test Coverage, Eslint, Prettier, Husky.
34
- - **DevOps**: Multi-stage Docker, CI/CD ready (GitHub/GitLab/Jenkins).
35
+ - **DevOps**: Multi-stage Docker, CI/CD ready (GitHub/GitLab/Jenkins/Bitbucket/CircleCI).
35
36
  <% if (includeSecurity) { %>- **Enterprise Hardening**: SonarCloud SAST, Security Policies.<% } %>
36
37
 
37
38
  ## 📂 Project Structure
@@ -95,6 +96,21 @@ Microservices communication handled via **Kafka**.
95
96
  API is exposed via **GraphQL**.
96
97
  The Apollo Sandbox UI for API exploration and documentation is available natively, fully embedded for offline development:
97
98
  - **URL**: `http://localhost:3000/graphql` (Dynamic based on PORT)
99
+
100
+ <%_ if (auth.includes('JWT')) { _%>
101
+ ### 🔐 Authorization
102
+ To access protected resolvers (Query: `getAllUsers`, Mutations: `updateUser`, `deleteUser`), you must provide a valid JWT token in the headers.
103
+
104
+ **How to authenticate:**
105
+ 1. Use the REST login endpoint: `POST /api/auth/login` (with your email and password).
106
+ 2. Copy the `accessToken` from the response.
107
+ 3. In the Apollo Sandbox "HTTP Headers" tab (bottom section), add:
108
+ ```json
109
+ {
110
+ "Authorization": "Bearer YOUR_ACCESS_TOKEN_HERE"
111
+ }
112
+ ```<%_ } _%>
113
+
98
114
  If you are opening `http://localhost:3000/graphql` in your browser, you can directly run the following in the Apollo Sandbox UI:
99
115
 
100
116
  **Query to get all users:**
@@ -111,7 +127,7 @@ query GetAllUsers {
111
127
  **Mutation to create a user:**
112
128
  ```graphql
113
129
  mutation CreateUser {
114
- createUser(name: "John Doe", email: "john@example.com") {
130
+ createUser(name: "John Doe", email: "john@example.com"<%_ if (auth.includes('JWT')) { _%>, password: "yourpassword123"<%_ } _%>) {
115
131
  id
116
132
  name
117
133
  email
@@ -119,7 +135,7 @@ mutation CreateUser {
119
135
  }
120
136
  ```
121
137
 
122
- **Mutation to update a user:**
138
+ **Mutation to update a user (Protected):**
123
139
  ```graphql
124
140
  mutation UpdateUser {
125
141
  updateUser(id: "1", name: "John Updated") {
@@ -130,7 +146,7 @@ mutation UpdateUser {
130
146
  }
131
147
  ```
132
148
 
133
- **Mutation to delete a user:**
149
+ **Mutation to delete a user (Protected):**
134
150
  ```graphql
135
151
  mutation DeleteUser {
136
152
  deleteUser(id: "1")
@@ -143,32 +159,61 @@ A Swagger UI for API documentation is available at:
143
159
 
144
160
  ### 🛣️ User Endpoints:
145
161
  - `GET /api/users`: List all users.
162
+ - `GET /api/users/:id`: Get a user by ID.
146
163
  - `POST /api/users`: Create a new user.
147
164
  - `PATCH /api/users/:id`: Partially update a user.
148
165
  - `DELETE /api/users/:id`: Delete a user (Soft Delete).
149
- <%_ } -%>
150
-
166
+ <%_ if (auth.includes('JWT')) { %>
167
+ ### 🔐 Auth Endpoints:
168
+ - `POST /api/auth/login`: Exchange credentials for a short-lived `accessToken` and a long-lived `refreshToken`.
169
+ - `POST /api/auth/refresh`: Submit a `refreshToken` to receive a new pair of tokens. (Includes theft-detection logic).
170
+ - `POST /api/auth/logout`: Revoke (blacklist) the active `accessToken` and delete the `refreshToken`.
171
+ - `POST /api/users`: Acts as Sign Up when password is provided.
172
+ *Note: To access protected user endpoints (GET/PATCH/DELETE), include `Authorization: Bearer <your_accessToken>` in the headers.*
173
+ <% } -%>
174
+ <% } -%>
151
175
  <% if (communication === 'Kafka') { -%>
152
176
  ## 📡 Testing Kafka Asynchronous Flow
153
177
  This project demonstrates a production-ready Kafka flow:
154
- 1. **Producer**: When a user is created via the API, a `USER_CREATED` event is sent to `user-topic`.
155
- 2. **Consumer**: `WelcomeEmailConsumer` listens to `user-topic` and simulates sending an email.
178
+ 1. **Producer**: When a user is created, updated, or deleted via the API, a corresponding event (`USER_CREATED`, `USER_UPDATED`, `USER_DELETED`) is sent to `user-topic`.
179
+ 2. **Consumer**: `WelcomeEmailConsumer` listens to `user-topic` and simulates processing (e.g., sending an email on creation).
156
180
 
157
181
  ### How to verify:
158
182
  1. Ensure infrastructure is running: `docker-compose up -d<% if (database !== 'None') { %> db<% } %><% if (caching === 'Redis') { %> redis<% } %><% if (communication === 'Kafka') { %> kafka<% } %>`
159
183
  2. Start the app: `npm run dev`
160
- 3. Trigger an event by creating a user (via Postman or curl):
161
- ```bash
162
- curl -X POST http://localhost:3000/api/users \
163
- -H "Content-Type: application/json" \
164
- -d '{"name": "Kafka Tester", "email": "kafka@example.com"}'
165
- ```
184
+ 3. Trigger internal events:
185
+
186
+ **Create User (Sign Up - Public):**
187
+ ```bash
188
+ curl -X POST http://localhost:3000/api/users \
189
+ -H "Content-Type: application/json" \
190
+ -d '{"name": "Kafka Tester", "email": "kafka@example.com"<%_ if (auth.includes('JWT')) { _%>, "password": "password123"<%_ } _%>}'
191
+ ```
192
+
193
+ <%_ if (auth.includes('JWT')) { _%>
194
+ **Update User (Protected - Requires Auth):**
195
+ 1. Login to get token: `POST /api/auth/login`
196
+ 2. Update user with token:
197
+ ```bash
198
+ curl -X PATCH http://localhost:3000/api/users/1 \
199
+ -H "Authorization: Bearer <YOUR_TOKEN>" \
200
+ -H "Content-Type: application/json" \
201
+ -d '{"name": "Updated Name"}'
202
+ ```
203
+ <%_ } else { _%>
204
+ **Update User:**
205
+ ```bash
206
+ curl -X PATCH http://localhost:3000/api/users/1 \
207
+ -H "Content-Type: application/json" \
208
+ -d '{"name": "Updated Name"}'
209
+ ```
210
+ <%_ } _%>
166
211
  4. Observe the logs:
167
- ```text
168
- [Kafka] Producer: Sent USER_CREATED event for 'kafka@example.com'
169
- [Kafka] Consumer: Received USER_CREATED.
170
- [Kafka] Consumer: 📧 Sending welcome email to 'kafka@example.com'... Done!
171
- ```
212
+ ```text
213
+ [Kafka] Producer: Sent USER_CREATED event for 'kafka@example.com'
214
+ [Kafka] Consumer: Received USER_CREATED.
215
+ [Kafka] Consumer: 📧 Sending welcome email to 'kafka@example.com'... Done!
216
+ ```
172
217
 
173
218
  ### 🛠️ Kafka Troubleshooting
174
219
  If the connection or events are failing:
@@ -0,0 +1,96 @@
1
+ version: 2.1
2
+
3
+ orbs:
4
+ node: circleci/node@5.2.0
5
+ <% if (includeSecurity) { %>
6
+ snyk: snyk/snyk@2.1.0
7
+ <% } %>
8
+
9
+ executors:
10
+ node-executor:
11
+ docker:
12
+ - image: cimg/node:22.0
13
+ machine-executor:
14
+ machine:
15
+ image: ubuntu-2204:current
16
+
17
+ jobs:
18
+ lint:
19
+ executor: node-executor
20
+ steps:
21
+ - checkout
22
+ - node/install-packages
23
+ - run: npm run lint
24
+
25
+ unit_test:
26
+ executor: node-executor
27
+ steps:
28
+ - checkout
29
+ - node/install-packages
30
+ - run:
31
+ name: Run Unit Tests with Coverage
32
+ command: npm run test:coverage -- --maxWorkers=2
33
+
34
+ e2e_test:
35
+ executor: machine-executor
36
+ steps:
37
+ - checkout
38
+ - node/install:
39
+ node-version: '22.0'
40
+ - run: npm ci
41
+ - run: npm run test:e2e
42
+
43
+ <% if (includeSecurity) { %>
44
+ security_scan:
45
+ executor: node-executor
46
+ steps:
47
+ - checkout
48
+ - node/install-packages
49
+ - snyk/scan:
50
+ fail-on-issues: true
51
+ severity-threshold: high
52
+
53
+ sonarqube_analysis:
54
+ executor: node-executor
55
+ steps:
56
+ - checkout
57
+ - node/install-packages
58
+ - run:
59
+ name: SonarQube Analysis
60
+ command: |
61
+ # Assuming sonar-scanner is available or run via npx
62
+ npx sonar-scanner
63
+ <% } %>
64
+
65
+ build:
66
+ executor: node-executor
67
+ steps:
68
+ - checkout
69
+ - node/install-packages
70
+ - run: npm run build --if-present
71
+
72
+ workflows:
73
+ build_and_test:
74
+ jobs:
75
+ - lint
76
+ - unit_test:
77
+ requires:
78
+ - lint
79
+ - e2e_test:
80
+ requires:
81
+ - unit_test
82
+ <% if (includeSecurity) { %>
83
+ - security_scan:
84
+ requires:
85
+ - unit_test
86
+ - sonarqube_analysis:
87
+ requires:
88
+ - unit_test
89
+ <% } %>
90
+ - build:
91
+ requires:
92
+ - e2e_test
93
+ <% if (includeSecurity) { %>
94
+ - security_scan
95
+ - sonarqube_analysis
96
+ <% } %>
@@ -30,7 +30,7 @@ jobs:
30
30
  run: npm run lint
31
31
 
32
32
  - name: Run Unit Tests
33
- run: npm run test:coverage
33
+ run: npm run test:coverage -- --maxWorkers=2
34
34
 
35
35
  - name: Run E2E Tests
36
36
  run: npm run test:e2e
@@ -0,0 +1,168 @@
1
+ const bcrypt = require('bcryptjs');
2
+ <% if (architecture === 'MVC') { -%>
3
+ const User = require('../models/User');
4
+ const JwtService = require('../services/jwtService');
5
+ <% if (caching !== 'None') { -%>
6
+ const cacheService = require('<% if (caching === "Redis") { %>../config/redisClient<% } else { %>../config/memoryCache<% } %>');
7
+ <% } -%>
8
+ const logger = require('../utils/logger');
9
+ <% } else { -%>
10
+ const User = require('../../../infrastructure/database/models/User');
11
+ const JwtService = require('../../../infrastructure/auth/jwtService');
12
+ <% if (caching !== 'None') { -%>
13
+ const cacheService = require('<% if (caching === "Redis") { %>../../../infrastructure/caching/redisClient<% } else { %>../../../infrastructure/caching/memoryCache<% } %>');
14
+ <% } -%>
15
+ const logger = require('../../../infrastructure/log/logger');
16
+ <% } -%>
17
+ const HTTP_STATUS = require('<% if (architecture === "MVC") { %>../utils/httpCodes<% } else { %>../../../utils/httpCodes<% } %>');
18
+
19
+ class AuthController {
20
+ constructor() {
21
+ this.login = this.login.bind(this);
22
+ this.refresh = this.refresh.bind(this);
23
+ this.logout = this.logout.bind(this);
24
+ }
25
+
26
+ async login(req, res, next) {
27
+ try {
28
+ const { email, password } = req.body;
29
+ <% if (database === 'MongoDB' || database === 'None') { %>
30
+ const user = await User.findOne({ email });
31
+ <% } else { %>
32
+ const user = await User.findOne({ where: { email } });
33
+ <% } %>
34
+ if (!user) {
35
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid credentials' });
36
+ }
37
+
38
+ const isPasswordValid = await bcrypt.compare(password, user.password);
39
+ if (!isPasswordValid) {
40
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid credentials' });
41
+ }
42
+
43
+ const userId = String(user.id || user._id);
44
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: user.email });
45
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
46
+ const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
47
+
48
+ <% if (caching !== 'None') { %>
49
+ const cacheKey = `refresh_tokens:${userId}`;
50
+ const activeTokens = await cacheService.get(cacheKey) || [];
51
+ activeTokens.push(refreshJti);
52
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
53
+ <% } else { -%>
54
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
55
+ activeTokens.push(refreshJti);
56
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
57
+ <% } %>
58
+
59
+ res.json({ token: accessToken, accessToken, refreshToken });
60
+ } catch (error) {
61
+ logger.error('Login error:', error);
62
+ next(error);
63
+ }
64
+ }
65
+
66
+ async refresh(req, res, next) {
67
+ try {
68
+ const { refreshToken } = req.body;
69
+ if (!refreshToken) {
70
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Refresh token is required' });
71
+ }
72
+
73
+ const decoded = JwtService.verifyRefreshToken(refreshToken);
74
+ if (!decoded || !decoded.id || !decoded.jti) {
75
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid refresh token' });
76
+ }
77
+
78
+ const userId = String(decoded.id);
79
+ const incomingJti = decoded.jti;
80
+
81
+ <% if (caching !== 'None') { %>
82
+ const cacheKey = `refresh_tokens:${userId}`;
83
+ let activeTokens = await cacheService.get(cacheKey) || [];
84
+
85
+ if (!activeTokens.includes(incomingJti)) {
86
+ logger.warn(`Token theft detected for user ${userId}. Revoking all sessions.`);
87
+ await cacheService.del(cacheKey);
88
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid session' });
89
+ }
90
+
91
+ activeTokens = activeTokens.filter(t => t !== incomingJti);
92
+ const newRefreshToken = JwtService.generateRefreshToken({ id: userId, email: decoded.email });
93
+ const newRefreshJti = JwtService.decodeToken(newRefreshToken)?.jti;
94
+ const newAccessToken = JwtService.generateToken({ id: userId, email: decoded.email, sid: newRefreshJti });
95
+
96
+ activeTokens.push(newRefreshJti);
97
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
98
+ <% } else { -%>
99
+ let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
100
+
101
+ if (!activeTokens.includes(incomingJti)) {
102
+ logger.warn(`Token theft detected for user ${userId}. Revoking all sessions.`);
103
+ JwtService.activeRefreshTokens.delete(userId);
104
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid session' });
105
+ }
106
+
107
+ activeTokens = activeTokens.filter(t => t !== incomingJti);
108
+ const newRefreshToken = JwtService.generateRefreshToken({ id: userId, email: decoded.email });
109
+ const newRefreshJti = JwtService.decodeToken(newRefreshToken)?.jti;
110
+ const newAccessToken = JwtService.generateToken({ id: userId, email: decoded.email, sid: newRefreshJti });
111
+
112
+ activeTokens.push(newRefreshJti);
113
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
114
+ <% } %>
115
+ res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
116
+ } catch (error) {
117
+ logger.error('Refresh token error:', error);
118
+ next(error);
119
+ }
120
+ }
121
+
122
+ async logout(req, res, next) {
123
+ try {
124
+ const authHeader = req.headers.authorization;
125
+ if (!authHeader) {
126
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'No token provided' });
127
+ }
128
+
129
+ const accessTokenStr = authHeader.split(' ')[1];
130
+ const decodedAccess = JwtService.decodeToken(accessTokenStr);
131
+
132
+ if (decodedAccess && decodedAccess.jti && decodedAccess.exp) {
133
+ const remainingTime = Math.max(0, decodedAccess.exp - Math.floor(Date.now() / 1000));
134
+ <%_ if (caching !== 'None') { -%>
135
+ if (remainingTime > 0) {
136
+ await cacheService.set(`blacklist:${decodedAccess.jti}`, true, remainingTime);
137
+ }<%_ } else { -%>
138
+ if (remainingTime > 0) {
139
+ JwtService.blacklistedTokens.set(decodedAccess.jti, Date.now() + remainingTime * 1000);
140
+ }<%_ } -%>
141
+ }
142
+
143
+ const { refreshToken } = req.body;
144
+ if (refreshToken) {
145
+ const decodedRefresh = JwtService.decodeToken(refreshToken);
146
+ if (decodedRefresh && decodedRefresh.id && decodedRefresh.jti) {
147
+ const userId = String(decodedRefresh.id);
148
+ <%_ if (caching !== 'None') { -%>
149
+ const cacheKey = `refresh_tokens:${userId}`;
150
+ let activeTokens = await cacheService.get(cacheKey) || [];
151
+ activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
152
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
153
+ <%_ } else { -%>
154
+ let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
155
+ activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
156
+ JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
157
+ }
158
+ }
159
+
160
+ res.json({ message: 'Logged out successfully' });
161
+ } catch (error) {
162
+ logger.error('Logout error:', error);
163
+ next(error);
164
+ }
165
+ }
166
+ }
167
+
168
+ module.exports = AuthController;