lambder 1.0.114 → 1.0.116
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/Readme.md +67 -10
- package/dist/Lambder.d.ts +10 -14
- package/dist/Lambder.js +23 -73
- package/dist/LambderSessionController.d.ts +21 -0
- package/dist/LambderSessionController.js +88 -0
- package/dist/{LambderSession.d.ts → LambderSessionManager.d.ts} +3 -3
- package/dist/{LambderSession.js → LambderSessionManager.js} +18 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/Lambder.ts +33 -66
- package/src/LambderSessionController.ts +94 -0
- package/src/{LambderSession.ts → LambderSessionManager.ts} +18 -18
- package/src/index.ts +1 -1
package/Readme.md
CHANGED
|
@@ -55,7 +55,7 @@ lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
|
|
|
55
55
|
// Start a session from an API
|
|
56
56
|
lambder.addApi("loginUser", async (ctx, res) => {
|
|
57
57
|
const user = await fetchUserData();
|
|
58
|
-
await lambder.getSessionController(ctx).
|
|
58
|
+
await lambder.getSessionController(ctx).createSession(user.id);
|
|
59
59
|
return res.api({ success: true });
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -218,25 +218,81 @@ lambder.addModule(async (lambder: Lambder): Promise<void> => {
|
|
|
218
218
|
|
|
219
219
|
### Session Management
|
|
220
220
|
|
|
221
|
+
You can enable session tracking by:
|
|
222
|
+
|
|
221
223
|
```typescript
|
|
222
|
-
|
|
224
|
+
// Enable session
|
|
225
|
+
lambder.enableDdbSession({
|
|
226
|
+
tableName: "website-session",
|
|
227
|
+
tableRegion: "us-east-1",
|
|
228
|
+
sessionSalt: "8p6Vt+4b1w3N8d/dcJ47QF3DRkp9koFg0G" // Change salt
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
After you enable the session, you can access to the session controller:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Create session controller:
|
|
223
236
|
const sessionController = lambder.getSessionController(ctx);
|
|
237
|
+
|
|
238
|
+
/*
|
|
239
|
+
sessionController: {
|
|
240
|
+
async createSession(sessionKey, data, ttlInSeconds):
|
|
241
|
+
// Starts a new session and persists the session data to DDB.
|
|
242
|
+
|
|
243
|
+
async fetchSession():
|
|
244
|
+
// Fetch and validate if there is an existing session
|
|
245
|
+
// This is automatically done for addSessionRoute and addSessionApi
|
|
246
|
+
// Throws if session not found
|
|
247
|
+
|
|
248
|
+
async fetchSessionIfExists():
|
|
249
|
+
// Runs fetchSession and returns the session if found. Otherwise return null
|
|
250
|
+
|
|
251
|
+
async updateSessionData(updatedData):
|
|
252
|
+
// Updates the active sessions data and persist it to ddb.
|
|
253
|
+
|
|
254
|
+
async endSession(): Start a new session
|
|
255
|
+
// End session and delete from DDB.
|
|
256
|
+
|
|
257
|
+
async endSessionAll(): Start a new session
|
|
258
|
+
// Ends and deletes all registered sessions for this sessionKey across all devices
|
|
259
|
+
}
|
|
260
|
+
*/
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
#### Session Examples
|
|
266
|
+
```typescript
|
|
267
|
+
lambder.addApi("getCompanyPage", async (ctx, res) => {
|
|
268
|
+
|
|
269
|
+
// createSession: Start a new session
|
|
224
270
|
const userId = "37234";
|
|
225
|
-
await
|
|
226
|
-
|
|
271
|
+
await lambder.getSessionController(ctx)
|
|
272
|
+
.createSession(userId, { "business": "Session data goes here" });
|
|
273
|
+
console.log(ctx.session?.sessionKey); // "37234"
|
|
227
274
|
console.log(ctx.session?.data?.business); // "Session data goes here"
|
|
228
275
|
|
|
229
|
-
|
|
230
|
-
|
|
276
|
+
// fetchSession: Fetch and validate if there is an existing session
|
|
277
|
+
// This is automatically done for addSessionRoute and addSessionApi
|
|
278
|
+
await lambder.getSessionController(ctx).fetchSession();
|
|
279
|
+
console.log(ctx.session?.sessionKey); // "37234"
|
|
280
|
+
|
|
281
|
+
// updateSessionData: Updates the active sessions data and persist it to ddb.
|
|
282
|
+
await lambder.getSessionController(ctx)
|
|
283
|
+
.updateSessionData({ "business2": "Session data updated" });
|
|
284
|
+
console.log(ctx.session?.sessionKey); // "37234"
|
|
231
285
|
console.log(ctx.session?.data?.business); // undefined
|
|
232
286
|
console.log(ctx.session?.data?.business2); // "Session data updated"
|
|
233
287
|
|
|
234
|
-
|
|
235
|
-
|
|
288
|
+
// endSession: Ends the session and removes it from ddb
|
|
289
|
+
await lambder.getSessionController(ctx).endSession(); // End session
|
|
290
|
+
console.log(ctx.session?.sessionKey); // undefined
|
|
236
291
|
console.log(ctx.session?.data?.business); // undefined
|
|
237
292
|
|
|
238
|
-
|
|
239
|
-
|
|
293
|
+
// endSessionAll: Ends all registered sessions for this user in all devices.
|
|
294
|
+
await lambder.getSessionController(ctx).endSessionAll();
|
|
295
|
+
console.log(ctx.session?.sessionKey); // undefined
|
|
240
296
|
console.log(ctx.session?.data?.business); // undefined
|
|
241
297
|
|
|
242
298
|
});
|
|
@@ -296,6 +352,7 @@ lambder.addApi("getCompanyName", async (ctx, res) => {
|
|
|
296
352
|
headers, // Request Headers in an object. Exp: { "User-Agent": "....", ... }
|
|
297
353
|
apiName, // In this function it would return "getCompanyName"
|
|
298
354
|
apiPayload, // Same as post.payload
|
|
355
|
+
session, // Stores session. Only available in addSessionRoute and addSessionApi, otherwise null.
|
|
299
356
|
} = ctx;
|
|
300
357
|
return res.json({});
|
|
301
358
|
});
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from
|
|
|
2
2
|
import LambderResolver from "./LambderResolver.js";
|
|
3
3
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
4
4
|
import LambderUtils from "./LambderUtils.js";
|
|
5
|
-
import { type LambderSessionContext } from "./
|
|
5
|
+
import { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
6
|
+
import LambderSessionController from "./LambderSessionController.js";
|
|
6
7
|
type Path = `/${string}`;
|
|
7
|
-
type LambderRenderContext = {
|
|
8
|
+
export type LambderRenderContext = {
|
|
8
9
|
host: string;
|
|
9
10
|
path: string;
|
|
10
11
|
pathParams: Record<string, any> | null;
|
|
@@ -17,6 +18,9 @@ type LambderRenderContext = {
|
|
|
17
18
|
headers: APIGatewayProxyEventHeaders;
|
|
18
19
|
session: LambderSessionContext | null;
|
|
19
20
|
lambdaContext: Context;
|
|
21
|
+
_otherInternal: {
|
|
22
|
+
isApiCall: boolean;
|
|
23
|
+
};
|
|
20
24
|
};
|
|
21
25
|
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
22
26
|
type ConditionFunction = (ctx: LambderRenderContext) => boolean;
|
|
@@ -41,7 +45,7 @@ export default class Lambder {
|
|
|
41
45
|
private routeFallbackHandler;
|
|
42
46
|
private apiFallbackHandler;
|
|
43
47
|
utils: LambderUtils;
|
|
44
|
-
private
|
|
48
|
+
private lambderSessionManager?;
|
|
45
49
|
private sessionTokenCookieKey;
|
|
46
50
|
private sessionCsrfCookieKey;
|
|
47
51
|
constructor({ publicPath, apiPath, ejsPath, apiVersion }: {
|
|
@@ -63,11 +67,8 @@ export default class Lambder {
|
|
|
63
67
|
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): void;
|
|
64
68
|
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): void;
|
|
65
69
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): void;
|
|
66
|
-
getPatternMatch
|
|
67
|
-
testPatternMatch
|
|
68
|
-
private fetchSession;
|
|
69
|
-
private validateSessionForRoute;
|
|
70
|
-
private validateSessionForAPI;
|
|
70
|
+
private getPatternMatch;
|
|
71
|
+
private testPatternMatch;
|
|
71
72
|
private handleNoMatchedAction;
|
|
72
73
|
addModule(moduleFn: LambderModuleFunction): Promise<void>;
|
|
73
74
|
importModule(moduleImport: Promise<{
|
|
@@ -81,12 +82,7 @@ export default class Lambder {
|
|
|
81
82
|
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
82
83
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
83
84
|
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
84
|
-
getSessionController(ctx: LambderRenderContext):
|
|
85
|
-
startSession: (userKey: string, data?: any, ttlInSeconds?: number) => Promise<LambderSessionContext>;
|
|
86
|
-
updateSessionData: (newData: any) => Promise<LambderSessionContext>;
|
|
87
|
-
deleteSession: () => Promise<void>;
|
|
88
|
-
deleteSessionAll: () => Promise<void>;
|
|
89
|
-
};
|
|
85
|
+
getSessionController(ctx: LambderRenderContext): LambderSessionController;
|
|
90
86
|
getResponseBuilder(): LambderResponseBuilder;
|
|
91
87
|
private getResolver;
|
|
92
88
|
render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
|
package/dist/Lambder.js
CHANGED
|
@@ -4,7 +4,8 @@ import { match } from "path-to-regexp";
|
|
|
4
4
|
import LambderResolver from "./LambderResolver.js";
|
|
5
5
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
6
6
|
import LambderUtils from "./LambderUtils.js";
|
|
7
|
-
import
|
|
7
|
+
import LambderSessionManager from "./LambderSessionManager.js";
|
|
8
|
+
import LambderSessionController from "./LambderSessionController.js";
|
|
8
9
|
export const createContext = (event, lambdaContext, apiPath) => {
|
|
9
10
|
const host = event.headers.Host || event.headers.host || "";
|
|
10
11
|
const path = event.path;
|
|
@@ -27,10 +28,16 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
27
28
|
}
|
|
28
29
|
catch (e) { }
|
|
29
30
|
// Parse api variables
|
|
30
|
-
const
|
|
31
|
-
const apiName =
|
|
32
|
-
const apiPayload =
|
|
33
|
-
return {
|
|
31
|
+
const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
32
|
+
const apiName = isApiCall ? post.apiName : null;
|
|
33
|
+
const apiPayload = isApiCall ? post.payload : null;
|
|
34
|
+
return {
|
|
35
|
+
host, path, pathParams, method,
|
|
36
|
+
get, post, cookie,
|
|
37
|
+
apiName, apiPayload,
|
|
38
|
+
headers, session, lambdaContext,
|
|
39
|
+
_otherInternal: { isApiCall }
|
|
40
|
+
};
|
|
34
41
|
};
|
|
35
42
|
export default class Lambder {
|
|
36
43
|
apiPath;
|
|
@@ -44,7 +51,7 @@ export default class Lambder {
|
|
|
44
51
|
routeFallbackHandler = null;
|
|
45
52
|
apiFallbackHandler = null;
|
|
46
53
|
utils;
|
|
47
|
-
|
|
54
|
+
lambderSessionManager;
|
|
48
55
|
sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
49
56
|
sessionCsrfCookieKey = "LMDRSESSIONCSTK";
|
|
50
57
|
constructor({ publicPath, apiPath, ejsPath, apiVersion }) {
|
|
@@ -64,7 +71,7 @@ export default class Lambder {
|
|
|
64
71
|
this.isCorsEnabled = isCorsEnabled;
|
|
65
72
|
}
|
|
66
73
|
enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
|
|
67
|
-
this.
|
|
74
|
+
this.lambderSessionManager = new LambderSessionManager({
|
|
68
75
|
tableName, tableRegion, partitionKey, sortKey, sessionSalt
|
|
69
76
|
});
|
|
70
77
|
}
|
|
@@ -90,32 +97,6 @@ export default class Lambder {
|
|
|
90
97
|
testPatternMatch(pattern, path) {
|
|
91
98
|
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
92
99
|
}
|
|
93
|
-
async fetchSession(ctx) {
|
|
94
|
-
if (!this.lambderSession)
|
|
95
|
-
throw "Session not found";
|
|
96
|
-
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
97
|
-
if (!sessionToken)
|
|
98
|
-
throw "Session not found";
|
|
99
|
-
const session = await this.lambderSession.getSession(sessionToken);
|
|
100
|
-
ctx.session = session;
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
;
|
|
104
|
-
validateSessionForRoute(ctx) {
|
|
105
|
-
if (!this.lambderSession)
|
|
106
|
-
return false;
|
|
107
|
-
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
108
|
-
return this.lambderSession.isSessionValid(ctx.session, sessionToken, null, true);
|
|
109
|
-
}
|
|
110
|
-
;
|
|
111
|
-
validateSessionForAPI(ctx) {
|
|
112
|
-
if (!this.lambderSession)
|
|
113
|
-
return false;
|
|
114
|
-
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
115
|
-
const csrfToken = ctx.post?.token;
|
|
116
|
-
return this.lambderSession.isSessionValid(ctx.session, sessionToken, csrfToken);
|
|
117
|
-
}
|
|
118
|
-
;
|
|
119
100
|
async handleNoMatchedAction(ctx, resolver) {
|
|
120
101
|
for (const hook of this.hookList["fallback"]) {
|
|
121
102
|
await hook.hookFn(ctx, resolver);
|
|
@@ -165,10 +146,7 @@ export default class Lambder {
|
|
|
165
146
|
(typeof condition === "function" && condition(ctx)) ||
|
|
166
147
|
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
167
148
|
actionFn: async (ctx, resolver) => {
|
|
168
|
-
await this.
|
|
169
|
-
const isSessionValid = this.validateSessionForRoute(ctx);
|
|
170
|
-
if (!isSessionValid)
|
|
171
|
-
throw new Error("Session not found");
|
|
149
|
+
await this.getSessionController(ctx).fetchSession();
|
|
172
150
|
if (typeof condition === "string") {
|
|
173
151
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
174
152
|
}
|
|
@@ -195,10 +173,7 @@ export default class Lambder {
|
|
|
195
173
|
(typeof apiName === "function" && apiName(ctx)) ||
|
|
196
174
|
(apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
|
|
197
175
|
actionFn: async (ctx, resolver) => {
|
|
198
|
-
await this.
|
|
199
|
-
const isSessionValid = this.validateSessionForAPI(ctx);
|
|
200
|
-
if (!isSessionValid)
|
|
201
|
-
throw new Error("Session not found");
|
|
176
|
+
await this.getSessionController(ctx).fetchSession();
|
|
202
177
|
return await actionFn(ctx, resolver);
|
|
203
178
|
}
|
|
204
179
|
});
|
|
@@ -214,38 +189,13 @@ export default class Lambder {
|
|
|
214
189
|
}
|
|
215
190
|
}
|
|
216
191
|
getSessionController(ctx) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
updateSessionData: async (newData) => {
|
|
225
|
-
if (!this.lambderSession)
|
|
226
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
|
|
227
|
-
if (!ctx.session)
|
|
228
|
-
throw "Session not found.";
|
|
229
|
-
ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
|
|
230
|
-
return ctx.session;
|
|
231
|
-
},
|
|
232
|
-
deleteSession: async () => {
|
|
233
|
-
if (!this.lambderSession)
|
|
234
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
|
|
235
|
-
if (!ctx.session)
|
|
236
|
-
throw "Session not found.";
|
|
237
|
-
await this.lambderSession.deleteSession(ctx.session);
|
|
238
|
-
ctx.session = null;
|
|
239
|
-
},
|
|
240
|
-
deleteSessionAll: async () => {
|
|
241
|
-
if (!this.lambderSession)
|
|
242
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
|
|
243
|
-
if (!ctx.session)
|
|
244
|
-
throw "Session not found.";
|
|
245
|
-
await this.lambderSession.deleteSessionAll(ctx.session);
|
|
246
|
-
ctx.session = null;
|
|
247
|
-
},
|
|
248
|
-
};
|
|
192
|
+
if (!this.lambderSessionManager)
|
|
193
|
+
throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
194
|
+
return new LambderSessionController({
|
|
195
|
+
lambderSessionManager: this.lambderSessionManager,
|
|
196
|
+
sessionTokenCookieKey: this.sessionTokenCookieKey,
|
|
197
|
+
ctx,
|
|
198
|
+
});
|
|
249
199
|
}
|
|
250
200
|
getResponseBuilder() {
|
|
251
201
|
return new LambderResponseBuilder({
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LambderRenderContext } from "./Lambder.js";
|
|
2
|
+
import type LambderSessionManager from "./LambderSessionManager.js";
|
|
3
|
+
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
4
|
+
export default class LambderSessionController {
|
|
5
|
+
lambderSessionManager: LambderSessionManager;
|
|
6
|
+
sessionTokenCookieKey: string;
|
|
7
|
+
ctx: LambderRenderContext;
|
|
8
|
+
constructor({ lambderSessionManager, sessionTokenCookieKey, ctx, }: {
|
|
9
|
+
lambderSessionManager: LambderSessionManager;
|
|
10
|
+
sessionTokenCookieKey: string;
|
|
11
|
+
ctx: LambderRenderContext;
|
|
12
|
+
});
|
|
13
|
+
private areRequestSessionTokensValid;
|
|
14
|
+
createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
|
|
15
|
+
fetchSession(): Promise<LambderSessionContext>;
|
|
16
|
+
fetchSessionIfExists(): Promise<LambderSessionContext | null>;
|
|
17
|
+
isSessionValid(session: any): boolean;
|
|
18
|
+
updateSessionData(newData: any): Promise<LambderSessionContext>;
|
|
19
|
+
endSession(): Promise<void>;
|
|
20
|
+
endSessionAll(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export default class LambderSessionController {
|
|
2
|
+
lambderSessionManager;
|
|
3
|
+
sessionTokenCookieKey;
|
|
4
|
+
ctx;
|
|
5
|
+
constructor({ lambderSessionManager, sessionTokenCookieKey, ctx, }) {
|
|
6
|
+
this.ctx = ctx;
|
|
7
|
+
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
8
|
+
this.lambderSessionManager = lambderSessionManager;
|
|
9
|
+
}
|
|
10
|
+
;
|
|
11
|
+
areRequestSessionTokensValid() {
|
|
12
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
13
|
+
const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
|
|
14
|
+
if (this.ctx._otherInternal.isApiCall) {
|
|
15
|
+
const csrfToken = this.ctx.post?.token;
|
|
16
|
+
const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0;
|
|
17
|
+
return isSessionTokenValid && isCsrfTokenValid;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
return isSessionTokenValid;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
;
|
|
24
|
+
async createSession(sessionKey, data, ttlInSeconds) {
|
|
25
|
+
this.ctx.session = await this.lambderSessionManager.createSession(sessionKey, data, ttlInSeconds);
|
|
26
|
+
return this.ctx.session;
|
|
27
|
+
}
|
|
28
|
+
;
|
|
29
|
+
async fetchSession() {
|
|
30
|
+
if (!this.areRequestSessionTokensValid()) {
|
|
31
|
+
throw new Error("Session tokens are invalid");
|
|
32
|
+
}
|
|
33
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
34
|
+
if (!sessionToken)
|
|
35
|
+
throw new Error("Session token not found");
|
|
36
|
+
const session = await this.lambderSessionManager.getSession(sessionToken);
|
|
37
|
+
if (!session)
|
|
38
|
+
throw new Error("Session not found");
|
|
39
|
+
if (!this.isSessionValid(session))
|
|
40
|
+
throw new Error("Invalid session");
|
|
41
|
+
this.ctx.session = session;
|
|
42
|
+
return session;
|
|
43
|
+
}
|
|
44
|
+
;
|
|
45
|
+
async fetchSessionIfExists() {
|
|
46
|
+
try {
|
|
47
|
+
return await this.fetchSession();
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
;
|
|
54
|
+
isSessionValid(session) {
|
|
55
|
+
if (this.ctx._otherInternal.isApiCall) {
|
|
56
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
57
|
+
const csrfToken = this.ctx.post?.token;
|
|
58
|
+
return this.lambderSessionManager.isSessionValid(session, sessionToken, csrfToken);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
62
|
+
return this.lambderSessionManager.isSessionValid(session, sessionToken, null, true);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
;
|
|
66
|
+
async updateSessionData(newData) {
|
|
67
|
+
if (!this.ctx.session)
|
|
68
|
+
throw new Error("Session not found.");
|
|
69
|
+
this.ctx.session = await this.lambderSessionManager.updateSessionData(this.ctx.session, newData);
|
|
70
|
+
return this.ctx.session;
|
|
71
|
+
}
|
|
72
|
+
;
|
|
73
|
+
async endSession() {
|
|
74
|
+
if (!this.ctx.session)
|
|
75
|
+
throw new Error("Session not found.");
|
|
76
|
+
await this.lambderSessionManager.deleteSession(this.ctx.session);
|
|
77
|
+
this.ctx.session = null;
|
|
78
|
+
}
|
|
79
|
+
;
|
|
80
|
+
async endSessionAll() {
|
|
81
|
+
if (!this.ctx.session)
|
|
82
|
+
throw new Error("Session not found.");
|
|
83
|
+
await this.lambderSessionManager.deleteSessionAll(this.ctx.session);
|
|
84
|
+
this.ctx.session = null;
|
|
85
|
+
}
|
|
86
|
+
;
|
|
87
|
+
}
|
|
88
|
+
;
|
|
@@ -2,13 +2,13 @@ export type LambderSessionContext = {
|
|
|
2
2
|
[x: string]: any;
|
|
3
3
|
sessionToken: string;
|
|
4
4
|
csrfToken: string;
|
|
5
|
-
|
|
5
|
+
sessionKey: string;
|
|
6
6
|
data: any;
|
|
7
7
|
createdAt: number;
|
|
8
8
|
expiresAt: number;
|
|
9
9
|
ttlInSeconds: number;
|
|
10
10
|
};
|
|
11
|
-
export default class
|
|
11
|
+
export default class LambderSessionManager {
|
|
12
12
|
private tableName;
|
|
13
13
|
private sessionSalt;
|
|
14
14
|
private partitionKey;
|
|
@@ -27,7 +27,7 @@ export default class LambderSession {
|
|
|
27
27
|
private ddbDeleteItem;
|
|
28
28
|
private ddbQueryAllByPartitionKey;
|
|
29
29
|
private ddbDeleteAllByPartitionKey;
|
|
30
|
-
createSession(
|
|
30
|
+
createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
|
|
31
31
|
updateSessionData(session: LambderSessionContext, newData?: any): Promise<LambderSessionContext>;
|
|
32
32
|
getSession(sessionToken: string): Promise<LambderSessionContext | null>;
|
|
33
33
|
isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck?: boolean): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
3
|
import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
4
|
-
export default class
|
|
4
|
+
export default class LambderSessionManager {
|
|
5
5
|
tableName;
|
|
6
6
|
sessionSalt;
|
|
7
7
|
partitionKey;
|
|
@@ -63,18 +63,18 @@ export default class LambderSession {
|
|
|
63
63
|
}));
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
async createSession(
|
|
67
|
-
const
|
|
66
|
+
async createSession(sessionKey, data = {}, ttlInSeconds = 30 * 24 * 60 * 60) {
|
|
67
|
+
const sessionKeyHash = this.sessionUserKeyHasher(sessionKey);
|
|
68
68
|
const sessionSortKey = crypto.randomBytes(32).toString("hex");
|
|
69
|
-
const sessionToken = `${
|
|
69
|
+
const sessionToken = `${sessionKeyHash}:${sessionSortKey}`;
|
|
70
70
|
const csrfToken = crypto.randomBytes(8).toString("hex");
|
|
71
71
|
const createdAt = Math.floor(Date.now() / 1000);
|
|
72
72
|
const expiresAt = Number(createdAt) + Number(ttlInSeconds);
|
|
73
73
|
const session = {
|
|
74
|
-
[this.partitionKey]:
|
|
74
|
+
[this.partitionKey]: sessionKeyHash,
|
|
75
75
|
[this.sortKey]: sessionSortKey,
|
|
76
76
|
sessionToken, csrfToken,
|
|
77
|
-
|
|
77
|
+
sessionKey, data,
|
|
78
78
|
createdAt, expiresAt, ttlInSeconds
|
|
79
79
|
};
|
|
80
80
|
await this.ddbPutItem(session);
|
|
@@ -82,32 +82,32 @@ export default class LambderSession {
|
|
|
82
82
|
}
|
|
83
83
|
async updateSessionData(session, newData) {
|
|
84
84
|
if (!session)
|
|
85
|
-
throw "Invalid session";
|
|
85
|
+
throw new Error("Invalid session");
|
|
86
86
|
session.data = newData;
|
|
87
87
|
await this.ddbPutItem(session);
|
|
88
88
|
return session;
|
|
89
89
|
}
|
|
90
90
|
async getSession(sessionToken) {
|
|
91
|
-
const [
|
|
92
|
-
if (!
|
|
91
|
+
const [sessionKeyHash, sessionSortKey] = sessionToken.split(":");
|
|
92
|
+
if (!sessionKeyHash || !sessionSortKey)
|
|
93
93
|
return null;
|
|
94
94
|
try {
|
|
95
95
|
let session = await this.ddbGetItem({
|
|
96
|
-
[this.partitionKey]:
|
|
96
|
+
[this.partitionKey]: sessionKeyHash,
|
|
97
97
|
[this.sortKey]: sessionSortKey
|
|
98
98
|
});
|
|
99
99
|
if (!session)
|
|
100
|
-
throw "Session not found";
|
|
100
|
+
throw new Error("Session not found");
|
|
101
101
|
if (!session.sessionToken || session.sessionToken !== sessionToken)
|
|
102
|
-
throw "Not found: session.sessionToken";
|
|
102
|
+
throw new Error("Not found: session.sessionToken");
|
|
103
103
|
if (!session.csrfToken)
|
|
104
|
-
throw "Not found: session.csrfToken";
|
|
105
|
-
if (!session.
|
|
106
|
-
throw "Not found: session.
|
|
104
|
+
throw new Error("Not found: session.csrfToken");
|
|
105
|
+
if (!session.sessionKey)
|
|
106
|
+
throw new Error("Not found: session.sessionKey");
|
|
107
107
|
if (!session.createdAt)
|
|
108
|
-
throw "Not found: session.createdAt";
|
|
108
|
+
throw new Error("Not found: session.createdAt");
|
|
109
109
|
if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
|
|
110
|
-
throw "Not found: session.expiresAt";
|
|
110
|
+
throw new Error("Not found: session.expiresAt");
|
|
111
111
|
return session;
|
|
112
112
|
}
|
|
113
113
|
catch (err) {
|
|
@@ -124,7 +124,7 @@ export default class LambderSession {
|
|
|
124
124
|
return false;
|
|
125
125
|
if (!session.csrfToken)
|
|
126
126
|
return false;
|
|
127
|
-
if (!session.
|
|
127
|
+
if (!session.sessionKey)
|
|
128
128
|
return false;
|
|
129
129
|
if (!session.createdAt)
|
|
130
130
|
return false;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export default Lambder;
|
|
|
3
3
|
export { default as LambderCaller } from "./LambderCaller.js";
|
|
4
4
|
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
5
5
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
6
|
-
export { default as
|
|
6
|
+
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
package/dist/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export default Lambder;
|
|
|
3
3
|
export { default as LambderCaller } from "./LambderCaller.js";
|
|
4
4
|
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
5
5
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
6
|
-
export { default as
|
|
6
|
+
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
package/package.json
CHANGED
package/src/Lambder.ts
CHANGED
|
@@ -6,11 +6,12 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from
|
|
|
6
6
|
import LambderResolver from "./LambderResolver.js";
|
|
7
7
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
8
8
|
import LambderUtils from "./LambderUtils.js";
|
|
9
|
-
import
|
|
9
|
+
import LambderSessionManager, { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
10
|
+
import LambderSessionController from "./LambderSessionController.js";
|
|
10
11
|
|
|
11
12
|
type Path = `/${string}`;
|
|
12
13
|
|
|
13
|
-
type LambderRenderContext = {
|
|
14
|
+
export type LambderRenderContext = {
|
|
14
15
|
host: string;
|
|
15
16
|
path: string;
|
|
16
17
|
pathParams: Record<string, any> | null;
|
|
@@ -23,6 +24,7 @@ type LambderRenderContext = {
|
|
|
23
24
|
headers: APIGatewayProxyEventHeaders;
|
|
24
25
|
session: LambderSessionContext|null;
|
|
25
26
|
lambdaContext: Context;
|
|
27
|
+
_otherInternal: { isApiCall: boolean };
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
@@ -55,6 +57,7 @@ export const createContext = (
|
|
|
55
57
|
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
56
58
|
const headers = event.headers;
|
|
57
59
|
const session = null;
|
|
60
|
+
|
|
58
61
|
// Decode body for the post
|
|
59
62
|
let post: Record<string, any> = {};
|
|
60
63
|
try {
|
|
@@ -63,11 +66,18 @@ export const createContext = (
|
|
|
63
66
|
catch(e){ post = querystring.parse(decodedBody) || {}; }
|
|
64
67
|
}catch(e){}
|
|
65
68
|
// Parse api variables
|
|
66
|
-
const isAPICall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
67
|
-
const apiName:string = isAPICall ? post.apiName : null;
|
|
68
|
-
const apiPayload:string = isAPICall ? post.payload : null;
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
71
|
+
const apiName:string = isApiCall ? post.apiName : null;
|
|
72
|
+
const apiPayload:string = isApiCall ? post.payload : null;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
host, path, pathParams, method,
|
|
76
|
+
get, post, cookie,
|
|
77
|
+
apiName, apiPayload,
|
|
78
|
+
headers, session, lambdaContext,
|
|
79
|
+
_otherInternal: { isApiCall }
|
|
80
|
+
};
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
|
|
@@ -90,7 +100,7 @@ export default class Lambder {
|
|
|
90
100
|
|
|
91
101
|
public utils: LambderUtils;
|
|
92
102
|
|
|
93
|
-
private
|
|
103
|
+
private lambderSessionManager?: LambderSessionManager;
|
|
94
104
|
private sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
95
105
|
private sessionCsrfCookieKey = "LMDRSESSIONCSTK";
|
|
96
106
|
|
|
@@ -121,7 +131,7 @@ export default class Lambder {
|
|
|
121
131
|
{ tableName, tableRegion, sessionSalt }: { tableName: string; tableRegion: string; sessionSalt: string; },
|
|
122
132
|
{ partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
|
|
123
133
|
){
|
|
124
|
-
this.
|
|
134
|
+
this.lambderSessionManager = new LambderSessionManager({
|
|
125
135
|
tableName, tableRegion, partitionKey, sortKey, sessionSalt
|
|
126
136
|
});
|
|
127
137
|
}
|
|
@@ -140,37 +150,15 @@ export default class Lambder {
|
|
|
140
150
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction){
|
|
141
151
|
this.globalErrorHandler = globalErrorHandler;
|
|
142
152
|
}
|
|
143
|
-
getPatternMatch(pattern: string, path: string): Record<string, any> {
|
|
153
|
+
private getPatternMatch(pattern: string, path: string): Record<string, any> {
|
|
144
154
|
const result = (match(pattern, { decode: decodeURIComponent }))(path);
|
|
145
155
|
if(!result) return {};
|
|
146
156
|
return result?.params || {};
|
|
147
157
|
}
|
|
148
|
-
testPatternMatch(pattern: string, path: string): boolean{
|
|
158
|
+
private testPatternMatch(pattern: string, path: string): boolean{
|
|
149
159
|
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
150
160
|
}
|
|
151
|
-
|
|
152
|
-
private async fetchSession (ctx: LambderRenderContext): Promise<boolean>{
|
|
153
|
-
if(!this.lambderSession) throw "Session not found";
|
|
154
|
-
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
155
|
-
if(!sessionToken) throw "Session not found";
|
|
156
|
-
const session = await this.lambderSession.getSession(sessionToken);
|
|
157
|
-
ctx.session = session;
|
|
158
|
-
return true;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
private validateSessionForRoute (ctx: LambderRenderContext): boolean{
|
|
162
|
-
if(!this.lambderSession) return false;
|
|
163
|
-
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
164
|
-
return this.lambderSession.isSessionValid(ctx.session, sessionToken, null, true);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
private validateSessionForAPI (ctx: LambderRenderContext): boolean{
|
|
168
|
-
if(!this.lambderSession) return false;
|
|
169
|
-
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
170
|
-
const csrfToken = ctx.post?.token;
|
|
171
|
-
return this.lambderSession.isSessionValid(ctx.session, sessionToken, csrfToken);
|
|
172
|
-
};
|
|
173
|
-
|
|
161
|
+
|
|
174
162
|
private async handleNoMatchedAction(ctx: LambderRenderContext, resolver: LambderResolver){
|
|
175
163
|
for(const hook of this.hookList["fallback"]){ await hook.hookFn(ctx, resolver); }
|
|
176
164
|
|
|
@@ -227,9 +215,8 @@ export default class Lambder {
|
|
|
227
215
|
)
|
|
228
216
|
),
|
|
229
217
|
actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
|
|
230
|
-
await this.
|
|
231
|
-
|
|
232
|
-
if(!isSessionValid) throw new Error("Session not found");
|
|
218
|
+
await this.getSessionController(ctx).fetchSession();
|
|
219
|
+
|
|
233
220
|
if(typeof condition === "string"){
|
|
234
221
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
235
222
|
}else if(condition?.constructor == RegExp){
|
|
@@ -263,9 +250,7 @@ export default class Lambder {
|
|
|
263
250
|
)
|
|
264
251
|
),
|
|
265
252
|
actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
|
|
266
|
-
await this.
|
|
267
|
-
const isSessionValid = this.validateSessionForAPI(ctx);
|
|
268
|
-
if(!isSessionValid) throw new Error("Session not found");
|
|
253
|
+
await this.getSessionController(ctx).fetchSession();
|
|
269
254
|
return await actionFn(ctx, resolver);
|
|
270
255
|
}
|
|
271
256
|
});
|
|
@@ -288,35 +273,17 @@ export default class Lambder {
|
|
|
288
273
|
}
|
|
289
274
|
}
|
|
290
275
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
|
|
300
|
-
if(!ctx.session) throw "Session not found.";
|
|
301
|
-
ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
|
|
302
|
-
return ctx.session;
|
|
303
|
-
},
|
|
304
|
-
deleteSession: async () => {
|
|
305
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
|
|
306
|
-
if(!ctx.session) throw "Session not found.";
|
|
307
|
-
await this.lambderSession.deleteSession(ctx.session);
|
|
308
|
-
ctx.session = null
|
|
309
|
-
},
|
|
310
|
-
deleteSessionAll: async () => {
|
|
311
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
|
|
312
|
-
if(!ctx.session) throw "Session not found.";
|
|
313
|
-
await this.lambderSession.deleteSessionAll(ctx.session);
|
|
314
|
-
ctx.session = null
|
|
315
|
-
},
|
|
316
|
-
}
|
|
276
|
+
getSessionController(ctx: LambderRenderContext): LambderSessionController{
|
|
277
|
+
if(!this.lambderSessionManager) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
278
|
+
|
|
279
|
+
return new LambderSessionController({
|
|
280
|
+
lambderSessionManager: this.lambderSessionManager,
|
|
281
|
+
sessionTokenCookieKey: this.sessionTokenCookieKey,
|
|
282
|
+
ctx,
|
|
283
|
+
});
|
|
317
284
|
}
|
|
318
285
|
|
|
319
|
-
|
|
286
|
+
getResponseBuilder(){
|
|
320
287
|
return new LambderResponseBuilder({
|
|
321
288
|
isCorsEnabled: this.isCorsEnabled,
|
|
322
289
|
publicPath: this.publicPath,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { LambderRenderContext } from "./Lambder.js";
|
|
2
|
+
import type LambderSessionManager from "./LambderSessionManager.js";
|
|
3
|
+
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
4
|
+
|
|
5
|
+
export default class LambderSessionController {
|
|
6
|
+
lambderSessionManager: LambderSessionManager;
|
|
7
|
+
sessionTokenCookieKey: string;
|
|
8
|
+
ctx: LambderRenderContext;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
{
|
|
12
|
+
lambderSessionManager,
|
|
13
|
+
sessionTokenCookieKey,
|
|
14
|
+
ctx,
|
|
15
|
+
}: {
|
|
16
|
+
lambderSessionManager: LambderSessionManager,
|
|
17
|
+
sessionTokenCookieKey: string,
|
|
18
|
+
ctx: LambderRenderContext,
|
|
19
|
+
}
|
|
20
|
+
){
|
|
21
|
+
this.ctx = ctx;
|
|
22
|
+
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
23
|
+
this.lambderSessionManager = lambderSessionManager;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
private areRequestSessionTokensValid(): boolean {
|
|
27
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
28
|
+
const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
|
|
29
|
+
|
|
30
|
+
if(this.ctx._otherInternal.isApiCall){
|
|
31
|
+
const csrfToken = this.ctx.post?.token;
|
|
32
|
+
const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0
|
|
33
|
+
return isSessionTokenValid && isCsrfTokenValid;
|
|
34
|
+
}else{
|
|
35
|
+
return isSessionTokenValid;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
async createSession (sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext> {
|
|
40
|
+
this.ctx.session = await this.lambderSessionManager.createSession(sessionKey, data, ttlInSeconds);
|
|
41
|
+
return this.ctx.session;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async fetchSession (): Promise<LambderSessionContext>{
|
|
45
|
+
if(!this.areRequestSessionTokensValid()){ throw new Error("Session tokens are invalid"); }
|
|
46
|
+
|
|
47
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
48
|
+
if(!sessionToken) throw new Error("Session token not found");
|
|
49
|
+
|
|
50
|
+
const session = await this.lambderSessionManager.getSession(sessionToken);
|
|
51
|
+
if(!session) throw new Error("Session not found");
|
|
52
|
+
|
|
53
|
+
if(!this.isSessionValid(session)) throw new Error("Invalid session");
|
|
54
|
+
this.ctx.session = session;
|
|
55
|
+
return session;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
async fetchSessionIfExists (): Promise<LambderSessionContext|null> {
|
|
59
|
+
try {
|
|
60
|
+
return await this.fetchSession();
|
|
61
|
+
}catch(err){
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
isSessionValid(session: any): boolean {
|
|
67
|
+
if(this.ctx._otherInternal.isApiCall){
|
|
68
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
69
|
+
const csrfToken = this.ctx.post?.token;
|
|
70
|
+
return this.lambderSessionManager.isSessionValid(session, sessionToken, csrfToken);
|
|
71
|
+
}else{
|
|
72
|
+
const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
|
|
73
|
+
return this.lambderSessionManager.isSessionValid(session, sessionToken, null, true);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
async updateSessionData (newData: any): Promise<LambderSessionContext> {
|
|
78
|
+
if(!this.ctx.session) throw new Error("Session not found.");
|
|
79
|
+
this.ctx.session = await this.lambderSessionManager.updateSessionData(this.ctx.session, newData);
|
|
80
|
+
return this.ctx.session;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
async endSession (){
|
|
84
|
+
if(!this.ctx.session) throw new Error("Session not found.");
|
|
85
|
+
await this.lambderSessionManager.deleteSession(this.ctx.session);
|
|
86
|
+
this.ctx.session = null
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
async endSessionAll (){
|
|
90
|
+
if(!this.ctx.session) throw new Error("Session not found.");
|
|
91
|
+
await this.lambderSessionManager.deleteSessionAll(this.ctx.session);
|
|
92
|
+
this.ctx.session = null
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -6,7 +6,7 @@ export type LambderSessionContext = {
|
|
|
6
6
|
[x: string]: any;
|
|
7
7
|
sessionToken: string;
|
|
8
8
|
csrfToken: string;
|
|
9
|
-
|
|
9
|
+
sessionKey: string;
|
|
10
10
|
data: any;
|
|
11
11
|
createdAt: number;
|
|
12
12
|
expiresAt: number;
|
|
@@ -14,7 +14,7 @@ export type LambderSessionContext = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
export default class
|
|
17
|
+
export default class LambderSessionManager{
|
|
18
18
|
private tableName: string;
|
|
19
19
|
private sessionSalt: string;
|
|
20
20
|
private partitionKey: string;
|
|
@@ -94,22 +94,22 @@ export default class LambderSession{
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
public async createSession(
|
|
97
|
-
|
|
97
|
+
sessionKey: string,
|
|
98
98
|
data: any = {},
|
|
99
99
|
ttlInSeconds:number = 30*24*60*60
|
|
100
100
|
): Promise<LambderSessionContext> {
|
|
101
|
-
const
|
|
101
|
+
const sessionKeyHash = this.sessionUserKeyHasher(sessionKey);
|
|
102
102
|
const sessionSortKey = crypto.randomBytes(32).toString("hex");
|
|
103
|
-
const sessionToken = `${
|
|
103
|
+
const sessionToken = `${sessionKeyHash}:${sessionSortKey}`;
|
|
104
104
|
const csrfToken = crypto.randomBytes(8).toString("hex");
|
|
105
105
|
const createdAt = Math.floor(Date.now()/1000);
|
|
106
106
|
const expiresAt = Number(createdAt) + Number(ttlInSeconds);
|
|
107
107
|
|
|
108
108
|
const session = {
|
|
109
|
-
[this.partitionKey]:
|
|
109
|
+
[this.partitionKey]: sessionKeyHash,
|
|
110
110
|
[this.sortKey]: sessionSortKey,
|
|
111
111
|
sessionToken, csrfToken,
|
|
112
|
-
|
|
112
|
+
sessionKey, data,
|
|
113
113
|
createdAt, expiresAt, ttlInSeconds
|
|
114
114
|
};
|
|
115
115
|
await this.ddbPutItem(session);
|
|
@@ -120,26 +120,26 @@ export default class LambderSession{
|
|
|
120
120
|
session: LambderSessionContext,
|
|
121
121
|
newData?: any
|
|
122
122
|
): Promise<LambderSessionContext> {
|
|
123
|
-
if(!session) throw "Invalid session";
|
|
123
|
+
if(!session) throw new Error("Invalid session");
|
|
124
124
|
session.data = newData;
|
|
125
125
|
await this.ddbPutItem(session);
|
|
126
126
|
return session;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
public async getSession(sessionToken: string): Promise<LambderSessionContext|null>{
|
|
130
|
-
const [
|
|
131
|
-
if(!
|
|
130
|
+
const [ sessionKeyHash, sessionSortKey ] = sessionToken.split(":");
|
|
131
|
+
if(!sessionKeyHash || !sessionSortKey) return null;
|
|
132
132
|
try{
|
|
133
133
|
let session = await this.ddbGetItem({
|
|
134
|
-
[this.partitionKey]:
|
|
134
|
+
[this.partitionKey]: sessionKeyHash,
|
|
135
135
|
[this.sortKey]: sessionSortKey
|
|
136
136
|
});
|
|
137
|
-
if(!session) throw "Session not found";
|
|
138
|
-
if(!session.sessionToken || session.sessionToken !== sessionToken) throw "Not found: session.sessionToken";
|
|
139
|
-
if(!session.csrfToken) throw "Not found: session.csrfToken";
|
|
140
|
-
if(!session.
|
|
141
|
-
if(!session.createdAt) throw "Not found: session.createdAt";
|
|
142
|
-
if(!session.expiresAt || session.expiresAt < Date.now()/1000) throw "Not found: session.expiresAt";
|
|
137
|
+
if(!session) throw new Error("Session not found");
|
|
138
|
+
if(!session.sessionToken || session.sessionToken !== sessionToken) throw new Error("Not found: session.sessionToken");
|
|
139
|
+
if(!session.csrfToken) throw new Error("Not found: session.csrfToken");
|
|
140
|
+
if(!session.sessionKey) throw new Error("Not found: session.sessionKey");
|
|
141
|
+
if(!session.createdAt) throw new Error("Not found: session.createdAt");
|
|
142
|
+
if(!session.expiresAt || session.expiresAt < Date.now()/1000) throw new Error("Not found: session.expiresAt");
|
|
143
143
|
return session;
|
|
144
144
|
}catch(err){
|
|
145
145
|
return null;
|
|
@@ -151,7 +151,7 @@ export default class LambderSession{
|
|
|
151
151
|
if(!sessionToken || typeof sessionToken !== "string") return false;
|
|
152
152
|
if(session.sessionToken !== sessionToken) return false;
|
|
153
153
|
if(!session.csrfToken) return false;
|
|
154
|
-
if(!session.
|
|
154
|
+
if(!session.sessionKey) return false;
|
|
155
155
|
if(!session.createdAt) return false;
|
|
156
156
|
if(!session.expiresAt || session.expiresAt < Date.now()/1000) return false;
|
|
157
157
|
if(!skipCsrfTokenCheck){
|
package/src/index.ts
CHANGED
|
@@ -4,4 +4,4 @@ export default Lambder;
|
|
|
4
4
|
export { default as LambderCaller } from "./LambderCaller.js";
|
|
5
5
|
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
6
6
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
7
|
-
export { default as
|
|
7
|
+
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|