nodejs-quickstart-structure 2.2.0 → 2.3.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 (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +14 -13
  3. package/bin/index.js +2 -1
  4. package/lib/generator.js +10 -1
  5. package/lib/modules/project-setup.js +41 -0
  6. package/lib/modules/terraform-setup.js +131 -0
  7. package/lib/prompts.js +22 -3
  8. package/package.json +1 -1
  9. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +2 -0
  10. package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +5 -2
  11. package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
  12. package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +4 -2
  13. package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
  14. package/templates/common/auth/js/controllers/authController.js.ejs +42 -12
  15. package/templates/common/auth/js/controllers/authController.spec.js.ejs +95 -5
  16. package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
  17. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +0 -2
  18. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +29 -7
  19. package/templates/common/auth/ts/controllers/authController.ts.ejs +34 -5
  20. package/templates/common/caching/clean/js/CreateUser.js.ejs +4 -2
  21. package/templates/common/caching/clean/ts/createUser.ts.ejs +4 -2
  22. package/templates/common/eslint.config.mjs.ejs +4 -1
  23. package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
  24. package/templates/common/package.json.ejs +2 -0
  25. package/templates/common/terraform/main.tf +52 -0
  26. package/templates/common/terraform/modules/cache/main.tf +41 -0
  27. package/templates/common/terraform/modules/cache/outputs.tf +7 -0
  28. package/templates/common/terraform/modules/cache/variables.tf +4 -0
  29. package/templates/common/terraform/modules/compute/main.tf +69 -0
  30. package/templates/common/terraform/modules/compute/outputs.tf +7 -0
  31. package/templates/common/terraform/modules/compute/variables.tf +20 -0
  32. package/templates/common/terraform/modules/database/main.tf +57 -0
  33. package/templates/common/terraform/modules/database/outputs.tf +16 -0
  34. package/templates/common/terraform/modules/database/variables.tf +27 -0
  35. package/templates/common/terraform/modules/security/main.tf +130 -0
  36. package/templates/common/terraform/modules/security/outputs.tf +15 -0
  37. package/templates/common/terraform/modules/security/variables.tf +12 -0
  38. package/templates/common/terraform/modules/vpc/main.tf +134 -0
  39. package/templates/common/terraform/modules/vpc/outputs.tf +19 -0
  40. package/templates/common/terraform/modules/vpc/variables.tf +45 -0
  41. package/templates/common/terraform/outputs.tf +29 -0
  42. package/templates/common/terraform/provider.tf +17 -0
  43. package/templates/common/terraform/variables.tf +33 -0
  44. package/templates/mvc/js/src/index.js.ejs +2 -0
  45. package/templates/mvc/js/src/utils/httpCodes.js +1 -0
  46. package/templates/mvc/ts/src/index.ts.ejs +2 -0
  47. package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
@@ -6,7 +6,8 @@ jest.mock('@/usecases/auth/socialLoginUseCase');
6
6
  jest.mock('@/infrastructure/repositories/UserRepository');
7
7
  <%_ } _%>
8
8
  jest.mock('@/infrastructure/database/models/User', () => ({
9
- findOne: jest.fn()
9
+ findOne: jest.fn(),
10
+ create: jest.fn()
10
11
  }));
11
12
  <%_ } else { _%>
12
13
  jest.mock('@/services/jwtService');
@@ -14,7 +15,8 @@ jest.mock('@/services/jwtService');
14
15
  jest.mock('@/services/socialAuthService');
15
16
  <%_ } _%>
16
17
  jest.mock('@/models/User', () => ({
17
- findOne: jest.fn()
18
+ findOne: jest.fn(),
19
+ create: jest.fn()
18
20
  }));
19
21
  <%_ } _%>
20
22
  jest.mock('bcryptjs');
@@ -58,12 +60,14 @@ describe('AuthController', () => {
58
60
  mockReq = {
59
61
  body: {},
60
62
  headers: {},
61
- query: {}
63
+ query: {},
64
+ cookies: {}
62
65
  };
63
66
  mockRes = {
64
67
  status: jest.fn().mockReturnThis(),
65
68
  json: jest.fn(),
66
69
  cookie: jest.fn(),
70
+ clearCookie: jest.fn(),
67
71
  redirect: jest.fn()
68
72
  };
69
73
  next = jest.fn();
@@ -269,7 +273,8 @@ describe('AuthController', () => {
269
273
  });
270
274
 
271
275
  it('googleCallback should handle Google callback', async () => {
272
- mockReq.query = { code: 'test-code' };
276
+ mockReq.query = { code: 'test-code', state: 'test-state' };
277
+ mockReq.cookies = { oauth_state: 'test-state' };
273
278
  const user = { id: 1, email: 'google@test.com' };
274
279
 
275
280
  <% if (architecture === 'Clean Architecture') { %>
@@ -294,6 +299,48 @@ describe('AuthController', () => {
294
299
  expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
295
300
  expect(mockRes.redirect).toHaveBeenCalledWith('/');
296
301
  });
302
+
303
+ it('googleCallback should return 403 if state is invalid', async () => {
304
+ mockReq.query = { code: 'test-code', state: 'invalid-state' };
305
+ mockReq.cookies = { oauth_state: 'valid-state' };
306
+ await controller.googleCallback(mockReq, mockRes, next);
307
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
308
+ });
309
+
310
+ it('googleCallback should create user if not exists (MVC)', async () => {
311
+ <% if (architecture === 'MVC') { %>
312
+ mockReq.query = { code: 'test-code', state: 'test-state' };
313
+ mockReq.cookies = { oauth_state: 'test-state' };
314
+ SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'new@google.com', id: 'google-id' });
315
+ User.findOne.mockResolvedValue(null);
316
+ User.create.mockResolvedValue({ id: '2', email: 'new@google.com' });
317
+ JwtService.generateToken.mockReturnValue('at');
318
+ JwtService.generateRefreshToken.mockReturnValue('rt');
319
+ JwtService.decodeToken.mockReturnValue({ jti: 'jti' });
320
+
321
+ await controller.googleCallback(mockReq, mockRes, next);
322
+ expect(User.create).toHaveBeenCalled();
323
+ <% } else { %>
324
+ expect(true).toBe(true);
325
+ <% } %>
326
+ });
327
+
328
+ it('googleCallback should redirect to login on error', async () => {
329
+ const error = new Error('Callback failed');
330
+ mockReq.query = { code: 'code', state: 'test-state' };
331
+ mockReq.cookies = { oauth_state: 'test-state' };
332
+ <% if (architecture === 'Clean Architecture') { %>
333
+ const mockUseCase = {
334
+ execute: jest.fn().mockRejectedValue(error)
335
+ };
336
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
337
+ <% } else { %>
338
+ SocialAuthService.getGoogleProfile.mockRejectedValue(error);
339
+ <% } %>
340
+
341
+ await controller.googleCallback(mockReq, mockRes, next);
342
+ expect(mockRes.redirect).toHaveBeenCalledWith('/login?error=social_auth_failed');
343
+ });
297
344
  <% } %>
298
345
 
299
346
  <% if (socialAuth.includes('GitHub')) { %>
@@ -303,7 +350,8 @@ describe('AuthController', () => {
303
350
  });
304
351
 
305
352
  it('githubCallback should handle GitHub callback', async () => {
306
- mockReq.query = { code: 'test-code' };
353
+ mockReq.query = { code: 'test-code', state: 'test-state' };
354
+ mockReq.cookies = { oauth_state: 'test-state' };
307
355
  const user = { id: 1, email: 'github@test.com' };
308
356
 
309
357
  <% if (architecture === 'Clean Architecture') { %>
@@ -328,6 +376,48 @@ describe('AuthController', () => {
328
376
  expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
329
377
  expect(mockRes.redirect).toHaveBeenCalledWith('/');
330
378
  });
379
+
380
+ it('githubCallback should return 403 if state is invalid', async () => {
381
+ mockReq.query = { code: 'test-code', state: 'invalid-state' };
382
+ mockReq.cookies = { oauth_state: 'valid-state' };
383
+ await controller.githubCallback(mockReq, mockRes, next);
384
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
385
+ });
386
+
387
+ it('githubCallback should create user if not exists (MVC)', async () => {
388
+ <% if (architecture === 'MVC') { %>
389
+ mockReq.query = { code: 'test-code', state: 'test-state' };
390
+ mockReq.cookies = { oauth_state: 'test-state' };
391
+ SocialAuthService.getGithubProfile.mockResolvedValue({ email: 'new@github.com', id: 'github-id' });
392
+ User.findOne.mockResolvedValue(null);
393
+ User.create.mockResolvedValue({ id: '2', email: 'new@github.com' });
394
+ JwtService.generateToken.mockReturnValue('at');
395
+ JwtService.generateRefreshToken.mockReturnValue('rt');
396
+ JwtService.decodeToken.mockReturnValue({ jti: 'jti' });
397
+
398
+ await controller.githubCallback(mockReq, mockRes, next);
399
+ expect(User.create).toHaveBeenCalled();
400
+ <% } else { %>
401
+ expect(true).toBe(true);
402
+ <% } %>
403
+ });
404
+
405
+ it('githubCallback should redirect to login on error', async () => {
406
+ const error = new Error('Callback failed');
407
+ mockReq.query = { code: 'code', state: 'test-state' };
408
+ mockReq.cookies = { oauth_state: 'test-state' };
409
+ <% if (architecture === 'Clean Architecture') { %>
410
+ const mockUseCase = {
411
+ execute: jest.fn().mockRejectedValue(error)
412
+ };
413
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
414
+ <% } else { %>
415
+ SocialAuthService.getGithubProfile.mockRejectedValue(error);
416
+ <% } %>
417
+
418
+ await controller.githubCallback(mockReq, mockRes, next);
419
+ expect(mockRes.redirect).toHaveBeenCalledWith('/login?error=social_auth_failed');
420
+ });
331
421
  <% } %>
332
422
  });
333
423
  <% } -%>
@@ -25,7 +25,7 @@ class JwtService {
25
25
  static verifyToken(token) {
26
26
  try {
27
27
  return jwt.verify(token, this.SECRET);
28
- } catch (error) {
28
+ } catch {
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 (error) {
36
+ } catch {
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 (error) {
44
+ } catch {
45
45
  return null;
46
46
  }
47
47
  }
@@ -2,8 +2,6 @@ const axios = require('axios');
2
2
  <% if (architecture === 'MVC') { -%>
3
3
  const { SocialAuthService } = require('@/services/socialAuthService');
4
4
  const logger = require('@/utils/logger');
5
- <% } else { -%>
6
- const logger = require('@/infrastructure/log/logger');
7
5
  <% } -%>
8
6
 
9
7
  jest.mock('axios');
@@ -56,12 +56,14 @@ describe('AuthController', () => {
56
56
  mockRequest = {
57
57
  body: {},
58
58
  headers: {},
59
- query: {}
59
+ query: {},
60
+ cookies: {}
60
61
  };
61
62
  mockResponse = {
62
63
  status: jest.fn().mockReturnThis(),
63
64
  json: jest.fn(),
64
65
  cookie: jest.fn(),
66
+ clearCookie: jest.fn(),
65
67
  redirect: jest.fn()
66
68
  };
67
69
  jest.clearAllMocks();
@@ -335,7 +337,8 @@ describe('AuthController', () => {
335
337
 
336
338
  <% if (socialAuth.includes('Google')) { %>
337
339
  it('googleCallback should handle Google callback', async () => {
338
- mockRequest.query = { code: 'test-code' };
340
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
341
+ mockRequest.cookies = { oauth_state: 'test-state' };
339
342
  const user = { id: 1, email: 'google@test.com' };
340
343
  <%_ if (architecture === 'Clean Architecture') { _%>
341
344
  const mockUseCaseInstance = { execute: jest.fn().mockResolvedValue({ user, accessToken: 'at', refreshToken: 'rt' }) };
@@ -354,9 +357,17 @@ describe('AuthController', () => {
354
357
  expect(mockResponse.redirect).toHaveBeenCalledWith('/');
355
358
  });
356
359
 
360
+ it('googleCallback should return 403 if state is invalid', async () => {
361
+ mockRequest.query = { code: 'test-code', state: 'invalid-state' };
362
+ mockRequest.cookies = { oauth_state: 'valid-state' };
363
+ await authController.googleCallback(mockRequest as Request, mockResponse as Response, nextFunction);
364
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
365
+ });
366
+
357
367
  it('googleCallback should create user if not exists (MVC)', async () => {
358
368
  <% if (architecture === 'MVC') { %>
359
- mockRequest.query = { code: 'test-code' };
369
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
370
+ mockRequest.cookies = { oauth_state: 'test-state' };
360
371
  const { SocialAuthService } = require('@/services/socialAuthService');
361
372
  (SocialAuthService.getGoogleProfile as jest.Mock).mockResolvedValue({ email: 'new@google.com', id: 'google-id' });
362
373
  (User.findOne as jest.Mock).mockResolvedValue(null);
@@ -372,7 +383,8 @@ describe('AuthController', () => {
372
383
 
373
384
  it('googleCallback should redirect to login on error', async () => {
374
385
  const error = new Error('Callback failed');
375
- mockRequest.query = { code: 'code' };
386
+ mockRequest.query = { code: 'code', state: 'test-state' };
387
+ mockRequest.cookies = { oauth_state: 'test-state' };
376
388
  <% if (architecture === 'MVC') { %>
377
389
  const { SocialAuthService } = require('@/services/socialAuthService');
378
390
  (SocialAuthService.getGoogleProfile as jest.Mock).mockRejectedValue(error);
@@ -387,7 +399,8 @@ describe('AuthController', () => {
387
399
 
388
400
  <% if (socialAuth.includes('GitHub')) { %>
389
401
  it('githubCallback should handle GitHub callback', async () => {
390
- mockRequest.query = { code: 'test-code' };
402
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
403
+ mockRequest.cookies = { oauth_state: 'test-state' };
391
404
  const user = { id: 1, email: 'github@test.com' };
392
405
  <%_ if (architecture === 'Clean Architecture') { _%>
393
406
  const mockUseCaseInstance = { execute: jest.fn().mockResolvedValue({ user, accessToken: 'at', refreshToken: 'rt' }) };
@@ -406,9 +419,17 @@ describe('AuthController', () => {
406
419
  expect(mockResponse.redirect).toHaveBeenCalledWith('/');
407
420
  });
408
421
 
422
+ it('githubCallback should return 403 if state is invalid', async () => {
423
+ mockRequest.query = { code: 'test-code', state: 'invalid-state' };
424
+ mockRequest.cookies = { oauth_state: 'valid-state' };
425
+ await authController.githubCallback(mockRequest as Request, mockResponse as Response, nextFunction);
426
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
427
+ });
428
+
409
429
  it('githubCallback should create user if not exists (MVC)', async () => {
410
430
  <% if (architecture === 'MVC') { %>
411
- mockRequest.query = { code: 'test-code' };
431
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
432
+ mockRequest.cookies = { oauth_state: 'test-state' };
412
433
  const { SocialAuthService } = require('@/services/socialAuthService');
413
434
  (SocialAuthService.getGithubProfile as jest.Mock).mockResolvedValue({ email: 'new@github.com', id: 'github-id' });
414
435
  (User.findOne as jest.Mock).mockResolvedValue(null);
@@ -424,7 +445,8 @@ describe('AuthController', () => {
424
445
 
425
446
  it('githubCallback should redirect to login on error', async () => {
426
447
  const error = new Error('Callback failed');
427
- mockRequest.query = { code: 'code' };
448
+ mockRequest.query = { code: 'code', state: 'test-state' };
449
+ mockRequest.cookies = { oauth_state: 'test-state' };
428
450
  <% if (architecture === 'MVC') { %>
429
451
  const { SocialAuthService } = require('@/services/socialAuthService');
430
452
  (SocialAuthService.getGithubProfile as jest.Mock).mockRejectedValue(error);
@@ -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';
@@ -26,6 +27,15 @@ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
26
27
  import { HTTP_STATUS } from '@/utils/httpCodes';
27
28
 
28
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
+
29
39
  async login(req: Request, res: Response, next: NextFunction) {
30
40
  try {
31
41
  const { email, password } = req.body;
@@ -188,7 +198,7 @@ export class AuthController {
188
198
  }
189
199
 
190
200
  <% if (architecture === 'Clean Architecture') { -%>
191
- let useCase;
201
+ let useCase: SocialLoginUseCase | undefined;
192
202
  const userRepository = new UserRepository();
193
203
  <%_ if (socialAuth.includes('Google')) { _%>
194
204
  if (provider === 'Google') useCase = new SocialLoginUseCase(new GoogleProvider(), userRepository);
@@ -304,6 +314,9 @@ export class AuthController {
304
314
  <% if (socialAuth.includes('Google')) { -%>
305
315
  async googleLogin(req: Request, res: Response) {
306
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
+
307
320
  const options = {
308
321
  redirect_uri: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback',
309
322
  client_id: process.env.GOOGLE_CLIENT_ID!,
@@ -314,7 +327,7 @@ export class AuthController {
314
327
  'https://www.googleapis.com/auth/userinfo.profile',
315
328
  'https://www.googleapis.com/auth/userinfo.email',
316
329
  ].join(' '),
317
- state: 'google'
330
+ state: state
318
331
  };
319
332
  const qs = new URLSearchParams(options);
320
333
  res.redirect(`${rootUrl}?${qs.toString()}`);
@@ -322,7 +335,14 @@ export class AuthController {
322
335
 
323
336
  async googleCallback(req: Request, res: Response, next: NextFunction) {
324
337
  try {
325
- const { code } = req.query;
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
+
326
346
  const redirectUri = process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback';
327
347
 
328
348
  <%_ if (architecture === 'Clean Architecture') { _%>
@@ -405,11 +425,14 @@ export class AuthController {
405
425
  <% if (socialAuth.includes('GitHub')) { -%>
406
426
  async githubLogin(req: Request, res: Response) {
407
427
  const rootUrl = 'https://github.com/login/oauth/authorize';
428
+ const state = crypto.randomBytes(16).toString('hex');
429
+ this.setOAuthStateCookie(res, state);
430
+
408
431
  const options = {
409
432
  client_id: process.env.GITHUB_CLIENT_ID!,
410
433
  redirect_uri: process.env.GITHUB_CALLBACK_URL || 'http://localhost:3000/api/auth/github/callback',
411
434
  scope: 'user:email',
412
- state: 'github'
435
+ state: state
413
436
  };
414
437
  const qs = new URLSearchParams(options);
415
438
  res.redirect(`${rootUrl}?${qs.toString()}`);
@@ -417,7 +440,13 @@ export class AuthController {
417
440
 
418
441
  async githubCallback(req: Request, res: Response, next: NextFunction) {
419
442
  try {
420
- const { code } = req.query;
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
+ }
421
450
 
422
451
  <%_ if (architecture === 'Clean Architecture') { _%>
423
452
  const useCase = new SocialLoginUseCase(new GitHubProvider(), new UserRepository());
@@ -15,13 +15,15 @@ class CreateUser {
15
15
  }
16
16
 
17
17
  async execute(name, email, password) {
18
- let finalPassword = password;
19
18
  <% if (auth.includes('JWT')) { -%>
19
+ let finalPassword = password;
20
20
  if (password) {
21
21
  finalPassword = await bcrypt.hash(password, 10);
22
22
  }
23
- <% } -%>
24
23
  const user = new User(null, name, email, finalPassword);
24
+ <% } else { -%>
25
+ const user = new User(null, name, email, password);
26
+ <% } -%>
25
27
  const savedUser = await this.userRepository.save(user);
26
28
 
27
29
  try {
@@ -14,13 +14,15 @@ export default class CreateUser {
14
14
  constructor(private userRepository: UserRepository) {}
15
15
 
16
16
  async execute(name: string, email: string, password?: string) {
17
- let finalPassword = password;
18
17
  <% if (auth.includes('JWT')) { -%>
18
+ let finalPassword = password;
19
19
  if (password) {
20
20
  finalPassword = await bcrypt.hash(password, 10);
21
21
  }
22
- <% } -%>
23
22
  const user = new User(null, name, email, finalPassword);
23
+ <% } else { -%>
24
+ const user = new User(null, name, email, password);
25
+ <% } -%>
24
26
  const savedUser = await this.userRepository.save(user);
25
27
 
26
28
  try {
@@ -73,7 +73,10 @@ export default [
73
73
  },
74
74
  rules: {
75
75
  "no-console": "warn",
76
- "no-unused-vars": "warn",
76
+ "no-unused-vars": ["warn", {
77
+ "argsIgnorePattern": "^_",
78
+ "varsIgnorePattern": "^_"
79
+ }],
77
80
  "import/no-unresolved": [2, { "caseSensitive": false }]
78
81
  }
79
82
  }
@@ -34,7 +34,7 @@ const connectKafka = async (retries = 10) => {
34
34
  await consumer.run({
35
35
  eachMessage: async (payload) => welcomeConsumer.onMessage(payload),
36
36
  });
37
- } catch (error) {
37
+ } catch {
38
38
  // Fallback or no consumers found
39
39
  await consumer.subscribe({ topic: 'user-topic', fromBeginning: true });
40
40
  await consumer.run({
@@ -62,6 +62,7 @@
62
62
  <% } -%>
63
63
  "cors": "^2.8.5",
64
64
  "helmet": "^7.1.0",
65
+ "cookie-parser": "^1.4.6",
65
66
  "hpp": "^0.2.3",
66
67
  "express-rate-limit": "^7.1.5",
67
68
  "winston": "^3.11.0",
@@ -80,6 +81,7 @@
80
81
  "ts-node": "^10.9.2",
81
82
  "@types/node": "^20.10.5",
82
83
  "@types/express": "^4.17.21",
84
+ "@types/cookie-parser": "^1.4.6",
83
85
  "@types/cors": "^2.8.17",
84
86
  "@types/hpp": "^0.2.3",
85
87
  <% if (caching === 'Memory Cache') { %> "@types/node-cache": "^4.2.5",
@@ -0,0 +1,52 @@
1
+ # --- Network Layer ---
2
+ module "vpc" {
3
+ source = "./modules/vpc"
4
+ project_name = var.project_name
5
+ environment = var.environment
6
+ is_production = var.is_production
7
+ }
8
+
9
+ # --- Security Layer (WAF, ALB, SGs) ---
10
+ module "security" {
11
+ source = "./modules/security"
12
+ project_name = var.project_name
13
+ environment = var.environment
14
+ vpc_id = module.vpc.vpc_id
15
+ public_subnet_ids = module.vpc.public_subnet_ids
16
+ enable_waf = var.is_production
17
+ }
18
+
19
+ # --- Data Layer (RDS Isolated) ---
20
+ module "database" {
21
+ source = "./modules/database"
22
+ project_name = var.project_name
23
+ environment = var.environment
24
+ vpc_id = module.vpc.vpc_id
25
+ isolated_subnet_ids = module.vpc.isolated_subnet_ids
26
+ app_sg_id = module.security.app_sg_id
27
+ db_engine = var.db_engine
28
+ db_name = var.db_name
29
+ db_user = var.db_user
30
+ multi_az = var.is_production
31
+ }
32
+
33
+ # --- Cache Layer (Redis ElastiCache) ---
34
+ module "cache" {
35
+ source = "./modules/cache"
36
+ project_name = var.project_name
37
+ vpc_id = module.vpc.vpc_id
38
+ private_subnet_ids = module.vpc.private_subnet_ids
39
+ app_sg_id = module.security.app_sg_id
40
+ }
41
+
42
+ # --- Compute Layer (App EC2) ---
43
+ module "compute" {
44
+ source = "./modules/compute"
45
+ project_name = var.project_name
46
+ environment = var.environment
47
+ vpc_id = module.vpc.vpc_id
48
+ private_subnet_ids = module.vpc.private_subnet_ids
49
+ app_sg_id = module.security.app_sg_id
50
+ target_group_arn = module.security.alb_target_group_arn
51
+ instance_count = var.is_production ? 2 : 1
52
+ }
@@ -0,0 +1,41 @@
1
+ resource "aws_security_group" "redis_sg" {
2
+ name = "${var.project_name}-redis-sg"
3
+ description = "Allow Redis traffic from App"
4
+ vpc_id = var.vpc_id
5
+
6
+ ingress {
7
+ from_port = 6379
8
+ to_port = 6379
9
+ protocol = "tcp"
10
+ security_groups = [var.app_sg_id]
11
+ }
12
+
13
+ egress {
14
+ from_port = 0
15
+ to_port = 0
16
+ protocol = "-1"
17
+ cidr_blocks = ["0.0.0.0/0"]
18
+ }
19
+
20
+ tags = { Name = "${var.project_name}-redis-sg" }
21
+ }
22
+
23
+ resource "aws_elasticache_subnet_group" "main" {
24
+ name = "${var.project_name}-redis-subnet-group"
25
+ subnet_ids = var.private_subnet_ids
26
+
27
+ tags = { Name = "${var.project_name}-redis-subnet-group" }
28
+ }
29
+
30
+ resource "aws_elasticache_cluster" "main" {
31
+ cluster_id = "${var.project_name}-redis"
32
+ engine = "redis"
33
+ node_type = "cache.t3.micro"
34
+ num_cache_nodes = 1
35
+ parameter_group_name = "default.redis7"
36
+ port = 6379
37
+ subnet_group_name = aws_elasticache_subnet_group.main.name
38
+ security_group_ids = [aws_security_group.redis_sg.id]
39
+
40
+ tags = { Name = "${var.project_name}-redis" }
41
+ }
@@ -0,0 +1,7 @@
1
+ output "redis_endpoint" {
2
+ value = aws_elasticache_cluster.main.cache_nodes[0].address
3
+ }
4
+
5
+ output "redis_port" {
6
+ value = aws_elasticache_cluster.main.port
7
+ }
@@ -0,0 +1,4 @@
1
+ variable "project_name" {}
2
+ variable "vpc_id" {}
3
+ variable "private_subnet_ids" { type = list(string) }
4
+ variable "app_sg_id" {}
@@ -0,0 +1,69 @@
1
+ # --- IAM Role for Systems Manager (SSM) ---
2
+ # This allows SSH-less access to the private instance
3
+ resource "aws_iam_role" "ssm_role" {
4
+ name = "${var.project_name}-ssm-role"
5
+
6
+ assume_role_policy = jsonencode({
7
+ Version = "2012-10-17"
8
+ Statement = [{
9
+ Action = "sts:AssumeRole"
10
+ Effect = "Allow"
11
+ Principal = { Service = "ec2.amazonaws.com" }
12
+ }]
13
+ })
14
+ }
15
+
16
+ resource "aws_iam_role_policy_attachment" "ssm_policy" {
17
+ role = aws_iam_role.ssm_role.name
18
+ policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
19
+ }
20
+
21
+ resource "aws_iam_instance_profile" "ssm_profile" {
22
+ name = "${var.project_name}-ssm-profile"
23
+ role = aws_iam_role.ssm_role.name
24
+ }
25
+
26
+ # --- EC2 Instance ---
27
+ data "aws_ami" "latest" {
28
+ most_recent = true
29
+ owners = ["amazon"]
30
+ filter {
31
+ name = "name"
32
+ values = ["amzn2-ami-hvm-*"]
33
+ }
34
+ }
35
+
36
+ resource "aws_instance" "app" {
37
+ count = var.instance_count
38
+ ami = data.aws_ami.latest.id
39
+ instance_type = var.instance_type
40
+ subnet_id = var.private_subnet_ids[count.index % length(var.private_subnet_ids)]
41
+ vpc_security_group_ids = [var.app_sg_id]
42
+ iam_instance_profile = aws_iam_instance_profile.ssm_profile.name
43
+
44
+ user_data = <<-EOF
45
+ #!/bin/bash
46
+ yum update -y
47
+ yum install -y docker
48
+ systemctl start docker
49
+ systemctl enable docker
50
+
51
+ # Add user to docker group
52
+ usermod -aG docker ec2-user
53
+
54
+ # Note: For production, you would pull your image and run it here
55
+ # docker run -d -p 3000:3000 my-node-app:latest
56
+ EOF
57
+
58
+ tags = {
59
+ Name = "${var.project_name}-app-server-${count.index + 1}"
60
+ }
61
+ }
62
+
63
+ # Attach to ALB Target Group
64
+ resource "aws_lb_target_group_attachment" "app" {
65
+ count = var.instance_count
66
+ target_group_arn = var.target_group_arn
67
+ target_id = aws_instance.app[count.index].id
68
+ port = 3000
69
+ }
@@ -0,0 +1,7 @@
1
+ output "instance_id" {
2
+ value = aws_instance.app[*].id
3
+ }
4
+
5
+ output "private_ip" {
6
+ value = aws_instance.app[*].private_ip
7
+ }
@@ -0,0 +1,20 @@
1
+ variable "project_name" { type = string }
2
+ variable "environment" { type = string }
3
+ variable "vpc_id" { type = string }
4
+ variable "private_subnet_ids" { type = list(string) }
5
+ variable "app_sg_id" { type = string }
6
+ variable "instance_count" {
7
+ type = number
8
+ default = 1
9
+ }
10
+ variable "target_group_arn" { type = string }
11
+
12
+ variable "instance_type" {
13
+ default = "t3.micro"
14
+ }
15
+
16
+ variable "ami_id" {
17
+ description = "Amazon Linux 2023 AMI"
18
+ type = string
19
+ default = "ami-051f8b211046e76c0" # Thay đổi tùy region
20
+ }