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,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
+ });
@@ -0,0 +1,167 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import bcrypt from 'bcryptjs';
3
+ <% if (architecture === 'MVC') { -%>
4
+ import User from '@/models/User';
5
+ import { JwtService } from '@/services/jwtService';
6
+ import logger from '@/utils/logger';
7
+ <% if (caching !== 'None') { -%>
8
+ import cacheService from '<% if (caching === "Redis") { %>@/config/redisClient<% } else { %>@/config/memoryCache<% } %>';
9
+ <% } -%>
10
+ <% } else { -%>
11
+ import User from '@/infrastructure/database/models/User';
12
+ import { JwtService } from '@/infrastructure/auth/jwtService';
13
+ import logger from '@/infrastructure/log/logger';
14
+ <% if (caching !== 'None') { -%>
15
+ import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
16
+ <% } -%>
17
+ <% } -%>
18
+ import { HTTP_STATUS } from '@/utils/httpCodes';
19
+
20
+ export class AuthController {
21
+ async login(req: Request, res: Response, next: NextFunction) {
22
+ try {
23
+ const { email, password } = req.body;
24
+ <%_ if (database === 'MongoDB' || database === 'None') { -%>
25
+ const user = await User.findOne({ email });
26
+ <%_ } else { -%>
27
+ const user = await User.findOne({ where: { email } });
28
+ <%_ } -%>
29
+ if (!user) {
30
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid credentials' });
31
+ }
32
+
33
+ const isPasswordValid = await bcrypt.compare(password, user.password!);
34
+ if (!isPasswordValid) {
35
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid credentials' });
36
+ }
37
+
38
+ const userId = String(user.id || ((user as unknown) as { _id?: string | number })._id);
39
+
40
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: user.email });
41
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
42
+ const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
43
+
44
+ // Store refresh token
45
+ <%_ if (caching !== 'None') { -%>
46
+ const cacheKey = `refresh_tokens:${userId}`;
47
+ const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
48
+ activeTokens.push(refreshJti!);
49
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60); // 7 days
50
+ <%_ } else { -%>
51
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
52
+ activeTokens.push(refreshJti!);
53
+ JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
54
+
55
+ res.json({ token: accessToken, accessToken, refreshToken });
56
+ } catch (error) {
57
+ logger.error('Login error:', error);
58
+ next(error);
59
+ }
60
+ }
61
+
62
+ async refresh(req: Request, res: Response, next: NextFunction) {
63
+ try {
64
+ const { refreshToken } = req.body;
65
+ if (!refreshToken) {
66
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Refresh token is required' });
67
+ }
68
+
69
+ const decoded = JwtService.verifyRefreshToken(refreshToken);
70
+ if (!decoded || !decoded.id || !decoded.jti) {
71
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid refresh token' });
72
+ }
73
+
74
+ const userId = String(decoded.id);
75
+ const incomingJti = decoded.jti;
76
+
77
+ <% if (caching !== 'None') { %>
78
+ const cacheKey = `refresh_tokens:${userId}`;
79
+ let activeTokens = await cacheService.get<string[]>(cacheKey) || [];
80
+
81
+ if (!activeTokens.includes(incomingJti)) {
82
+ // Theft detection! Revoke all sessions
83
+ logger.warn(`Token theft detected for user ${userId}. Revoking all sessions.`);
84
+ await cacheService.del(cacheKey);
85
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid session' });
86
+ }
87
+
88
+ // Valid rotation
89
+ activeTokens = activeTokens.filter(t => t !== incomingJti);
90
+ const newRefreshToken = JwtService.generateRefreshToken({ id: userId, email: decoded.email });
91
+ const newRefreshJti = JwtService.decodeToken(newRefreshToken)?.jti;
92
+ const newAccessToken = JwtService.generateToken({ id: userId, email: decoded.email, sid: newRefreshJti });
93
+
94
+ activeTokens.push(newRefreshJti!);
95
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
96
+ <% } else { %>
97
+ let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
98
+
99
+ if (!activeTokens.includes(incomingJti)) {
100
+ // Theft detection!
101
+ logger.warn(`Token theft detected for user ${userId}. Revoking all sessions.`);
102
+ JwtService.activeRefreshTokens.delete(userId);
103
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid session' });
104
+ }
105
+
106
+ activeTokens = activeTokens.filter(t => t !== incomingJti);
107
+ const newRefreshToken = JwtService.generateRefreshToken({ id: userId, email: decoded.email });
108
+ const newRefreshJti = JwtService.decodeToken(newRefreshToken)?.jti;
109
+ const newAccessToken = JwtService.generateToken({ id: userId, email: decoded.email, sid: newRefreshJti });
110
+
111
+ activeTokens.push(newRefreshJti!);
112
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
113
+ <% } %>
114
+ res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
115
+ } catch (error) {
116
+ logger.error('Refresh token error:', error);
117
+ next(error);
118
+ }
119
+ }
120
+
121
+ async logout(req: Request, res: Response, next: NextFunction) {
122
+ try {
123
+ const authHeader = req.headers.authorization;
124
+ if (!authHeader) {
125
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'No token provided' });
126
+ }
127
+
128
+ const accessTokenStr = authHeader.split(' ')[1];
129
+ const decodedAccess = JwtService.decodeToken(accessTokenStr);
130
+
131
+ if (decodedAccess && decodedAccess.jti && decodedAccess.exp) {
132
+ const remainingTime = Math.max(0, decodedAccess.exp - Math.floor(Date.now() / 1000));
133
+ <%_ if (caching !== 'None') { -%>
134
+ if (remainingTime > 0) {
135
+ await cacheService.set(`blacklist:${decodedAccess.jti}`, true, remainingTime);
136
+ }
137
+ <% } else { %>
138
+ if (remainingTime > 0) {
139
+ JwtService.blacklistedTokens.set(decodedAccess.jti, Date.now() + remainingTime * 1000);
140
+ }
141
+ <%_ } -%>
142
+ }
143
+
144
+ const { refreshToken } = req.body;
145
+ if (refreshToken) {
146
+ const decodedRefresh = JwtService.decodeToken(refreshToken);
147
+ if (decodedRefresh && decodedRefresh.id && decodedRefresh.jti) {
148
+ const userId = String(decodedRefresh.id);
149
+ <%_ if (caching !== 'None') { -%>
150
+ const cacheKey = `refresh_tokens:${userId}`;
151
+ let activeTokens = await cacheService.get<string[]>(cacheKey) || [];
152
+ activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
153
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
154
+ <% } else { %>
155
+ let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
156
+ activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
157
+ JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
158
+ }
159
+ }
160
+
161
+ res.json({ message: 'Logged out successfully' });
162
+ } catch (error) {
163
+ logger.error('Logout error:', error);
164
+ next(error);
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,128 @@
1
+ <%_ if (architecture === 'Clean Architecture') { _%>
2
+ import { authMiddleware } from '@/infrastructure/webserver/middleware/authMiddleware';
3
+ import { JwtService } from '@/infrastructure/auth/jwtService';
4
+ <%_ } else { _%>
5
+ import { authMiddleware } from '@/middleware/authMiddleware';
6
+ import { JwtService } from '@/services/jwtService';
7
+ <%_ } _%>
8
+ import { HTTP_STATUS } from '@/utils/httpCodes';
9
+ import { Request, Response, NextFunction } from 'express';
10
+
11
+ <%_ if (architecture === 'Clean Architecture') { _%>
12
+ jest.mock('@/infrastructure/auth/jwtService');
13
+ <%_ } else { _%>
14
+ jest.mock('@/services/jwtService');
15
+ <%_ } _%>
16
+
17
+ <%_ if (caching !== 'None') { _%>
18
+ <%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
19
+ jest.mock('<%= cachePath %>', () => ({
20
+ __esModule: true,
21
+ default: {
22
+ get: jest.fn(),
23
+ set: jest.fn(),
24
+ del: jest.fn()
25
+ }
26
+ }), { virtual: true });
27
+ import cacheService from '<%= cachePath %>';
28
+ <%_ } _%>
29
+
30
+ describe('AuthMiddleware', () => {
31
+ let mockRequest: any;
32
+ let mockResponse: any;
33
+ const nextFunction: NextFunction = jest.fn();
34
+
35
+ beforeEach(() => {
36
+ mockRequest = {
37
+ headers: {}
38
+ };
39
+ mockResponse = {
40
+ status: jest.fn().mockReturnThis(),
41
+ json: jest.fn()
42
+ };
43
+ jest.clearAllMocks();
44
+ });
45
+
46
+ it('should return 401 if no authorization header is provided', async () => {
47
+ await authMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
48
+
49
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
50
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'No token provided' });
51
+ expect(nextFunction).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('should return 401 if token is invalid', async () => {
55
+ mockRequest.headers.authorization = 'Bearer invalid-token';
56
+ (JwtService.verifyToken as jest.Mock).mockReturnValue(null);
57
+
58
+ await authMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
59
+
60
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
61
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid or expired token' });
62
+ expect(nextFunction).not.toHaveBeenCalled();
63
+ });
64
+
65
+ it('should return 401 if token is blacklisted', async () => {
66
+ const payload = { id: 1, email: 'test@example.com', jti: 'blacklisted-jti' };
67
+ mockRequest.headers.authorization = 'Bearer valid-token';
68
+ (JwtService.verifyToken as jest.Mock).mockReturnValue(payload);
69
+
70
+ // Mock the blacklist check
71
+ <% if (caching !== 'None') { %>
72
+ (cacheService.get as jest.Mock).mockResolvedValue(true);
73
+ <% } else { %>
74
+ JwtService.blacklistedTokens.set('blacklisted-jti', Date.now() + 10000);
75
+ <% } %>
76
+
77
+ await authMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
78
+
79
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
80
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Token revoked' });
81
+ expect(nextFunction).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('should return 401 if session is expired (sid not in activeTokens)', async () => {
85
+ const payload = { id: 1, email: 'test@example.com', jti: 'valid-jti', sid: 'expired-sid' };
86
+ mockRequest.headers.authorization = 'Bearer valid-token';
87
+ (JwtService.verifyToken as jest.Mock).mockReturnValue(payload);
88
+
89
+ <% if (caching !== 'None') { %>
90
+ (cacheService.get as jest.Mock).mockImplementation((key: string) => {
91
+ if (key.startsWith('blacklist:')) return Promise.resolve(false);
92
+ if (key === 'refresh_tokens:1') return Promise.resolve(['other-sid']);
93
+ return Promise.resolve(null);
94
+ });
95
+ <% } else { %>
96
+ JwtService.blacklistedTokens.delete('valid-jti');
97
+ JwtService.activeRefreshTokens.set('1', ['other-sid']);
98
+ <% } %>
99
+
100
+ await authMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
101
+
102
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
103
+ expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Session expired' });
104
+ expect(nextFunction).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it('should call next() and set req.user if token is valid, not blacklisted and session is active', async () => {
108
+ const payload = { id: 1, email: 'test@example.com', jti: 'valid-jti', sid: 'active-sid' };
109
+ mockRequest.headers.authorization = 'Bearer valid-token';
110
+ (JwtService.verifyToken as jest.Mock).mockReturnValue(payload);
111
+
112
+ <% if (caching !== 'None') { %>
113
+ (cacheService.get as jest.Mock).mockImplementation((key: string) => {
114
+ if (key.startsWith('blacklist:')) return Promise.resolve(false);
115
+ if (key === 'refresh_tokens:1') return Promise.resolve(['active-sid']);
116
+ return Promise.resolve(null);
117
+ });
118
+ <% } else { %>
119
+ JwtService.blacklistedTokens.delete('valid-jti');
120
+ JwtService.activeRefreshTokens.set('1', ['active-sid']);
121
+ <% } %>
122
+
123
+ await authMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
124
+
125
+ expect(mockRequest.user).toEqual(payload);
126
+ expect(nextFunction).toHaveBeenCalled();
127
+ });
128
+ });
@@ -0,0 +1,59 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ <% if (architecture === 'MVC') { %>
3
+ import { JwtService, JwtPayload } from '@/services/jwtService';
4
+ <% if (caching !== 'None') { -%>
5
+ import cacheService from '<% if (caching === "Redis") { %>@/config/redisClient<% } else { %>@/config/memoryCache<% } %>';
6
+ <% } -%>
7
+ <% } else { %>
8
+ import { JwtService, JwtPayload } from '@/infrastructure/auth/jwtService';
9
+ <% if (caching !== 'None') { -%>
10
+ import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
11
+ <% } -%>
12
+ <% } %>
13
+ import { HTTP_STATUS } from '@/utils/httpCodes';
14
+
15
+ interface CustomRequest extends Request {
16
+ user?: JwtPayload;
17
+ }
18
+
19
+ export const authMiddleware = async (req: CustomRequest, res: Response, next: NextFunction) => {
20
+ const authHeader = req.headers.authorization;
21
+
22
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
23
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'No token provided' });
24
+ }
25
+
26
+ const token = authHeader.split(' ')[1];
27
+ const decoded = JwtService.verifyToken(token);
28
+
29
+ if (!decoded) {
30
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid or expired token' });
31
+ }
32
+
33
+ if (decoded.jti) {
34
+ <%_ if (caching !== 'None') { -%>
35
+ const isBlacklisted = await cacheService.get(`blacklist:${decoded.jti}`);
36
+ if (isBlacklisted) {
37
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
38
+ }<%_ } else { -%>
39
+ const expiryDate = JwtService.blacklistedTokens.get(decoded.jti);
40
+ if (expiryDate && Date.now() < expiryDate) {
41
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
42
+ }<% } %>
43
+ }
44
+
45
+ if (decoded.sid) {
46
+ <%_ if (caching !== 'None') { -%>
47
+ const activeTokens = await cacheService.get<string[]>(`refresh_tokens:${decoded.id}`) || [];
48
+ if (!activeTokens.includes(decoded.sid)) {
49
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
50
+ }<%_ } else { -%>
51
+ const activeTokens = JwtService.activeRefreshTokens.get(String(decoded.id)) || [];
52
+ if (!activeTokens.includes(decoded.sid)) {
53
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
54
+ }<% } %>
55
+ }
56
+
57
+ req.user = decoded;
58
+ next();
59
+ };
@@ -0,0 +1,20 @@
1
+ import { Router } from 'express';
2
+ <%_ if (architecture === 'MVC') { -%>
3
+ import { AuthController } from '@/controllers/authController';
4
+ <%_ } else { -%>
5
+ import { AuthController } from '@/interfaces/controllers/auth/authController';
6
+ <%_ } -%>
7
+ <%_ if (architecture === 'MVC') { -%>
8
+ import { authMiddleware } from '@/middleware/authMiddleware';
9
+ <%_ } else { -%>
10
+ import { authMiddleware } from '@/infrastructure/webserver/middleware/authMiddleware';
11
+ <%_ } -%>
12
+
13
+ const router = Router();
14
+ const authController = new AuthController();
15
+
16
+ router.post('/login', (req, res, next) => authController.login(req, res, next));
17
+ router.post('/refresh', (req, res, next) => authController.refresh(req, res, next));
18
+ router.post('/logout', authMiddleware, (req, res, next) => authController.logout(req, res, next));
19
+
20
+ export default router;
@@ -0,0 +1,89 @@
1
+ <% if (architecture === 'Clean Architecture') { -%>
2
+ import { JwtService } from '@/infrastructure/auth/jwtService';
3
+ <% } else { -%>
4
+ import { JwtService } from '@/services/jwtService';
5
+ <% } -%>
6
+ import jwt from 'jsonwebtoken';
7
+
8
+ jest.mock('jsonwebtoken');
9
+ jest.mock('@/config/env', () => ({
10
+ env: {
11
+ JWT_SECRET: 'test-secret',
12
+ JWT_REFRESH_SECRET: 'test-refresh-secret',
13
+ JWT_EXPIRES_IN: '15m'
14
+ }
15
+ }));
16
+
17
+ describe('JwtService', () => {
18
+ const secret = 'test-secret';
19
+ const refreshSecret = 'test-refresh-secret';
20
+ const payload = { id: 1, email: 'test@example.com' };
21
+ const token = 'mock-token';
22
+
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ describe('generateToken', () => {
28
+ it('should generate a token with standard expiration and jti', () => {
29
+ (jwt.sign as jest.Mock).mockReturnValue(token);
30
+
31
+ const result = JwtService.generateToken(payload);
32
+
33
+ expect(jwt.sign).toHaveBeenCalledWith(
34
+ expect.objectContaining({ ...payload, jti: expect.any(String) }),
35
+ secret,
36
+ { expiresIn: '15m' }
37
+ );
38
+ expect(result).toBe(token);
39
+ });
40
+ });
41
+
42
+ describe('generateRefreshToken', () => {
43
+ it('should generate a refresh token with refresh secret and jti', () => {
44
+ (jwt.sign as jest.Mock).mockReturnValue(token);
45
+
46
+ const result = JwtService.generateRefreshToken(payload);
47
+
48
+ expect(jwt.sign).toHaveBeenCalledWith(
49
+ expect.objectContaining({ ...payload, jti: expect.any(String) }),
50
+ refreshSecret,
51
+ { expiresIn: '7d' }
52
+ );
53
+ expect(result).toBe(token);
54
+ });
55
+ });
56
+
57
+ describe('verifyToken', () => {
58
+ it('should return decoded payload for a valid token', () => {
59
+ (jwt.verify as jest.Mock).mockReturnValue(payload);
60
+
61
+ const result = JwtService.verifyToken(token);
62
+
63
+ expect(jwt.verify).toHaveBeenCalledWith(token, secret);
64
+ expect(result).toEqual(payload);
65
+ });
66
+ });
67
+
68
+ describe('verifyRefreshToken', () => {
69
+ it('should return decoded payload for a valid refresh token', () => {
70
+ (jwt.verify as jest.Mock).mockReturnValue(payload);
71
+
72
+ const result = JwtService.verifyRefreshToken(token);
73
+
74
+ expect(jwt.verify).toHaveBeenCalledWith(token, refreshSecret);
75
+ expect(result).toEqual(payload);
76
+ });
77
+ });
78
+
79
+ describe('decodeToken', () => {
80
+ it('should return decoded payload without verification', () => {
81
+ (jwt.decode as jest.Mock).mockReturnValue(payload);
82
+
83
+ const result = JwtService.decodeToken(token);
84
+
85
+ expect(jwt.decode).toHaveBeenCalledWith(token);
86
+ expect(result).toEqual(payload);
87
+ });
88
+ });
89
+ });