nodejs-quickstart-structure 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +43 -39
- package/bin/index.js +5 -2
- package/lib/generator.js +10 -4
- package/lib/modules/app-setup.js +76 -6
- package/lib/modules/auth-setup.js +143 -0
- package/lib/modules/caching-setup.js +8 -1
- package/lib/modules/database-setup.js +2 -1
- package/lib/modules/project-setup.js +1 -0
- package/lib/prompts.js +39 -0
- package/package.json +5 -4
- package/templates/clean-architecture/js/src/domain/models/User.js +3 -1
- package/templates/clean-architecture/js/src/index.js.ejs +2 -0
- package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -3
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +25 -2
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +27 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +3 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.spec.js.ejs +49 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/swagger.spec.js.ejs +14 -0
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +41 -4
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +69 -4
- package/templates/clean-architecture/js/src/interfaces/graphql/context.js.ejs +13 -6
- package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +38 -21
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +10 -5
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +32 -10
- package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +1 -1
- package/templates/clean-architecture/js/src/interfaces/routes/api.js.ejs +15 -0
- package/templates/clean-architecture/js/src/interfaces/routes/api.spec.js.ejs +4 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +34 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +3 -2
- package/templates/clean-architecture/js/src/usecases/DeleteUser.js.ejs +27 -0
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.js.ejs +36 -0
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +14 -0
- package/templates/clean-architecture/js/src/usecases/GetUserById.js.ejs +36 -0
- package/templates/clean-architecture/js/src/usecases/GetUserById.spec.js.ejs +48 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.js.ejs +28 -0
- package/templates/clean-architecture/js/src/utils/errorMessages.js +1 -0
- package/templates/clean-architecture/js/src/utils/httpCodes.js +2 -0
- package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -3
- package/templates/clean-architecture/ts/src/domain/user.ts +3 -1
- package/templates/clean-architecture/ts/src/index.ts.ejs +4 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +55 -9
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +32 -3
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +26 -6
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +57 -15
- package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +38 -23
- package/templates/clean-architecture/ts/src/interfaces/graphql/context.ts.ejs +14 -8
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +33 -10
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +15 -5
- package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +1 -1
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +9 -1
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts.ejs +16 -0
- package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +3 -2
- package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +35 -0
- package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +1 -0
- package/templates/clean-architecture/ts/src/usecases/deleteUser.ts.ejs +24 -0
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts.ejs +21 -0
- package/templates/clean-architecture/ts/src/usecases/getUserById.spec.ts.ejs +47 -0
- package/templates/clean-architecture/ts/src/usecases/getUserById.ts.ejs +23 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +1 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.ts.ejs +25 -0
- package/templates/clean-architecture/ts/src/utils/errorMessages.ts +1 -0
- package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
- package/templates/common/.cursorrules.ejs +9 -0
- package/templates/common/.env.example.ejs +17 -10
- package/templates/common/README.md.ejs +63 -18
- package/templates/common/auth/js/controllers/authController.js.ejs +168 -0
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +148 -0
- package/templates/common/auth/js/middleware/authMiddleware.js.ejs +58 -0
- package/templates/common/auth/js/middleware/authMiddleware.spec.js.ejs +108 -0
- package/templates/common/auth/js/routes/authRoutes.js.ejs +16 -0
- package/templates/common/auth/js/services/jwtService.js.ejs +54 -0
- package/templates/common/auth/js/services/jwtService.spec.js.ejs +84 -0
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +161 -0
- package/templates/common/auth/ts/controllers/authController.ts.ejs +165 -0
- package/templates/common/auth/ts/middleware/authMiddleware.spec.ts.ejs +128 -0
- package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +59 -0
- package/templates/common/auth/ts/routes/authRoutes.ts.ejs +20 -0
- package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +89 -0
- package/templates/common/auth/ts/services/jwtService.ts.ejs +60 -0
- package/templates/common/caching/clean/js/CreateUser.js.ejs +14 -5
- package/templates/common/caching/clean/js/DeleteUser.js.ejs +2 -1
- package/templates/common/caching/clean/js/GetUserById.js.ejs +39 -0
- package/templates/common/caching/clean/js/UpdateUser.js.ejs +2 -1
- package/templates/common/caching/clean/ts/createUser.ts.ejs +14 -6
- package/templates/common/caching/clean/ts/deleteUser.ts.ejs +2 -1
- package/templates/common/caching/clean/ts/getUserById.ts.ejs +32 -0
- package/templates/common/caching/clean/ts/updateUser.ts.ejs +2 -1
- package/templates/common/database/js/models/User.js.ejs +14 -1
- package/templates/common/database/js/models/User.js.mongoose.ejs +7 -0
- package/templates/common/database/js/models/User.spec.js.ejs +12 -0
- package/templates/common/database/ts/models/User.spec.ts.ejs +10 -0
- package/templates/common/database/ts/models/User.ts.ejs +17 -0
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +8 -0
- package/templates/common/docker-compose.yml.ejs +12 -0
- package/templates/common/ecosystem.config.js.ejs +9 -3
- package/templates/common/eslint.config.mjs.ejs +3 -0
- package/templates/common/jest.config.js.ejs +11 -9
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +1 -1
- package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
- package/templates/common/migrations/init.js.ejs +5 -4
- package/templates/common/package.json.ejs +8 -1
- package/templates/common/prompts/project-context.md.ejs +8 -1
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +149 -107
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +88 -47
- package/templates/common/swagger.yml.ejs +148 -0
- package/templates/common/tsconfig.eslint.json +15 -0
- package/templates/common/tsconfig.json +2 -1
- package/templates/common/views/ejs/index.ejs +264 -30
- package/templates/common/views/ejs/login.ejs.ejs +244 -0
- package/templates/common/views/ejs/signup.ejs.ejs +282 -0
- package/templates/common/views/pug/index.pug +269 -38
- package/templates/common/views/pug/login.pug.ejs +195 -0
- package/templates/common/views/pug/signup.pug.ejs +241 -0
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +6 -0
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +6 -0
- package/templates/mvc/js/src/config/env.js.ejs +12 -3
- package/templates/mvc/js/src/controllers/userController.js.ejs +29 -5
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +27 -12
- package/templates/mvc/js/src/graphql/context.js.ejs +14 -3
- package/templates/mvc/js/src/graphql/context.spec.js.ejs +36 -21
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +10 -5
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +32 -10
- package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +1 -1
- package/templates/mvc/js/src/index.js.ejs +16 -3
- package/templates/mvc/js/src/routes/api.js.ejs +14 -0
- package/templates/mvc/js/src/routes/api.spec.js.ejs +3 -0
- package/templates/mvc/js/src/utils/errorMessages.js +1 -0
- package/templates/mvc/js/src/utils/httpCodes.js +1 -0
- package/templates/mvc/ts/src/config/env.ts.ejs +12 -3
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +95 -7
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +68 -11
- package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +36 -23
- package/templates/mvc/ts/src/graphql/context.ts.ejs +15 -6
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +32 -10
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +15 -5
- package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +1 -1
- package/templates/mvc/ts/src/index.ts.ejs +15 -3
- package/templates/mvc/ts/src/routes/api.spec.ts.ejs +6 -0
- package/templates/mvc/ts/src/routes/api.ts.ejs +15 -0
- package/templates/mvc/ts/src/utils/errorMessages.ts +1 -0
- package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
- package/templates/clean-architecture/js/src/interfaces/routes/api.js +0 -12
- package/templates/clean-architecture/js/src/usecases/CreateUser.js +0 -14
- package/templates/clean-architecture/js/src/usecases/DeleteUser.js +0 -11
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.js +0 -12
- package/templates/clean-architecture/js/src/usecases/UpdateUser.js +0 -11
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +0 -13
- package/templates/clean-architecture/ts/src/usecases/createUser.ts +0 -13
- package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +0 -9
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts +0 -10
- package/templates/clean-architecture/ts/src/usecases/updateUser.ts +0 -9
- package/templates/mvc/js/src/routes/api.js +0 -10
- package/templates/mvc/ts/src/routes/api.ts +0 -12
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
2
|
+
const authMiddleware = require('@/infrastructure/webserver/middleware/authMiddleware');
|
|
3
|
+
const JwtService = require('@/infrastructure/auth/jwtService');
|
|
4
|
+
<%_ } else { _%>
|
|
5
|
+
const authMiddleware = require('@/middleware/authMiddleware');
|
|
6
|
+
const JwtService = require('@/services/jwtService');
|
|
7
|
+
<%_ } _%>
|
|
8
|
+
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
9
|
+
|
|
10
|
+
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
11
|
+
jest.mock('@/infrastructure/auth/jwtService');
|
|
12
|
+
<%_ } else { _%>
|
|
13
|
+
jest.mock('@/services/jwtService');
|
|
14
|
+
<%_ } _%>
|
|
15
|
+
<%_ if (caching !== 'None') { _%>
|
|
16
|
+
<%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
|
|
17
|
+
jest.mock('<%= cachePath %>', () => ({
|
|
18
|
+
get: jest.fn(),
|
|
19
|
+
set: jest.fn(),
|
|
20
|
+
del: jest.fn()
|
|
21
|
+
}), { virtual: true });
|
|
22
|
+
const cacheService = require('<%= cachePath %>');
|
|
23
|
+
<%_ } _%>
|
|
24
|
+
|
|
25
|
+
describe('AuthMiddleware', () => {
|
|
26
|
+
let mockReq;
|
|
27
|
+
let mockRes;
|
|
28
|
+
let next;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
mockReq = {
|
|
32
|
+
headers: {}
|
|
33
|
+
};
|
|
34
|
+
mockRes = {
|
|
35
|
+
status: jest.fn().mockReturnThis(),
|
|
36
|
+
json: jest.fn()
|
|
37
|
+
};
|
|
38
|
+
next = jest.fn();
|
|
39
|
+
jest.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return 401 if no authorization header', async () => {
|
|
43
|
+
await authMiddleware(mockReq, mockRes, next);
|
|
44
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
45
|
+
expect(next).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return 401 if token is blacklisted', async () => {
|
|
49
|
+
const user = { id: 1, email: 'test@example.com', jti: 'blacklisted-jti' };
|
|
50
|
+
mockReq.headers.authorization = 'Bearer valid-token';
|
|
51
|
+
JwtService.verifyToken.mockReturnValue(user);
|
|
52
|
+
|
|
53
|
+
// Mock the blacklist check
|
|
54
|
+
<% if (caching !== 'None') { %>
|
|
55
|
+
cacheService.get.mockResolvedValue(true);
|
|
56
|
+
<% } else { %>
|
|
57
|
+
JwtService.blacklistedTokens.set('blacklisted-jti', Date.now() + 10000);<% } %>
|
|
58
|
+
|
|
59
|
+
await authMiddleware(mockReq, mockRes, next);
|
|
60
|
+
|
|
61
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
62
|
+
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Token revoked' });
|
|
63
|
+
expect(next).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return 401 if session is expired (sid not in activeTokens)', async () => {
|
|
67
|
+
const user = { id: 1, email: 'test@example.com', jti: 'valid-jti', sid: 'expired-sid' };
|
|
68
|
+
mockReq.headers.authorization = 'Bearer valid-token';
|
|
69
|
+
JwtService.verifyToken.mockReturnValue(user);
|
|
70
|
+
|
|
71
|
+
<% if (caching !== 'None') { %>
|
|
72
|
+
cacheService.get.mockImplementation((key) => {
|
|
73
|
+
if (key.startsWith('blacklist:')) return Promise.resolve(false);
|
|
74
|
+
if (key === 'refresh_tokens:1') return Promise.resolve(['other-sid']);
|
|
75
|
+
return Promise.resolve(null);
|
|
76
|
+
});
|
|
77
|
+
<% } else { %>
|
|
78
|
+
JwtService.blacklistedTokens.delete('valid-jti');
|
|
79
|
+
JwtService.activeRefreshTokens.set('1', ['other-sid']);<% } %>
|
|
80
|
+
|
|
81
|
+
await authMiddleware(mockReq, mockRes, next);
|
|
82
|
+
|
|
83
|
+
expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
84
|
+
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Session expired' });
|
|
85
|
+
expect(next).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should set req.user and call next if token is valid, not blacklisted and session is active', async () => {
|
|
89
|
+
const user = { id: 1, jti: 'valid-jti', sid: 'active-sid' };
|
|
90
|
+
mockReq.headers.authorization = 'Bearer valid-token';
|
|
91
|
+
JwtService.verifyToken.mockReturnValue(user);
|
|
92
|
+
|
|
93
|
+
<% if (caching !== 'None') { %>
|
|
94
|
+
cacheService.get.mockImplementation((key) => {
|
|
95
|
+
if (key.startsWith('blacklist:')) return Promise.resolve(false);
|
|
96
|
+
if (key === 'refresh_tokens:1') return Promise.resolve(['active-sid']);
|
|
97
|
+
return Promise.resolve(null);
|
|
98
|
+
});
|
|
99
|
+
<% } else { %>
|
|
100
|
+
JwtService.blacklistedTokens.delete('valid-jti');
|
|
101
|
+
JwtService.activeRefreshTokens.set('1', ['active-sid']);<% } %>
|
|
102
|
+
|
|
103
|
+
await authMiddleware(mockReq, mockRes, next);
|
|
104
|
+
|
|
105
|
+
expect(mockReq.user).toEqual(user);
|
|
106
|
+
expect(next).toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { Router } = require('express');
|
|
2
|
+
const AuthController = require('<% if (architecture === "MVC") { %>../controllers/authController<% } else { %>../controllers/auth/authController<% } %>');
|
|
3
|
+
<%_ if (architecture === 'MVC') { -%>
|
|
4
|
+
const authMiddleware = require('../middleware/authMiddleware');
|
|
5
|
+
<%_ } else { -%>
|
|
6
|
+
const authMiddleware = require('../../infrastructure/webserver/middleware/authMiddleware');
|
|
7
|
+
<%_ } -%>
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
const authController = new AuthController();
|
|
11
|
+
|
|
12
|
+
router.post('/login', (req, res, next) => authController.login(req, res, next));
|
|
13
|
+
router.post('/refresh', (req, res, next) => authController.refresh(req, res, next));
|
|
14
|
+
router.post('/logout', authMiddleware, (req, res, next) => authController.logout(req, res, next));
|
|
15
|
+
|
|
16
|
+
module.exports = router;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
<% if (architecture === 'Clean Architecture') { %>
|
|
4
|
+
const { env } = require('../config/env');
|
|
5
|
+
<% } else { %>
|
|
6
|
+
const { env } = require('../config/env');
|
|
7
|
+
<% } %>
|
|
8
|
+
|
|
9
|
+
class JwtService {
|
|
10
|
+
static SECRET = env.JWT_SECRET || 'your-secret-key';
|
|
11
|
+
static REFRESH_SECRET = env.JWT_REFRESH_SECRET || 'your-refresh-secret-key';
|
|
12
|
+
static EXPIRES_IN = env.JWT_EXPIRES_IN || '15m'; // Access tokens should be short-lived
|
|
13
|
+
static REFRESH_EXPIRES_IN = env.JWT_REFRESH_EXPIRES_IN || '7d';
|
|
14
|
+
|
|
15
|
+
static generateToken(payload, expiresIn) {
|
|
16
|
+
const jti = crypto.randomUUID();
|
|
17
|
+
return jwt.sign({ ...payload, jti }, this.SECRET, { expiresIn: expiresIn || this.EXPIRES_IN });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static generateRefreshToken(payload, expiresIn) {
|
|
21
|
+
const jti = crypto.randomUUID();
|
|
22
|
+
return jwt.sign({ ...payload, jti }, this.REFRESH_SECRET, { expiresIn: expiresIn || this.REFRESH_EXPIRES_IN });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static verifyToken(token) {
|
|
26
|
+
try {
|
|
27
|
+
return jwt.verify(token, this.SECRET);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static verifyRefreshToken(token) {
|
|
34
|
+
try {
|
|
35
|
+
return jwt.verify(token, this.REFRESH_SECRET);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static decodeToken(token) {
|
|
42
|
+
try {
|
|
43
|
+
return jwt.decode(token);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback in-memory storage if caching = 'None'
|
|
50
|
+
static activeRefreshTokens = new Map();
|
|
51
|
+
static blacklistedTokens = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = JwtService;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
3
|
+
const JwtService = require('@/infrastructure/auth/jwtService');
|
|
4
|
+
<%_ } else { _%>
|
|
5
|
+
const JwtService = require('@/services/jwtService');
|
|
6
|
+
<%_ } _%>
|
|
7
|
+
|
|
8
|
+
jest.mock('jsonwebtoken');
|
|
9
|
+
|
|
10
|
+
describe('JwtService', () => {
|
|
11
|
+
const secret = 'test-secret';
|
|
12
|
+
const payload = { id: 1, email: 'test@example.com' };
|
|
13
|
+
const token = 'mock-token';
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
JwtService.SECRET = secret;
|
|
17
|
+
JwtService.REFRESH_SECRET = 'test-refresh-secret';
|
|
18
|
+
process.env.JWT_SECRET = secret;
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('generateToken', () => {
|
|
23
|
+
it('should generate a token with standard expiration and jti', () => {
|
|
24
|
+
jwt.sign.mockReturnValue(token);
|
|
25
|
+
|
|
26
|
+
const result = JwtService.generateToken(payload);
|
|
27
|
+
|
|
28
|
+
expect(jwt.sign).toHaveBeenCalledWith(
|
|
29
|
+
expect.objectContaining({ ...payload, jti: expect.any(String) }),
|
|
30
|
+
secret,
|
|
31
|
+
{ expiresIn: '15m' }
|
|
32
|
+
);
|
|
33
|
+
expect(result).toBe(token);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('generateRefreshToken', () => {
|
|
38
|
+
it('should generate a refresh token with refresh secret and jti', () => {
|
|
39
|
+
jwt.sign.mockReturnValue(token);
|
|
40
|
+
|
|
41
|
+
const result = JwtService.generateRefreshToken(payload);
|
|
42
|
+
|
|
43
|
+
expect(jwt.sign).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining({ ...payload, jti: expect.any(String) }),
|
|
45
|
+
'test-refresh-secret',
|
|
46
|
+
{ expiresIn: '7d' }
|
|
47
|
+
);
|
|
48
|
+
expect(result).toBe(token);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('verifyToken', () => {
|
|
53
|
+
it('should return decoded payload for a valid token', () => {
|
|
54
|
+
jwt.verify.mockReturnValue(payload);
|
|
55
|
+
|
|
56
|
+
const result = JwtService.verifyToken(token);
|
|
57
|
+
|
|
58
|
+
expect(jwt.verify).toHaveBeenCalledWith(token, secret);
|
|
59
|
+
expect(result).toEqual(payload);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('verifyRefreshToken', () => {
|
|
64
|
+
it('should return decoded payload for a valid refresh token', () => {
|
|
65
|
+
jwt.verify.mockReturnValue(payload);
|
|
66
|
+
|
|
67
|
+
const result = JwtService.verifyRefreshToken(token);
|
|
68
|
+
|
|
69
|
+
expect(jwt.verify).toHaveBeenCalledWith(token, 'test-refresh-secret');
|
|
70
|
+
expect(result).toEqual(payload);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('decodeToken', () => {
|
|
75
|
+
it('should return decoded payload without verification', () => {
|
|
76
|
+
jwt.decode.mockReturnValue(payload);
|
|
77
|
+
|
|
78
|
+
const result = JwtService.decodeToken(token);
|
|
79
|
+
|
|
80
|
+
expect(jwt.decode).toHaveBeenCalledWith(token);
|
|
81
|
+
expect(result).toEqual(payload);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
2
|
+
import { AuthController } from '@/interfaces/controllers/auth/authController';
|
|
3
|
+
import { JwtService } from '@/infrastructure/auth/jwtService';
|
|
4
|
+
<%_ } else { _%>
|
|
5
|
+
import { AuthController } from '@/controllers/authController';
|
|
6
|
+
import { JwtService } from '@/services/jwtService';
|
|
7
|
+
<%_ } _%>
|
|
8
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
9
|
+
import bcrypt from 'bcryptjs';
|
|
10
|
+
import { Request, Response, NextFunction } from 'express';
|
|
11
|
+
|
|
12
|
+
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
13
|
+
jest.mock('@/infrastructure/auth/jwtService');
|
|
14
|
+
<%_ } else { _%>
|
|
15
|
+
jest.mock('@/services/jwtService');
|
|
16
|
+
<%_ } _%>
|
|
17
|
+
jest.mock('bcryptjs');
|
|
18
|
+
|
|
19
|
+
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
20
|
+
jest.mock('@/infrastructure/database/models/User', () => ({ findOne: jest.fn() }));
|
|
21
|
+
const User = require('@/infrastructure/database/models/User');
|
|
22
|
+
<%_ } else { _%>
|
|
23
|
+
jest.mock('@/models/User', () => ({ findOne: jest.fn() }));
|
|
24
|
+
const User = require('@/models/User');
|
|
25
|
+
<%_ } _%>
|
|
26
|
+
|
|
27
|
+
<%_ if (caching !== 'None') { _%>
|
|
28
|
+
<%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
|
|
29
|
+
jest.mock('<%= cachePath %>', () => ({
|
|
30
|
+
__esModule: true,
|
|
31
|
+
default: {
|
|
32
|
+
get: jest.fn(),
|
|
33
|
+
set: jest.fn(),
|
|
34
|
+
del: jest.fn()
|
|
35
|
+
}
|
|
36
|
+
}), { virtual: true });
|
|
37
|
+
import cacheService from '<%= cachePath %>';
|
|
38
|
+
<%_ } _%>
|
|
39
|
+
|
|
40
|
+
describe('AuthController', () => {
|
|
41
|
+
let authController: AuthController;
|
|
42
|
+
let mockRequest: any;
|
|
43
|
+
let mockResponse: any;
|
|
44
|
+
const nextFunction: NextFunction = jest.fn();
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
authController = new AuthController();
|
|
48
|
+
mockRequest = {
|
|
49
|
+
body: {}
|
|
50
|
+
};
|
|
51
|
+
mockResponse = {
|
|
52
|
+
status: jest.fn().mockReturnThis(),
|
|
53
|
+
json: jest.fn()
|
|
54
|
+
};
|
|
55
|
+
jest.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('login', () => {
|
|
59
|
+
it('should return 401 if user not found', async () => {
|
|
60
|
+
mockRequest.body = { email: 'notfound@test.com', password: 'password' };
|
|
61
|
+
(User.findOne as jest.Mock).mockResolvedValue(null);
|
|
62
|
+
|
|
63
|
+
await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
|
|
64
|
+
|
|
65
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
66
|
+
expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid credentials' });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return 401 if password does not match', async () => {
|
|
70
|
+
const user = { email: 'test@test.com', password: 'hashedpassword' };
|
|
71
|
+
mockRequest.body = { email: 'test@test.com', password: 'wrongpassword' };
|
|
72
|
+
(User.findOne as jest.Mock).mockResolvedValue(user);
|
|
73
|
+
(bcrypt.compare as jest.Mock).mockResolvedValue(false);
|
|
74
|
+
|
|
75
|
+
await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
|
|
76
|
+
|
|
77
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
78
|
+
expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid credentials' });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return 200 and a token if credentials are valid', async () => {
|
|
82
|
+
const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
|
|
83
|
+
mockRequest.body = { email: 'test@test.com', password: 'password123' };
|
|
84
|
+
(User.findOne as jest.Mock).mockResolvedValue(user);
|
|
85
|
+
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
|
|
86
|
+
(JwtService.generateToken as jest.Mock).mockReturnValue('mock-token');
|
|
87
|
+
(JwtService.generateRefreshToken as jest.Mock).mockReturnValue('mock-refresh-token');
|
|
88
|
+
(JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'test-jti' });
|
|
89
|
+
|
|
90
|
+
await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
|
|
91
|
+
|
|
92
|
+
expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'test-jti' }));
|
|
93
|
+
expect(mockResponse.json).toHaveBeenCalledWith({ token: 'mock-token', accessToken: 'mock-token', refreshToken: 'mock-refresh-token' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should call next with error if something fails', async () => {
|
|
97
|
+
const error = new Error('DB Error');
|
|
98
|
+
mockRequest.body = { email: 'test@test.com', password: 'password123' };
|
|
99
|
+
(User.findOne as jest.Mock).mockRejectedValue(error);
|
|
100
|
+
|
|
101
|
+
await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
|
|
102
|
+
|
|
103
|
+
expect(nextFunction).toHaveBeenCalledWith(error);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('refresh', () => {
|
|
108
|
+
it('should return new tokens for a valid refresh token', async () => {
|
|
109
|
+
mockRequest.body = { refreshToken: 'valid-refresh' };
|
|
110
|
+
const decoded = { id: '1', email: 'test@test.com', jti: 'old-jti' };
|
|
111
|
+
(JwtService.verifyRefreshToken as jest.Mock).mockReturnValue(decoded);
|
|
112
|
+
(JwtService.generateToken as jest.Mock).mockReturnValue('new-access');
|
|
113
|
+
(JwtService.generateRefreshToken as jest.Mock).mockReturnValue('new-refresh');
|
|
114
|
+
(JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'new-jti' });
|
|
115
|
+
|
|
116
|
+
// Mock cache success
|
|
117
|
+
<% if (caching !== 'None') { %>
|
|
118
|
+
(cacheService.get as jest.Mock).mockResolvedValue(['old-jti']);
|
|
119
|
+
<% } else { %>
|
|
120
|
+
JwtService.activeRefreshTokens.set('1', ['old-jti']);
|
|
121
|
+
<% } %>
|
|
122
|
+
|
|
123
|
+
await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
|
|
124
|
+
|
|
125
|
+
expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'new-jti' }));
|
|
126
|
+
expect(mockResponse.json).toHaveBeenCalledWith({ accessToken: 'new-access', refreshToken: 'new-refresh' });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should detect theft and revoke all tokens if jti is not active', async () => {
|
|
130
|
+
mockRequest.body = { refreshToken: 'stolen-refresh' };
|
|
131
|
+
const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
|
|
132
|
+
(JwtService.verifyRefreshToken as jest.Mock).mockReturnValue(decoded);
|
|
133
|
+
|
|
134
|
+
<% if (caching !== 'None') { %>
|
|
135
|
+
(cacheService.get as jest.Mock).mockResolvedValue(['some-other-jti']);
|
|
136
|
+
<% } else { %>
|
|
137
|
+
JwtService.activeRefreshTokens.set('1', ['some-other-jti']);
|
|
138
|
+
<% } %>
|
|
139
|
+
|
|
140
|
+
await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
|
|
141
|
+
|
|
142
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
|
|
143
|
+
expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid session' });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('logout', () => {
|
|
148
|
+
it('should blacklist the access token and remove the refresh token', async () => {
|
|
149
|
+
mockRequest.headers = { authorization: 'Bearer access-token' };
|
|
150
|
+
mockRequest.body = { refreshToken: 'refresh-token' };
|
|
151
|
+
|
|
152
|
+
(JwtService.decodeToken as jest.Mock)
|
|
153
|
+
.mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 }) // for access token
|
|
154
|
+
.mockReturnValueOnce({ id: '1', jti: 'refresh-jti' }); // for refresh token
|
|
155
|
+
|
|
156
|
+
await authController.logout(mockRequest as Request, mockResponse as Response, nextFunction);
|
|
157
|
+
|
|
158
|
+
expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
<% if (architecture === 'MVC') { -%>
|
|
4
|
+
import User from '@/models/User';
|
|
5
|
+
import { JwtService } from '@/services/jwtService';
|
|
6
|
+
import logger from '@/utils/logger';
|
|
7
|
+
<% if (caching !== 'None') { -%>
|
|
8
|
+
import cacheService from '<% if (caching === "Redis") { %>@/config/redisClient<% } else { %>@/config/memoryCache<% } %>';
|
|
9
|
+
<% } -%>
|
|
10
|
+
<% } else { -%>
|
|
11
|
+
import User from '@/infrastructure/database/models/User';
|
|
12
|
+
import { JwtService } from '@/infrastructure/auth/jwtService';
|
|
13
|
+
import logger from '@/infrastructure/log/logger';
|
|
14
|
+
<% if (caching !== 'None') { -%>
|
|
15
|
+
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
16
|
+
<% } -%>
|
|
17
|
+
<% } -%>
|
|
18
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
19
|
+
|
|
20
|
+
export class AuthController {
|
|
21
|
+
async login(req: Request, res: Response, next: NextFunction) {
|
|
22
|
+
try {
|
|
23
|
+
const { email, password } = req.body;
|
|
24
|
+
<%_ if (database === 'MongoDB' || database === 'None') { -%>
|
|
25
|
+
const user = await User.findOne({ email });
|
|
26
|
+
<%_ } else { -%>
|
|
27
|
+
const user = await User.findOne({ where: { email } });
|
|
28
|
+
<%_ } -%>
|
|
29
|
+
if (!user) {
|
|
30
|
+
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid credentials' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isPasswordValid = await bcrypt.compare(password, user.password!);
|
|
34
|
+
if (!isPasswordValid) {
|
|
35
|
+
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid credentials' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const userId = String(user.id || ((user as unknown) as { _id?: string | number })._id);
|
|
39
|
+
|
|
40
|
+
const refreshToken = JwtService.generateRefreshToken({ id: userId, email: user.email });
|
|
41
|
+
const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
|
|
42
|
+
const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
|
|
43
|
+
|
|
44
|
+
// Store refresh token
|
|
45
|
+
<%_ if (caching !== 'None') { -%>
|
|
46
|
+
const cacheKey = `refresh_tokens:${userId}`;
|
|
47
|
+
const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
|
|
48
|
+
activeTokens.push(refreshJti!);
|
|
49
|
+
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60); // 7 days
|
|
50
|
+
<%_ } else { -%>
|
|
51
|
+
const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
52
|
+
activeTokens.push(refreshJti!);
|
|
53
|
+
JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
|
|
54
|
+
|
|
55
|
+
res.json({ token: accessToken, accessToken, refreshToken });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.error('Login error:', error);
|
|
58
|
+
next(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async refresh(req: Request, res: Response, next: NextFunction) {
|
|
63
|
+
try {
|
|
64
|
+
const { refreshToken } = req.body;
|
|
65
|
+
if (!refreshToken) {
|
|
66
|
+
return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Refresh token is required' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const decoded = JwtService.verifyRefreshToken(refreshToken);
|
|
70
|
+
if (!decoded || !decoded.id || !decoded.jti) {
|
|
71
|
+
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid refresh token' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const userId = String(decoded.id);
|
|
75
|
+
const incomingJti = decoded.jti;
|
|
76
|
+
|
|
77
|
+
<% if (caching !== 'None') { %>
|
|
78
|
+
const cacheKey = `refresh_tokens:${userId}`;
|
|
79
|
+
let activeTokens = await cacheService.get<string[]>(cacheKey) || [];
|
|
80
|
+
|
|
81
|
+
if (!activeTokens.includes(incomingJti)) {
|
|
82
|
+
// Theft detection! Revoke all sessions
|
|
83
|
+
logger.warn(`Token theft detected for user ${userId}. Revoking all sessions.`);
|
|
84
|
+
await cacheService.del(cacheKey);
|
|
85
|
+
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid session' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Valid rotation
|
|
89
|
+
activeTokens = activeTokens.filter(t => t !== incomingJti);
|
|
90
|
+
const newRefreshToken = JwtService.generateRefreshToken({ id: userId, email: decoded.email });
|
|
91
|
+
const newRefreshJti = JwtService.decodeToken(newRefreshToken)?.jti;
|
|
92
|
+
const newAccessToken = JwtService.generateToken({ id: userId, email: decoded.email, sid: newRefreshJti });
|
|
93
|
+
|
|
94
|
+
activeTokens.push(newRefreshJti!);
|
|
95
|
+
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
|
|
96
|
+
<% } else { -%>
|
|
97
|
+
let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
98
|
+
|
|
99
|
+
if (!activeTokens.includes(incomingJti)) {
|
|
100
|
+
// Theft detection!
|
|
101
|
+
logger.warn(`Token theft detected for user ${userId}. Revoking all sessions.`);
|
|
102
|
+
JwtService.activeRefreshTokens.delete(userId);
|
|
103
|
+
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Invalid session' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
activeTokens = activeTokens.filter(t => t !== incomingJti);
|
|
107
|
+
const newRefreshToken = JwtService.generateRefreshToken({ id: userId, email: decoded.email });
|
|
108
|
+
const newRefreshJti = JwtService.decodeToken(newRefreshToken)?.jti;
|
|
109
|
+
const newAccessToken = JwtService.generateToken({ id: userId, email: decoded.email, sid: newRefreshJti });
|
|
110
|
+
|
|
111
|
+
activeTokens.push(newRefreshJti!);
|
|
112
|
+
JwtService.activeRefreshTokens.set(userId, activeTokens);
|
|
113
|
+
<% } %>
|
|
114
|
+
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.error('Refresh token error:', error);
|
|
117
|
+
next(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async logout(req: Request, res: Response, next: NextFunction) {
|
|
122
|
+
try {
|
|
123
|
+
const authHeader = req.headers.authorization;
|
|
124
|
+
if (!authHeader) {
|
|
125
|
+
return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'No token provided' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const accessTokenStr = authHeader.split(' ')[1];
|
|
129
|
+
const decodedAccess = JwtService.decodeToken(accessTokenStr);
|
|
130
|
+
|
|
131
|
+
if (decodedAccess && decodedAccess.jti && decodedAccess.exp) {
|
|
132
|
+
const remainingTime = Math.max(0, decodedAccess.exp - Math.floor(Date.now() / 1000));
|
|
133
|
+
<%_ if (caching !== 'None') { -%>
|
|
134
|
+
if (remainingTime > 0) {
|
|
135
|
+
await cacheService.set(`blacklist:${decodedAccess.jti}`, true, remainingTime);
|
|
136
|
+
}<%_ } else { -%>
|
|
137
|
+
if (remainingTime > 0) {
|
|
138
|
+
JwtService.blacklistedTokens.set(decodedAccess.jti, Date.now() + remainingTime * 1000);
|
|
139
|
+
}<%_ } -%>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { refreshToken } = req.body;
|
|
143
|
+
if (refreshToken) {
|
|
144
|
+
const decodedRefresh = JwtService.decodeToken(refreshToken);
|
|
145
|
+
if (decodedRefresh && decodedRefresh.id && decodedRefresh.jti) {
|
|
146
|
+
const userId = String(decodedRefresh.id);
|
|
147
|
+
<%_ if (caching !== 'None') { -%>
|
|
148
|
+
const cacheKey = `refresh_tokens:${userId}`;
|
|
149
|
+
let activeTokens = await cacheService.get<string[]>(cacheKey) || [];
|
|
150
|
+
activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
|
|
151
|
+
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
|
|
152
|
+
<%_ } else { -%>
|
|
153
|
+
let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
154
|
+
activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
|
|
155
|
+
JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
res.json({ message: 'Logged out successfully' });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error('Logout error:', error);
|
|
162
|
+
next(error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|