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