lambder 1.0.114 → 1.0.115
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 +63 -10
- package/dist/Lambder.d.ts +10 -8
- package/dist/Lambder.js +62 -33
- package/dist/LambderSession.d.ts +2 -2
- package/dist/LambderSession.js +17 -17
- package/package.json +1 -1
- package/src/Lambder.ts +67 -36
- package/src/LambderSession.ts +17 -17
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,77 @@ 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
|
+
|
|
247
|
+
async updateSessionData(updatedData):
|
|
248
|
+
// Updates the active sessions data and persist it to ddb.
|
|
249
|
+
|
|
250
|
+
async endSession(): Start a new session
|
|
251
|
+
// End session and delete from DDB.
|
|
252
|
+
|
|
253
|
+
async endSessionAll(): Start a new session
|
|
254
|
+
// Ends and deletes all registered sessions for this sessionKey across all devices
|
|
255
|
+
}
|
|
256
|
+
*/
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
#### Session Examples
|
|
262
|
+
```typescript
|
|
263
|
+
lambder.addApi("getCompanyPage", async (ctx, res) => {
|
|
264
|
+
|
|
265
|
+
// createSession: Start a new session
|
|
224
266
|
const userId = "37234";
|
|
225
|
-
await
|
|
226
|
-
|
|
267
|
+
await lambder.getSessionController(ctx)
|
|
268
|
+
.createSession(userId, { "business": "Session data goes here" });
|
|
269
|
+
console.log(ctx.session?.sessionKey); // "37234"
|
|
227
270
|
console.log(ctx.session?.data?.business); // "Session data goes here"
|
|
228
271
|
|
|
229
|
-
|
|
230
|
-
|
|
272
|
+
// fetchSession: Fetch and validate if there is an existing session
|
|
273
|
+
// This is automatically done for addSessionRoute and addSessionApi
|
|
274
|
+
await lambder.getSessionController(ctx).fetchSession();
|
|
275
|
+
console.log(ctx.session?.sessionKey); // "37234"
|
|
276
|
+
|
|
277
|
+
// updateSessionData: Updates the active sessions data and persist it to ddb.
|
|
278
|
+
await lambder.getSessionController(ctx)
|
|
279
|
+
.updateSessionData({ "business2": "Session data updated" });
|
|
280
|
+
console.log(ctx.session?.sessionKey); // "37234"
|
|
231
281
|
console.log(ctx.session?.data?.business); // undefined
|
|
232
282
|
console.log(ctx.session?.data?.business2); // "Session data updated"
|
|
233
283
|
|
|
234
|
-
|
|
235
|
-
|
|
284
|
+
// endSession: Ends the session and removes it from ddb
|
|
285
|
+
await lambder.getSessionController(ctx).endSession(); // End session
|
|
286
|
+
console.log(ctx.session?.sessionKey); // undefined
|
|
236
287
|
console.log(ctx.session?.data?.business); // undefined
|
|
237
288
|
|
|
238
|
-
|
|
239
|
-
|
|
289
|
+
// endSessionAll: Ends all registered sessions for this user in all devices.
|
|
290
|
+
await lambder.getSessionController(ctx).endSessionAll();
|
|
291
|
+
console.log(ctx.session?.sessionKey); // undefined
|
|
240
292
|
console.log(ctx.session?.data?.business); // undefined
|
|
241
293
|
|
|
242
294
|
});
|
|
@@ -296,6 +348,7 @@ lambder.addApi("getCompanyName", async (ctx, res) => {
|
|
|
296
348
|
headers, // Request Headers in an object. Exp: { "User-Agent": "....", ... }
|
|
297
349
|
apiName, // In this function it would return "getCompanyName"
|
|
298
350
|
apiPayload, // Same as post.payload
|
|
351
|
+
session, // Stores session. Only available in addSessionRoute and addSessionApi, otherwise null.
|
|
299
352
|
} = ctx;
|
|
300
353
|
return res.json({});
|
|
301
354
|
});
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -63,11 +63,12 @@ export default class Lambder {
|
|
|
63
63
|
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): void;
|
|
64
64
|
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): void;
|
|
65
65
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): void;
|
|
66
|
-
getPatternMatch
|
|
67
|
-
testPatternMatch
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
private
|
|
66
|
+
private getPatternMatch;
|
|
67
|
+
private testPatternMatch;
|
|
68
|
+
fetchSessionIfExist(ctx: LambderRenderContext): Promise<LambderSessionContext | null>;
|
|
69
|
+
fetchSession(ctx: LambderRenderContext): Promise<LambderSessionContext>;
|
|
70
|
+
private areRequestSessionTokensValid;
|
|
71
|
+
private isSessionValid;
|
|
71
72
|
private handleNoMatchedAction;
|
|
72
73
|
addModule(moduleFn: LambderModuleFunction): Promise<void>;
|
|
73
74
|
importModule(moduleImport: Promise<{
|
|
@@ -82,10 +83,11 @@ export default class Lambder {
|
|
|
82
83
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
83
84
|
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
84
85
|
getSessionController(ctx: LambderRenderContext): {
|
|
85
|
-
|
|
86
|
+
createSession: (sessionKey: string, data?: any, ttlInSeconds?: number) => Promise<LambderSessionContext>;
|
|
87
|
+
fetchSession: () => Promise<LambderSessionContext>;
|
|
86
88
|
updateSessionData: (newData: any) => Promise<LambderSessionContext>;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
endSession: () => Promise<void>;
|
|
90
|
+
endSessionAll: () => Promise<void>;
|
|
89
91
|
};
|
|
90
92
|
getResponseBuilder(): LambderResponseBuilder;
|
|
91
93
|
private getResolver;
|
package/dist/Lambder.js
CHANGED
|
@@ -5,6 +5,9 @@ import LambderResolver from "./LambderResolver.js";
|
|
|
5
5
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
6
6
|
import LambderUtils from "./LambderUtils.js";
|
|
7
7
|
import LambderSession from "./LambderSession.js";
|
|
8
|
+
const isApiCallChecker = (method, path, post, apiPath) => {
|
|
9
|
+
return method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
10
|
+
};
|
|
8
11
|
export const createContext = (event, lambdaContext, apiPath) => {
|
|
9
12
|
const host = event.headers.Host || event.headers.host || "";
|
|
10
13
|
const path = event.path;
|
|
@@ -27,9 +30,9 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
27
30
|
}
|
|
28
31
|
catch (e) { }
|
|
29
32
|
// Parse api variables
|
|
30
|
-
const
|
|
31
|
-
const apiName =
|
|
32
|
-
const apiPayload =
|
|
33
|
+
const isApiCall = isApiCallChecker(method, path, post, apiPath);
|
|
34
|
+
const apiName = isApiCall ? post.apiName : null;
|
|
35
|
+
const apiPayload = isApiCall ? post.payload : null;
|
|
33
36
|
return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
|
|
34
37
|
};
|
|
35
38
|
export default class Lambder {
|
|
@@ -90,32 +93,59 @@ export default class Lambder {
|
|
|
90
93
|
testPatternMatch(pattern, path) {
|
|
91
94
|
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
92
95
|
}
|
|
96
|
+
async fetchSessionIfExist(ctx) {
|
|
97
|
+
try {
|
|
98
|
+
return await this.fetchSession(ctx);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
93
104
|
async fetchSession(ctx) {
|
|
94
105
|
if (!this.lambderSession)
|
|
95
|
-
throw "Session not
|
|
106
|
+
throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
107
|
+
if (!this.areRequestSessionTokensValid(ctx)) {
|
|
108
|
+
throw new Error("Session tokens are invalid");
|
|
109
|
+
}
|
|
96
110
|
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
97
111
|
if (!sessionToken)
|
|
98
|
-
throw "Session not found";
|
|
112
|
+
throw new Error("Session token not found");
|
|
99
113
|
const session = await this.lambderSession.getSession(sessionToken);
|
|
114
|
+
if (!session)
|
|
115
|
+
throw new Error("Session not found");
|
|
116
|
+
if (!this.isSessionValid(ctx, session))
|
|
117
|
+
throw new Error("Invalid session");
|
|
100
118
|
ctx.session = session;
|
|
101
|
-
return
|
|
119
|
+
return session;
|
|
102
120
|
}
|
|
103
121
|
;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return false;
|
|
122
|
+
areRequestSessionTokensValid(ctx) {
|
|
123
|
+
const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
|
|
107
124
|
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
108
|
-
|
|
125
|
+
const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
|
|
126
|
+
if (isApiCall) {
|
|
127
|
+
const csrfToken = ctx.post?.token;
|
|
128
|
+
const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0;
|
|
129
|
+
return isSessionTokenValid && isCsrfTokenValid;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
return isSessionTokenValid;
|
|
133
|
+
}
|
|
109
134
|
}
|
|
110
|
-
|
|
111
|
-
validateSessionForAPI(ctx) {
|
|
135
|
+
isSessionValid(ctx, session) {
|
|
112
136
|
if (!this.lambderSession)
|
|
113
137
|
return false;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
138
|
+
const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
|
|
139
|
+
if (isApiCall) {
|
|
140
|
+
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
141
|
+
const csrfToken = ctx.post?.token;
|
|
142
|
+
return this.lambderSession.isSessionValid(session, sessionToken, csrfToken);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
146
|
+
return this.lambderSession.isSessionValid(session, sessionToken, null, true);
|
|
147
|
+
}
|
|
117
148
|
}
|
|
118
|
-
;
|
|
119
149
|
async handleNoMatchedAction(ctx, resolver) {
|
|
120
150
|
for (const hook of this.hookList["fallback"]) {
|
|
121
151
|
await hook.hookFn(ctx, resolver);
|
|
@@ -166,9 +196,6 @@ export default class Lambder {
|
|
|
166
196
|
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
167
197
|
actionFn: async (ctx, resolver) => {
|
|
168
198
|
await this.fetchSession(ctx);
|
|
169
|
-
const isSessionValid = this.validateSessionForRoute(ctx);
|
|
170
|
-
if (!isSessionValid)
|
|
171
|
-
throw new Error("Session not found");
|
|
172
199
|
if (typeof condition === "string") {
|
|
173
200
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
174
201
|
}
|
|
@@ -196,9 +223,6 @@ export default class Lambder {
|
|
|
196
223
|
(apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
|
|
197
224
|
actionFn: async (ctx, resolver) => {
|
|
198
225
|
await this.fetchSession(ctx);
|
|
199
|
-
const isSessionValid = this.validateSessionForAPI(ctx);
|
|
200
|
-
if (!isSessionValid)
|
|
201
|
-
throw new Error("Session not found");
|
|
202
226
|
return await actionFn(ctx, resolver);
|
|
203
227
|
}
|
|
204
228
|
});
|
|
@@ -215,33 +239,38 @@ export default class Lambder {
|
|
|
215
239
|
}
|
|
216
240
|
getSessionController(ctx) {
|
|
217
241
|
return {
|
|
218
|
-
|
|
242
|
+
createSession: async (sessionKey, data, ttlInSeconds) => {
|
|
219
243
|
if (!this.lambderSession)
|
|
220
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
221
|
-
ctx.session = await this.lambderSession.createSession(
|
|
244
|
+
throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
245
|
+
ctx.session = await this.lambderSession.createSession(sessionKey, data, ttlInSeconds);
|
|
222
246
|
return ctx.session;
|
|
223
247
|
},
|
|
248
|
+
fetchSession: async () => {
|
|
249
|
+
if (!this.lambderSession)
|
|
250
|
+
throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
251
|
+
return this.fetchSession(ctx);
|
|
252
|
+
},
|
|
224
253
|
updateSessionData: async (newData) => {
|
|
225
254
|
if (!this.lambderSession)
|
|
226
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
255
|
+
throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
227
256
|
if (!ctx.session)
|
|
228
|
-
throw "Session not found.";
|
|
257
|
+
throw new Error("Session not found.");
|
|
229
258
|
ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
|
|
230
259
|
return ctx.session;
|
|
231
260
|
},
|
|
232
|
-
|
|
261
|
+
endSession: async () => {
|
|
233
262
|
if (!this.lambderSession)
|
|
234
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
263
|
+
throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
235
264
|
if (!ctx.session)
|
|
236
|
-
throw "Session not found.";
|
|
265
|
+
throw new Error("Session not found.");
|
|
237
266
|
await this.lambderSession.deleteSession(ctx.session);
|
|
238
267
|
ctx.session = null;
|
|
239
268
|
},
|
|
240
|
-
|
|
269
|
+
endSessionAll: async () => {
|
|
241
270
|
if (!this.lambderSession)
|
|
242
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
271
|
+
throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
243
272
|
if (!ctx.session)
|
|
244
|
-
throw "Session not found.";
|
|
273
|
+
throw new Error("Session not found.");
|
|
245
274
|
await this.lambderSession.deleteSessionAll(ctx.session);
|
|
246
275
|
ctx.session = null;
|
|
247
276
|
},
|
package/dist/LambderSession.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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;
|
|
@@ -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;
|
package/dist/LambderSession.js
CHANGED
|
@@ -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/package.json
CHANGED
package/src/Lambder.ts
CHANGED
|
@@ -41,6 +41,9 @@ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext|null, r
|
|
|
41
41
|
type RouteFallbackHandlerFunction = (ctx:LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
|
|
42
42
|
type ApiFallbackHandlerFunction = (ctx:LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
|
|
43
43
|
|
|
44
|
+
const isApiCallChecker = (method: string, path: string, post: Record<any,any>, apiPath: string): boolean => {
|
|
45
|
+
return method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
46
|
+
}
|
|
44
47
|
|
|
45
48
|
export const createContext = (
|
|
46
49
|
event: APIGatewayProxyEvent,
|
|
@@ -63,9 +66,9 @@ export const createContext = (
|
|
|
63
66
|
catch(e){ post = querystring.parse(decodedBody) || {}; }
|
|
64
67
|
}catch(e){}
|
|
65
68
|
// Parse api variables
|
|
66
|
-
const
|
|
67
|
-
const apiName:string =
|
|
68
|
-
const apiPayload:string =
|
|
69
|
+
const isApiCall = isApiCallChecker(method, path, post, apiPath);
|
|
70
|
+
const apiName:string = isApiCall ? post.apiName : null;
|
|
71
|
+
const apiPayload:string = isApiCall ? post.payload : null;
|
|
69
72
|
|
|
70
73
|
return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
|
|
71
74
|
}
|
|
@@ -140,37 +143,65 @@ export default class Lambder {
|
|
|
140
143
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction){
|
|
141
144
|
this.globalErrorHandler = globalErrorHandler;
|
|
142
145
|
}
|
|
143
|
-
getPatternMatch(pattern: string, path: string): Record<string, any> {
|
|
146
|
+
private getPatternMatch(pattern: string, path: string): Record<string, any> {
|
|
144
147
|
const result = (match(pattern, { decode: decodeURIComponent }))(path);
|
|
145
148
|
if(!result) return {};
|
|
146
149
|
return result?.params || {};
|
|
147
150
|
}
|
|
148
|
-
testPatternMatch(pattern: string, path: string): boolean{
|
|
151
|
+
private testPatternMatch(pattern: string, path: string): boolean{
|
|
149
152
|
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
async fetchSessionIfExist(ctx: LambderRenderContext): Promise<LambderSessionContext|null>{
|
|
156
|
+
try {
|
|
157
|
+
return await this.fetchSession(ctx);
|
|
158
|
+
}catch(err){
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async fetchSession (ctx: LambderRenderContext): Promise<LambderSessionContext>{
|
|
164
|
+
if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
165
|
+
if(!this.areRequestSessionTokensValid(ctx)){ throw new Error("Session tokens are invalid"); }
|
|
166
|
+
|
|
154
167
|
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
155
|
-
if(!sessionToken) throw "Session not found";
|
|
168
|
+
if(!sessionToken) throw new Error("Session token not found");
|
|
169
|
+
|
|
156
170
|
const session = await this.lambderSession.getSession(sessionToken);
|
|
171
|
+
if(!session) throw new Error("Session not found");
|
|
172
|
+
|
|
173
|
+
if(!this.isSessionValid(ctx, session)) throw new Error("Invalid session");
|
|
157
174
|
ctx.session = session;
|
|
158
|
-
return
|
|
175
|
+
return session;
|
|
159
176
|
};
|
|
160
177
|
|
|
161
|
-
private
|
|
162
|
-
|
|
178
|
+
private areRequestSessionTokensValid(ctx: LambderRenderContext): boolean {
|
|
179
|
+
const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
|
|
163
180
|
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
164
|
-
|
|
165
|
-
};
|
|
181
|
+
const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
|
|
166
182
|
|
|
167
|
-
|
|
183
|
+
if(isApiCall){
|
|
184
|
+
const csrfToken = ctx.post?.token;
|
|
185
|
+
const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0
|
|
186
|
+
return isSessionTokenValid && isCsrfTokenValid;
|
|
187
|
+
}else{
|
|
188
|
+
return isSessionTokenValid;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private isSessionValid(ctx: LambderRenderContext, session: any): boolean {
|
|
168
193
|
if(!this.lambderSession) return false;
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
|
|
195
|
+
if(isApiCall){
|
|
196
|
+
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
197
|
+
const csrfToken = ctx.post?.token;
|
|
198
|
+
return this.lambderSession.isSessionValid(session, sessionToken, csrfToken);
|
|
199
|
+
}else{
|
|
200
|
+
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
201
|
+
return this.lambderSession.isSessionValid(session, sessionToken, null, true);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
174
205
|
private async handleNoMatchedAction(ctx: LambderRenderContext, resolver: LambderResolver){
|
|
175
206
|
for(const hook of this.hookList["fallback"]){ await hook.hookFn(ctx, resolver); }
|
|
176
207
|
|
|
@@ -228,8 +259,6 @@ export default class Lambder {
|
|
|
228
259
|
),
|
|
229
260
|
actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
|
|
230
261
|
await this.fetchSession(ctx);
|
|
231
|
-
const isSessionValid = this.validateSessionForRoute(ctx);
|
|
232
|
-
if(!isSessionValid) throw new Error("Session not found");
|
|
233
262
|
if(typeof condition === "string"){
|
|
234
263
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
235
264
|
}else if(condition?.constructor == RegExp){
|
|
@@ -264,8 +293,6 @@ export default class Lambder {
|
|
|
264
293
|
),
|
|
265
294
|
actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
|
|
266
295
|
await this.fetchSession(ctx);
|
|
267
|
-
const isSessionValid = this.validateSessionForAPI(ctx);
|
|
268
|
-
if(!isSessionValid) throw new Error("Session not found");
|
|
269
296
|
return await actionFn(ctx, resolver);
|
|
270
297
|
}
|
|
271
298
|
});
|
|
@@ -288,35 +315,39 @@ export default class Lambder {
|
|
|
288
315
|
}
|
|
289
316
|
}
|
|
290
317
|
|
|
291
|
-
|
|
318
|
+
getSessionController(ctx: LambderRenderContext){
|
|
292
319
|
return {
|
|
293
|
-
|
|
294
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
295
|
-
ctx.session = await this.lambderSession.createSession(
|
|
320
|
+
createSession: async (sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext> => {
|
|
321
|
+
if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
322
|
+
ctx.session = await this.lambderSession.createSession(sessionKey, data, ttlInSeconds);
|
|
296
323
|
return ctx.session;
|
|
297
324
|
},
|
|
325
|
+
fetchSession: async (): Promise<LambderSessionContext> => {
|
|
326
|
+
if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
327
|
+
return this.fetchSession(ctx);
|
|
328
|
+
},
|
|
298
329
|
updateSessionData: async (newData: any): Promise<LambderSessionContext> => {
|
|
299
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
300
|
-
if(!ctx.session) throw "Session not found.";
|
|
330
|
+
if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
331
|
+
if(!ctx.session) throw new Error("Session not found.");
|
|
301
332
|
ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
|
|
302
333
|
return ctx.session;
|
|
303
334
|
},
|
|
304
|
-
|
|
305
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
306
|
-
if(!ctx.session) throw "Session not found.";
|
|
335
|
+
endSession: async () => {
|
|
336
|
+
if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
337
|
+
if(!ctx.session) throw new Error("Session not found.");
|
|
307
338
|
await this.lambderSession.deleteSession(ctx.session);
|
|
308
339
|
ctx.session = null
|
|
309
340
|
},
|
|
310
|
-
|
|
311
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
312
|
-
if(!ctx.session) throw "Session not found.";
|
|
341
|
+
endSessionAll: async () => {
|
|
342
|
+
if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
343
|
+
if(!ctx.session) throw new Error("Session not found.");
|
|
313
344
|
await this.lambderSession.deleteSessionAll(ctx.session);
|
|
314
345
|
ctx.session = null
|
|
315
346
|
},
|
|
316
347
|
}
|
|
317
348
|
}
|
|
318
349
|
|
|
319
|
-
|
|
350
|
+
getResponseBuilder(){
|
|
320
351
|
return new LambderResponseBuilder({
|
|
321
352
|
isCorsEnabled: this.isCorsEnabled,
|
|
322
353
|
publicPath: this.publicPath,
|
package/src/LambderSession.ts
CHANGED
|
@@ -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;
|
|
@@ -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){
|