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.
- package/CHANGELOG.md +20 -0
- package/README.md +12 -17
- package/bin/index.js +1 -0
- package/lib/generator.js +1 -1
- package/lib/modules/app-setup.js +16 -0
- package/lib/modules/auth-setup.js +46 -4
- package/lib/prompts.js +49 -5
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +5 -1
- package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
- package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
- package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
- package/templates/common/.env.example.ejs +10 -0
- package/templates/common/README.md.ejs +65 -14
- package/templates/common/auth/js/controllers/authController.js.ejs +356 -13
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +329 -53
- package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
- package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
- package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
- package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
- package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +192 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +366 -64
- package/templates/common/auth/ts/controllers/authController.ts.ejs +370 -9
- package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
- package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
- package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
- package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
- package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
- package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
- package/templates/common/database/js/models/User.js.ejs +13 -5
- package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
- package/templates/common/database/ts/models/User.ts.ejs +23 -7
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
- package/templates/common/docker-compose.yml.ejs +21 -0
- package/templates/common/ecosystem.config.js.ejs +10 -0
- package/templates/common/eslint.config.mjs.ejs +4 -1
- package/templates/common/jest.config.js.ejs +1 -1
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
- package/templates/common/package.json.ejs +4 -0
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
- package/templates/common/swagger.yml.ejs +62 -3
- package/templates/common/views/ejs/login.ejs.ejs +84 -0
- package/templates/common/views/ejs/signup.ejs.ejs +84 -0
- package/templates/common/views/pug/login.pug.ejs +78 -0
- package/templates/common/views/pug/signup.pug.ejs +78 -0
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/mvc/js/src/config/env.js.ejs +12 -2
- package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
- 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/config/env.ts.ejs +12 -2
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/mvc/ts/src/index.ts.ejs +4 -1
- package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
- package/templates/clean-architecture/ts/src/domain/user.ts +0 -9
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
2
|
-
const AuthController = require('@/interfaces/controllers/auth/authController');
|
|
3
|
-
const JwtService = require('@/infrastructure/auth/jwtService');
|
|
4
|
-
<%_ } else { _%>
|
|
5
|
-
const AuthController = require('@/controllers/authController');
|
|
6
|
-
const JwtService = require('@/services/jwtService');
|
|
7
|
-
<%_ } _%>
|
|
8
|
-
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
9
|
-
const bcrypt = require('bcryptjs');
|
|
10
|
-
|
|
11
|
-
jest.mock('bcryptjs');
|
|
12
|
-
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
13
2
|
jest.mock('@/infrastructure/auth/jwtService');
|
|
3
|
+
<%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
|
|
4
|
+
jest.mock('@/infrastructure/auth/socialAuthService');
|
|
5
|
+
jest.mock('@/usecases/auth/socialLoginUseCase');
|
|
6
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
7
|
+
<%_ } _%>
|
|
14
8
|
jest.mock('@/infrastructure/database/models/User', () => ({
|
|
15
|
-
findOne: jest.fn()
|
|
9
|
+
findOne: jest.fn(),
|
|
10
|
+
create: jest.fn()
|
|
16
11
|
}));
|
|
17
|
-
const User = require('@/infrastructure/database/models/User');
|
|
18
12
|
<%_ } else { _%>
|
|
19
13
|
jest.mock('@/services/jwtService');
|
|
14
|
+
<%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
|
|
15
|
+
jest.mock('@/services/socialAuthService');
|
|
16
|
+
<%_ } _%>
|
|
20
17
|
jest.mock('@/models/User', () => ({
|
|
21
|
-
findOne: jest.fn()
|
|
18
|
+
findOne: jest.fn(),
|
|
19
|
+
create: jest.fn()
|
|
22
20
|
}));
|
|
21
|
+
<%_ } _%>
|
|
22
|
+
jest.mock('bcryptjs');
|
|
23
|
+
|
|
24
|
+
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
25
|
+
const AuthController = require('@/interfaces/controllers/auth/authController');
|
|
26
|
+
const JwtService = require('@/infrastructure/auth/jwtService');
|
|
27
|
+
<%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
|
|
28
|
+
const { SocialLoginUseCase } = require('@/usecases/auth/socialLoginUseCase');
|
|
29
|
+
<%_ } _%>
|
|
30
|
+
const User = require('@/infrastructure/database/models/User');
|
|
31
|
+
<%_ } else { _%>
|
|
32
|
+
const AuthController = require('@/controllers/authController');
|
|
33
|
+
const JwtService = require('@/services/jwtService');
|
|
34
|
+
<%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
|
|
35
|
+
const { SocialAuthService } = require('@/services/socialAuthService');
|
|
36
|
+
<%_ } _%>
|
|
23
37
|
const User = require('@/models/User');
|
|
24
38
|
<%_ } _%>
|
|
39
|
+
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
40
|
+
const bcrypt = require('bcryptjs');
|
|
41
|
+
|
|
25
42
|
<%_ if (caching !== 'None') { _%>
|
|
26
43
|
<%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
|
|
27
44
|
jest.mock('<%= cachePath %>', () => ({
|
|
@@ -42,11 +59,16 @@ describe('AuthController', () => {
|
|
|
42
59
|
controller = new AuthController();
|
|
43
60
|
mockReq = {
|
|
44
61
|
body: {},
|
|
45
|
-
headers: {}
|
|
62
|
+
headers: {},
|
|
63
|
+
query: {},
|
|
64
|
+
cookies: {}
|
|
46
65
|
};
|
|
47
66
|
mockRes = {
|
|
48
67
|
status: jest.fn().mockReturnThis(),
|
|
49
|
-
json: jest.fn()
|
|
68
|
+
json: jest.fn(),
|
|
69
|
+
cookie: jest.fn(),
|
|
70
|
+
clearCookie: jest.fn(),
|
|
71
|
+
redirect: jest.fn()
|
|
50
72
|
};
|
|
51
73
|
next = jest.fn();
|
|
52
74
|
jest.clearAllMocks();
|
|
@@ -62,7 +84,6 @@ describe('AuthController', () => {
|
|
|
62
84
|
JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
|
|
63
85
|
JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
|
|
64
86
|
|
|
65
|
-
// Mock cacheService
|
|
66
87
|
<% if (caching !== 'None') { %>
|
|
67
88
|
cacheService.get.mockResolvedValue([]);
|
|
68
89
|
cacheService.set.mockResolvedValue();<% } %>
|
|
@@ -73,7 +94,7 @@ describe('AuthController', () => {
|
|
|
73
94
|
expect(mockRes.json).toHaveBeenCalledWith({ token: 'mock-access-token', accessToken: 'mock-access-token', refreshToken: 'mock-refresh-token' });
|
|
74
95
|
});
|
|
75
96
|
|
|
76
|
-
it('should return 401
|
|
97
|
+
it('should return 401 if user not found', async () => {
|
|
77
98
|
mockReq.body = { email: 'wrong@test.com', password: 'password123' };
|
|
78
99
|
User.findOne.mockResolvedValue(null);
|
|
79
100
|
|
|
@@ -82,7 +103,7 @@ describe('AuthController', () => {
|
|
|
82
103
|
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
83
104
|
});
|
|
84
105
|
|
|
85
|
-
it('should return 401
|
|
106
|
+
it('should return 401 if password invalid', async () => {
|
|
86
107
|
const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
|
|
87
108
|
mockReq.body = { email: 'test@test.com', password: 'wrongpassword' };
|
|
88
109
|
User.findOne.mockResolvedValue(user);
|
|
@@ -92,57 +113,312 @@ describe('AuthController', () => {
|
|
|
92
113
|
|
|
93
114
|
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
94
115
|
});
|
|
116
|
+
|
|
117
|
+
it('should call next on error', async () => {
|
|
118
|
+
const error = new Error('Database error');
|
|
119
|
+
User.findOne.mockRejectedValue(error);
|
|
120
|
+
mockReq.body = { email: 'test@test.com', password: 'password123' };
|
|
121
|
+
|
|
122
|
+
await controller.login(mockReq, mockRes, next);
|
|
123
|
+
|
|
124
|
+
expect(next).toHaveBeenCalledWith(error);
|
|
125
|
+
});
|
|
95
126
|
});
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
JwtService.verifyRefreshToken.mockReturnValue(decoded);
|
|
102
|
-
JwtService.generateToken.mockReturnValue('new-access');
|
|
103
|
-
JwtService.generateRefreshToken.mockReturnValue('new-refresh');
|
|
104
|
-
JwtService.decodeToken.mockReturnValue({ jti: 'new-jti' });
|
|
128
|
+
describe('refresh', () => {
|
|
129
|
+
it('should return 401 if refresh token is invalid', async () => {
|
|
130
|
+
mockReq.body = { refreshToken: 'invalid-token' };
|
|
131
|
+
JwtService.verifyRefreshToken.mockReturnValue(null);
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
await controller.refresh(mockReq, mockRes, next);
|
|
134
|
+
|
|
135
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return new tokens if refresh token is valid', async () => {
|
|
139
|
+
mockReq.body = { refreshToken: 'valid-token' };
|
|
140
|
+
const decoded = { id: '1', email: 'test@test.com', jti: 'test-jti' };
|
|
141
|
+
JwtService.verifyRefreshToken.mockReturnValue(decoded);
|
|
142
|
+
JwtService.generateRefreshToken.mockReturnValue('new-refresh-token');
|
|
143
|
+
JwtService.generateToken.mockReturnValue('new-access-token');
|
|
144
|
+
JwtService.decodeToken.mockReturnValue({ jti: 'new-jti' });
|
|
145
|
+
|
|
146
|
+
<% if (caching !== 'None') { %>
|
|
147
|
+
cacheService.get.mockResolvedValue(['test-jti']);
|
|
148
|
+
<% } else { %>
|
|
149
|
+
JwtService.activeRefreshTokens.set('1', ['test-jti']);
|
|
150
|
+
<% } %>
|
|
151
|
+
|
|
152
|
+
await controller.refresh(mockReq, mockRes, next);
|
|
153
|
+
|
|
154
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'new-access-token' }));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return 401 if token theft detected', async () => {
|
|
158
|
+
mockReq.body = { refreshToken: 'stolen-token' };
|
|
159
|
+
const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
|
|
160
|
+
JwtService.verifyRefreshToken.mockReturnValue(decoded);
|
|
161
|
+
<% if (caching !== 'None') { %>
|
|
162
|
+
cacheService.get.mockResolvedValue(['other-jti']);
|
|
163
|
+
cacheService.del.mockResolvedValue();
|
|
164
|
+
<% } %>
|
|
165
|
+
|
|
166
|
+
await controller.refresh(mockReq, mockRes, next);
|
|
167
|
+
|
|
168
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should call next on refresh error', async () => {
|
|
172
|
+
const error = new Error('Redis error');
|
|
173
|
+
mockReq.body = { refreshToken: 'valid-token' };
|
|
174
|
+
JwtService.verifyRefreshToken.mockImplementation(() => { throw error; });
|
|
175
|
+
|
|
176
|
+
await controller.refresh(mockReq, mockRes, next);
|
|
177
|
+
|
|
178
|
+
expect(next).toHaveBeenCalledWith(error);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('logout', () => {
|
|
183
|
+
it('should return 400 if no token provided', async () => {
|
|
184
|
+
mockReq.headers = {};
|
|
185
|
+
await controller.logout(mockReq, mockRes, next);
|
|
186
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should logout successfully', async () => {
|
|
190
|
+
mockReq.headers = { authorization: 'Bearer valid-token' };
|
|
191
|
+
mockReq.body = { refreshToken: 'valid-refresh-token' };
|
|
192
|
+
JwtService.decodeToken.mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 })
|
|
193
|
+
.mockReturnValueOnce({ id: '1', jti: 'refresh-jti' });
|
|
194
|
+
|
|
195
|
+
<% if (caching !== 'None') { %>
|
|
196
|
+
cacheService.get.mockResolvedValue(['refresh-jti']);
|
|
197
|
+
<% } else { %>
|
|
198
|
+
JwtService.activeRefreshTokens.set('1', ['refresh-jti']);
|
|
199
|
+
<% } %>
|
|
200
|
+
|
|
201
|
+
await controller.logout(mockReq, mockRes, next);
|
|
202
|
+
|
|
203
|
+
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle logout even if no refresh token provided', async () => {
|
|
207
|
+
mockReq.headers = { authorization: 'Bearer valid-token' };
|
|
208
|
+
mockReq.body = {};
|
|
209
|
+
JwtService.decodeToken.mockReturnValue({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 });
|
|
210
|
+
|
|
211
|
+
await controller.logout(mockReq, mockRes, next);
|
|
212
|
+
|
|
213
|
+
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should call next on logout error', async () => {
|
|
217
|
+
const error = new Error('Logout error');
|
|
218
|
+
mockReq.headers = { authorization: 'Bearer valid-token' };
|
|
219
|
+
JwtService.decodeToken.mockImplementation(() => { throw error; });
|
|
220
|
+
|
|
221
|
+
await controller.logout(mockReq, mockRes, next);
|
|
222
|
+
|
|
223
|
+
expect(next).toHaveBeenCalledWith(error);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
<% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
|
|
228
|
+
describe('socialExchange', () => {
|
|
229
|
+
it('should return 400 if code or provider missing', async () => {
|
|
230
|
+
mockReq.body = {};
|
|
231
|
+
await controller.socialExchange(mockReq, mockRes, next);
|
|
232
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should exchange code for JWT tokens', async () => {
|
|
236
|
+
mockReq.body = { code: 'test-code', provider: 'Google' };
|
|
237
|
+
const user = { id: 1, email: 'social@test.com' };
|
|
238
|
+
|
|
239
|
+
<% if (architecture === 'Clean Architecture') { %>
|
|
240
|
+
const mockUseCase = {
|
|
241
|
+
execute: jest.fn().mockResolvedValue({
|
|
242
|
+
user,
|
|
243
|
+
accessToken: 'mock-token',
|
|
244
|
+
refreshToken: 'mock-refresh-token'
|
|
245
|
+
})
|
|
246
|
+
};
|
|
247
|
+
SocialLoginUseCase.mockImplementation(() => mockUseCase);
|
|
109
248
|
<% } else { %>
|
|
110
|
-
|
|
249
|
+
SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'social@test.com', id: 'google-id', name: 'Google User' });
|
|
250
|
+
User.findOne.mockResolvedValue(user);
|
|
251
|
+
JwtService.generateToken.mockReturnValue('mock-token');
|
|
252
|
+
JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
|
|
253
|
+
<% } %>
|
|
254
|
+
JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
|
|
111
255
|
|
|
112
|
-
await controller.
|
|
256
|
+
await controller.socialExchange(mockReq, mockRes, next);
|
|
113
257
|
|
|
114
|
-
expect(
|
|
115
|
-
expect(mockRes.json).toHaveBeenCalledWith({ accessToken: 'new-access', refreshToken: 'new-refresh' });
|
|
258
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'mock-token' }));
|
|
116
259
|
});
|
|
117
260
|
|
|
118
|
-
it('should
|
|
119
|
-
mockReq.body = {
|
|
120
|
-
|
|
121
|
-
|
|
261
|
+
it('should return 400 for invalid provider', async () => {
|
|
262
|
+
mockReq.body = { code: 'test-code', provider: 'Invalid' };
|
|
263
|
+
await controller.socialExchange(mockReq, mockRes, next);
|
|
264
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
122
267
|
|
|
123
|
-
|
|
124
|
-
|
|
268
|
+
describe('social redirect methods', () => {
|
|
269
|
+
<% if (socialAuth.includes('Google')) { %>
|
|
270
|
+
it('googleLogin should redirect to Google', async () => {
|
|
271
|
+
await controller.googleLogin(mockReq, mockRes);
|
|
272
|
+
expect(mockRes.redirect).toHaveBeenCalledWith(expect.stringContaining('accounts.google.com'));
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('googleCallback should handle Google callback', async () => {
|
|
276
|
+
mockReq.query = { code: 'test-code', state: 'test-state' };
|
|
277
|
+
mockReq.cookies = { oauth_state: 'test-state' };
|
|
278
|
+
const user = { id: 1, email: 'google@test.com' };
|
|
279
|
+
|
|
280
|
+
<% if (architecture === 'Clean Architecture') { %>
|
|
281
|
+
const mockUseCase = {
|
|
282
|
+
execute: jest.fn().mockResolvedValue({
|
|
283
|
+
user,
|
|
284
|
+
accessToken: 'mock-token',
|
|
285
|
+
refreshToken: 'mock-refresh-token'
|
|
286
|
+
})
|
|
287
|
+
};
|
|
288
|
+
SocialLoginUseCase.mockImplementation(() => mockUseCase);
|
|
125
289
|
<% } else { %>
|
|
126
|
-
|
|
290
|
+
SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'google@test.com' });
|
|
291
|
+
User.findOne.mockResolvedValue(user);
|
|
292
|
+
JwtService.generateToken.mockReturnValue('mock-token');
|
|
293
|
+
JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
|
|
294
|
+
<% } %>
|
|
295
|
+
JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
|
|
127
296
|
|
|
128
|
-
await controller.
|
|
297
|
+
await controller.googleCallback(mockReq, mockRes, next);
|
|
129
298
|
|
|
130
|
-
expect(mockRes.
|
|
299
|
+
expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
|
|
300
|
+
expect(mockRes.redirect).toHaveBeenCalledWith('/');
|
|
131
301
|
});
|
|
132
|
-
});
|
|
133
302
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
mockReq.
|
|
137
|
-
mockReq
|
|
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
|
+
});
|
|
344
|
+
<% } %>
|
|
345
|
+
|
|
346
|
+
<% if (socialAuth.includes('GitHub')) { %>
|
|
347
|
+
it('githubLogin should redirect to GitHub', async () => {
|
|
348
|
+
await controller.githubLogin(mockReq, mockRes);
|
|
349
|
+
expect(mockRes.redirect).toHaveBeenCalledWith(expect.stringContaining('github.com'));
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('githubCallback should handle GitHub callback', async () => {
|
|
353
|
+
mockReq.query = { code: 'test-code', state: 'test-state' };
|
|
354
|
+
mockReq.cookies = { oauth_state: 'test-state' };
|
|
355
|
+
const user = { id: 1, email: 'github@test.com' };
|
|
138
356
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
.
|
|
357
|
+
<% if (architecture === 'Clean Architecture') { %>
|
|
358
|
+
const mockUseCase = {
|
|
359
|
+
execute: jest.fn().mockResolvedValue({
|
|
360
|
+
user,
|
|
361
|
+
accessToken: 'mock-token',
|
|
362
|
+
refreshToken: 'mock-refresh-token'
|
|
363
|
+
})
|
|
364
|
+
};
|
|
365
|
+
SocialLoginUseCase.mockImplementation(() => mockUseCase);
|
|
366
|
+
<% } else { %>
|
|
367
|
+
SocialAuthService.getGithubProfile.mockResolvedValue({ email: 'github@test.com' });
|
|
368
|
+
User.findOne.mockResolvedValue(user);
|
|
369
|
+
JwtService.generateToken.mockReturnValue('mock-token');
|
|
370
|
+
JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
|
|
371
|
+
<% } %>
|
|
372
|
+
JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
|
|
373
|
+
|
|
374
|
+
await controller.githubCallback(mockReq, mockRes, next);
|
|
375
|
+
|
|
376
|
+
expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
|
|
377
|
+
expect(mockRes.redirect).toHaveBeenCalledWith('/');
|
|
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' });
|
|
142
397
|
|
|
143
|
-
await controller.
|
|
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
|
+
<% } %>
|
|
144
417
|
|
|
145
|
-
|
|
418
|
+
await controller.githubCallback(mockReq, mockRes, next);
|
|
419
|
+
expect(mockRes.redirect).toHaveBeenCalledWith('/login?error=social_auth_failed');
|
|
146
420
|
});
|
|
421
|
+
<% } %>
|
|
147
422
|
});
|
|
423
|
+
<% } -%>
|
|
148
424
|
});
|
|
@@ -27,28 +27,32 @@ const authMiddleware = async (req, res, next) => {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
if (decoded.jti) {
|
|
30
|
-
<%_ if (caching !== 'None') {
|
|
30
|
+
<%_ if (caching !== 'None') { _%>
|
|
31
31
|
const isBlacklisted = await cacheService.get(`blacklist:${decoded.jti}`);
|
|
32
32
|
if (isBlacklisted) {
|
|
33
33
|
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
|
|
34
|
-
}
|
|
34
|
+
}
|
|
35
|
+
<%_ } else { _%>
|
|
35
36
|
const expiryDate = JwtService.blacklistedTokens.get(decoded.jti);
|
|
36
37
|
if (expiryDate && Date.now() < expiryDate) {
|
|
37
38
|
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
|
|
38
|
-
}
|
|
39
|
+
}
|
|
40
|
+
<%_ } _%>
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
if (decoded.sid) {
|
|
42
|
-
<%_ if (caching !== 'None') { %>
|
|
44
|
+
<%_ if (caching !== 'None') { _%>
|
|
43
45
|
const cacheKey = `refresh_tokens:${decoded.id}`;
|
|
44
46
|
const activeTokens = await cacheService.get(cacheKey) || [];
|
|
45
47
|
if (!activeTokens.includes(decoded.sid)) {
|
|
46
48
|
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
|
|
47
|
-
}
|
|
49
|
+
}
|
|
50
|
+
<%_ } else { _%>
|
|
48
51
|
const activeTokens = JwtService.activeRefreshTokens.get(String(decoded.id)) || [];
|
|
49
52
|
if (!activeTokens.includes(decoded.sid)) {
|
|
50
53
|
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
|
|
51
|
-
}
|
|
54
|
+
}
|
|
55
|
+
<%_ } _%>
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
req.user = decoded;
|
|
@@ -12,5 +12,16 @@ const authController = new AuthController();
|
|
|
12
12
|
router.post('/login', (req, res, next) => authController.login(req, res, next));
|
|
13
13
|
router.post('/refresh', (req, res, next) => authController.refresh(req, res, next));
|
|
14
14
|
router.post('/logout', authMiddleware, (req, res, next) => authController.logout(req, res, next));
|
|
15
|
+
<%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
|
|
16
|
+
router.post('/social/exchange', (req, res, next) => authController.socialExchange(req, res, next));
|
|
17
|
+
<%_ if (socialAuth.includes('Google')) { _%>
|
|
18
|
+
router.get('/google', (req, res) => authController.googleLogin(req, res));
|
|
19
|
+
router.get('/google/callback', (req, res, next) => authController.googleCallback(req, res, next));
|
|
20
|
+
<%_ } _%>
|
|
21
|
+
<%_ if (socialAuth.includes('GitHub')) { _%>
|
|
22
|
+
router.get('/github', (req, res) => authController.githubLogin(req, res));
|
|
23
|
+
router.get('/github/callback', (req, res, next) => authController.githubCallback(req, res, next));
|
|
24
|
+
<%_ } _%>
|
|
25
|
+
<%_ } _%>
|
|
15
26
|
|
|
16
27
|
module.exports = router;
|
|
@@ -58,6 +58,12 @@ describe('JwtService', () => {
|
|
|
58
58
|
expect(jwt.verify).toHaveBeenCalledWith(token, secret);
|
|
59
59
|
expect(result).toEqual(payload);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
it('should return null for an invalid token', () => {
|
|
63
|
+
jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });
|
|
64
|
+
const result = JwtService.verifyToken(token);
|
|
65
|
+
expect(result).toBeNull();
|
|
66
|
+
});
|
|
61
67
|
});
|
|
62
68
|
|
|
63
69
|
describe('verifyRefreshToken', () => {
|
|
@@ -69,6 +75,12 @@ describe('JwtService', () => {
|
|
|
69
75
|
expect(jwt.verify).toHaveBeenCalledWith(token, 'test-refresh-secret');
|
|
70
76
|
expect(result).toEqual(payload);
|
|
71
77
|
});
|
|
78
|
+
|
|
79
|
+
it('should return null for an invalid refresh token', () => {
|
|
80
|
+
jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });
|
|
81
|
+
const result = JwtService.verifyRefreshToken(token);
|
|
82
|
+
expect(result).toBeNull();
|
|
83
|
+
});
|
|
72
84
|
});
|
|
73
85
|
|
|
74
86
|
describe('decodeToken', () => {
|
|
@@ -80,5 +92,23 @@ describe('JwtService', () => {
|
|
|
80
92
|
expect(jwt.decode).toHaveBeenCalledWith(token);
|
|
81
93
|
expect(result).toEqual(payload);
|
|
82
94
|
});
|
|
95
|
+
|
|
96
|
+
it('should return null if decode fails', () => {
|
|
97
|
+
jwt.decode.mockImplementation(() => { throw new Error('Decode failed'); });
|
|
98
|
+
const result = JwtService.decodeToken(token);
|
|
99
|
+
expect(result).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('fallback values', () => {
|
|
104
|
+
it('should use default values if env not set', () => {
|
|
105
|
+
delete process.env.JWT_SECRET;
|
|
106
|
+
delete process.env.JWT_REFRESH_SECRET;
|
|
107
|
+
delete process.env.JWT_EXPIRES_IN;
|
|
108
|
+
delete process.env.JWT_REFRESH_EXPIRES_IN;
|
|
109
|
+
|
|
110
|
+
expect(JwtService.SECRET).toBeDefined();
|
|
111
|
+
expect(JwtService.REFRESH_SECRET).toBeDefined();
|
|
112
|
+
});
|
|
83
113
|
});
|
|
84
114
|
});
|