nodejs-quickstart-structure 2.1.1 → 2.2.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 (66) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +12 -17
  3. package/bin/index.js +1 -0
  4. package/lib/generator.js +1 -1
  5. package/lib/modules/app-setup.js +16 -0
  6. package/lib/modules/auth-setup.js +46 -4
  7. package/lib/prompts.js +44 -4
  8. package/package.json +3 -2
  9. package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
  10. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
  11. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
  12. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +3 -1
  13. package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
  14. package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
  15. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
  16. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
  17. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  18. package/templates/common/.env.example.ejs +10 -0
  19. package/templates/common/README.md.ejs +65 -14
  20. package/templates/common/auth/js/controllers/authController.js.ejs +326 -13
  21. package/templates/common/auth/js/controllers/authController.spec.js.ejs +237 -51
  22. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
  23. package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
  24. package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
  25. package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
  26. package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
  27. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +194 -0
  28. package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
  29. package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
  30. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +344 -64
  31. package/templates/common/auth/ts/controllers/authController.ts.ejs +341 -9
  32. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
  33. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
  34. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
  35. package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
  36. package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
  37. package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
  38. package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
  39. package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
  40. package/templates/common/database/js/models/User.js.ejs +13 -5
  41. package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
  42. package/templates/common/database/ts/models/User.ts.ejs +23 -7
  43. package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
  44. package/templates/common/docker-compose.yml.ejs +21 -0
  45. package/templates/common/ecosystem.config.js.ejs +10 -0
  46. package/templates/common/jest.config.js.ejs +1 -1
  47. package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
  48. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  49. package/templates/common/package.json.ejs +2 -0
  50. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
  51. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
  52. package/templates/common/swagger.yml.ejs +62 -3
  53. package/templates/common/views/ejs/login.ejs.ejs +84 -0
  54. package/templates/common/views/ejs/signup.ejs.ejs +84 -0
  55. package/templates/common/views/pug/login.pug.ejs +78 -0
  56. package/templates/common/views/pug/signup.pug.ejs +78 -0
  57. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
  58. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
  59. package/templates/mvc/js/src/config/env.js.ejs +12 -2
  60. package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
  61. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
  62. package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
  63. package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
  64. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  65. package/templates/mvc/ts/src/index.ts.ejs +2 -1
  66. package/templates/clean-architecture/ts/src/domain/user.ts +0 -9
@@ -1,27 +1,42 @@
1
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
2
  jest.mock('@/infrastructure/auth/jwtService');
3
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
4
+ jest.mock('@/infrastructure/auth/socialAuthService');
5
+ jest.mock('@/usecases/auth/socialLoginUseCase');
6
+ jest.mock('@/infrastructure/repositories/UserRepository');
7
+ <%_ } _%>
14
8
  jest.mock('@/infrastructure/database/models/User', () => ({
15
9
  findOne: jest.fn()
16
10
  }));
17
- const User = require('@/infrastructure/database/models/User');
18
11
  <%_ } else { _%>
19
12
  jest.mock('@/services/jwtService');
13
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
14
+ jest.mock('@/services/socialAuthService');
15
+ <%_ } _%>
20
16
  jest.mock('@/models/User', () => ({
21
17
  findOne: jest.fn()
22
18
  }));
19
+ <%_ } _%>
20
+ jest.mock('bcryptjs');
21
+
22
+ <%_ if (architecture === 'Clean Architecture') { _%>
23
+ const AuthController = require('@/interfaces/controllers/auth/authController');
24
+ const JwtService = require('@/infrastructure/auth/jwtService');
25
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
26
+ const { SocialLoginUseCase } = require('@/usecases/auth/socialLoginUseCase');
27
+ <%_ } _%>
28
+ const User = require('@/infrastructure/database/models/User');
29
+ <%_ } else { _%>
30
+ const AuthController = require('@/controllers/authController');
31
+ const JwtService = require('@/services/jwtService');
32
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
33
+ const { SocialAuthService } = require('@/services/socialAuthService');
34
+ <%_ } _%>
23
35
  const User = require('@/models/User');
24
36
  <%_ } _%>
37
+ const HTTP_STATUS = require('@/utils/httpCodes');
38
+ const bcrypt = require('bcryptjs');
39
+
25
40
  <%_ if (caching !== 'None') { _%>
26
41
  <%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
27
42
  jest.mock('<%= cachePath %>', () => ({
@@ -42,11 +57,14 @@ describe('AuthController', () => {
42
57
  controller = new AuthController();
43
58
  mockReq = {
44
59
  body: {},
45
- headers: {}
60
+ headers: {},
61
+ query: {}
46
62
  };
47
63
  mockRes = {
48
64
  status: jest.fn().mockReturnThis(),
49
- json: jest.fn()
65
+ json: jest.fn(),
66
+ cookie: jest.fn(),
67
+ redirect: jest.fn()
50
68
  };
51
69
  next = jest.fn();
52
70
  jest.clearAllMocks();
@@ -62,7 +80,6 @@ describe('AuthController', () => {
62
80
  JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
63
81
  JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
64
82
 
65
- // Mock cacheService
66
83
  <% if (caching !== 'None') { %>
67
84
  cacheService.get.mockResolvedValue([]);
68
85
  cacheService.set.mockResolvedValue();<% } %>
@@ -73,7 +90,7 @@ describe('AuthController', () => {
73
90
  expect(mockRes.json).toHaveBeenCalledWith({ token: 'mock-access-token', accessToken: 'mock-access-token', refreshToken: 'mock-refresh-token' });
74
91
  });
75
92
 
76
- it('should return 401 on invalid email', async () => {
93
+ it('should return 401 if user not found', async () => {
77
94
  mockReq.body = { email: 'wrong@test.com', password: 'password123' };
78
95
  User.findOne.mockResolvedValue(null);
79
96
 
@@ -82,7 +99,7 @@ describe('AuthController', () => {
82
99
  expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
83
100
  });
84
101
 
85
- it('should return 401 on invalid password', async () => {
102
+ it('should return 401 if password invalid', async () => {
86
103
  const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
87
104
  mockReq.body = { email: 'test@test.com', password: 'wrongpassword' };
88
105
  User.findOne.mockResolvedValue(user);
@@ -92,57 +109,226 @@ describe('AuthController', () => {
92
109
 
93
110
  expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
94
111
  });
112
+
113
+ it('should call next on error', async () => {
114
+ const error = new Error('Database error');
115
+ User.findOne.mockRejectedValue(error);
116
+ mockReq.body = { email: 'test@test.com', password: 'password123' };
117
+
118
+ await controller.login(mockReq, mockRes, next);
119
+
120
+ expect(next).toHaveBeenCalledWith(error);
121
+ });
95
122
  });
96
123
 
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' });
124
+ describe('refresh', () => {
125
+ it('should return 401 if refresh token is invalid', async () => {
126
+ mockReq.body = { refreshToken: 'invalid-token' };
127
+ JwtService.verifyRefreshToken.mockReturnValue(null);
105
128
 
106
- <% if (caching !== 'None') { %>
107
- cacheService.get.mockResolvedValue(['old-jti']);
108
- cacheService.set.mockResolvedValue();
129
+ await controller.refresh(mockReq, mockRes, next);
130
+
131
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
132
+ });
133
+
134
+ it('should return new tokens if refresh token is valid', async () => {
135
+ mockReq.body = { refreshToken: 'valid-token' };
136
+ const decoded = { id: '1', email: 'test@test.com', jti: 'test-jti' };
137
+ JwtService.verifyRefreshToken.mockReturnValue(decoded);
138
+ JwtService.generateRefreshToken.mockReturnValue('new-refresh-token');
139
+ JwtService.generateToken.mockReturnValue('new-access-token');
140
+ JwtService.decodeToken.mockReturnValue({ jti: 'new-jti' });
141
+
142
+ <% if (caching !== 'None') { %>
143
+ cacheService.get.mockResolvedValue(['test-jti']);
144
+ <% } else { %>
145
+ JwtService.activeRefreshTokens.set('1', ['test-jti']);
146
+ <% } %>
147
+
148
+ await controller.refresh(mockReq, mockRes, next);
149
+
150
+ expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'new-access-token' }));
151
+ });
152
+
153
+ it('should return 401 if token theft detected', async () => {
154
+ mockReq.body = { refreshToken: 'stolen-token' };
155
+ const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
156
+ JwtService.verifyRefreshToken.mockReturnValue(decoded);
157
+ <% if (caching !== 'None') { %>
158
+ cacheService.get.mockResolvedValue(['other-jti']);
159
+ cacheService.del.mockResolvedValue();
160
+ <% } %>
161
+
162
+ await controller.refresh(mockReq, mockRes, next);
163
+
164
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
165
+ });
166
+
167
+ it('should call next on refresh error', async () => {
168
+ const error = new Error('Redis error');
169
+ mockReq.body = { refreshToken: 'valid-token' };
170
+ JwtService.verifyRefreshToken.mockImplementation(() => { throw error; });
171
+
172
+ await controller.refresh(mockReq, mockRes, next);
173
+
174
+ expect(next).toHaveBeenCalledWith(error);
175
+ });
176
+ });
177
+
178
+ describe('logout', () => {
179
+ it('should return 400 if no token provided', async () => {
180
+ mockReq.headers = {};
181
+ await controller.logout(mockReq, mockRes, next);
182
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
183
+ });
184
+
185
+ it('should logout successfully', async () => {
186
+ mockReq.headers = { authorization: 'Bearer valid-token' };
187
+ mockReq.body = { refreshToken: 'valid-refresh-token' };
188
+ JwtService.decodeToken.mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 })
189
+ .mockReturnValueOnce({ id: '1', jti: 'refresh-jti' });
190
+
191
+ <% if (caching !== 'None') { %>
192
+ cacheService.get.mockResolvedValue(['refresh-jti']);
193
+ <% } else { %>
194
+ JwtService.activeRefreshTokens.set('1', ['refresh-jti']);
195
+ <% } %>
196
+
197
+ await controller.logout(mockReq, mockRes, next);
198
+
199
+ expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
200
+ });
201
+
202
+ it('should handle logout even if no refresh token provided', async () => {
203
+ mockReq.headers = { authorization: 'Bearer valid-token' };
204
+ mockReq.body = {};
205
+ JwtService.decodeToken.mockReturnValue({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 });
206
+
207
+ await controller.logout(mockReq, mockRes, next);
208
+
209
+ expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
210
+ });
211
+
212
+ it('should call next on logout error', async () => {
213
+ const error = new Error('Logout error');
214
+ mockReq.headers = { authorization: 'Bearer valid-token' };
215
+ JwtService.decodeToken.mockImplementation(() => { throw error; });
216
+
217
+ await controller.logout(mockReq, mockRes, next);
218
+
219
+ expect(next).toHaveBeenCalledWith(error);
220
+ });
221
+ });
222
+
223
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
224
+ describe('socialExchange', () => {
225
+ it('should return 400 if code or provider missing', async () => {
226
+ mockReq.body = {};
227
+ await controller.socialExchange(mockReq, mockRes, next);
228
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
229
+ });
230
+
231
+ it('should exchange code for JWT tokens', async () => {
232
+ mockReq.body = { code: 'test-code', provider: 'Google' };
233
+ const user = { id: 1, email: 'social@test.com' };
234
+
235
+ <% if (architecture === 'Clean Architecture') { %>
236
+ const mockUseCase = {
237
+ execute: jest.fn().mockResolvedValue({
238
+ user,
239
+ accessToken: 'mock-token',
240
+ refreshToken: 'mock-refresh-token'
241
+ })
242
+ };
243
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
109
244
  <% } else { %>
110
- JwtService.activeRefreshTokens.set('1', ['old-jti']);<% } %>
245
+ SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'social@test.com', id: 'google-id', name: 'Google User' });
246
+ User.findOne.mockResolvedValue(user);
247
+ JwtService.generateToken.mockReturnValue('mock-token');
248
+ JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
249
+ <% } %>
250
+ JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
111
251
 
112
- await controller.refresh(mockReq, mockRes, next);
252
+ await controller.socialExchange(mockReq, mockRes, next);
113
253
 
114
- expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'new-jti' }));
115
- expect(mockRes.json).toHaveBeenCalledWith({ accessToken: 'new-access', refreshToken: 'new-refresh' });
254
+ expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'mock-token' }));
116
255
  });
117
256
 
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);
257
+ it('should return 400 for invalid provider', async () => {
258
+ mockReq.body = { code: 'test-code', provider: 'Invalid' };
259
+ await controller.socialExchange(mockReq, mockRes, next);
260
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
261
+ });
262
+ });
122
263
 
123
- <% if (caching !== 'None') { %>
124
- cacheService.get.mockResolvedValue(['different-jti']);
264
+ describe('social redirect methods', () => {
265
+ <% if (socialAuth.includes('Google')) { %>
266
+ it('googleLogin should redirect to Google', async () => {
267
+ await controller.googleLogin(mockReq, mockRes);
268
+ expect(mockRes.redirect).toHaveBeenCalledWith(expect.stringContaining('accounts.google.com'));
269
+ });
270
+
271
+ it('googleCallback should handle Google callback', async () => {
272
+ mockReq.query = { code: 'test-code' };
273
+ const user = { id: 1, email: 'google@test.com' };
274
+
275
+ <% if (architecture === 'Clean Architecture') { %>
276
+ const mockUseCase = {
277
+ execute: jest.fn().mockResolvedValue({
278
+ user,
279
+ accessToken: 'mock-token',
280
+ refreshToken: 'mock-refresh-token'
281
+ })
282
+ };
283
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
125
284
  <% } else { %>
126
- JwtService.activeRefreshTokens.set('1', ['different-jti']);<% } %>
285
+ SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'google@test.com' });
286
+ User.findOne.mockResolvedValue(user);
287
+ JwtService.generateToken.mockReturnValue('mock-token');
288
+ JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
289
+ <% } %>
290
+ JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
127
291
 
128
- await controller.refresh(mockReq, mockRes, next);
292
+ await controller.googleCallback(mockReq, mockRes, next);
129
293
 
130
- expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
294
+ expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
295
+ expect(mockRes.redirect).toHaveBeenCalledWith('/');
131
296
  });
132
- });
297
+ <% } %>
133
298
 
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' };
299
+ <% if (socialAuth.includes('GitHub')) { %>
300
+ it('githubLogin should redirect to GitHub', async () => {
301
+ await controller.githubLogin(mockReq, mockRes);
302
+ expect(mockRes.redirect).toHaveBeenCalledWith(expect.stringContaining('github.com'));
303
+ });
304
+
305
+ it('githubCallback should handle GitHub callback', async () => {
306
+ mockReq.query = { code: 'test-code' };
307
+ const user = { id: 1, email: 'github@test.com' };
138
308
 
139
- JwtService.decodeToken
140
- .mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 })
141
- .mockReturnValueOnce({ id: '1', jti: 'refresh-jti' });
309
+ <% if (architecture === 'Clean Architecture') { %>
310
+ const mockUseCase = {
311
+ execute: jest.fn().mockResolvedValue({
312
+ user,
313
+ accessToken: 'mock-token',
314
+ refreshToken: 'mock-refresh-token'
315
+ })
316
+ };
317
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
318
+ <% } else { %>
319
+ SocialAuthService.getGithubProfile.mockResolvedValue({ email: 'github@test.com' });
320
+ User.findOne.mockResolvedValue(user);
321
+ JwtService.generateToken.mockReturnValue('mock-token');
322
+ JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
323
+ <% } %>
324
+ JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
142
325
 
143
- await controller.logout(mockReq, mockRes, next);
326
+ await controller.githubCallback(mockReq, mockRes, next);
144
327
 
145
- expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
328
+ expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
329
+ expect(mockRes.redirect).toHaveBeenCalledWith('/');
146
330
  });
331
+ <% } %>
147
332
  });
333
+ <% } -%>
148
334
  });
@@ -27,28 +27,32 @@ const authMiddleware = async (req, res, next) => {
27
27
  }
28
28
 
29
29
  if (decoded.jti) {
30
- <%_ if (caching !== 'None') { -%>
30
+ <%_ if (caching !== 'None') { _%>
31
31
  const isBlacklisted = await cacheService.get(`blacklist:${decoded.jti}`);
32
32
  if (isBlacklisted) {
33
33
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
34
- }<%_ } else { -%>
34
+ }
35
+ <%_ } else { _%>
35
36
  const expiryDate = JwtService.blacklistedTokens.get(decoded.jti);
36
37
  if (expiryDate && Date.now() < expiryDate) {
37
38
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
38
- }<% } %>
39
+ }
40
+ <%_ } _%>
39
41
  }
40
42
 
41
43
  if (decoded.sid) {
42
- <%_ if (caching !== 'None') { %>
44
+ <%_ if (caching !== 'None') { _%>
43
45
  const cacheKey = `refresh_tokens:${decoded.id}`;
44
46
  const activeTokens = await cacheService.get(cacheKey) || [];
45
47
  if (!activeTokens.includes(decoded.sid)) {
46
48
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
47
- }<%_ } else { -%>
49
+ }
50
+ <%_ } else { _%>
48
51
  const activeTokens = JwtService.activeRefreshTokens.get(String(decoded.id)) || [];
49
52
  if (!activeTokens.includes(decoded.sid)) {
50
53
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
51
- }<%_ } -%>
54
+ }
55
+ <%_ } _%>
52
56
  }
53
57
 
54
58
  req.user = decoded;
@@ -12,5 +12,16 @@ const authController = new AuthController();
12
12
  router.post('/login', (req, res, next) => authController.login(req, res, next));
13
13
  router.post('/refresh', (req, res, next) => authController.refresh(req, res, next));
14
14
  router.post('/logout', authMiddleware, (req, res, next) => authController.logout(req, res, next));
15
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
16
+ router.post('/social/exchange', (req, res, next) => authController.socialExchange(req, res, next));
17
+ <%_ if (socialAuth.includes('Google')) { _%>
18
+ router.get('/google', (req, res) => authController.googleLogin(req, res));
19
+ router.get('/google/callback', (req, res, next) => authController.googleCallback(req, res, next));
20
+ <%_ } _%>
21
+ <%_ if (socialAuth.includes('GitHub')) { _%>
22
+ router.get('/github', (req, res) => authController.githubLogin(req, res));
23
+ router.get('/github/callback', (req, res, next) => authController.githubCallback(req, res, next));
24
+ <%_ } _%>
25
+ <%_ } _%>
15
26
 
16
27
  module.exports = router;
@@ -25,7 +25,7 @@ class JwtService {
25
25
  static verifyToken(token) {
26
26
  try {
27
27
  return jwt.verify(token, this.SECRET);
28
- } catch {
28
+ } catch (error) {
29
29
  return null;
30
30
  }
31
31
  }
@@ -33,7 +33,7 @@ class JwtService {
33
33
  static verifyRefreshToken(token) {
34
34
  try {
35
35
  return jwt.verify(token, this.REFRESH_SECRET);
36
- } catch {
36
+ } catch (error) {
37
37
  return null;
38
38
  }
39
39
  }
@@ -41,7 +41,7 @@ class JwtService {
41
41
  static decodeToken(token) {
42
42
  try {
43
43
  return jwt.decode(token);
44
- } catch {
44
+ } catch (error) {
45
45
  return null;
46
46
  }
47
47
  }
@@ -58,6 +58,12 @@ describe('JwtService', () => {
58
58
  expect(jwt.verify).toHaveBeenCalledWith(token, secret);
59
59
  expect(result).toEqual(payload);
60
60
  });
61
+
62
+ it('should return null for an invalid token', () => {
63
+ jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });
64
+ const result = JwtService.verifyToken(token);
65
+ expect(result).toBeNull();
66
+ });
61
67
  });
62
68
 
63
69
  describe('verifyRefreshToken', () => {
@@ -69,6 +75,12 @@ describe('JwtService', () => {
69
75
  expect(jwt.verify).toHaveBeenCalledWith(token, 'test-refresh-secret');
70
76
  expect(result).toEqual(payload);
71
77
  });
78
+
79
+ it('should return null for an invalid refresh token', () => {
80
+ jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });
81
+ const result = JwtService.verifyRefreshToken(token);
82
+ expect(result).toBeNull();
83
+ });
72
84
  });
73
85
 
74
86
  describe('decodeToken', () => {
@@ -80,5 +92,23 @@ describe('JwtService', () => {
80
92
  expect(jwt.decode).toHaveBeenCalledWith(token);
81
93
  expect(result).toEqual(payload);
82
94
  });
95
+
96
+ it('should return null if decode fails', () => {
97
+ jwt.decode.mockImplementation(() => { throw new Error('Decode failed'); });
98
+ const result = JwtService.decodeToken(token);
99
+ expect(result).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe('fallback values', () => {
104
+ it('should use default values if env not set', () => {
105
+ delete process.env.JWT_SECRET;
106
+ delete process.env.JWT_REFRESH_SECRET;
107
+ delete process.env.JWT_EXPIRES_IN;
108
+ delete process.env.JWT_REFRESH_EXPIRES_IN;
109
+
110
+ expect(JwtService.SECRET).toBeDefined();
111
+ expect(JwtService.REFRESH_SECRET).toBeDefined();
112
+ });
83
113
  });
84
114
  });
@@ -0,0 +1,175 @@
1
+ <% if (architecture === 'MVC') { -%>
2
+ const logger = require('../utils/logger');
3
+ <% } else { -%>
4
+ const logger = require('../log/logger');
5
+ <% } -%>
6
+ const axios = require('axios');
7
+
8
+ <% if (architecture === 'Clean Architecture') { -%>
9
+ // Provider implementations for Clean Architecture
10
+ <% if (socialAuth.includes('Google')) { -%>
11
+ class GoogleProvider {
12
+ constructor() {
13
+ this.name = 'Google';
14
+ }
15
+ async getProfile(code, redirectUri) {
16
+ try {
17
+ const params = new URLSearchParams();
18
+ params.append('code', code);
19
+ params.append('client_id', process.env.GOOGLE_CLIENT_ID);
20
+ params.append('client_secret', process.env.GOOGLE_CLIENT_SECRET);
21
+ params.append('redirect_uri', redirectUri);
22
+ params.append('grant_type', 'authorization_code');
23
+
24
+ const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', params.toString(), {
25
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
26
+ });
27
+
28
+ const { access_token } = tokenResponse.data;
29
+ const profileResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
30
+ headers: { Authorization: `Bearer ${access_token}` },
31
+ });
32
+
33
+ return {
34
+ id: profileResponse.data.id,
35
+ email: profileResponse.data.email,
36
+ name: profileResponse.data.name,
37
+ picture: profileResponse.data.picture
38
+ };
39
+ } catch (error) {
40
+ logger.error('Google OAuth error:', error.response?.data || error.message);
41
+ throw new Error('Failed to authenticate with Google');
42
+ }
43
+ }
44
+ }
45
+ <% } -%>
46
+
47
+ <% if (socialAuth.includes('GitHub')) { -%>
48
+ class GitHubProvider {
49
+ constructor() {
50
+ this.name = 'GitHub';
51
+ }
52
+ async getProfile(code) {
53
+ try {
54
+ const tokenResponse = await axios.post(
55
+ 'https://github.com/login/oauth/access_token',
56
+ {
57
+ client_id: process.env.GITHUB_CLIENT_ID,
58
+ client_secret: process.env.GITHUB_CLIENT_SECRET,
59
+ code,
60
+ },
61
+ { headers: { Accept: 'application/json' } }
62
+ );
63
+
64
+ const { access_token } = tokenResponse.data;
65
+ if (!access_token) throw new Error('No access token returned from GitHub');
66
+
67
+ const [profileRes, emailsRes] = await Promise.all([
68
+ axios.get('https://api.github.com/user', { headers: { Authorization: `Bearer ${access_token}` } }),
69
+ axios.get('https://api.github.com/user/emails', { headers: { Authorization: `Bearer ${access_token}` } })
70
+ ]);
71
+
72
+ const email = emailsRes.data.find((e) => e.primary)?.email || emailsRes.data[0]?.email;
73
+
74
+ return {
75
+ id: profileRes.data.id.toString(),
76
+ email,
77
+ name: profileRes.data.name || profileRes.data.login,
78
+ };
79
+ } catch (error) {
80
+ logger.error('GitHub OAuth error:', error.response?.data || error.message);
81
+ throw new Error('Failed to authenticate with GitHub');
82
+ }
83
+ }
84
+ }
85
+ <% } -%>
86
+
87
+ module.exports = {
88
+ <% if (socialAuth.includes('Google')) { %>GoogleProvider,<% } %>
89
+ <% if (socialAuth.includes('GitHub')) { %>GitHubProvider,<% } %>
90
+ };
91
+ <% } else { -%>
92
+ class SocialAuthService {
93
+ <% if (socialAuth.includes('Google')) { -%>
94
+ static async getGoogleProfile(code, redirectUri) {
95
+ try {
96
+ const params = new URLSearchParams();
97
+ params.append('code', code);
98
+ params.append('client_id', process.env.GOOGLE_CLIENT_ID);
99
+ params.append('client_secret', process.env.GOOGLE_CLIENT_SECRET);
100
+ params.append('redirect_uri', redirectUri);
101
+ params.append('grant_type', 'authorization_code');
102
+
103
+ const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', params.toString(), {
104
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
105
+ });
106
+
107
+ const { access_token } = tokenResponse.data;
108
+
109
+ const profileResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
110
+ headers: { Authorization: `Bearer ${access_token}` },
111
+ });
112
+
113
+ return {
114
+ id: profileResponse.data.id,
115
+ email: profileResponse.data.email,
116
+ name: profileResponse.data.name,
117
+ picture: profileResponse.data.picture
118
+ };
119
+ } catch (error) {
120
+ if (axios.isAxiosError(error)) {
121
+ const detail = error.response?.data;
122
+ logger.error('Google OAuth error:', detail || error.message);
123
+ if (detail?.error === 'invalid_grant') {
124
+ logger.error('HINT: The code is likely expired or already used. Get a new one!');
125
+ }
126
+ } else {
127
+ logger.error('Google OAuth error:', error.message);
128
+ }
129
+ throw new Error('Failed to authenticate with Google');
130
+ }
131
+ }
132
+ <% } -%>
133
+
134
+ <% if (socialAuth.includes('GitHub')) { -%>
135
+ static async getGithubProfile(code) {
136
+ try {
137
+ const tokenResponse = await axios.post(
138
+ 'https://github.com/login/oauth/access_token',
139
+ {
140
+ client_id: process.env.GITHUB_CLIENT_ID,
141
+ client_secret: process.env.GITHUB_CLIENT_SECRET,
142
+ code,
143
+ },
144
+ { headers: { Accept: 'application/json' } }
145
+ );
146
+
147
+ const { access_token } = tokenResponse.data;
148
+ if (!access_token) {
149
+ throw new Error('No access token returned from GitHub');
150
+ }
151
+
152
+ const profileResponse = await axios.get('https://api.github.com/user', {
153
+ headers: { Authorization: `Bearer ${access_token}` },
154
+ });
155
+
156
+ const emailsResponse = await axios.get('https://api.github.com/user/emails', {
157
+ headers: { Authorization: `Bearer ${access_token}` },
158
+ });
159
+
160
+ const primaryEmail = emailsResponse.data.find((e) => e.primary)?.email || emailsResponse.data[0]?.email;
161
+
162
+ return {
163
+ id: profileResponse.data.id.toString(),
164
+ email: primaryEmail,
165
+ name: profileResponse.data.name || profileResponse.data.login,
166
+ };
167
+ } catch (error) {
168
+ logger.error('GitHub OAuth error:', error.response?.data || error.message);
169
+ throw new Error('Failed to authenticate with GitHub');
170
+ }
171
+ }
172
+ <% } -%>
173
+ }
174
+ module.exports = { SocialAuthService };
175
+ <% } -%>