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.
- package/CHANGELOG.md +15 -0
- package/README.md +14 -13
- package/bin/index.js +2 -1
- package/lib/generator.js +10 -1
- package/lib/modules/project-setup.js +41 -0
- package/lib/modules/terraform-setup.js +131 -0
- package/lib/prompts.js +22 -3
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +2 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +5 -2
- package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
- package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +4 -2
- package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
- package/templates/common/auth/js/controllers/authController.js.ejs +42 -12
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +95 -5
- package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
- package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +0 -2
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +29 -7
- package/templates/common/auth/ts/controllers/authController.ts.ejs +34 -5
- package/templates/common/caching/clean/js/CreateUser.js.ejs +4 -2
- package/templates/common/caching/clean/ts/createUser.ts.ejs +4 -2
- package/templates/common/eslint.config.mjs.ejs +4 -1
- package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
- package/templates/common/package.json.ejs +2 -0
- package/templates/common/terraform/main.tf +52 -0
- package/templates/common/terraform/modules/cache/main.tf +41 -0
- package/templates/common/terraform/modules/cache/outputs.tf +7 -0
- package/templates/common/terraform/modules/cache/variables.tf +4 -0
- package/templates/common/terraform/modules/compute/main.tf +69 -0
- package/templates/common/terraform/modules/compute/outputs.tf +7 -0
- package/templates/common/terraform/modules/compute/variables.tf +20 -0
- package/templates/common/terraform/modules/database/main.tf +57 -0
- package/templates/common/terraform/modules/database/outputs.tf +16 -0
- package/templates/common/terraform/modules/database/variables.tf +27 -0
- package/templates/common/terraform/modules/security/main.tf +130 -0
- package/templates/common/terraform/modules/security/outputs.tf +15 -0
- package/templates/common/terraform/modules/security/variables.tf +12 -0
- package/templates/common/terraform/modules/vpc/main.tf +134 -0
- package/templates/common/terraform/modules/vpc/outputs.tf +19 -0
- package/templates/common/terraform/modules/vpc/variables.tf +45 -0
- package/templates/common/terraform/outputs.tf +29 -0
- package/templates/common/terraform/provider.tf +17 -0
- package/templates/common/terraform/variables.tf +33 -0
- package/templates/mvc/js/src/index.js.ejs +2 -0
- package/templates/mvc/js/src/utils/httpCodes.js +1 -0
- package/templates/mvc/ts/src/index.ts.ejs +2 -0
- 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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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,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,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
|
+
}
|