@soapjs/soap-auth 0.3.1 → 0.3.2
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/build/__tests__/soap-auth.test.d.ts +1 -0
- package/build/__tests__/soap-auth.test.js +42 -0
- package/build/errors.d.ts +14 -3
- package/build/errors.js +29 -8
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/services/__tests__/account-lock.service.test.d.ts +1 -0
- package/build/services/__tests__/account-lock.service.test.js +55 -0
- package/build/services/__tests__/auth-throttle.service.test.d.ts +1 -0
- package/build/services/__tests__/auth-throttle.service.test.js +48 -0
- package/build/services/__tests__/jwks.service.test.d.ts +1 -0
- package/build/services/__tests__/jwks.service.test.js +39 -0
- package/build/services/__tests__/mfa.service.test.d.ts +1 -0
- package/build/services/__tests__/mfa.service.test.js +66 -0
- package/build/services/__tests__/password.service.test.d.ts +1 -0
- package/build/services/__tests__/password.service.test.js +66 -0
- package/build/services/__tests__/pkce.service.test.d.ts +1 -0
- package/build/services/__tests__/pkce.service.test.js +77 -0
- package/build/services/__tests__/rate-limit.service.test.d.ts +1 -0
- package/build/services/__tests__/rate-limit.service.test.js +37 -0
- package/build/services/__tests__/role.service.test.d.ts +1 -0
- package/build/services/__tests__/role.service.test.js +31 -0
- package/build/services/account-lock.service.d.ts +12 -0
- package/build/services/account-lock.service.js +39 -0
- package/build/services/auth-throttle.service.d.ts +10 -0
- package/build/services/auth-throttle.service.js +43 -0
- package/build/services/index.d.ts +8 -0
- package/build/{factories → services}/index.js +8 -3
- package/build/services/jwks.service.d.ts +7 -0
- package/build/services/jwks.service.js +41 -0
- package/build/services/mfa.service.d.ts +12 -0
- package/build/services/mfa.service.js +74 -0
- package/build/services/password.service.d.ts +14 -0
- package/build/services/password.service.js +78 -0
- package/build/services/pkce.service.d.ts +14 -0
- package/build/services/pkce.service.js +81 -0
- package/build/services/rate-limit.service.d.ts +9 -0
- package/build/services/rate-limit.service.js +26 -0
- package/build/services/role.service.d.ts +9 -0
- package/build/services/role.service.js +26 -0
- package/build/session/__tests__/file.session-store.test.d.ts +1 -0
- package/build/session/__tests__/file.session-store.test.js +117 -0
- package/build/session/__tests__/memory.session-store.test.d.ts +1 -0
- package/build/session/__tests__/memory.session-store.test.js +77 -0
- package/build/session/__tests__/session-handler.test.d.ts +1 -0
- package/build/session/__tests__/session-handler.test.js +337 -0
- package/build/session/file.session-store.d.ts +1 -0
- package/build/session/file.session-store.js +7 -0
- package/build/session/memory.session-store.d.ts +4 -1
- package/build/session/memory.session-store.js +11 -5
- package/build/session/session-handler.d.ts +12 -7
- package/build/session/session-handler.js +46 -13
- package/build/session/session.errors.d.ts +6 -0
- package/build/session/session.errors.js +15 -0
- package/build/soap-auth.d.ts +9 -8
- package/build/soap-auth.js +42 -29
- package/build/strategies/__tests__/base-auth.strategy.test.d.ts +14 -0
- package/build/strategies/__tests__/base-auth.strategy.test.js +137 -0
- package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +14 -0
- package/build/strategies/__tests__/credential-auth.strategy.test.js +265 -0
- package/build/strategies/__tests__/token-auth.strategy.test.d.ts +28 -0
- package/build/strategies/__tests__/token-auth.strategy.test.js +298 -0
- package/build/strategies/api-key/__tests__/api-key.strategy.test.d.ts +1 -0
- package/build/strategies/api-key/__tests__/api-key.strategy.test.js +103 -0
- package/build/strategies/api-key/api-key.strategy.d.ts +5 -2
- package/build/strategies/api-key/api-key.strategy.js +43 -35
- package/build/strategies/api-key/api-key.tools.d.ts +2 -0
- package/build/strategies/api-key/api-key.tools.js +39 -0
- package/build/strategies/api-key/api-key.types.d.ts +10 -2
- package/build/strategies/base-auth.strategy.d.ts +11 -5
- package/build/strategies/base-auth.strategy.js +45 -52
- package/build/strategies/basic/__tests__/basic.strategy.test.d.ts +1 -0
- package/build/strategies/basic/__tests__/basic.strategy.test.js +104 -0
- package/build/strategies/basic/basic.strategy.d.ts +5 -7
- package/build/strategies/basic/basic.strategy.js +6 -6
- package/build/strategies/basic/basic.tools.d.ts +2 -0
- package/build/strategies/basic/basic.tools.js +44 -0
- package/build/strategies/credential-auth.strategy.d.ts +7 -17
- package/build/strategies/credential-auth.strategy.js +116 -181
- package/build/strategies/jwt/__tests__/jwt.strategy.test.d.ts +1 -0
- package/build/strategies/jwt/__tests__/jwt.strategy.test.js +156 -0
- package/build/strategies/jwt/__tests__/jwt.tools.test.d.ts +1 -0
- package/build/strategies/jwt/__tests__/jwt.tools.test.js +98 -0
- package/build/strategies/jwt/jwt.strategy.d.ts +13 -14
- package/build/strategies/jwt/jwt.strategy.js +57 -44
- package/build/strategies/jwt/jwt.tools.d.ts +20 -7
- package/build/strategies/jwt/jwt.tools.js +180 -81
- package/build/strategies/local/__tests__/local.strategy.test.d.ts +1 -0
- package/build/strategies/local/__tests__/local.strategy.test.js +115 -0
- package/build/strategies/local/local.strategy.d.ts +4 -3
- package/build/strategies/local/local.strategy.js +7 -6
- package/build/strategies/local/local.tools.d.ts +2 -0
- package/build/strategies/local/local.tools.js +44 -0
- package/build/strategies/oauth2/hybrid.oauth2.strategy.d.ts +5 -0
- package/build/strategies/oauth2/hybrid.oauth2.strategy.js +92 -0
- package/build/strategies/oauth2/oauth2.errors.d.ts +12 -0
- package/build/strategies/oauth2/oauth2.errors.js +24 -0
- package/build/strategies/oauth2/oauth2.strategy.d.ts +25 -15
- package/build/strategies/oauth2/oauth2.strategy.js +131 -141
- package/build/strategies/oauth2/oauth2.tools.d.ts +7 -2
- package/build/strategies/oauth2/oauth2.tools.js +119 -14
- package/build/strategies/oauth2/oauth2.types.d.ts +32 -1
- package/build/strategies/token-auth.strategy.d.ts +14 -8
- package/build/strategies/token-auth.strategy.js +162 -38
- package/build/tools/index.d.ts +0 -2
- package/build/tools/index.js +0 -2
- package/build/tools/tools.d.ts +2 -1
- package/build/tools/tools.js +9 -12
- package/build/types.d.ts +88 -57
- package/package.json +1 -1
- package/build/factories/auth-strategy.factory.d.ts +0 -9
- package/build/factories/auth-strategy.factory.js +0 -16
- package/build/factories/http-auth-strategy.factory.d.ts +0 -5
- package/build/factories/http-auth-strategy.factory.js +0 -41
- package/build/factories/index.d.ts +0 -3
- package/build/factories/socket-auth-strategy.factory.d.ts +0 -5
- package/build/factories/socket-auth-strategy.factory.js +0 -27
- package/build/tools/session.tools.d.ts +0 -6
- package/build/tools/session.tools.js +0 -15
- package/build/tools/token.tools.d.ts +0 -7
- package/build/tools/token.tools.js +0 -32
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const memory_session_store_1 = require("../memory.session-store");
|
|
4
|
+
describe("MemorySessionStore", () => {
|
|
5
|
+
let store;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
store = new memory_session_store_1.MemorySessionStore();
|
|
8
|
+
});
|
|
9
|
+
describe("getSession", () => {
|
|
10
|
+
it("should return null if session does not exist", async () => {
|
|
11
|
+
const data = await store.getSession("nonExistent");
|
|
12
|
+
expect(data).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
it("should return the stored session data if present", async () => {
|
|
15
|
+
await store.setSession("session1", {
|
|
16
|
+
user: { id: "123", name: "John" },
|
|
17
|
+
});
|
|
18
|
+
const session = await store.getSession("session1");
|
|
19
|
+
expect(session).toEqual({ user: { id: "123", name: "John" } });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("setSession", () => {
|
|
23
|
+
it("should store session data in memory", async () => {
|
|
24
|
+
await store.setSession("session2", {
|
|
25
|
+
user: { id: "abc", name: "Alice" },
|
|
26
|
+
});
|
|
27
|
+
const session = await store.getSession("session2");
|
|
28
|
+
expect(session).toEqual({ user: { id: "abc", name: "Alice" } });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("destroySession", () => {
|
|
32
|
+
it("should delete the session from memory", async () => {
|
|
33
|
+
await store.setSession("sessionDelete", {
|
|
34
|
+
user: { id: "xyz", name: "Jane" },
|
|
35
|
+
});
|
|
36
|
+
await store.destroySession("sessionDelete");
|
|
37
|
+
const session = await store.getSession("sessionDelete");
|
|
38
|
+
expect(session).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it("should not throw if the session does not exist", async () => {
|
|
41
|
+
await expect(store.destroySession("noSession")).resolves.not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("touchSession", () => {
|
|
45
|
+
it("should update the session data if it exists", async () => {
|
|
46
|
+
await store.setSession("sessionTouch", {
|
|
47
|
+
user: { id: "initial", name: "Old" },
|
|
48
|
+
});
|
|
49
|
+
await store.touchSession("sessionTouch", {
|
|
50
|
+
user: { id: "updated", name: "New" },
|
|
51
|
+
});
|
|
52
|
+
const updated = await store.getSession("sessionTouch");
|
|
53
|
+
expect(updated).toEqual({ user: { id: "updated", name: "New" } });
|
|
54
|
+
});
|
|
55
|
+
it("should create a new session if it doesn't exist (similar to setSession)", async () => {
|
|
56
|
+
await store.touchSession("sessionNew", {
|
|
57
|
+
user: { id: "newId", name: "New Name" },
|
|
58
|
+
});
|
|
59
|
+
const data = await store.getSession("sessionNew");
|
|
60
|
+
expect(data).toEqual({ user: { id: "newId", name: "New Name" } });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("getSessionIds", () => {
|
|
64
|
+
it("should return an empty array if no sessions exist", async () => {
|
|
65
|
+
const ids = await store.getSessionIds();
|
|
66
|
+
expect(ids).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
it("should return all session IDs currently in memory", async () => {
|
|
69
|
+
await store.setSession("id1", { user: { id: "one" } });
|
|
70
|
+
await store.setSession("id2", { user: { id: "two" } });
|
|
71
|
+
const ids = await store.getSessionIds();
|
|
72
|
+
expect(ids).toContain("id1");
|
|
73
|
+
expect(ids).toContain("id2");
|
|
74
|
+
expect(ids.length).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const session_handler_1 = require("../session-handler");
|
|
4
|
+
const session_errors_1 = require("../session.errors");
|
|
5
|
+
describe("SessionHandler", () => {
|
|
6
|
+
let mockStore;
|
|
7
|
+
let mockLogger;
|
|
8
|
+
let sessionHandler;
|
|
9
|
+
let config;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockStore = {
|
|
12
|
+
getSession: jest.fn(),
|
|
13
|
+
setSession: jest.fn(),
|
|
14
|
+
touchSession: jest.fn(),
|
|
15
|
+
destroySession: jest.fn(),
|
|
16
|
+
getSessionIds: jest.fn().mockResolvedValue(["s1", "s2"]),
|
|
17
|
+
};
|
|
18
|
+
mockLogger = {
|
|
19
|
+
info: jest.fn(),
|
|
20
|
+
error: jest.fn(),
|
|
21
|
+
warn: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
config = {
|
|
24
|
+
store: mockStore,
|
|
25
|
+
sessionKey: "CUSTOMSESSION",
|
|
26
|
+
sessionHeader: "x-custom-session",
|
|
27
|
+
logger: mockLogger,
|
|
28
|
+
cookie: {
|
|
29
|
+
maxAge: 3600,
|
|
30
|
+
},
|
|
31
|
+
generateSessionId: jest.fn(),
|
|
32
|
+
getSessionId: undefined,
|
|
33
|
+
embedSessionId: undefined,
|
|
34
|
+
createSessionData: undefined,
|
|
35
|
+
};
|
|
36
|
+
sessionHandler = new session_handler_1.SessionHandler(config, mockLogger);
|
|
37
|
+
});
|
|
38
|
+
describe("constructor", () => {
|
|
39
|
+
it("should throw if store is not provided", () => {
|
|
40
|
+
expect(() => {
|
|
41
|
+
new session_handler_1.SessionHandler({});
|
|
42
|
+
}).toThrowError("Session store is required.");
|
|
43
|
+
});
|
|
44
|
+
it("should set default sessionKey and headerKey if not provided", () => {
|
|
45
|
+
const newHandler = new session_handler_1.SessionHandler({ store: mockStore });
|
|
46
|
+
expect(newHandler.sessionKey).toBe("SESSIONID");
|
|
47
|
+
expect(newHandler.headerKey).toBe("x-session-id");
|
|
48
|
+
});
|
|
49
|
+
it("should use provided sessionKey and headerKey if available", () => {
|
|
50
|
+
expect(sessionHandler.sessionKey).toBe("CUSTOMSESSION");
|
|
51
|
+
expect(sessionHandler.headerKey).toBe("x-custom-session");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("setSessionId", () => {
|
|
55
|
+
it("should call config.embedSessionId if defined", () => {
|
|
56
|
+
config.embedSessionId = jest.fn();
|
|
57
|
+
const handler = new session_handler_1.SessionHandler(config, mockLogger);
|
|
58
|
+
const context = {};
|
|
59
|
+
handler.setSessionId(context, "session123");
|
|
60
|
+
expect(config.embedSessionId).toHaveBeenCalledWith(context, "session123");
|
|
61
|
+
});
|
|
62
|
+
it("should set session ID in cookie if res.cookie is available", () => {
|
|
63
|
+
const context = {
|
|
64
|
+
res: {
|
|
65
|
+
cookie: jest.fn(),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
sessionHandler.setSessionId(context, "sessionABC");
|
|
69
|
+
expect(context.res.cookie).toHaveBeenCalledWith("CUSTOMSESSION", "sessionABC", expect.objectContaining({
|
|
70
|
+
httpOnly: true,
|
|
71
|
+
secure: true,
|
|
72
|
+
sameSite: "strict",
|
|
73
|
+
}));
|
|
74
|
+
expect(mockLogger.info).toHaveBeenCalledWith(`Session ID set in cookie: CUSTOMSESSION`);
|
|
75
|
+
});
|
|
76
|
+
it("should set session ID in header if res.setHeader is available", () => {
|
|
77
|
+
const context = {
|
|
78
|
+
res: {
|
|
79
|
+
setHeader: jest.fn(),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
sessionHandler.setSessionId(context, "headerSessionId");
|
|
83
|
+
expect(context.res.setHeader).toHaveBeenCalledWith("x-custom-session", "headerSessionId");
|
|
84
|
+
expect(mockLogger.info).toHaveBeenCalledWith(`Session ID set in header: x-custom-session`);
|
|
85
|
+
});
|
|
86
|
+
it("should set sessionId directly on context object if no res", () => {
|
|
87
|
+
const context = {};
|
|
88
|
+
sessionHandler.setSessionId(context, "direct123");
|
|
89
|
+
expect(context.sessionId).toBe("direct123");
|
|
90
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Session ID set in context object");
|
|
91
|
+
});
|
|
92
|
+
it("should log error if something goes wrong", () => {
|
|
93
|
+
config.embedSessionId = jest.fn().mockImplementation(() => {
|
|
94
|
+
throw new Error("Test error");
|
|
95
|
+
});
|
|
96
|
+
const handler = new session_handler_1.SessionHandler(config, mockLogger);
|
|
97
|
+
handler.setSessionId({}, "boom");
|
|
98
|
+
expect(mockLogger.error).toHaveBeenCalledWith("Error setting session ID:", expect.any(Error));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("getSessionId", () => {
|
|
102
|
+
it("should use config.getSessionId if defined", () => {
|
|
103
|
+
config.getSessionId = jest.fn().mockReturnValue("customSessionId");
|
|
104
|
+
const handler = new session_handler_1.SessionHandler(config, mockLogger);
|
|
105
|
+
const sid = handler.getSessionId({});
|
|
106
|
+
expect(sid).toBe("customSessionId");
|
|
107
|
+
expect(config.getSessionId).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
it("should return cookie session if present", () => {
|
|
110
|
+
const context = { cookies: { CUSTOMSESSION: "cookieValue" } };
|
|
111
|
+
const sid = sessionHandler.getSessionId(context);
|
|
112
|
+
expect(sid).toBe("cookieValue");
|
|
113
|
+
});
|
|
114
|
+
it("should return header session if present", () => {
|
|
115
|
+
const context = { headers: { "x-custom-session": "headerValue" } };
|
|
116
|
+
const sid = sessionHandler.getSessionId(context);
|
|
117
|
+
expect(sid).toBe("headerValue");
|
|
118
|
+
});
|
|
119
|
+
it("should return sessionId if set directly on context", () => {
|
|
120
|
+
const context = { sessionId: "directValue" };
|
|
121
|
+
const sid = sessionHandler.getSessionId(context);
|
|
122
|
+
expect(sid).toBe("directValue");
|
|
123
|
+
});
|
|
124
|
+
it("should return null if not found", () => {
|
|
125
|
+
const context = {};
|
|
126
|
+
const sid = sessionHandler.getSessionId(context);
|
|
127
|
+
expect(sid).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
it("should log error and return null if something goes wrong", () => {
|
|
130
|
+
config.getSessionId = jest.fn().mockImplementation(() => {
|
|
131
|
+
throw new Error("Get error");
|
|
132
|
+
});
|
|
133
|
+
const handler = new session_handler_1.SessionHandler(config, mockLogger);
|
|
134
|
+
const sid = handler.getSessionId({});
|
|
135
|
+
expect(mockLogger.error).toHaveBeenCalledWith("Error retrieving session ID:", expect.any(Error));
|
|
136
|
+
expect(sid).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("generateSessionId", () => {
|
|
140
|
+
it("should use config.generateSessionId if provided", () => {
|
|
141
|
+
config.generateSessionId.mockReturnValue("customGenerated");
|
|
142
|
+
const sid = sessionHandler.generateSessionId();
|
|
143
|
+
expect(sid).toBe("customGenerated");
|
|
144
|
+
expect(config.generateSessionId).toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
it("should fallback to uuid if no config.generateSessionId is given", () => {
|
|
147
|
+
config.generateSessionId = undefined;
|
|
148
|
+
const sid = sessionHandler.generateSessionId();
|
|
149
|
+
expect(typeof sid).toBe("string");
|
|
150
|
+
expect(sid.length).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe("buildSessionData", () => {
|
|
154
|
+
it("should call config.createSessionData if provided", () => {
|
|
155
|
+
config.createSessionData = jest
|
|
156
|
+
.fn()
|
|
157
|
+
.mockReturnValue({ user: "mockUser" });
|
|
158
|
+
const data = sessionHandler.buildSessionData({ id: "test" }, {});
|
|
159
|
+
expect(config.createSessionData).toHaveBeenCalledWith({ id: "test" }, {});
|
|
160
|
+
expect(data).toEqual({ user: "mockUser" });
|
|
161
|
+
});
|
|
162
|
+
it("should default to { user: data } if createSessionData not provided", () => {
|
|
163
|
+
config.createSessionData = undefined;
|
|
164
|
+
const input = { id: "testId" };
|
|
165
|
+
const data = sessionHandler.buildSessionData(input);
|
|
166
|
+
expect(data).toEqual({ user: input });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("getSessionData", () => {
|
|
170
|
+
it("should return data from the store", async () => {
|
|
171
|
+
mockStore.getSession.mockResolvedValue({ user: { id: "1" } });
|
|
172
|
+
const data = await sessionHandler.getSessionData("session123");
|
|
173
|
+
expect(data).toEqual({ user: { id: "1" } });
|
|
174
|
+
});
|
|
175
|
+
it("should return null if store.getSession fails", async () => {
|
|
176
|
+
mockStore.getSession.mockRejectedValue(new Error("Store error"));
|
|
177
|
+
const data = await sessionHandler.getSessionData("sessionX");
|
|
178
|
+
expect(data).toBeNull();
|
|
179
|
+
expect(mockLogger.error).toHaveBeenCalledWith("Error retrieving session data:", expect.any(Error));
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe("setSessionData", () => {
|
|
183
|
+
it("should warn if sessionId is falsy", async () => {
|
|
184
|
+
await sessionHandler.setSessionData("", { user: {} });
|
|
185
|
+
expect(mockLogger.warn).toHaveBeenCalledWith("No session ID found, unable to set session.");
|
|
186
|
+
expect(mockStore.setSession).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
it("should call store.setSession with provided data", async () => {
|
|
189
|
+
await sessionHandler.setSessionData("sessionXYZ", {
|
|
190
|
+
user: { id: "abc" },
|
|
191
|
+
});
|
|
192
|
+
expect(mockStore.setSession).toHaveBeenCalledWith("sessionXYZ", {
|
|
193
|
+
user: { id: "abc" },
|
|
194
|
+
});
|
|
195
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Session set for ID: sessionXYZ");
|
|
196
|
+
});
|
|
197
|
+
it("should log error if store.setSession fails", async () => {
|
|
198
|
+
mockStore.setSession.mockRejectedValue(new Error("Store error"));
|
|
199
|
+
await sessionHandler.setSessionData("session123", {
|
|
200
|
+
user: { id: "xyz" },
|
|
201
|
+
});
|
|
202
|
+
expect(mockLogger.error).toHaveBeenCalledWith("Error storing session data:", expect.any(Error));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe("touch", () => {
|
|
206
|
+
it("should warn if no sessionId is provided", async () => {
|
|
207
|
+
await sessionHandler.touch("");
|
|
208
|
+
expect(mockLogger.warn).toHaveBeenCalledWith("No session ID found, unable to touch session.");
|
|
209
|
+
});
|
|
210
|
+
it("should warn if session not found", async () => {
|
|
211
|
+
mockStore.getSession.mockResolvedValue(null);
|
|
212
|
+
await sessionHandler.touch("noSuchId", { user: { id: "1" } });
|
|
213
|
+
expect(mockLogger.warn).toHaveBeenCalledWith("Session not found for ID: noSuchId");
|
|
214
|
+
});
|
|
215
|
+
it("should merge data and call store.touchSession if session found", async () => {
|
|
216
|
+
mockStore.getSession.mockResolvedValue({ user: { id: "old" } });
|
|
217
|
+
await sessionHandler.touch("session123", { user: { id: "new" } });
|
|
218
|
+
expect(mockStore.touchSession).toHaveBeenCalledWith("session123", {
|
|
219
|
+
user: { id: "new" },
|
|
220
|
+
});
|
|
221
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Session touched for ID: session123");
|
|
222
|
+
});
|
|
223
|
+
it("should log error on store errors", async () => {
|
|
224
|
+
mockStore.getSession.mockResolvedValue({ user: {} });
|
|
225
|
+
mockStore.touchSession.mockRejectedValue(new Error("Touch error"));
|
|
226
|
+
await sessionHandler.touch("session123");
|
|
227
|
+
expect(mockLogger.error).toHaveBeenCalledWith("Error touching session:", expect.any(Error));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe("destroy", () => {
|
|
231
|
+
it("should call store.destroySession with sessionId", async () => {
|
|
232
|
+
await sessionHandler.destroy("sessionABC");
|
|
233
|
+
expect(mockStore.destroySession).toHaveBeenCalledWith("sessionABC");
|
|
234
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Session destroyed for ID: sessionABC");
|
|
235
|
+
});
|
|
236
|
+
it("should do nothing if no sessionId is provided", async () => {
|
|
237
|
+
await sessionHandler.destroy("");
|
|
238
|
+
expect(mockStore.destroySession).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
it("should log error on store error", async () => {
|
|
241
|
+
mockStore.destroySession.mockRejectedValue(new Error("Destroy error"));
|
|
242
|
+
await sessionHandler.destroy("sessionXYZ");
|
|
243
|
+
expect(mockLogger.error).toHaveBeenCalledWith("Error destroying session:", expect.any(Error));
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe("isSessionExpired", () => {
|
|
247
|
+
it("should return false if no sessionData or no createdAt", () => {
|
|
248
|
+
expect(sessionHandler.isSessionExpired({})).toBe(false);
|
|
249
|
+
expect(sessionHandler.isSessionExpired(null)).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
it("should return true if now - createdAt > maxAge", () => {
|
|
252
|
+
const oldTime = Date.now() - 7200 * 1000;
|
|
253
|
+
config.cookie.maxAge = 3600 * 1000;
|
|
254
|
+
const data = { createdAt: oldTime };
|
|
255
|
+
expect(sessionHandler.isSessionExpired(data)).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
it("should return false if within maxAge", () => {
|
|
258
|
+
const recentTime = Date.now() - 1000 * 100;
|
|
259
|
+
config.cookie.maxAge = 600000;
|
|
260
|
+
const data = { createdAt: recentTime };
|
|
261
|
+
expect(sessionHandler.isSessionExpired(data)).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe("issueSession", () => {
|
|
265
|
+
it("should reuse existing sessionId if present in context", async () => {
|
|
266
|
+
jest
|
|
267
|
+
.spyOn(sessionHandler, "getSessionId")
|
|
268
|
+
.mockReturnValue("ctxSessionId");
|
|
269
|
+
await sessionHandler.issueSession({ id: "u1" }, {});
|
|
270
|
+
expect(mockStore.setSession).toHaveBeenCalledWith("ctxSessionId", {
|
|
271
|
+
user: { id: "u1" },
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
it("should generate a new sessionId if none found in context", async () => {
|
|
275
|
+
jest.spyOn(sessionHandler, "getSessionId").mockReturnValue(null);
|
|
276
|
+
jest.spyOn(sessionHandler, "generateSessionId").mockReturnValue("newID");
|
|
277
|
+
const result = await sessionHandler.issueSession({ id: "u1" }, {});
|
|
278
|
+
expect(mockStore.setSession).toHaveBeenCalledWith("newID", {
|
|
279
|
+
user: { id: "u1" },
|
|
280
|
+
});
|
|
281
|
+
expect(result.sessionId).toBe("newID");
|
|
282
|
+
});
|
|
283
|
+
it("should call session.createSessionData if provided in config", async () => {
|
|
284
|
+
config.createSessionData = jest
|
|
285
|
+
.fn()
|
|
286
|
+
.mockReturnValue({ user: { id: "custom" }, extra: "hello" });
|
|
287
|
+
config.embedSessionId = jest.fn();
|
|
288
|
+
const handler = new session_handler_1.SessionHandler(config, mockLogger);
|
|
289
|
+
jest.spyOn(handler, "getSessionId").mockReturnValue("testSid");
|
|
290
|
+
const result = await handler.issueSession({ id: "u2" }, {});
|
|
291
|
+
expect(config.createSessionData).toHaveBeenCalledWith({ id: "u2" }, {});
|
|
292
|
+
expect(mockStore.setSession).toHaveBeenCalledWith("testSid", {
|
|
293
|
+
user: { id: "custom" },
|
|
294
|
+
extra: "hello",
|
|
295
|
+
});
|
|
296
|
+
expect(result.data).toEqual({ user: { id: "custom" }, extra: "hello" });
|
|
297
|
+
});
|
|
298
|
+
it("should embed sessionId if embedSessionId is defined in config.session", async () => {
|
|
299
|
+
config.embedSessionId = jest.fn();
|
|
300
|
+
const handler = new session_handler_1.SessionHandler(config, mockLogger);
|
|
301
|
+
jest.spyOn(handler, "getSessionId").mockReturnValue("abc123");
|
|
302
|
+
await handler.issueSession({ id: "u5" }, {});
|
|
303
|
+
expect(config.embedSessionId).toHaveBeenCalledWith({}, "abc123");
|
|
304
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Stored user session with ID: abc123");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
describe("logoutSession", () => {
|
|
308
|
+
it("should throw MissingSessionIdError if sessionId not found", async () => {
|
|
309
|
+
jest.spyOn(sessionHandler, "getSessionId").mockReturnValue(null);
|
|
310
|
+
await expect(sessionHandler.logoutSession({})).rejects.toThrow(session_errors_1.MissingSessionIdError);
|
|
311
|
+
});
|
|
312
|
+
it("should destroy session with found sessionId", async () => {
|
|
313
|
+
jest
|
|
314
|
+
.spyOn(sessionHandler, "getSessionId")
|
|
315
|
+
.mockReturnValue("logoutSessionId");
|
|
316
|
+
const destroySpy = jest.spyOn(sessionHandler, "destroy");
|
|
317
|
+
await sessionHandler.logoutSession({});
|
|
318
|
+
expect(destroySpy).toHaveBeenCalledWith("logoutSessionId");
|
|
319
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Session destroyed: logoutSessionId");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
describe("clearAllSessions", () => {
|
|
323
|
+
it("should destroy all sessions if store.getSessionIds is available", async () => {
|
|
324
|
+
mockStore.getSessionIds.mockResolvedValue(["s1", "s2", "s3"]);
|
|
325
|
+
await sessionHandler.clearAllSessions();
|
|
326
|
+
expect(mockStore.destroySession).toHaveBeenCalledWith("s1");
|
|
327
|
+
expect(mockStore.destroySession).toHaveBeenCalledWith("s2");
|
|
328
|
+
expect(mockStore.destroySession).toHaveBeenCalledWith("s3");
|
|
329
|
+
expect(mockLogger.info).toHaveBeenCalledWith("All sessions have been cleared.");
|
|
330
|
+
});
|
|
331
|
+
it("should log error if something goes wrong", async () => {
|
|
332
|
+
mockStore.getSessionIds.mockRejectedValue(new Error("Store read error"));
|
|
333
|
+
await sessionHandler.clearAllSessions();
|
|
334
|
+
expect(mockLogger.error).toHaveBeenCalledWith("Error clearing all sessions:", expect.any(Error));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -7,4 +7,5 @@ export declare class FileSessionStore implements SessionStore {
|
|
|
7
7
|
setSession<SessionData>(sessionId: string, sessionData: SessionData): Promise<void>;
|
|
8
8
|
destroySession(sessionId: string): Promise<void>;
|
|
9
9
|
touchSession(sessionId: string, session: SessionData): Promise<void>;
|
|
10
|
+
getSessionIds(): Promise<string[]>;
|
|
10
11
|
}
|
|
@@ -55,5 +55,12 @@ class FileSessionStore {
|
|
|
55
55
|
async touchSession(sessionId, session) {
|
|
56
56
|
await this.setSession(sessionId, session);
|
|
57
57
|
}
|
|
58
|
+
async getSessionIds() {
|
|
59
|
+
const entries = await promises_1.default.readdir(this.sessionsDir, { withFileTypes: true });
|
|
60
|
+
const ids = entries
|
|
61
|
+
.filter((entry) => entry.isFile())
|
|
62
|
+
.map((file) => path_1.default.parse(file.name).name);
|
|
63
|
+
return ids;
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
exports.FileSessionStore = FileSessionStore;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { SessionStore } from "../types";
|
|
1
|
+
import { SessionData, SessionStore } from "../types";
|
|
2
2
|
export declare class MemorySessionStore implements SessionStore {
|
|
3
|
+
private sessions;
|
|
4
|
+
constructor(sessions?: Record<string, SessionData>);
|
|
3
5
|
getSession<SessionData>(sessionId: string): Promise<SessionData | null>;
|
|
4
6
|
setSession<SessionData>(sessionId: string, sessionData: SessionData): Promise<void>;
|
|
5
7
|
destroySession(sessionId: string): Promise<void>;
|
|
6
8
|
touchSession<SessionData>(sessionId: string, session: SessionData): Promise<void>;
|
|
9
|
+
getSessionIds(): Promise<string[]>;
|
|
7
10
|
}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemorySessionStore = void 0;
|
|
4
|
-
const sessions = {};
|
|
5
4
|
class MemorySessionStore {
|
|
5
|
+
sessions;
|
|
6
|
+
constructor(sessions) {
|
|
7
|
+
this.sessions = sessions || {};
|
|
8
|
+
}
|
|
6
9
|
async getSession(sessionId) {
|
|
7
|
-
return sessions[sessionId] || null;
|
|
10
|
+
return this.sessions[sessionId] || null;
|
|
8
11
|
}
|
|
9
12
|
async setSession(sessionId, sessionData) {
|
|
10
|
-
sessions[sessionId] = sessionData;
|
|
13
|
+
this.sessions[sessionId] = sessionData;
|
|
11
14
|
}
|
|
12
15
|
async destroySession(sessionId) {
|
|
13
|
-
delete sessions[sessionId];
|
|
16
|
+
delete this.sessions[sessionId];
|
|
14
17
|
}
|
|
15
18
|
async touchSession(sessionId, session) {
|
|
16
|
-
sessions[sessionId] = session;
|
|
19
|
+
this.sessions[sessionId] = session;
|
|
20
|
+
}
|
|
21
|
+
async getSessionIds() {
|
|
22
|
+
return Object.keys(this.sessions);
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
exports.MemorySessionStore = MemorySessionStore;
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { SessionConfig, SessionData, SessionInfo } from "../types";
|
|
3
|
+
export declare class SessionHandler<TContext = unknown, TUser = unknown, TData = SessionData> {
|
|
3
4
|
private config;
|
|
5
|
+
private logger?;
|
|
4
6
|
private store;
|
|
5
7
|
private sessionKey;
|
|
6
8
|
private headerKey;
|
|
7
|
-
constructor(config: SessionConfig);
|
|
9
|
+
constructor(config: SessionConfig, logger?: Soap.Logger);
|
|
8
10
|
setSessionId(context: TContext, sessionId: string): void;
|
|
9
11
|
getSessionId(context: TContext): string | null;
|
|
10
12
|
generateSessionId(): string;
|
|
11
13
|
buildSessionData(data: unknown, context?: TContext): TData;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
touch(
|
|
15
|
-
destroy(
|
|
14
|
+
getSessionData(sessionId: string): Promise<TData | null>;
|
|
15
|
+
setSessionData(sessionId: string, data: TData): Promise<void>;
|
|
16
|
+
touch(sessionId: string, data?: Partial<TData>): Promise<void>;
|
|
17
|
+
destroy(sessionId: string): Promise<void>;
|
|
16
18
|
isSessionExpired(sessionData: TData): boolean;
|
|
19
|
+
issueSession(user: TUser, context: TContext): Promise<SessionInfo<TData>>;
|
|
20
|
+
logoutSession(context: TContext): Promise<void>;
|
|
21
|
+
clearAllSessions(): Promise<void>;
|
|
17
22
|
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SessionHandler = void 0;
|
|
4
4
|
const uuid_1 = require("uuid");
|
|
5
|
+
const session_errors_1 = require("./session.errors");
|
|
5
6
|
class SessionHandler {
|
|
6
7
|
config;
|
|
8
|
+
logger;
|
|
7
9
|
store;
|
|
8
10
|
sessionKey;
|
|
9
11
|
headerKey;
|
|
10
|
-
constructor(config) {
|
|
12
|
+
constructor(config, logger) {
|
|
11
13
|
this.config = config;
|
|
14
|
+
this.logger = logger;
|
|
12
15
|
if (!config.store) {
|
|
13
16
|
throw new Error("Session store is required.");
|
|
14
17
|
}
|
|
@@ -78,22 +81,22 @@ class SessionHandler {
|
|
|
78
81
|
? this.config.createSessionData(data, context)
|
|
79
82
|
: { user: data };
|
|
80
83
|
}
|
|
81
|
-
async
|
|
84
|
+
async getSessionData(sessionId) {
|
|
82
85
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
return null;
|
|
86
|
-
return await this.store.getSession(sessionId);
|
|
86
|
+
const data = await this.store.getSession(sessionId);
|
|
87
|
+
return data;
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
90
|
this.config.logger?.error("Error retrieving session data:", error);
|
|
90
91
|
return null;
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
async
|
|
94
|
+
async setSessionData(sessionId, data) {
|
|
94
95
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
if (!sessionId) {
|
|
97
|
+
this.config.logger?.warn("No session ID found, unable to set session.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
97
100
|
await this.store.setSession(sessionId, data);
|
|
98
101
|
this.config.logger?.info(`Session set for ID: ${sessionId}`);
|
|
99
102
|
}
|
|
@@ -101,9 +104,8 @@ class SessionHandler {
|
|
|
101
104
|
this.config.logger?.error("Error storing session data:", error);
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
|
-
async touch(
|
|
107
|
+
async touch(sessionId, data) {
|
|
105
108
|
try {
|
|
106
|
-
const sessionId = this.getSessionId(context);
|
|
107
109
|
if (!sessionId) {
|
|
108
110
|
this.config.logger?.warn("No session ID found, unable to touch session.");
|
|
109
111
|
return;
|
|
@@ -121,9 +123,8 @@ class SessionHandler {
|
|
|
121
123
|
this.config.logger?.error("Error touching session:", error);
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
|
-
async destroy(
|
|
126
|
+
async destroy(sessionId) {
|
|
125
127
|
try {
|
|
126
|
-
const sessionId = this.getSessionId(context);
|
|
127
128
|
if (sessionId) {
|
|
128
129
|
await this.store.destroySession(sessionId);
|
|
129
130
|
this.config.logger?.info(`Session destroyed for ID: ${sessionId}`);
|
|
@@ -141,5 +142,37 @@ class SessionHandler {
|
|
|
141
142
|
const sessionExpiryMs = this.config.cookie?.maxAge || 3600000;
|
|
142
143
|
return now - sessionData.createdAt > sessionExpiryMs;
|
|
143
144
|
}
|
|
145
|
+
async issueSession(user, context) {
|
|
146
|
+
const sessionId = this.getSessionId(context) || this.generateSessionId();
|
|
147
|
+
const data = this.config.createSessionData
|
|
148
|
+
? this.config.createSessionData(user, context)
|
|
149
|
+
: { user };
|
|
150
|
+
await this.store.setSession(sessionId, data);
|
|
151
|
+
this.config.embedSessionId?.(context, sessionId);
|
|
152
|
+
this.logger?.info(`Stored user session with ID: ${sessionId}`);
|
|
153
|
+
return { sessionId, data };
|
|
154
|
+
}
|
|
155
|
+
async logoutSession(context) {
|
|
156
|
+
const sessionId = this.getSessionId?.(context);
|
|
157
|
+
if (!sessionId) {
|
|
158
|
+
throw new session_errors_1.MissingSessionIdError();
|
|
159
|
+
}
|
|
160
|
+
await this.destroy(sessionId);
|
|
161
|
+
this.logger?.info(`Session destroyed: ${sessionId}`);
|
|
162
|
+
}
|
|
163
|
+
async clearAllSessions() {
|
|
164
|
+
try {
|
|
165
|
+
if (typeof this.store.destroySession === "function") {
|
|
166
|
+
const sessionIds = await this.store.getSessionIds();
|
|
167
|
+
for (const sessionId of sessionIds) {
|
|
168
|
+
await this.store.destroySession(sessionId);
|
|
169
|
+
}
|
|
170
|
+
this.config.logger?.info("All sessions have been cleared.");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
this.config.logger?.error("Error clearing all sessions:", error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
144
177
|
}
|
|
145
178
|
exports.SessionHandler = SessionHandler;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MissingSessionIdError = exports.InvalidSessionError = void 0;
|
|
4
|
+
class InvalidSessionError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Session is invalid or expired.");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
exports.InvalidSessionError = InvalidSessionError;
|
|
10
|
+
class MissingSessionIdError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("Session ID is missing.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.MissingSessionIdError = MissingSessionIdError;
|