nodejs-quickstart-structure 2.0.1 → 2.1.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 (165) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +64 -66
  3. package/bin/index.js +5 -2
  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 +6 -0
  9. package/lib/modules/database-setup.js +2 -1
  10. package/lib/modules/project-setup.js +1 -0
  11. package/lib/prompts.js +39 -0
  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 +38 -1
  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 +51 -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 +70 -5
  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 +55 -22
  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 +12 -3
  32. package/templates/clean-architecture/js/src/usecases/DeleteUser.js.ejs +27 -0
  33. package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +9 -1
  34. package/templates/clean-architecture/js/src/usecases/GetAllUsers.js.ejs +36 -0
  35. package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +23 -1
  36. package/templates/clean-architecture/js/src/usecases/GetUserById.js.ejs +36 -0
  37. package/templates/clean-architecture/js/src/usecases/GetUserById.spec.js.ejs +48 -0
  38. package/templates/clean-architecture/js/src/usecases/UpdateUser.js.ejs +28 -0
  39. package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +9 -1
  40. package/templates/clean-architecture/js/src/utils/errorMessages.js +1 -0
  41. package/templates/clean-architecture/js/src/utils/httpCodes.js +2 -0
  42. package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -3
  43. package/templates/clean-architecture/ts/src/domain/user.ts +3 -1
  44. package/templates/clean-architecture/ts/src/index.ts.ejs +4 -0
  45. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +71 -10
  46. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +32 -3
  47. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +43 -9
  48. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +57 -15
  49. package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +57 -24
  50. package/templates/clean-architecture/ts/src/interfaces/graphql/context.ts.ejs +14 -8
  51. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +33 -10
  52. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +15 -5
  53. package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +1 -1
  54. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +9 -1
  55. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts.ejs +16 -0
  56. package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +12 -3
  57. package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +35 -0
  58. package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +10 -1
  59. package/templates/clean-architecture/ts/src/usecases/deleteUser.ts.ejs +24 -0
  60. package/templates/clean-architecture/ts/src/usecases/getAllUsers.spec.ts.ejs +9 -1
  61. package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts.ejs +21 -0
  62. package/templates/clean-architecture/ts/src/usecases/getUserById.spec.ts.ejs +55 -0
  63. package/templates/clean-architecture/ts/src/usecases/getUserById.ts.ejs +23 -0
  64. package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +10 -1
  65. package/templates/clean-architecture/ts/src/usecases/updateUser.ts.ejs +25 -0
  66. package/templates/clean-architecture/ts/src/utils/errorMessages.ts +1 -0
  67. package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
  68. package/templates/common/.cursorrules.ejs +9 -0
  69. package/templates/common/.env.example.ejs +17 -10
  70. package/templates/common/README.md.ejs +63 -18
  71. package/templates/common/auth/js/controllers/authController.js.ejs +170 -0
  72. package/templates/common/auth/js/controllers/authController.spec.js.ejs +148 -0
  73. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +58 -0
  74. package/templates/common/auth/js/middleware/authMiddleware.spec.js.ejs +108 -0
  75. package/templates/common/auth/js/routes/authRoutes.js.ejs +16 -0
  76. package/templates/common/auth/js/services/jwtService.js.ejs +54 -0
  77. package/templates/common/auth/js/services/jwtService.spec.js.ejs +84 -0
  78. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +161 -0
  79. package/templates/common/auth/ts/controllers/authController.ts.ejs +167 -0
  80. package/templates/common/auth/ts/middleware/authMiddleware.spec.ts.ejs +128 -0
  81. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +59 -0
  82. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +20 -0
  83. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +89 -0
  84. package/templates/common/auth/ts/services/jwtService.ts.ejs +60 -0
  85. package/templates/common/babel.config.js.ejs +5 -0
  86. package/templates/common/caching/clean/js/CreateUser.js.ejs +14 -5
  87. package/templates/common/caching/clean/js/DeleteUser.js.ejs +2 -1
  88. package/templates/common/caching/clean/js/GetUserById.js.ejs +39 -0
  89. package/templates/common/caching/clean/js/UpdateUser.js.ejs +2 -1
  90. package/templates/common/caching/clean/ts/createUser.ts.ejs +14 -6
  91. package/templates/common/caching/clean/ts/deleteUser.ts.ejs +2 -1
  92. package/templates/common/caching/clean/ts/getUserById.ts.ejs +32 -0
  93. package/templates/common/caching/clean/ts/updateUser.ts.ejs +2 -1
  94. package/templates/common/caching/js/memoryCache.spec.js.ejs +2 -0
  95. package/templates/common/caching/js/redisClient.spec.js.ejs +2 -0
  96. package/templates/common/caching/ts/memoryCache.spec.ts.ejs +2 -0
  97. package/templates/common/caching/ts/redisClient.spec.ts.ejs +2 -0
  98. package/templates/common/database/js/models/User.js.ejs +14 -1
  99. package/templates/common/database/js/models/User.js.mongoose.ejs +7 -0
  100. package/templates/common/database/js/models/User.spec.js.ejs +12 -0
  101. package/templates/common/database/js/mongoose.spec.js.ejs +2 -0
  102. package/templates/common/database/ts/models/User.spec.ts.ejs +10 -0
  103. package/templates/common/database/ts/models/User.ts.ejs +17 -0
  104. package/templates/common/database/ts/models/User.ts.mongoose.ejs +8 -0
  105. package/templates/common/database/ts/mongoose.spec.ts.ejs +2 -0
  106. package/templates/common/docker-compose.yml.ejs +12 -0
  107. package/templates/common/ecosystem.config.js.ejs +9 -3
  108. package/templates/common/eslint.config.mjs.ejs +3 -0
  109. package/templates/common/jest.config.js.ejs +13 -9
  110. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +1 -1
  111. package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
  112. package/templates/common/migrations/init.js.ejs +5 -4
  113. package/templates/common/package.json.ejs +11 -2
  114. package/templates/common/prompts/project-context.md.ejs +8 -1
  115. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +149 -107
  116. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +88 -47
  117. package/templates/common/swagger.yml.ejs +148 -0
  118. package/templates/common/tsconfig.eslint.json +15 -0
  119. package/templates/common/tsconfig.json +3 -1
  120. package/templates/common/views/ejs/index.ejs +264 -30
  121. package/templates/common/views/ejs/login.ejs.ejs +244 -0
  122. package/templates/common/views/ejs/signup.ejs.ejs +282 -0
  123. package/templates/common/views/pug/index.pug +269 -38
  124. package/templates/common/views/pug/login.pug.ejs +195 -0
  125. package/templates/common/views/pug/signup.pug.ejs +241 -0
  126. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +6 -0
  127. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +6 -0
  128. package/templates/mvc/js/src/config/env.js.ejs +12 -3
  129. package/templates/mvc/js/src/controllers/userController.js.ejs +29 -5
  130. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +27 -12
  131. package/templates/mvc/js/src/graphql/context.js.ejs +14 -3
  132. package/templates/mvc/js/src/graphql/context.spec.js.ejs +36 -21
  133. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +10 -5
  134. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +32 -10
  135. package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +1 -1
  136. package/templates/mvc/js/src/index.js.ejs +16 -3
  137. package/templates/mvc/js/src/routes/api.js.ejs +14 -0
  138. package/templates/mvc/js/src/routes/api.spec.js.ejs +3 -0
  139. package/templates/mvc/js/src/utils/errorMessages.js +1 -0
  140. package/templates/mvc/js/src/utils/httpCodes.js +1 -0
  141. package/templates/mvc/ts/src/config/env.ts.ejs +12 -3
  142. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +95 -7
  143. package/templates/mvc/ts/src/controllers/userController.ts.ejs +68 -11
  144. package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +36 -23
  145. package/templates/mvc/ts/src/graphql/context.ts.ejs +15 -6
  146. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +32 -10
  147. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +15 -5
  148. package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +1 -1
  149. package/templates/mvc/ts/src/index.ts.ejs +15 -3
  150. package/templates/mvc/ts/src/routes/api.spec.ts.ejs +6 -0
  151. package/templates/mvc/ts/src/routes/api.ts.ejs +15 -0
  152. package/templates/mvc/ts/src/utils/errorMessages.ts +1 -0
  153. package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
  154. package/templates/clean-architecture/js/src/interfaces/routes/api.js +0 -12
  155. package/templates/clean-architecture/js/src/usecases/CreateUser.js +0 -14
  156. package/templates/clean-architecture/js/src/usecases/DeleteUser.js +0 -11
  157. package/templates/clean-architecture/js/src/usecases/GetAllUsers.js +0 -12
  158. package/templates/clean-architecture/js/src/usecases/UpdateUser.js +0 -11
  159. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +0 -13
  160. package/templates/clean-architecture/ts/src/usecases/createUser.ts +0 -13
  161. package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +0 -9
  162. package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts +0 -10
  163. package/templates/clean-architecture/ts/src/usecases/updateUser.ts +0 -9
  164. package/templates/mvc/js/src/routes/api.js +0 -10
  165. package/templates/mvc/ts/src/routes/api.ts +0 -12
@@ -0,0 +1,170 @@
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
+ }
138
+ <% } else { %>
139
+ if (remainingTime > 0) {
140
+ JwtService.blacklistedTokens.set(decodedAccess.jti, Date.now() + remainingTime * 1000);
141
+ }
142
+ <%_ } -%>
143
+ }
144
+
145
+ const { refreshToken } = req.body;
146
+ if (refreshToken) {
147
+ const decodedRefresh = JwtService.decodeToken(refreshToken);
148
+ if (decodedRefresh && decodedRefresh.id && decodedRefresh.jti) {
149
+ const userId = String(decodedRefresh.id);
150
+ <%_ if (caching !== 'None') { -%>
151
+ const cacheKey = `refresh_tokens:${userId}`;
152
+ let activeTokens = await cacheService.get(cacheKey) || [];
153
+ activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
154
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
155
+ <%_ } else { -%>
156
+ let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
157
+ activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
158
+ JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
159
+ }
160
+ }
161
+
162
+ res.json({ message: 'Logged out successfully' });
163
+ } catch (error) {
164
+ logger.error('Logout error:', error);
165
+ next(error);
166
+ }
167
+ }
168
+ }
169
+
170
+ module.exports = AuthController;
@@ -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
+ });