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,148 @@
1
+ <%_ if (architecture === 'Clean Architecture') { _%>
2
+ const AuthController = require('@/interfaces/controllers/auth/authController');
3
+ const JwtService = require('@/infrastructure/auth/jwtService');
4
+ <%_ } else { _%>
5
+ const AuthController = require('@/controllers/authController');
6
+ const JwtService = require('@/services/jwtService');
7
+ <%_ } _%>
8
+ const HTTP_STATUS = require('@/utils/httpCodes');
9
+ const bcrypt = require('bcryptjs');
10
+
11
+ jest.mock('bcryptjs');
12
+ <%_ if (architecture === 'Clean Architecture') { _%>
13
+ jest.mock('@/infrastructure/auth/jwtService');
14
+ jest.mock('@/infrastructure/database/models/User', () => ({
15
+ findOne: jest.fn()
16
+ }));
17
+ const User = require('@/infrastructure/database/models/User');
18
+ <%_ } else { _%>
19
+ jest.mock('@/services/jwtService');
20
+ jest.mock('@/models/User', () => ({
21
+ findOne: jest.fn()
22
+ }));
23
+ const User = require('@/models/User');
24
+ <%_ } _%>
25
+ <%_ if (caching !== 'None') { _%>
26
+ <%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
27
+ jest.mock('<%= cachePath %>', () => ({
28
+ get: jest.fn(),
29
+ set: jest.fn(),
30
+ del: jest.fn()
31
+ }), { virtual: true });
32
+ const cacheService = require('<%= cachePath %>');
33
+ <%_ } _%>
34
+
35
+ describe('AuthController', () => {
36
+ let controller;
37
+ let mockReq;
38
+ let mockRes;
39
+ let next;
40
+
41
+ beforeEach(() => {
42
+ controller = new AuthController();
43
+ mockReq = {
44
+ body: {},
45
+ headers: {}
46
+ };
47
+ mockRes = {
48
+ status: jest.fn().mockReturnThis(),
49
+ json: jest.fn()
50
+ };
51
+ next = jest.fn();
52
+ jest.clearAllMocks();
53
+ });
54
+
55
+ describe('login', () => {
56
+ it('should return tokens on success', async () => {
57
+ const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
58
+ mockReq.body = { email: 'test@test.com', password: 'password123' };
59
+ User.findOne.mockResolvedValue(user);
60
+ bcrypt.compare.mockResolvedValue(true);
61
+ JwtService.generateToken.mockReturnValue('mock-access-token');
62
+ JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
63
+ JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
64
+
65
+ // Mock cacheService
66
+ <% if (caching !== 'None') { %>
67
+ cacheService.get.mockResolvedValue([]);
68
+ cacheService.set.mockResolvedValue();<% } %>
69
+
70
+ await controller.login(mockReq, mockRes, next);
71
+
72
+ expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'test-jti' }));
73
+ expect(mockRes.json).toHaveBeenCalledWith({ token: 'mock-access-token', accessToken: 'mock-access-token', refreshToken: 'mock-refresh-token' });
74
+ });
75
+
76
+ it('should return 401 on invalid email', async () => {
77
+ mockReq.body = { email: 'wrong@test.com', password: 'password123' };
78
+ User.findOne.mockResolvedValue(null);
79
+
80
+ await controller.login(mockReq, mockRes, next);
81
+
82
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
83
+ });
84
+
85
+ it('should return 401 on invalid password', async () => {
86
+ const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
87
+ mockReq.body = { email: 'test@test.com', password: 'wrongpassword' };
88
+ User.findOne.mockResolvedValue(user);
89
+ bcrypt.compare.mockResolvedValue(false);
90
+
91
+ await controller.login(mockReq, mockRes, next);
92
+
93
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
94
+ });
95
+ });
96
+
97
+ describe('refresh', () => {
98
+ it('should return new tokens for a valid refresh token', async () => {
99
+ mockReq.body = { refreshToken: 'valid-refresh' };
100
+ const decoded = { id: '1', email: 'test@test.com', jti: 'old-jti' };
101
+ JwtService.verifyRefreshToken.mockReturnValue(decoded);
102
+ JwtService.generateToken.mockReturnValue('new-access');
103
+ JwtService.generateRefreshToken.mockReturnValue('new-refresh');
104
+ JwtService.decodeToken.mockReturnValue({ jti: 'new-jti' });
105
+
106
+ <% if (caching !== 'None') { %>
107
+ cacheService.get.mockResolvedValue(['old-jti']);
108
+ cacheService.set.mockResolvedValue();
109
+ <% } else { %>
110
+ JwtService.activeRefreshTokens.set('1', ['old-jti']);<% } %>
111
+
112
+ await controller.refresh(mockReq, mockRes, next);
113
+
114
+ expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'new-jti' }));
115
+ expect(mockRes.json).toHaveBeenCalledWith({ accessToken: 'new-access', refreshToken: 'new-refresh' });
116
+ });
117
+
118
+ it('should detect theft and revoke if jti is not active', async () => {
119
+ mockReq.body = { refreshToken: 'stolen-refresh' };
120
+ const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
121
+ JwtService.verifyRefreshToken.mockReturnValue(decoded);
122
+
123
+ <% if (caching !== 'None') { %>
124
+ cacheService.get.mockResolvedValue(['different-jti']);
125
+ <% } else { %>
126
+ JwtService.activeRefreshTokens.set('1', ['different-jti']);<% } %>
127
+
128
+ await controller.refresh(mockReq, mockRes, next);
129
+
130
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
131
+ });
132
+ });
133
+
134
+ describe('logout', () => {
135
+ it('should blacklist access jti and remove refresh jti', async () => {
136
+ mockReq.headers.authorization = 'Bearer access-token';
137
+ mockReq.body = { refreshToken: 'refresh-token' };
138
+
139
+ JwtService.decodeToken
140
+ .mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 })
141
+ .mockReturnValueOnce({ id: '1', jti: 'refresh-jti' });
142
+
143
+ await controller.logout(mockReq, mockRes, next);
144
+
145
+ expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,58 @@
1
+ <% if (architecture === 'MVC') { -%>
2
+ const JwtService = require('../services/jwtService');
3
+ const HTTP_STATUS = require('../utils/httpCodes');
4
+ <% if (caching !== 'None') { -%>
5
+ const cacheService = require('<% if (caching === "Redis") { %>../config/redisClient<% } else { %>../config/memoryCache<% } %>');
6
+ <% } -%>
7
+ <% } else { -%>
8
+ const JwtService = require('../../../infrastructure/auth/jwtService');
9
+ const HTTP_STATUS = require('../../../utils/httpCodes');
10
+ <% if (caching !== 'None') { -%>
11
+ const cacheService = require('<% if (caching === "Redis") { %>../../../infrastructure/caching/redisClient<% } else { %>../../../infrastructure/caching/memoryCache<% } %>');
12
+ <% } -%>
13
+ <% } -%>
14
+
15
+ const authMiddleware = async (req, res, next) => {
16
+ const authHeader = req.headers.authorization;
17
+
18
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
19
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'No token provided' });
20
+ }
21
+
22
+ const token = authHeader.split(' ')[1];
23
+ const decoded = JwtService.verifyToken(token);
24
+
25
+ if (!decoded) {
26
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid or expired token' });
27
+ }
28
+
29
+ if (decoded.jti) {
30
+ <%_ if (caching !== 'None') { -%>
31
+ const isBlacklisted = await cacheService.get(`blacklist:${decoded.jti}`);
32
+ if (isBlacklisted) {
33
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
34
+ }<%_ } else { -%>
35
+ const expiryDate = JwtService.blacklistedTokens.get(decoded.jti);
36
+ if (expiryDate && Date.now() < expiryDate) {
37
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
38
+ }<% } %>
39
+ }
40
+
41
+ if (decoded.sid) {
42
+ <%_ if (caching !== 'None') { %>
43
+ const cacheKey = `refresh_tokens:${decoded.id}`;
44
+ const activeTokens = await cacheService.get(cacheKey) || [];
45
+ if (!activeTokens.includes(decoded.sid)) {
46
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
47
+ }<%_ } else { -%>
48
+ const activeTokens = JwtService.activeRefreshTokens.get(String(decoded.id)) || [];
49
+ if (!activeTokens.includes(decoded.sid)) {
50
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
51
+ }<%_ } -%>
52
+ }
53
+
54
+ req.user = decoded;
55
+ next();
56
+ };
57
+
58
+ module.exports = authMiddleware;
@@ -0,0 +1,108 @@
1
+ <%_ if (architecture === 'Clean Architecture') { _%>
2
+ const authMiddleware = require('@/infrastructure/webserver/middleware/authMiddleware');
3
+ const JwtService = require('@/infrastructure/auth/jwtService');
4
+ <%_ } else { _%>
5
+ const authMiddleware = require('@/middleware/authMiddleware');
6
+ const JwtService = require('@/services/jwtService');
7
+ <%_ } _%>
8
+ const HTTP_STATUS = require('@/utils/httpCodes');
9
+
10
+ <%_ if (architecture === 'Clean Architecture') { _%>
11
+ jest.mock('@/infrastructure/auth/jwtService');
12
+ <%_ } else { _%>
13
+ jest.mock('@/services/jwtService');
14
+ <%_ } _%>
15
+ <%_ if (caching !== 'None') { _%>
16
+ <%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
17
+ jest.mock('<%= cachePath %>', () => ({
18
+ get: jest.fn(),
19
+ set: jest.fn(),
20
+ del: jest.fn()
21
+ }), { virtual: true });
22
+ const cacheService = require('<%= cachePath %>');
23
+ <%_ } _%>
24
+
25
+ describe('AuthMiddleware', () => {
26
+ let mockReq;
27
+ let mockRes;
28
+ let next;
29
+
30
+ beforeEach(() => {
31
+ mockReq = {
32
+ headers: {}
33
+ };
34
+ mockRes = {
35
+ status: jest.fn().mockReturnThis(),
36
+ json: jest.fn()
37
+ };
38
+ next = jest.fn();
39
+ jest.clearAllMocks();
40
+ });
41
+
42
+ it('should return 401 if no authorization header', async () => {
43
+ await authMiddleware(mockReq, mockRes, next);
44
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
45
+ expect(next).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it('should return 401 if token is blacklisted', async () => {
49
+ const user = { id: 1, email: 'test@example.com', jti: 'blacklisted-jti' };
50
+ mockReq.headers.authorization = 'Bearer valid-token';
51
+ JwtService.verifyToken.mockReturnValue(user);
52
+
53
+ // Mock the blacklist check
54
+ <% if (caching !== 'None') { %>
55
+ cacheService.get.mockResolvedValue(true);
56
+ <% } else { %>
57
+ JwtService.blacklistedTokens.set('blacklisted-jti', Date.now() + 10000);<% } %>
58
+
59
+ await authMiddleware(mockReq, mockRes, next);
60
+
61
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
62
+ expect(mockRes.json).toHaveBeenCalledWith({ message: 'Token revoked' });
63
+ expect(next).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it('should return 401 if session is expired (sid not in activeTokens)', async () => {
67
+ const user = { id: 1, email: 'test@example.com', jti: 'valid-jti', sid: 'expired-sid' };
68
+ mockReq.headers.authorization = 'Bearer valid-token';
69
+ JwtService.verifyToken.mockReturnValue(user);
70
+
71
+ <% if (caching !== 'None') { %>
72
+ cacheService.get.mockImplementation((key) => {
73
+ if (key.startsWith('blacklist:')) return Promise.resolve(false);
74
+ if (key === 'refresh_tokens:1') return Promise.resolve(['other-sid']);
75
+ return Promise.resolve(null);
76
+ });
77
+ <% } else { %>
78
+ JwtService.blacklistedTokens.delete('valid-jti');
79
+ JwtService.activeRefreshTokens.set('1', ['other-sid']);<% } %>
80
+
81
+ await authMiddleware(mockReq, mockRes, next);
82
+
83
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
84
+ expect(mockRes.json).toHaveBeenCalledWith({ message: 'Session expired' });
85
+ expect(next).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('should set req.user and call next if token is valid, not blacklisted and session is active', async () => {
89
+ const user = { id: 1, jti: 'valid-jti', sid: 'active-sid' };
90
+ mockReq.headers.authorization = 'Bearer valid-token';
91
+ JwtService.verifyToken.mockReturnValue(user);
92
+
93
+ <% if (caching !== 'None') { %>
94
+ cacheService.get.mockImplementation((key) => {
95
+ if (key.startsWith('blacklist:')) return Promise.resolve(false);
96
+ if (key === 'refresh_tokens:1') return Promise.resolve(['active-sid']);
97
+ return Promise.resolve(null);
98
+ });
99
+ <% } else { %>
100
+ JwtService.blacklistedTokens.delete('valid-jti');
101
+ JwtService.activeRefreshTokens.set('1', ['active-sid']);<% } %>
102
+
103
+ await authMiddleware(mockReq, mockRes, next);
104
+
105
+ expect(mockReq.user).toEqual(user);
106
+ expect(next).toHaveBeenCalled();
107
+ });
108
+ });
@@ -0,0 +1,16 @@
1
+ const { Router } = require('express');
2
+ const AuthController = require('<% if (architecture === "MVC") { %>../controllers/authController<% } else { %>../controllers/auth/authController<% } %>');
3
+ <%_ if (architecture === 'MVC') { -%>
4
+ const authMiddleware = require('../middleware/authMiddleware');
5
+ <%_ } else { -%>
6
+ const authMiddleware = require('../../infrastructure/webserver/middleware/authMiddleware');
7
+ <%_ } -%>
8
+
9
+ const router = Router();
10
+ const authController = new AuthController();
11
+
12
+ router.post('/login', (req, res, next) => authController.login(req, res, next));
13
+ router.post('/refresh', (req, res, next) => authController.refresh(req, res, next));
14
+ router.post('/logout', authMiddleware, (req, res, next) => authController.logout(req, res, next));
15
+
16
+ module.exports = router;
@@ -0,0 +1,54 @@
1
+ const jwt = require('jsonwebtoken');
2
+ const crypto = require('crypto');
3
+ <% if (architecture === 'Clean Architecture') { %>
4
+ const { env } = require('../config/env');
5
+ <% } else { %>
6
+ const { env } = require('../config/env');
7
+ <% } %>
8
+
9
+ class JwtService {
10
+ static SECRET = env.JWT_SECRET || 'your-secret-key';
11
+ static REFRESH_SECRET = env.JWT_REFRESH_SECRET || 'your-refresh-secret-key';
12
+ static EXPIRES_IN = env.JWT_EXPIRES_IN || '15m'; // Access tokens should be short-lived
13
+ static REFRESH_EXPIRES_IN = env.JWT_REFRESH_EXPIRES_IN || '7d';
14
+
15
+ static generateToken(payload, expiresIn) {
16
+ const jti = crypto.randomUUID();
17
+ return jwt.sign({ ...payload, jti }, this.SECRET, { expiresIn: expiresIn || this.EXPIRES_IN });
18
+ }
19
+
20
+ static generateRefreshToken(payload, expiresIn) {
21
+ const jti = crypto.randomUUID();
22
+ return jwt.sign({ ...payload, jti }, this.REFRESH_SECRET, { expiresIn: expiresIn || this.REFRESH_EXPIRES_IN });
23
+ }
24
+
25
+ static verifyToken(token) {
26
+ try {
27
+ return jwt.verify(token, this.SECRET);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ static verifyRefreshToken(token) {
34
+ try {
35
+ return jwt.verify(token, this.REFRESH_SECRET);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ static decodeToken(token) {
42
+ try {
43
+ return jwt.decode(token);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // Fallback in-memory storage if caching = 'None'
50
+ static activeRefreshTokens = new Map();
51
+ static blacklistedTokens = new Map();
52
+ }
53
+
54
+ module.exports = JwtService;
@@ -0,0 +1,84 @@
1
+ const jwt = require('jsonwebtoken');
2
+ <%_ if (architecture === 'Clean Architecture') { _%>
3
+ const JwtService = require('@/infrastructure/auth/jwtService');
4
+ <%_ } else { _%>
5
+ const JwtService = require('@/services/jwtService');
6
+ <%_ } _%>
7
+
8
+ jest.mock('jsonwebtoken');
9
+
10
+ describe('JwtService', () => {
11
+ const secret = 'test-secret';
12
+ const payload = { id: 1, email: 'test@example.com' };
13
+ const token = 'mock-token';
14
+
15
+ beforeEach(() => {
16
+ JwtService.SECRET = secret;
17
+ JwtService.REFRESH_SECRET = 'test-refresh-secret';
18
+ process.env.JWT_SECRET = secret;
19
+ jest.clearAllMocks();
20
+ });
21
+
22
+ describe('generateToken', () => {
23
+ it('should generate a token with standard expiration and jti', () => {
24
+ jwt.sign.mockReturnValue(token);
25
+
26
+ const result = JwtService.generateToken(payload);
27
+
28
+ expect(jwt.sign).toHaveBeenCalledWith(
29
+ expect.objectContaining({ ...payload, jti: expect.any(String) }),
30
+ secret,
31
+ { expiresIn: '15m' }
32
+ );
33
+ expect(result).toBe(token);
34
+ });
35
+ });
36
+
37
+ describe('generateRefreshToken', () => {
38
+ it('should generate a refresh token with refresh secret and jti', () => {
39
+ jwt.sign.mockReturnValue(token);
40
+
41
+ const result = JwtService.generateRefreshToken(payload);
42
+
43
+ expect(jwt.sign).toHaveBeenCalledWith(
44
+ expect.objectContaining({ ...payload, jti: expect.any(String) }),
45
+ 'test-refresh-secret',
46
+ { expiresIn: '7d' }
47
+ );
48
+ expect(result).toBe(token);
49
+ });
50
+ });
51
+
52
+ describe('verifyToken', () => {
53
+ it('should return decoded payload for a valid token', () => {
54
+ jwt.verify.mockReturnValue(payload);
55
+
56
+ const result = JwtService.verifyToken(token);
57
+
58
+ expect(jwt.verify).toHaveBeenCalledWith(token, secret);
59
+ expect(result).toEqual(payload);
60
+ });
61
+ });
62
+
63
+ describe('verifyRefreshToken', () => {
64
+ it('should return decoded payload for a valid refresh token', () => {
65
+ jwt.verify.mockReturnValue(payload);
66
+
67
+ const result = JwtService.verifyRefreshToken(token);
68
+
69
+ expect(jwt.verify).toHaveBeenCalledWith(token, 'test-refresh-secret');
70
+ expect(result).toEqual(payload);
71
+ });
72
+ });
73
+
74
+ describe('decodeToken', () => {
75
+ it('should return decoded payload without verification', () => {
76
+ jwt.decode.mockReturnValue(payload);
77
+
78
+ const result = JwtService.decodeToken(token);
79
+
80
+ expect(jwt.decode).toHaveBeenCalledWith(token);
81
+ expect(result).toEqual(payload);
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,161 @@
1
+ <%_ if (architecture === 'Clean Architecture') { _%>
2
+ import { AuthController } from '@/interfaces/controllers/auth/authController';
3
+ import { JwtService } from '@/infrastructure/auth/jwtService';
4
+ <%_ } else { _%>
5
+ import { AuthController } from '@/controllers/authController';
6
+ import { JwtService } from '@/services/jwtService';
7
+ <%_ } _%>
8
+ import { HTTP_STATUS } from '@/utils/httpCodes';
9
+ import bcrypt from 'bcryptjs';
10
+ import { Request, Response, NextFunction } from 'express';
11
+
12
+ <%_ if (architecture === 'Clean Architecture') { _%>
13
+ jest.mock('@/infrastructure/auth/jwtService');
14
+ <%_ } else { _%>
15
+ jest.mock('@/services/jwtService');
16
+ <%_ } _%>
17
+ jest.mock('bcryptjs');
18
+
19
+ <%_ if (architecture === 'Clean Architecture') { _%>
20
+ jest.mock('@/infrastructure/database/models/User', () => ({ findOne: jest.fn() }));
21
+ const User = require('@/infrastructure/database/models/User');
22
+ <%_ } else { _%>
23
+ jest.mock('@/models/User', () => ({ findOne: jest.fn() }));
24
+ const User = require('@/models/User');
25
+ <%_ } _%>
26
+
27
+ <%_ if (caching !== 'None') { _%>
28
+ <%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
29
+ jest.mock('<%= cachePath %>', () => ({
30
+ __esModule: true,
31
+ default: {
32
+ get: jest.fn(),
33
+ set: jest.fn(),
34
+ del: jest.fn()
35
+ }
36
+ }), { virtual: true });
37
+ import cacheService from '<%= cachePath %>';
38
+ <%_ } _%>
39
+
40
+ describe('AuthController', () => {
41
+ let authController: AuthController;
42
+ let mockRequest: any;
43
+ let mockResponse: any;
44
+ const nextFunction: NextFunction = jest.fn();
45
+
46
+ beforeEach(() => {
47
+ authController = new AuthController();
48
+ mockRequest = {
49
+ body: {}
50
+ };
51
+ mockResponse = {
52
+ status: jest.fn().mockReturnThis(),
53
+ json: jest.fn()
54
+ };
55
+ jest.clearAllMocks();
56
+ });
57
+
58
+ describe('login', () => {
59
+ it('should return 401 if user not found', async () => {
60
+ mockRequest.body = { email: 'notfound@test.com', password: 'password' };
61
+ (User.findOne as jest.Mock).mockResolvedValue(null);
62
+
63
+ await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
64
+
65
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
66
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid credentials' });
67
+ });
68
+
69
+ it('should return 401 if password does not match', async () => {
70
+ const user = { email: 'test@test.com', password: 'hashedpassword' };
71
+ mockRequest.body = { email: 'test@test.com', password: 'wrongpassword' };
72
+ (User.findOne as jest.Mock).mockResolvedValue(user);
73
+ (bcrypt.compare as jest.Mock).mockResolvedValue(false);
74
+
75
+ await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
76
+
77
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
78
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid credentials' });
79
+ });
80
+
81
+ it('should return 200 and a token if credentials are valid', async () => {
82
+ const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
83
+ mockRequest.body = { email: 'test@test.com', password: 'password123' };
84
+ (User.findOne as jest.Mock).mockResolvedValue(user);
85
+ (bcrypt.compare as jest.Mock).mockResolvedValue(true);
86
+ (JwtService.generateToken as jest.Mock).mockReturnValue('mock-token');
87
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('mock-refresh-token');
88
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'test-jti' });
89
+
90
+ await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
91
+
92
+ expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'test-jti' }));
93
+ expect(mockResponse.json).toHaveBeenCalledWith({ token: 'mock-token', accessToken: 'mock-token', refreshToken: 'mock-refresh-token' });
94
+ });
95
+
96
+ it('should call next with error if something fails', async () => {
97
+ const error = new Error('DB Error');
98
+ mockRequest.body = { email: 'test@test.com', password: 'password123' };
99
+ (User.findOne as jest.Mock).mockRejectedValue(error);
100
+
101
+ await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
102
+
103
+ expect(nextFunction).toHaveBeenCalledWith(error);
104
+ });
105
+ });
106
+
107
+ describe('refresh', () => {
108
+ it('should return new tokens for a valid refresh token', async () => {
109
+ mockRequest.body = { refreshToken: 'valid-refresh' };
110
+ const decoded = { id: '1', email: 'test@test.com', jti: 'old-jti' };
111
+ (JwtService.verifyRefreshToken as jest.Mock).mockReturnValue(decoded);
112
+ (JwtService.generateToken as jest.Mock).mockReturnValue('new-access');
113
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('new-refresh');
114
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'new-jti' });
115
+
116
+ // Mock cache success
117
+ <% if (caching !== 'None') { %>
118
+ (cacheService.get as jest.Mock).mockResolvedValue(['old-jti']);
119
+ <% } else { %>
120
+ JwtService.activeRefreshTokens.set('1', ['old-jti']);
121
+ <% } %>
122
+
123
+ await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
124
+
125
+ expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'new-jti' }));
126
+ expect(mockResponse.json).toHaveBeenCalledWith({ accessToken: 'new-access', refreshToken: 'new-refresh' });
127
+ });
128
+
129
+ it('should detect theft and revoke all tokens if jti is not active', async () => {
130
+ mockRequest.body = { refreshToken: 'stolen-refresh' };
131
+ const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
132
+ (JwtService.verifyRefreshToken as jest.Mock).mockReturnValue(decoded);
133
+
134
+ <% if (caching !== 'None') { %>
135
+ (cacheService.get as jest.Mock).mockResolvedValue(['some-other-jti']);
136
+ <% } else { %>
137
+ JwtService.activeRefreshTokens.set('1', ['some-other-jti']);
138
+ <% } %>
139
+
140
+ await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
141
+
142
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
143
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid session' });
144
+ });
145
+ });
146
+
147
+ describe('logout', () => {
148
+ it('should blacklist the access token and remove the refresh token', async () => {
149
+ mockRequest.headers = { authorization: 'Bearer access-token' };
150
+ mockRequest.body = { refreshToken: 'refresh-token' };
151
+
152
+ (JwtService.decodeToken as jest.Mock)
153
+ .mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 }) // for access token
154
+ .mockReturnValueOnce({ id: '1', jti: 'refresh-jti' }); // for refresh token
155
+
156
+ await authController.logout(mockRequest as Request, mockResponse as Response, nextFunction);
157
+
158
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
159
+ });
160
+ });
161
+ });