nodejs-quickstart-structure 2.1.2 → 2.2.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 (70) hide show
  1. package/CHANGELOG.md +20 -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 +49 -5
  8. package/package.json +1 -1
  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 +5 -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/index.ts.ejs +2 -0
  16. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
  17. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
  18. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  19. package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
  20. package/templates/common/.env.example.ejs +10 -0
  21. package/templates/common/README.md.ejs +65 -14
  22. package/templates/common/auth/js/controllers/authController.js.ejs +356 -13
  23. package/templates/common/auth/js/controllers/authController.spec.js.ejs +329 -53
  24. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
  25. package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
  26. package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
  27. package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
  28. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +192 -0
  29. package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
  30. package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
  31. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +366 -64
  32. package/templates/common/auth/ts/controllers/authController.ts.ejs +370 -9
  33. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
  34. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
  35. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
  36. package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
  37. package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
  38. package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
  39. package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
  40. package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
  41. package/templates/common/database/js/models/User.js.ejs +13 -5
  42. package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
  43. package/templates/common/database/ts/models/User.ts.ejs +23 -7
  44. package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
  45. package/templates/common/docker-compose.yml.ejs +21 -0
  46. package/templates/common/ecosystem.config.js.ejs +10 -0
  47. package/templates/common/eslint.config.mjs.ejs +4 -1
  48. package/templates/common/jest.config.js.ejs +1 -1
  49. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  50. package/templates/common/package.json.ejs +4 -0
  51. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
  52. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
  53. package/templates/common/swagger.yml.ejs +62 -3
  54. package/templates/common/views/ejs/login.ejs.ejs +84 -0
  55. package/templates/common/views/ejs/signup.ejs.ejs +84 -0
  56. package/templates/common/views/pug/login.pug.ejs +78 -0
  57. package/templates/common/views/pug/signup.pug.ejs +78 -0
  58. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
  59. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
  60. package/templates/mvc/js/src/config/env.js.ejs +12 -2
  61. package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
  62. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
  63. package/templates/mvc/js/src/index.js.ejs +2 -0
  64. package/templates/mvc/js/src/utils/httpCodes.js +1 -0
  65. package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
  66. package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
  67. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  68. package/templates/mvc/ts/src/index.ts.ejs +4 -1
  69. package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
  70. package/templates/clean-architecture/ts/src/domain/user.ts +0 -9
@@ -1,5 +1,6 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
2
  import bcrypt from 'bcryptjs';
3
+ import crypto from 'crypto';
3
4
  <% if (architecture === 'MVC') { -%>
4
5
  import User from '@/models/User';
5
6
  import { JwtService } from '@/services/jwtService';
@@ -7,6 +8,9 @@ import logger from '@/utils/logger';
7
8
  <% if (caching !== 'None') { -%>
8
9
  import cacheService from '<% if (caching === "Redis") { %>@/config/redisClient<% } else { %>@/config/memoryCache<% } %>';
9
10
  <% } -%>
11
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
12
+ import { SocialAuthService } from '@/services/socialAuthService';
13
+ <% } -%>
10
14
  <% } else { -%>
11
15
  import User from '@/infrastructure/database/models/User';
12
16
  import { JwtService } from '@/infrastructure/auth/jwtService';
@@ -14,10 +18,24 @@ import logger from '@/infrastructure/log/logger';
14
18
  <% if (caching !== 'None') { -%>
15
19
  import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
16
20
  <% } -%>
21
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
22
+ import { SocialLoginUseCase } from '@/usecases/auth/socialLoginUseCase';
23
+ import { GoogleProvider, GitHubProvider } from '@/infrastructure/auth/socialAuthService';
24
+ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
25
+ <% } -%>
17
26
  <% } -%>
18
27
  import { HTTP_STATUS } from '@/utils/httpCodes';
19
28
 
20
29
  export class AuthController {
30
+ private setOAuthStateCookie(res: Response, state: string) {
31
+ res.cookie('oauth_state', state, {
32
+ httpOnly: true,
33
+ secure: process.env.NODE_ENV === 'production',
34
+ sameSite: 'lax',
35
+ maxAge: 10 * 60 * 1000
36
+ });
37
+ }
38
+
21
39
  async login(req: Request, res: Response, next: NextFunction) {
22
40
  try {
23
41
  const { email, password } = req.body;
@@ -42,15 +60,16 @@ export class AuthController {
42
60
  const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
43
61
 
44
62
  // Store refresh token
45
- <%_ if (caching !== 'None') { -%>
63
+ <%_ if (caching !== 'None') { _%>
46
64
  const cacheKey = `refresh_tokens:${userId}`;
47
65
  const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
48
66
  activeTokens.push(refreshJti!);
49
67
  await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60); // 7 days
50
- <%_ } else { -%>
68
+ <%_ } else { _%>
51
69
  const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
52
70
  activeTokens.push(refreshJti!);
53
- JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
71
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
72
+ <%_ } _%>
54
73
 
55
74
  res.json({ token: accessToken, accessToken, refreshToken });
56
75
  } catch (error) {
@@ -74,7 +93,7 @@ export class AuthController {
74
93
  const userId = String(decoded.id);
75
94
  const incomingJti = decoded.jti;
76
95
 
77
- <% if (caching !== 'None') { %>
96
+ <%_ if (caching !== 'None') { _%>
78
97
  const cacheKey = `refresh_tokens:${userId}`;
79
98
  let activeTokens = await cacheService.get<string[]>(cacheKey) || [];
80
99
 
@@ -93,7 +112,7 @@ export class AuthController {
93
112
 
94
113
  activeTokens.push(newRefreshJti!);
95
114
  await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
96
- <% } else { %>
115
+ <%_ } else { _%>
97
116
  let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
98
117
 
99
118
  if (!activeTokens.includes(incomingJti)) {
@@ -110,7 +129,7 @@ export class AuthController {
110
129
 
111
130
  activeTokens.push(newRefreshJti!);
112
131
  JwtService.activeRefreshTokens.set(userId, activeTokens);
113
- <% } %>
132
+ <%_ } _%>
114
133
  res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
115
134
  } catch (error) {
116
135
  logger.error('Refresh token error:', error);
@@ -146,15 +165,16 @@ export class AuthController {
146
165
  const decodedRefresh = JwtService.decodeToken(refreshToken);
147
166
  if (decodedRefresh && decodedRefresh.id && decodedRefresh.jti) {
148
167
  const userId = String(decodedRefresh.id);
149
- <%_ if (caching !== 'None') { -%>
168
+ <%_ if (caching !== 'None') { _%>
150
169
  const cacheKey = `refresh_tokens:${userId}`;
151
170
  let activeTokens = await cacheService.get<string[]>(cacheKey) || [];
152
171
  activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
153
172
  await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
154
- <% } else { %>
173
+ <%_ } else { _%>
155
174
  let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
156
175
  activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
157
- JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
176
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
177
+ <%_ } _%>
158
178
  }
159
179
  }
160
180
 
@@ -164,4 +184,345 @@ export class AuthController {
164
184
  next(error);
165
185
  }
166
186
  }
187
+
188
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
189
+ async socialExchange(req: Request, res: Response, next: NextFunction) {
190
+ try {
191
+ const { code, provider, redirectUri } = req.body;
192
+ if (!code || !provider) {
193
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Code and provider are required' });
194
+ }
195
+
196
+ if (!['Google', 'GitHub'].includes(provider)) {
197
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Invalid social provider' });
198
+ }
199
+
200
+ <% if (architecture === 'Clean Architecture') { -%>
201
+ let useCase: SocialLoginUseCase | undefined;
202
+ const userRepository = new UserRepository();
203
+ <%_ if (socialAuth.includes('Google')) { _%>
204
+ if (provider === 'Google') useCase = new SocialLoginUseCase(new GoogleProvider(), userRepository);
205
+ <%_ } _%>
206
+ <%_ if (socialAuth.includes('GitHub')) { _%>
207
+ if (provider === 'GitHub') useCase = new SocialLoginUseCase(new GitHubProvider(), userRepository);
208
+ <%_ } _%>
209
+
210
+ if (!useCase) {
211
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Invalid social provider' });
212
+ }
213
+
214
+ const { user, accessToken, refreshToken } = await useCase.execute(code, redirectUri);
215
+ const userId = String(user.id || ((user as unknown) as { _id?: string | number })._id);
216
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
217
+
218
+ // Store refresh token
219
+ <%_ if (caching !== 'None') { _%>
220
+ const cacheKey = `refresh_tokens:${userId}`;
221
+ const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
222
+ activeTokens.push(refreshJti!);
223
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
224
+ <%_ } else { _%>
225
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
226
+ activeTokens.push(refreshJti!);
227
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
228
+ <%_ } _%>
229
+
230
+ res.json({ token: accessToken, accessToken, refreshToken });
231
+ <% } else { -%>
232
+ let profile;
233
+ <%_ if (socialAuth.includes('Google')) { _%>
234
+ if (provider === 'Google') {
235
+ profile = await SocialAuthService.getGoogleProfile(code, redirectUri);
236
+ }
237
+ <%_ } _%>
238
+ <%_ if (socialAuth.includes('GitHub')) { _%>
239
+ if (provider === 'GitHub') {
240
+ profile = await SocialAuthService.getGithubProfile(code);
241
+ }
242
+ <%_ } _%>
243
+
244
+ if (!profile) {
245
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Invalid social provider' });
246
+ }
247
+
248
+ if (!profile || !profile.email) {
249
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Failed to retrieve profile from provider' });
250
+ }
251
+
252
+ <%_ if (database === 'MongoDB' || database === 'None') { -%>
253
+ let user = await User.findOne({ email: profile.email });
254
+ <%_ } else { -%>
255
+ let user = await User.findOne({ where: { email: profile.email } });
256
+ <%_ } -%>
257
+
258
+ if (!user) {
259
+ <%_ if (database === 'MongoDB' || database === 'None') { -%>
260
+ user = await User.create({
261
+ email: profile.email,
262
+ name: profile.name,
263
+ password: null,
264
+ <%_ if (socialAuth.includes('Google')) { _%>
265
+ googleId: provider === 'Google' ? profile.id : null,
266
+ <%_ } _%>
267
+ <%_ if (socialAuth.includes('GitHub')) { _%>
268
+ githubId: provider === 'GitHub' ? profile.id : null,
269
+ <%_ } _%>
270
+ });
271
+ await (user as unknown as { save: () => Promise<void> }).save();
272
+ <%_ } else { _%>
273
+ user = await User.create({
274
+ email: profile.email,
275
+ name: profile.name,
276
+ password: null,
277
+ <%_ if (socialAuth.includes('Google')) { _%>
278
+ googleId: provider === 'Google' ? profile.id : null,
279
+ <%_ } _%>
280
+ <%_ if (socialAuth.includes('GitHub')) { _%>
281
+ githubId: provider === 'GitHub' ? profile.id : null,
282
+ <%_ } _%>
283
+ }) as unknown as User;
284
+ <%_ } _%>
285
+ }
286
+
287
+ const activeUser = user!;
288
+ const userId = String(activeUser.id || ((activeUser as unknown) as { _id?: string | number })._id);
289
+
290
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: activeUser.email });
291
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
292
+ const accessToken = JwtService.generateToken({ id: userId, email: activeUser.email, sid: refreshJti });
293
+
294
+ // Store refresh token
295
+ <%_ if (caching !== 'None') { -%>
296
+ const cacheKey = `refresh_tokens:${userId}`;
297
+ const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
298
+ activeTokens.push(refreshJti!);
299
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
300
+ <%_ } else { -%>
301
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
302
+ activeTokens.push(refreshJti!);
303
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
304
+ <%_ } _%>
305
+
306
+ res.json({ token: accessToken, accessToken, refreshToken });
307
+ <%_ } _%>
308
+ } catch (error) {
309
+ logger.error('Social exchange error:', error);
310
+ next(error);
311
+ }
312
+ }
313
+
314
+ <% if (socialAuth.includes('Google')) { -%>
315
+ async googleLogin(req: Request, res: Response) {
316
+ const rootUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
317
+ const state = crypto.randomBytes(16).toString('hex');
318
+ this.setOAuthStateCookie(res, state);
319
+
320
+ const options = {
321
+ redirect_uri: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback',
322
+ client_id: process.env.GOOGLE_CLIENT_ID!,
323
+ access_type: 'offline',
324
+ response_type: 'code',
325
+ prompt: 'consent',
326
+ scope: [
327
+ 'https://www.googleapis.com/auth/userinfo.profile',
328
+ 'https://www.googleapis.com/auth/userinfo.email',
329
+ ].join(' '),
330
+ state: state
331
+ };
332
+ const qs = new URLSearchParams(options);
333
+ res.redirect(`${rootUrl}?${qs.toString()}`);
334
+ }
335
+
336
+ async googleCallback(req: Request, res: Response, next: NextFunction) {
337
+ try {
338
+ const { code, state } = req.query;
339
+ const savedState = req.cookies?.oauth_state;
340
+ res.clearCookie('oauth_state');
341
+
342
+ if (!state || state !== savedState) {
343
+ return res.status(HTTP_STATUS.FORBIDDEN).json({ message: 'Invalid state parameter' });
344
+ }
345
+
346
+ const redirectUri = process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback';
347
+
348
+ <%_ if (architecture === 'Clean Architecture') { _%>
349
+ const useCase = new SocialLoginUseCase(new GoogleProvider(), new UserRepository());
350
+ const { user, accessToken, refreshToken } = await useCase.execute(code as string, redirectUri);
351
+ const userId = String(user.id || ((user as unknown) as { _id?: string | number })._id);
352
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
353
+
354
+ // Store refresh token
355
+ <%_ if (caching !== 'None') { _%>
356
+ const cacheKey = `refresh_tokens:${userId}`;
357
+ const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
358
+ activeTokens.push(refreshJti!);
359
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
360
+ <%_ } else { _%>
361
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
362
+ activeTokens.push(refreshJti!);
363
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
364
+ <%_ } _%>
365
+
366
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
367
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
368
+ res.redirect('/');
369
+ <%_ } else { _%>
370
+ const profile = await SocialAuthService.getGoogleProfile(code as string, redirectUri);
371
+
372
+ <% if (database === 'MongoDB' || database === 'None') { -%>
373
+ let user = await User.findOne({ email: profile.email });
374
+ <% } else { -%>
375
+ let user = await User.findOne({ where: { email: profile.email } });
376
+ <% } -%>
377
+
378
+ if (!user) {
379
+ <%_ if (database === 'MongoDB' || database === 'None') { -%>
380
+ user = await User.create({
381
+ email: profile.email,
382
+ name: profile.name,
383
+ password: null,
384
+ googleId: profile.id
385
+ });
386
+ <%_ } else { -%>
387
+ user = await User.create({
388
+ email: profile.email,
389
+ name: profile.name,
390
+ password: null,
391
+ googleId: profile.id
392
+ }) as unknown as User;
393
+ <%_ } -%>
394
+ }
395
+
396
+ const activeUser = user!;
397
+ const userId = String(activeUser.id || ((activeUser as unknown) as { _id?: string | number })._id);
398
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: activeUser.email });
399
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
400
+ const accessToken = JwtService.generateToken({ id: userId, email: activeUser.email, sid: refreshJti });
401
+
402
+ // Store refresh token
403
+ <%_ if (caching !== 'None') { _%>
404
+ const cacheKey = `refresh_tokens:${userId}`;
405
+ const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
406
+ activeTokens.push(refreshJti!);
407
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
408
+ <%_ } else { _%>
409
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
410
+ activeTokens.push(refreshJti!);
411
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
412
+ <%_ } _%>
413
+
414
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
415
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
416
+ res.redirect('/');
417
+ <%_ } _%>
418
+ } catch (error) {
419
+ logger.error('Google callback error:', error);
420
+ res.redirect('/login?error=social_auth_failed');
421
+ }
422
+ }
423
+ <% } -%>
424
+
425
+ <% if (socialAuth.includes('GitHub')) { -%>
426
+ async githubLogin(req: Request, res: Response) {
427
+ const rootUrl = 'https://github.com/login/oauth/authorize';
428
+ const state = crypto.randomBytes(16).toString('hex');
429
+ this.setOAuthStateCookie(res, state);
430
+
431
+ const options = {
432
+ client_id: process.env.GITHUB_CLIENT_ID!,
433
+ redirect_uri: process.env.GITHUB_CALLBACK_URL || 'http://localhost:3000/api/auth/github/callback',
434
+ scope: 'user:email',
435
+ state: state
436
+ };
437
+ const qs = new URLSearchParams(options);
438
+ res.redirect(`${rootUrl}?${qs.toString()}`);
439
+ }
440
+
441
+ async githubCallback(req: Request, res: Response, next: NextFunction) {
442
+ try {
443
+ const { code, state } = req.query;
444
+ const savedState = req.cookies?.oauth_state;
445
+ res.clearCookie('oauth_state');
446
+
447
+ if (!state || state !== savedState) {
448
+ return res.status(HTTP_STATUS.FORBIDDEN).json({ message: 'Invalid state parameter' });
449
+ }
450
+
451
+ <%_ if (architecture === 'Clean Architecture') { _%>
452
+ const useCase = new SocialLoginUseCase(new GitHubProvider(), new UserRepository());
453
+ const { user, accessToken, refreshToken } = await useCase.execute(code as string);
454
+ const userId = String(user.id || ((user as unknown) as { _id?: string | number })._id);
455
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
456
+
457
+ // Store refresh token
458
+ <%_ if (caching !== 'None') { _%>
459
+ const cacheKey = `refresh_tokens:${userId}`;
460
+ const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
461
+ activeTokens.push(refreshJti!);
462
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
463
+ <%_ } else { _%>
464
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
465
+ activeTokens.push(refreshJti!);
466
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
467
+ <%_ } _%>
468
+
469
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
470
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
471
+ res.redirect('/');
472
+ <%_ } else { _%>
473
+ const profile = await SocialAuthService.getGithubProfile(code as string);
474
+
475
+ <% if (database === 'MongoDB' || database === 'None') { -%>
476
+ let user = await User.findOne({ email: profile.email });
477
+ <% } else { -%>
478
+ let user = await User.findOne({ where: { email: profile.email } });
479
+ <% } -%>
480
+
481
+ if (!user) {
482
+ <%_ if (database === 'MongoDB' || database === 'None') { -%>
483
+ user = await User.create({
484
+ email: profile.email,
485
+ name: profile.name,
486
+ password: null,
487
+ githubId: profile.id
488
+ });
489
+ <%_ } else { -%>
490
+ user = await User.create({
491
+ email: profile.email,
492
+ name: profile.name,
493
+ password: null,
494
+ githubId: profile.id
495
+ }) as unknown as User;
496
+ <%_ } _%>
497
+ }
498
+
499
+ const activeUser = user!;
500
+ const userId = String(activeUser.id || ((activeUser as unknown) as { _id?: string | number })._id);
501
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: activeUser.email });
502
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
503
+ const accessToken = JwtService.generateToken({ id: userId, email: activeUser.email, sid: refreshJti });
504
+
505
+ // Store refresh token
506
+ <%_ if (caching !== 'None') { -%>
507
+ const cacheKey = `refresh_tokens:${userId}`;
508
+ const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
509
+ activeTokens.push(refreshJti!);
510
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
511
+ <%_ } else { -%>
512
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
513
+ activeTokens.push(refreshJti!);
514
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
515
+ <%_ } _%>
516
+
517
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
518
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
519
+ res.redirect('/');
520
+ <%_ } _%>
521
+ } catch (error) {
522
+ logger.error('GitHub callback error:', error);
523
+ res.redirect('/login?error=social_auth_failed');
524
+ }
525
+ }
526
+ <% } -%>
527
+ <% } -%>
167
528
  }
@@ -31,27 +31,31 @@ export const authMiddleware = async (req: CustomRequest, res: Response, next: Ne
31
31
  }
32
32
 
33
33
  if (decoded.jti) {
34
- <%_ if (caching !== 'None') { -%>
34
+ <%_ if (caching !== 'None') { _%>
35
35
  const isBlacklisted = await cacheService.get(`blacklist:${decoded.jti}`);
36
36
  if (isBlacklisted) {
37
37
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
38
- }<%_ } else { -%>
38
+ }
39
+ <%_ } else { _%>
39
40
  const expiryDate = JwtService.blacklistedTokens.get(decoded.jti);
40
41
  if (expiryDate && Date.now() < expiryDate) {
41
42
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
42
- }<% } %>
43
+ }
44
+ <%_ } _%>
43
45
  }
44
46
 
45
47
  if (decoded.sid) {
46
- <%_ if (caching !== 'None') { -%>
48
+ <%_ if (caching !== 'None') { _%>
47
49
  const activeTokens = await cacheService.get<string[]>(`refresh_tokens:${decoded.id}`) || [];
48
50
  if (!activeTokens.includes(decoded.sid)) {
49
51
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
50
- }<%_ } else { -%>
52
+ }
53
+ <%_ } else { _%>
51
54
  const activeTokens = JwtService.activeRefreshTokens.get(String(decoded.id)) || [];
52
55
  if (!activeTokens.includes(decoded.sid)) {
53
56
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
54
- }<% } %>
57
+ }
58
+ <%_ } _%>
55
59
  }
56
60
 
57
61
  req.user = decoded;
@@ -16,5 +16,16 @@ const authController = new AuthController();
16
16
  router.post('/login', (req, res, next) => authController.login(req, res, next));
17
17
  router.post('/refresh', (req, res, next) => authController.refresh(req, res, next));
18
18
  router.post('/logout', authMiddleware, (req, res, next) => authController.logout(req, res, next));
19
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
20
+ router.post('/social/exchange', (req, res, next) => authController.socialExchange(req, res, next));
21
+ <%_ if (socialAuth.includes('Google')) { _%>
22
+ router.get('/google', (req, res) => authController.googleLogin(req, res));
23
+ router.get('/google/callback', (req, res, next) => authController.googleCallback(req, res, next));
24
+ <%_ } _%>
25
+ <%_ if (socialAuth.includes('GitHub')) { _%>
26
+ router.get('/github', (req, res) => authController.githubLogin(req, res));
27
+ router.get('/github/callback', (req, res, next) => authController.githubCallback(req, res, next));
28
+ <%_ } _%>
29
+ <%_ } _%>
19
30
 
20
31
  export default router;
@@ -63,6 +63,12 @@ describe('JwtService', () => {
63
63
  expect(jwt.verify).toHaveBeenCalledWith(token, secret);
64
64
  expect(result).toEqual(payload);
65
65
  });
66
+
67
+ it('should return null for an invalid token', () => {
68
+ (jwt.verify as jest.Mock).mockImplementation(() => { throw new Error('Invalid token'); });
69
+ const result = JwtService.verifyToken(token);
70
+ expect(result).toBeNull();
71
+ });
66
72
  });
67
73
 
68
74
  describe('verifyRefreshToken', () => {
@@ -74,6 +80,12 @@ describe('JwtService', () => {
74
80
  expect(jwt.verify).toHaveBeenCalledWith(token, refreshSecret);
75
81
  expect(result).toEqual(payload);
76
82
  });
83
+
84
+ it('should return null for an invalid refresh token', () => {
85
+ (jwt.verify as jest.Mock).mockImplementation(() => { throw new Error('Invalid token'); });
86
+ const result = JwtService.verifyRefreshToken(token);
87
+ expect(result).toBeNull();
88
+ });
77
89
  });
78
90
 
79
91
  describe('decodeToken', () => {
@@ -85,5 +97,11 @@ describe('JwtService', () => {
85
97
  expect(jwt.decode).toHaveBeenCalledWith(token);
86
98
  expect(result).toEqual(payload);
87
99
  });
100
+
101
+ it('should return null if decode fails', () => {
102
+ (jwt.decode as jest.Mock).mockImplementation(() => { throw new Error('Decode failed'); });
103
+ const result = JwtService.decodeToken(token);
104
+ expect(result).toBeNull();
105
+ });
88
106
  });
89
107
  });
@@ -33,7 +33,7 @@ export class JwtService {
33
33
  static verifyToken(token: string): JwtPayload | null {
34
34
  try {
35
35
  return jwt.verify(token, this.SECRET) as JwtPayload;
36
- } catch {
36
+ } catch (error) {
37
37
  return null;
38
38
  }
39
39
  }
@@ -41,7 +41,7 @@ export class JwtService {
41
41
  static verifyRefreshToken(token: string): JwtPayload | null {
42
42
  try {
43
43
  return jwt.verify(token, this.REFRESH_SECRET) as JwtPayload;
44
- } catch {
44
+ } catch (error) {
45
45
  return null;
46
46
  }
47
47
  }
@@ -49,7 +49,7 @@ export class JwtService {
49
49
  static decodeToken(token: string): JwtPayload | null {
50
50
  try {
51
51
  return jwt.decode(token) as JwtPayload;
52
- } catch {
52
+ } catch (error) {
53
53
  return null;
54
54
  }
55
55
  }