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
@@ -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
+ <% } -%>
@@ -0,0 +1,192 @@
1
+ const axios = require('axios');
2
+ <% if (architecture === 'MVC') { -%>
3
+ const { SocialAuthService } = require('@/services/socialAuthService');
4
+ const logger = require('@/utils/logger');
5
+ <% } -%>
6
+
7
+ jest.mock('axios');
8
+ jest.mock('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>', () => ({
9
+ error: jest.fn(),
10
+ info: jest.fn(),
11
+ warn: jest.fn(),
12
+ }));
13
+
14
+ // Mock environment variables for testing
15
+ process.env.GOOGLE_CLIENT_ID = 'test-google-id';
16
+ process.env.GOOGLE_CLIENT_SECRET = 'test-google-secret';
17
+ process.env.GITHUB_CLIENT_ID = 'test-github-id';
18
+ process.env.GITHUB_CLIENT_SECRET = 'test-github-secret';
19
+
20
+ describe('SocialAuthService', () => {
21
+ afterEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ <% if (socialAuth.includes('Google')) { -%>
26
+ describe('Google Provider', () => {
27
+ it('should exchange code for profile', async () => {
28
+ const mockTokenResponse = { data: { access_token: 'mock_access_token' } };
29
+ const mockProfileResponse = {
30
+ data: {
31
+ id: '123',
32
+ email: 'test@gmail.com',
33
+ name: 'Test User',
34
+ picture: 'http://pic.com/123.jpg'
35
+ }
36
+ };
37
+
38
+ axios.post.mockResolvedValue(mockTokenResponse);
39
+ axios.get.mockResolvedValue(mockProfileResponse);
40
+
41
+ let profile;
42
+ <% if (architecture === 'Clean Architecture') { %>
43
+ const { GoogleProvider } = require('@/infrastructure/auth/socialAuthService');
44
+ const provider = new GoogleProvider();
45
+ profile = await provider.getProfile('test_code', 'http://localhost/callback');
46
+ <% } else { %>
47
+ profile = await SocialAuthService.getGoogleProfile('test_code', 'http://localhost/callback');
48
+ <% } %>
49
+
50
+ expect(axios.post).toHaveBeenCalled();
51
+ expect(axios.get).toHaveBeenCalledWith('https://www.googleapis.com/oauth2/v2/userinfo', {
52
+ headers: { Authorization: 'Bearer mock_access_token' },
53
+ });
54
+ expect(profile.email).toBe('test@gmail.com');
55
+ expect(profile.name).toBe('Test User');
56
+ });
57
+
58
+ it('should throw error if token exchange fails', async () => {
59
+ axios.post.mockRejectedValue(new Error('Network error'));
60
+ <%_ if (architecture === 'Clean Architecture') { _%>
61
+ const { GoogleProvider } = require('@/infrastructure/auth/socialAuthService');
62
+ const provider = new GoogleProvider();
63
+ await expect(provider.getProfile('test_code', 'url')).rejects.toThrow('Failed to authenticate with Google');
64
+ <%_ } else { _%>
65
+ await expect(SocialAuthService.getGoogleProfile('test_code', 'url')).rejects.toThrow('Failed to authenticate with Google');
66
+ <%_ } _%>
67
+ });
68
+
69
+ it('should handle Axios errors', async () => {
70
+ const axiosError = new Error('Request failed');
71
+ axiosError.isAxiosError = true;
72
+ axiosError.response = { data: { message: 'OAuth Error' } };
73
+ axios.isAxiosError.mockReturnValue(true);
74
+ axios.post.mockRejectedValue(axiosError);
75
+
76
+ <%_ if (architecture === 'Clean Architecture') { _%>
77
+ const { GoogleProvider } = require('@/infrastructure/auth/socialAuthService');
78
+ const provider = new GoogleProvider();
79
+ await expect(provider.getProfile('test_code', 'url')).rejects.toThrow('Failed to authenticate with Google');
80
+ <%_ } else { _%>
81
+ await expect(SocialAuthService.getGoogleProfile('test_code', 'url')).rejects.toThrow('Failed to authenticate with Google');
82
+ <%_ } _%>
83
+ });
84
+
85
+ it('should log hint on invalid_grant error', async () => {
86
+ const axiosError = new Error('Request failed');
87
+ axiosError.isAxiosError = true;
88
+ axiosError.response = { data: { error: 'invalid_grant' } };
89
+ axios.isAxiosError.mockReturnValue(true);
90
+ axios.post.mockRejectedValue(axiosError);
91
+
92
+ <%_ if (architecture === 'MVC') { _%>
93
+ await expect(SocialAuthService.getGoogleProfile('test_code', 'url')).rejects.toThrow('Failed to authenticate with Google');
94
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('HINT'));
95
+ <%_ } else { _%>
96
+ const { GoogleProvider } = require('@/infrastructure/auth/socialAuthService');
97
+ const provider = new GoogleProvider();
98
+ await expect(provider.getProfile('test_code', 'url')).rejects.toThrow('Failed to authenticate with Google');
99
+ <%_ } _%>
100
+ });
101
+ });
102
+ <% } -%>
103
+
104
+ <% if (socialAuth.includes('GitHub')) { -%>
105
+ describe('GitHub Provider', () => {
106
+ it('should exchange code for profile', async () => {
107
+ const mockTokenResponse = { data: { access_token: 'mock_access_token' } };
108
+ const mockProfileResponse = {
109
+ data: { id: 456, login: 'testuser', name: 'Test Github User' }
110
+ };
111
+ const mockEmailsResponse = {
112
+ data: [{ email: 'github@test.com', primary: true }]
113
+ };
114
+
115
+ axios.post.mockResolvedValue(mockTokenResponse);
116
+ axios.get
117
+ .mockResolvedValueOnce(mockProfileResponse)
118
+ .mockResolvedValueOnce(mockEmailsResponse);
119
+
120
+ let profile;
121
+ <% if (architecture === 'Clean Architecture') { %>
122
+ const { GitHubProvider } = require('@/infrastructure/auth/socialAuthService');
123
+ const provider = new GitHubProvider();
124
+ profile = await provider.getProfile('test_code');
125
+ <% } else { %>
126
+ profile = await SocialAuthService.getGithubProfile('test_code');
127
+ <% } %>
128
+
129
+ expect(axios.post).toHaveBeenCalled();
130
+ expect(axios.get).toHaveBeenCalledTimes(2);
131
+ expect(profile.email).toBe('github@test.com');
132
+ expect(profile.id).toBe('456');
133
+ });
134
+
135
+ it('should throw error if token exchange fails', async () => {
136
+ axios.post.mockRejectedValue(new Error('Network error'));
137
+ <%_ if (architecture === 'Clean Architecture') { _%>
138
+ const { GitHubProvider } = require('@/infrastructure/auth/socialAuthService');
139
+ const provider = new GitHubProvider();
140
+ await expect(provider.getProfile('test_code')).rejects.toThrow('Failed to authenticate with GitHub');
141
+ <%_ } else { _%>
142
+ await expect(SocialAuthService.getGithubProfile('test_code')).rejects.toThrow('Failed to authenticate with GitHub');
143
+ <%_ } _%>
144
+ });
145
+
146
+ it('should handle Axios errors', async () => {
147
+ const axiosError = new Error('Request failed');
148
+ axiosError.isAxiosError = true;
149
+ axiosError.response = { data: { message: 'OAuth Error' } };
150
+ axios.isAxiosError.mockReturnValue(true);
151
+ axios.post.mockRejectedValue(axiosError);
152
+
153
+ <%_ if (architecture === 'Clean Architecture') { _%>
154
+ const { GitHubProvider } = require('@/infrastructure/auth/socialAuthService');
155
+ const provider = new GitHubProvider();
156
+ await expect(provider.getProfile('test_code')).rejects.toThrow('Failed to authenticate with GitHub');
157
+ <%_ } else { _%>
158
+ await expect(SocialAuthService.getGithubProfile('test_code')).rejects.toThrow('Failed to authenticate with GitHub');
159
+ <%_ } _%>
160
+ });
161
+
162
+ it('should fallback to first email if primary not found', async () => {
163
+ const mockTokenResponse = { data: { access_token: 'mock_at' } };
164
+ axios.post.mockResolvedValue(mockTokenResponse);
165
+ axios.get
166
+ .mockResolvedValueOnce({ data: { id: 789, login: 'test' } })
167
+ .mockResolvedValueOnce({ data: [{ email: 'secondary@test.com', primary: false }] });
168
+
169
+ let profile;
170
+ <% if (architecture === 'Clean Architecture') { %>
171
+ const { GitHubProvider } = require('@/infrastructure/auth/socialAuthService');
172
+ const provider = new GitHubProvider();
173
+ profile = await provider.getProfile('code');
174
+ <% } else { %>
175
+ profile = await SocialAuthService.getGithubProfile('code');
176
+ <% } %>
177
+ expect(profile.email).toBe('secondary@test.com');
178
+ });
179
+
180
+ it('should throw error if no access token returned', async () => {
181
+ axios.post.mockResolvedValue({ data: {} });
182
+ <% if (architecture === 'Clean Architecture') { %>
183
+ const { GitHubProvider } = require('@/infrastructure/auth/socialAuthService');
184
+ const provider = new GitHubProvider();
185
+ await expect(provider.getProfile('code')).rejects.toThrow('Failed to authenticate with GitHub');
186
+ <% } else { %>
187
+ await expect(SocialAuthService.getGithubProfile('code')).rejects.toThrow('Failed to authenticate with GitHub');
188
+ <% } %>
189
+ });
190
+ });
191
+ <% } -%>
192
+ });
@@ -0,0 +1,114 @@
1
+ const JwtService = require('<% if (architecture === "MVC") { %>../services/jwtService<% } else { %>../../infrastructure/auth/jwtService<% } %>');
2
+ <%_ if (architecture === 'Clean Architecture') { _%>
3
+ const User = require('../../domain/models/User');
4
+ <%_ } else { _%>
5
+ const User = require('<% if (architecture === "MVC") { %>../models/User<% } else { %>../../infrastructure/database/models/User<% } %>');
6
+ <%_ } _%>
7
+
8
+ class SocialLoginUseCase {
9
+ constructor(provider<% if (architecture === 'Clean Architecture') { %>, userRepository<% } %>) {
10
+ this.provider = provider;
11
+ <% if (architecture === 'Clean Architecture') { %>this.userRepository = userRepository;<% } %>
12
+ }
13
+
14
+ async execute(code, redirectUri) {
15
+ const profile = await this.provider.getProfile(code, redirectUri);
16
+
17
+ if (!profile || !profile.email) {
18
+ throw new Error('No email associated with this social account');
19
+ }
20
+
21
+ // 1. Find or create user
22
+ <% if (architecture === 'Clean Architecture') { -%>
23
+ let user = await this.userRepository.findByEmail(profile.email);
24
+
25
+ if (!user) {
26
+ user = new User(
27
+ null,
28
+ profile.name,
29
+ profile.email,
30
+ null,
31
+ <%_ if (socialAuth.includes('Google')) { _%>
32
+ this.provider.name === 'Google' ? profile.id : null,
33
+ <%_ } _%>
34
+ <%_ if (socialAuth.includes('GitHub')) { _%>
35
+ this.provider.name === 'GitHub' ? profile.id : null,
36
+ <%_ } _%>
37
+ );
38
+ user = await this.userRepository.save(user);
39
+ } else {
40
+ // Link social ID if not already linked
41
+ let updated = false;
42
+ <%_ if (socialAuth.includes('Google')) { _%>
43
+ if (this.provider.name === 'Google' && !user.googleId) {
44
+ user.googleId = profile.id;
45
+ updated = true;
46
+ }
47
+ <%_ } _%>
48
+ <%_ if (socialAuth.includes('GitHub')) { _%>
49
+ if (this.provider.name === 'GitHub' && !user.githubId) {
50
+ user.githubId = profile.id;
51
+ updated = true;
52
+ }
53
+ <%_ } _%>
54
+ if (updated) {
55
+ await this.userRepository.update(user.id, user);
56
+ }
57
+ }
58
+ <% } else { -%>
59
+ <% if (database === 'MongoDB' || database === 'None') { %>
60
+ let user = await User.findOne({ email: profile.email });
61
+ <% } else { %>
62
+ let user = await User.findOne({ where: { email: profile.email } });
63
+ <% } %>
64
+
65
+ if (!user) {
66
+ user = await User.create({
67
+ email: profile.email,
68
+ name: profile.name,
69
+ password: null, // Social login users don't have passwords
70
+ <%_ if (socialAuth.includes('Google')) { _%>
71
+ googleId: this.provider.name === 'Google' ? profile.id : null,
72
+ <%_ } _%>
73
+ <%_ if (socialAuth.includes('GitHub')) { _%>
74
+ githubId: this.provider.name === 'GitHub' ? profile.id : null,
75
+ <%_ } _%>
76
+ });
77
+ } else {
78
+ // Link social ID if not already linked
79
+ let updated = false;
80
+ <%_ if (socialAuth.includes('Google')) { _%>
81
+ if (this.provider.name === 'Google' && !user.googleId) {
82
+ user.googleId = profile.id;
83
+ updated = true;
84
+ }
85
+ <%_ } _%>
86
+ <%_ if (socialAuth.includes('GitHub')) { _%>
87
+ if (this.provider.name === 'GitHub' && !user.githubId) {
88
+ user.githubId = profile.id;
89
+ updated = true;
90
+ }
91
+ <%_ } _%>
92
+ if (updated) {
93
+ <%_ if (database === 'MongoDB') { _%>
94
+ await user.save();
95
+ <%_ } else if (database === 'None') { _%>
96
+ await User.update(user.id, { googleId: user.googleId, githubId: user.githubId });
97
+ <%_ } else { _%>
98
+ await user.save();
99
+ <%_ } _%>
100
+ }
101
+ }
102
+ <% } -%>
103
+
104
+ // 2. Generate tokens (jti is handled inside JwtService)
105
+ const userId = String(user.id || user._id);
106
+ const payload = { id: userId, email: user.email };
107
+ const accessToken = JwtService.generateToken(payload);
108
+ const refreshToken = JwtService.generateRefreshToken(payload);
109
+
110
+ return { user, accessToken, refreshToken };
111
+ }
112
+ }
113
+
114
+ module.exports = { SocialLoginUseCase };
@@ -0,0 +1,143 @@
1
+ jest.mock('@/infrastructure/auth/jwtService');
2
+ <% if (architecture === 'Clean Architecture') { -%>
3
+ jest.mock('@/infrastructure/repositories/UserRepository', () => {
4
+ return jest.fn().mockImplementation(() => ({
5
+ findByEmail: jest.fn(),
6
+ save: jest.fn(),
7
+ update: jest.fn(),
8
+ }));
9
+ });
10
+ const UserRepository = require('@/infrastructure/repositories/UserRepository');
11
+ <% } else { -%>
12
+ jest.mock('@/infrastructure/database/models/User', () => ({
13
+ findOne: jest.fn(),
14
+ create: jest.fn(),
15
+ update: jest.fn(),
16
+ }));
17
+ const User = require('@/infrastructure/database/models/User');
18
+ <% } -%>
19
+
20
+ const { SocialLoginUseCase } = require('@/usecases/auth/socialLoginUseCase');
21
+ const JwtService = require('@/infrastructure/auth/jwtService');
22
+
23
+ describe('SocialLoginUseCase', () => {
24
+ let useCase;
25
+ let mockProvider;
26
+ <%_ if (architecture === 'Clean Architecture') { _%>
27
+ let mockRepo;
28
+ <%_ } _%>
29
+
30
+ beforeEach(() => {
31
+ mockProvider = {
32
+ name: 'Google',
33
+ getProfile: jest.fn(),
34
+ };
35
+ <%_ if (architecture === 'Clean Architecture') { _%>
36
+ mockRepo = new UserRepository();
37
+ useCase = new SocialLoginUseCase(mockProvider, mockRepo);
38
+ <%_ } else { _%>
39
+ useCase = new SocialLoginUseCase(mockProvider);
40
+ <%_ } _%>
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ it('should find existing user and generate tokens', async () => {
45
+ const mockProfile = { id: 'google-123', email: 'test@test.com', name: 'Test User' };
46
+ const mockUser = { id: '1', email: 'test@test.com', googleId: null };
47
+
48
+ mockProvider.getProfile.mockResolvedValue(mockProfile);
49
+ <%_ if (architecture === 'Clean Architecture') { _%>
50
+ mockRepo.findByEmail.mockResolvedValue(mockUser);
51
+ <%_ } else { _%>
52
+ User.findOne.mockResolvedValue(mockUser);
53
+ <%_ } _%>
54
+ JwtService.generateToken.mockReturnValue('access-token');
55
+ JwtService.generateRefreshToken.mockReturnValue('refresh-token');
56
+
57
+ const result = await useCase.execute('test-code');
58
+
59
+ <%_ if (architecture === 'Clean Architecture') { _%>
60
+ expect(mockRepo.findByEmail).toHaveBeenCalled();
61
+ <%_ } else { _%>
62
+ expect(User.findOne).toHaveBeenCalled();
63
+ <%_ } _%>
64
+ expect(result.accessToken).toBe('access-token');
65
+ });
66
+
67
+ it('should create new user if not exists', async () => {
68
+ const mockProfile = { id: 'google-456', email: 'new@test.com', name: 'New User' };
69
+ const mockUser = { id: '2', email: 'new@test.com' };
70
+
71
+ mockProvider.getProfile.mockResolvedValue(mockProfile);
72
+ <%_ if (architecture === 'Clean Architecture') { _%>
73
+ mockRepo.findByEmail.mockResolvedValue(null);
74
+ mockRepo.save.mockResolvedValue(mockUser);
75
+ <%_ } else { _%>
76
+ User.findOne.mockResolvedValue(null);
77
+ User.create.mockResolvedValue(mockUser);
78
+ <%_ } _%>
79
+ JwtService.generateToken.mockReturnValue('access-token');
80
+ JwtService.generateRefreshToken.mockReturnValue('refresh-token');
81
+
82
+ await useCase.execute('test-code');
83
+
84
+ <%_ if (architecture === 'Clean Architecture') { _%>
85
+ expect(mockRepo.save).toHaveBeenCalled();
86
+ <%_ } else { _%>
87
+ expect(User.create).toHaveBeenCalled();
88
+ <%_ } _%>
89
+ });
90
+
91
+ it('should link GitHub ID if existing user does not have it', async () => {
92
+ mockProvider.name = 'GitHub';
93
+ const mockProfile = { id: 'github-789', email: 'test@test.com', name: 'Test User' };
94
+ const mockUser = { id: '1', email: 'test@test.com', githubId: null };
95
+
96
+ mockProvider.getProfile.mockResolvedValue(mockProfile);
97
+ <%_ if (architecture === 'Clean Architecture') { _%>
98
+ mockRepo.findByEmail.mockResolvedValue(mockUser);
99
+ <%_ } else { _%>
100
+ User.findOne.mockResolvedValue(mockUser);
101
+ <%_ } _%>
102
+ JwtService.generateToken.mockReturnValue('access-token');
103
+ JwtService.generateRefreshToken.mockReturnValue('refresh-token');
104
+
105
+ await useCase.execute('test-code');
106
+
107
+ <%_ if (architecture === 'Clean Architecture') { _%>
108
+ expect(mockRepo.update).toHaveBeenCalledWith('1', expect.objectContaining({ githubId: 'github-789' }));
109
+ <%_ } else { _%>
110
+ <%_ if (database === 'MongoDB') { _%>
111
+ expect(mockUser.save || jest.fn()).toHaveBeenCalled();
112
+ <%_ } else { _%>
113
+ expect(User.update || jest.fn()).toHaveBeenCalled();
114
+ <%_ } _%>
115
+ <%_ } _%>
116
+ });
117
+
118
+ it('should link Google ID if existing user does not have it', async () => {
119
+ mockProvider.name = 'Google';
120
+ const mockProfile = { id: 'google-789', email: 'test@test.com', name: 'Test User' };
121
+ const mockUser = { id: '1', email: 'test@test.com', googleId: null };
122
+
123
+ mockProvider.getProfile.mockResolvedValue(mockProfile);
124
+ <%_ if (architecture === 'Clean Architecture') { _%>
125
+ mockRepo.findByEmail.mockResolvedValue(mockUser);
126
+ <%_ } else { _%>
127
+ User.findOne.mockResolvedValue(mockUser);
128
+ <%_ } _%>
129
+ JwtService.generateToken.mockReturnValue('access-token');
130
+ JwtService.generateRefreshToken.mockReturnValue('refresh-token');
131
+
132
+ await useCase.execute('test-code');
133
+
134
+ <%_ if (architecture === 'Clean Architecture') { _%>
135
+ expect(mockRepo.update).toHaveBeenCalledWith('1', expect.objectContaining({ googleId: 'google-789' }));
136
+ <%_ } _%>
137
+ });
138
+
139
+ it('should throw error if profile has no email', async () => {
140
+ mockProvider.getProfile.mockResolvedValue({ id: '123' }); // No email
141
+ await expect(useCase.execute('test-code')).rejects.toThrow('No email associated with this social account');
142
+ });
143
+ });