lambder 1.0.106 → 1.0.109
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/dist/Lambder.d.ts +19 -12
- package/dist/Lambder.js +44 -23
- package/dist/LambderSession.d.ts +35 -0
- package/dist/LambderSession.js +148 -0
- package/package.json +3 -1
- package/src/Lambder.ts +49 -30
- package/src/LambderSession.ts +166 -0
package/dist/Lambder.d.ts
CHANGED
|
@@ -2,16 +2,8 @@ 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 "./LambderSession.js";
|
|
5
6
|
type Path = `/${string}`;
|
|
6
|
-
type LambderSession = {
|
|
7
|
-
userId: string;
|
|
8
|
-
userIdHash: string;
|
|
9
|
-
sessionHash: string;
|
|
10
|
-
csrfToken: string;
|
|
11
|
-
createdTimeStamp: number;
|
|
12
|
-
expiresTimeStamp: number;
|
|
13
|
-
data: Record<string, any>;
|
|
14
|
-
};
|
|
15
7
|
type LambderRenderContext = {
|
|
16
8
|
host: string;
|
|
17
9
|
path: string;
|
|
@@ -23,7 +15,7 @@ type LambderRenderContext = {
|
|
|
23
15
|
apiName: string;
|
|
24
16
|
apiPayload: any;
|
|
25
17
|
headers: APIGatewayProxyEventHeaders;
|
|
26
|
-
session:
|
|
18
|
+
session: LambderSessionContext | null;
|
|
27
19
|
lambdaContext: Context;
|
|
28
20
|
};
|
|
29
21
|
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
@@ -49,19 +41,34 @@ export default class Lambder {
|
|
|
49
41
|
private routeFallbackHandler;
|
|
50
42
|
private apiFallbackHandler;
|
|
51
43
|
utils: LambderUtils;
|
|
52
|
-
|
|
44
|
+
private lambderSession?;
|
|
45
|
+
private sessionTokenCookieKey;
|
|
46
|
+
private sessionCsrfCookieKey;
|
|
47
|
+
constructor({ publicPath, apiPath, ejsPath, apiVersion }: {
|
|
53
48
|
publicPath: string;
|
|
54
49
|
apiPath?: string;
|
|
55
50
|
ejsPath?: string;
|
|
56
51
|
apiVersion?: string;
|
|
57
52
|
isCorsEnabled?: boolean;
|
|
58
53
|
});
|
|
54
|
+
enableCORS(isCorsEnabled: boolean): void;
|
|
55
|
+
enableDdbSession({ tableName, tableRegion, sessionSalt }: {
|
|
56
|
+
tableName: string;
|
|
57
|
+
tableRegion: string;
|
|
58
|
+
sessionSalt: string;
|
|
59
|
+
}, { partitionKey, sortKey }?: {
|
|
60
|
+
partitionKey: string;
|
|
61
|
+
sortKey: string;
|
|
62
|
+
}): void;
|
|
63
|
+
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
|
|
59
64
|
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): void;
|
|
60
65
|
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): void;
|
|
61
66
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): void;
|
|
62
67
|
getPatternMatch(pattern: string, path: string): Record<string, any>;
|
|
63
68
|
testPatternMatch(pattern: string, path: string): boolean;
|
|
64
|
-
private
|
|
69
|
+
private fetchSession;
|
|
70
|
+
private validateSessionForRoute;
|
|
71
|
+
private validateSessionForAPI;
|
|
65
72
|
private handleNoMatchedAction;
|
|
66
73
|
addModule(moduleFn: LambderModuleFunction): Promise<void>;
|
|
67
74
|
importModule(moduleImport: Promise<{
|
package/dist/Lambder.js
CHANGED
|
@@ -4,6 +4,7 @@ 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 LambderSession from "./LambderSession.js";
|
|
7
8
|
export const createContext = (event, lambdaContext, apiPath) => {
|
|
8
9
|
const host = event.headers.Host || event.headers.host || "";
|
|
9
10
|
const path = event.path;
|
|
@@ -34,7 +35,7 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
34
35
|
export default class Lambder {
|
|
35
36
|
apiPath;
|
|
36
37
|
apiVersion;
|
|
37
|
-
isCorsEnabled;
|
|
38
|
+
isCorsEnabled = false;
|
|
38
39
|
publicPath;
|
|
39
40
|
ejsPath;
|
|
40
41
|
actionList;
|
|
@@ -43,12 +44,14 @@ export default class Lambder {
|
|
|
43
44
|
routeFallbackHandler = null;
|
|
44
45
|
apiFallbackHandler = null;
|
|
45
46
|
utils;
|
|
46
|
-
|
|
47
|
+
lambderSession;
|
|
48
|
+
sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
49
|
+
sessionCsrfCookieKey = "LMDRSESSIONCSID";
|
|
50
|
+
constructor({ publicPath, apiPath, ejsPath, apiVersion }) {
|
|
47
51
|
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
48
52
|
this.ejsPath = ejsPath || "/incorrect-ejs-path-not-found";
|
|
49
53
|
this.apiPath = apiPath ?? "/api";
|
|
50
54
|
this.apiVersion = apiVersion ?? null;
|
|
51
|
-
this.isCorsEnabled = isCorsEnabled ?? false;
|
|
52
55
|
this.actionList = [];
|
|
53
56
|
this.hookList = {
|
|
54
57
|
"beforeRender": [],
|
|
@@ -57,6 +60,18 @@ export default class Lambder {
|
|
|
57
60
|
};
|
|
58
61
|
this.utils = new LambderUtils({ ejsPath });
|
|
59
62
|
}
|
|
63
|
+
enableCORS(isCorsEnabled) {
|
|
64
|
+
this.isCorsEnabled = isCorsEnabled;
|
|
65
|
+
}
|
|
66
|
+
enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
|
|
67
|
+
this.lambderSession = new LambderSession({
|
|
68
|
+
tableName, tableRegion, partitionKey, sortKey, sessionSalt
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
setSessionCookieKey(sessionTokenCookieKey, sessionCsrfCookieKey) {
|
|
72
|
+
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
73
|
+
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
74
|
+
}
|
|
60
75
|
setRouteFallbackHandler(routeFallbackHandler) {
|
|
61
76
|
this.routeFallbackHandler = routeFallbackHandler;
|
|
62
77
|
}
|
|
@@ -75,26 +90,30 @@ export default class Lambder {
|
|
|
75
90
|
testPatternMatch(pattern, path) {
|
|
76
91
|
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
77
92
|
}
|
|
78
|
-
async
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!
|
|
83
|
-
|
|
84
|
-
|
|
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)
|
|
85
106
|
return false;
|
|
86
|
-
|
|
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)
|
|
87
113
|
return false;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (post.token !== cookie.LMBDRTOKEN)
|
|
92
|
-
return false;
|
|
93
|
-
if (post.token !== session.csrfToken)
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
// Check DDB;
|
|
97
|
-
return false;
|
|
114
|
+
const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
|
|
115
|
+
const csrfToken = ctx.post?.token;
|
|
116
|
+
return this.lambderSession.isSessionValid(ctx.session, sessionToken, csrfToken);
|
|
98
117
|
}
|
|
99
118
|
;
|
|
100
119
|
async handleNoMatchedAction(ctx, resolver) {
|
|
@@ -146,7 +165,8 @@ export default class Lambder {
|
|
|
146
165
|
(typeof condition === "function" && condition(ctx)) ||
|
|
147
166
|
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
148
167
|
actionFn: async (ctx, resolver) => {
|
|
149
|
-
|
|
168
|
+
await this.fetchSession(ctx);
|
|
169
|
+
const isSessionValid = this.validateSessionForRoute(ctx);
|
|
150
170
|
if (!isSessionValid)
|
|
151
171
|
throw new Error("Session not found");
|
|
152
172
|
if (typeof condition === "string") {
|
|
@@ -175,7 +195,8 @@ export default class Lambder {
|
|
|
175
195
|
(typeof apiName === "function" && apiName(ctx)) ||
|
|
176
196
|
(apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
|
|
177
197
|
actionFn: async (ctx, resolver) => {
|
|
178
|
-
|
|
198
|
+
await this.fetchSession(ctx);
|
|
199
|
+
const isSessionValid = this.validateSessionForAPI(ctx);
|
|
179
200
|
if (!isSessionValid)
|
|
180
201
|
throw new Error("Session not found");
|
|
181
202
|
return await actionFn(ctx, resolver);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type LambderSessionContext = {
|
|
2
|
+
[x: string]: any;
|
|
3
|
+
sessionToken: string;
|
|
4
|
+
csrfToken: string;
|
|
5
|
+
userKey: string;
|
|
6
|
+
data: any;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
ttlInSeconds: number;
|
|
10
|
+
};
|
|
11
|
+
export default class LambderSession {
|
|
12
|
+
private tableName;
|
|
13
|
+
private sessionSalt;
|
|
14
|
+
private partitionKey;
|
|
15
|
+
private sortKey;
|
|
16
|
+
private ddbDocumentClient;
|
|
17
|
+
constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, }: {
|
|
18
|
+
tableName: string;
|
|
19
|
+
tableRegion: string;
|
|
20
|
+
partitionKey: string;
|
|
21
|
+
sortKey: string;
|
|
22
|
+
sessionSalt: string;
|
|
23
|
+
});
|
|
24
|
+
private sessionUserKeyHasher;
|
|
25
|
+
private ddbGetItem;
|
|
26
|
+
private ddbPutItem;
|
|
27
|
+
private ddbDeleteItem;
|
|
28
|
+
private ddbQueryAllByPartitionKey;
|
|
29
|
+
private ddbDeleteAllByPartitionKey;
|
|
30
|
+
createSession(userKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
|
|
31
|
+
getSession(sessionToken: string): Promise<LambderSessionContext | null>;
|
|
32
|
+
isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck?: boolean): boolean;
|
|
33
|
+
deleteSession(session: Record<string, any>): Promise<boolean>;
|
|
34
|
+
deleteSessionAll(session: Record<string, any>): Promise<boolean>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
|
+
import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
4
|
+
export default class LambderSession {
|
|
5
|
+
tableName;
|
|
6
|
+
sessionSalt;
|
|
7
|
+
partitionKey;
|
|
8
|
+
sortKey;
|
|
9
|
+
ddbDocumentClient;
|
|
10
|
+
constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, }) {
|
|
11
|
+
this.tableName = tableName;
|
|
12
|
+
this.sessionSalt = sessionSalt;
|
|
13
|
+
this.partitionKey = partitionKey;
|
|
14
|
+
this.sortKey = sortKey;
|
|
15
|
+
const ddbClient = new DynamoDBClient({ region: tableRegion });
|
|
16
|
+
this.ddbDocumentClient = DynamoDBDocumentClient.from(ddbClient);
|
|
17
|
+
}
|
|
18
|
+
sessionUserKeyHasher(password) {
|
|
19
|
+
return crypto.createHash("sha256")
|
|
20
|
+
.update(`${password}${this.sessionSalt}`)
|
|
21
|
+
.digest("hex");
|
|
22
|
+
}
|
|
23
|
+
async ddbGetItem(key) {
|
|
24
|
+
const response = await this.ddbDocumentClient.send(new GetCommand({ TableName: this.tableName, Key: key, ConsistentRead: true }));
|
|
25
|
+
if (response.Item)
|
|
26
|
+
return response.Item;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
;
|
|
30
|
+
async ddbPutItem(item) {
|
|
31
|
+
return await this.ddbDocumentClient.send(new PutCommand({ TableName: this.tableName, Item: item, }));
|
|
32
|
+
}
|
|
33
|
+
;
|
|
34
|
+
async ddbDeleteItem(key) {
|
|
35
|
+
return await this.ddbDocumentClient.send(new DeleteCommand({ TableName: this.tableName, Key: key, }));
|
|
36
|
+
}
|
|
37
|
+
;
|
|
38
|
+
async ddbQueryAllByPartitionKey(partitionValue) {
|
|
39
|
+
const params = {
|
|
40
|
+
TableName: this.tableName,
|
|
41
|
+
KeyConditionExpression: "#pk = :pv",
|
|
42
|
+
ExpressionAttributeNames: { "#pk": this.partitionKey },
|
|
43
|
+
ExpressionAttributeValues: { ":pv": partitionValue },
|
|
44
|
+
};
|
|
45
|
+
const queryResults = [];
|
|
46
|
+
do {
|
|
47
|
+
const { Items, LastEvaluatedKey } = await this.ddbDocumentClient.send(new QueryCommand(params));
|
|
48
|
+
if (Items)
|
|
49
|
+
queryResults.push(...Items);
|
|
50
|
+
params.ExclusiveStartKey = LastEvaluatedKey;
|
|
51
|
+
if (typeof LastEvaluatedKey == "undefined")
|
|
52
|
+
return queryResults;
|
|
53
|
+
// eslint-disable-next-line no-constant-condition
|
|
54
|
+
} while (true);
|
|
55
|
+
}
|
|
56
|
+
;
|
|
57
|
+
async ddbDeleteAllByPartitionKey(partitionValue) {
|
|
58
|
+
const queryResults = await this.ddbQueryAllByPartitionKey(partitionValue);
|
|
59
|
+
for (const item of queryResults) {
|
|
60
|
+
await this.ddbDocumentClient.send(new DeleteCommand({
|
|
61
|
+
TableName: this.tableName,
|
|
62
|
+
Key: { [this.partitionKey]: partitionValue, [this.sortKey]: item[this.sortKey] }
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async createSession(userKey, data = {}, ttlInSeconds = 30 * 24 * 60 * 60) {
|
|
67
|
+
const userKeyHash = this.sessionUserKeyHasher(userKey);
|
|
68
|
+
const sessionSortKey = crypto.randomBytes(32).toString("hex");
|
|
69
|
+
const sessionToken = `${userKeyHash}:${sessionSortKey}`;
|
|
70
|
+
const csrfToken = crypto.randomBytes(8).toString("hex");
|
|
71
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
72
|
+
const expiresAt = Number(createdAt) + Number(ttlInSeconds);
|
|
73
|
+
const session = {
|
|
74
|
+
[this.partitionKey]: userKeyHash,
|
|
75
|
+
[this.sortKey]: sessionSortKey,
|
|
76
|
+
sessionToken, csrfToken,
|
|
77
|
+
userKey, data,
|
|
78
|
+
createdAt, expiresAt, ttlInSeconds
|
|
79
|
+
};
|
|
80
|
+
await this.ddbPutItem(session);
|
|
81
|
+
return session;
|
|
82
|
+
}
|
|
83
|
+
async getSession(sessionToken) {
|
|
84
|
+
const [userKeyHash, sessionSortKey] = sessionToken.split(":");
|
|
85
|
+
if (!userKeyHash || !sessionSortKey)
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
let session = await this.ddbGetItem({
|
|
89
|
+
[this.partitionKey]: userKeyHash,
|
|
90
|
+
[this.sortKey]: sessionSortKey
|
|
91
|
+
});
|
|
92
|
+
if (!session)
|
|
93
|
+
throw "Session not found";
|
|
94
|
+
if (!session.sessionToken || session.sessionToken !== sessionToken)
|
|
95
|
+
throw "Not found: session.sessionToken";
|
|
96
|
+
if (!session.csrfToken)
|
|
97
|
+
throw "Not found: session.csrfToken";
|
|
98
|
+
if (!session.userKey)
|
|
99
|
+
throw "Not found: session.userKey";
|
|
100
|
+
if (!session.createdAt)
|
|
101
|
+
throw "Not found: session.createdAt";
|
|
102
|
+
if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
|
|
103
|
+
throw "Not found: session.expiresAt";
|
|
104
|
+
return session;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
;
|
|
111
|
+
isSessionValid(session, sessionToken, csrfToken, skipCsrfTokenCheck = false) {
|
|
112
|
+
if (!session)
|
|
113
|
+
return false;
|
|
114
|
+
if (!sessionToken || typeof sessionToken !== "string")
|
|
115
|
+
return false;
|
|
116
|
+
if (session.sessionToken !== sessionToken)
|
|
117
|
+
return false;
|
|
118
|
+
if (!session.csrfToken)
|
|
119
|
+
return false;
|
|
120
|
+
if (!session.userKey)
|
|
121
|
+
return false;
|
|
122
|
+
if (!session.createdAt)
|
|
123
|
+
return false;
|
|
124
|
+
if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
|
|
125
|
+
return false;
|
|
126
|
+
if (!skipCsrfTokenCheck) {
|
|
127
|
+
if (!csrfToken || typeof csrfToken !== "string")
|
|
128
|
+
return false;
|
|
129
|
+
if (session.csrfToken !== csrfToken)
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
async deleteSession(session) {
|
|
135
|
+
await this.ddbDeleteItem({
|
|
136
|
+
[this.partitionKey]: session[this.partitionKey],
|
|
137
|
+
[this.sortKey]: session[this.sortKey],
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
;
|
|
142
|
+
async deleteSessionAll(session) {
|
|
143
|
+
await this.ddbDeleteAllByPartitionKey(session[this.partitionKey]);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
;
|
|
147
|
+
}
|
|
148
|
+
;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.109",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"author": "",
|
|
14
14
|
"license": "ISC",
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@aws-sdk/client-dynamodb": "^3.574.0",
|
|
17
|
+
"@aws-sdk/lib-dynamodb": "^3.574.0",
|
|
16
18
|
"cookie": "^0.6.0",
|
|
17
19
|
"ejs": "^3.1.9",
|
|
18
20
|
"js-cookie": "^3.0.5",
|
package/src/Lambder.ts
CHANGED
|
@@ -6,19 +6,10 @@ 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 LambderSession, { type LambderSessionContext } from "./LambderSession.js";
|
|
9
10
|
|
|
10
11
|
type Path = `/${string}`;
|
|
11
12
|
|
|
12
|
-
type LambderSession = {
|
|
13
|
-
userId: string,
|
|
14
|
-
userIdHash: string;
|
|
15
|
-
sessionHash: string;
|
|
16
|
-
csrfToken: string;
|
|
17
|
-
createdTimeStamp: number;
|
|
18
|
-
expiresTimeStamp: number;
|
|
19
|
-
data: Record<string, any>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
13
|
type LambderRenderContext = {
|
|
23
14
|
host: string;
|
|
24
15
|
path: string;
|
|
@@ -30,7 +21,7 @@ type LambderRenderContext = {
|
|
|
30
21
|
apiName: string;
|
|
31
22
|
apiPayload: any;
|
|
32
23
|
headers: APIGatewayProxyEventHeaders;
|
|
33
|
-
session:
|
|
24
|
+
session: LambderSessionContext|null;
|
|
34
25
|
lambdaContext: Context;
|
|
35
26
|
};
|
|
36
27
|
|
|
@@ -83,7 +74,7 @@ export const createContext = (
|
|
|
83
74
|
export default class Lambder {
|
|
84
75
|
public apiPath: string;
|
|
85
76
|
public apiVersion: null|string;
|
|
86
|
-
public isCorsEnabled: boolean;
|
|
77
|
+
public isCorsEnabled: boolean = false;
|
|
87
78
|
public publicPath: string;
|
|
88
79
|
public ejsPath: string;
|
|
89
80
|
|
|
@@ -99,16 +90,18 @@ export default class Lambder {
|
|
|
99
90
|
|
|
100
91
|
public utils: LambderUtils;
|
|
101
92
|
|
|
93
|
+
private lambderSession?: LambderSession;
|
|
94
|
+
private sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
95
|
+
private sessionCsrfCookieKey = "LMDRSESSIONCSID";
|
|
102
96
|
|
|
103
97
|
constructor(
|
|
104
|
-
{ publicPath, apiPath, ejsPath, apiVersion
|
|
98
|
+
{ publicPath, apiPath, ejsPath, apiVersion }:
|
|
105
99
|
{ publicPath: string, apiPath?: string, ejsPath?: string, apiVersion?: string, isCorsEnabled?: boolean, }
|
|
106
100
|
){
|
|
107
101
|
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
108
102
|
this.ejsPath = ejsPath || "/incorrect-ejs-path-not-found";
|
|
109
103
|
this.apiPath = apiPath ?? "/api";
|
|
110
104
|
this.apiVersion = apiVersion ?? null;
|
|
111
|
-
this.isCorsEnabled = isCorsEnabled ?? false;
|
|
112
105
|
|
|
113
106
|
this.actionList = [];
|
|
114
107
|
this.hookList = {
|
|
@@ -120,6 +113,24 @@ export default class Lambder {
|
|
|
120
113
|
this.utils = new LambderUtils({ ejsPath });
|
|
121
114
|
}
|
|
122
115
|
|
|
116
|
+
enableCORS(isCorsEnabled: boolean){
|
|
117
|
+
this.isCorsEnabled = isCorsEnabled;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
enableDdbSession(
|
|
121
|
+
{ tableName, tableRegion, sessionSalt }: { tableName: string; tableRegion: string; sessionSalt: string; },
|
|
122
|
+
{ partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
|
|
123
|
+
){
|
|
124
|
+
this.lambderSession = new LambderSession({
|
|
125
|
+
tableName, tableRegion, partitionKey, sortKey, sessionSalt
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string){
|
|
130
|
+
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
131
|
+
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction){
|
|
124
135
|
this.routeFallbackHandler = routeFallbackHandler;
|
|
125
136
|
}
|
|
@@ -138,20 +149,26 @@ export default class Lambder {
|
|
|
138
149
|
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
139
150
|
}
|
|
140
151
|
|
|
141
|
-
private async
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if(!
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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);
|
|
155
172
|
};
|
|
156
173
|
|
|
157
174
|
private async handleNoMatchedAction(ctx: LambderRenderContext, resolver: LambderResolver){
|
|
@@ -210,7 +227,8 @@ export default class Lambder {
|
|
|
210
227
|
)
|
|
211
228
|
),
|
|
212
229
|
actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
|
|
213
|
-
|
|
230
|
+
await this.fetchSession(ctx);
|
|
231
|
+
const isSessionValid = this.validateSessionForRoute(ctx);
|
|
214
232
|
if(!isSessionValid) throw new Error("Session not found");
|
|
215
233
|
if(typeof condition === "string"){
|
|
216
234
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
@@ -245,7 +263,8 @@ export default class Lambder {
|
|
|
245
263
|
)
|
|
246
264
|
),
|
|
247
265
|
actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
|
|
248
|
-
|
|
266
|
+
await this.fetchSession(ctx);
|
|
267
|
+
const isSessionValid = this.validateSessionForAPI(ctx);
|
|
249
268
|
if(!isSessionValid) throw new Error("Session not found");
|
|
250
269
|
return await actionFn(ctx, resolver);
|
|
251
270
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
|
+
import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
4
|
+
|
|
5
|
+
export type LambderSessionContext = {
|
|
6
|
+
[x: string]: any;
|
|
7
|
+
sessionToken: string;
|
|
8
|
+
csrfToken: string;
|
|
9
|
+
userKey: string;
|
|
10
|
+
data: any;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
ttlInSeconds: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export default class LambderSession{
|
|
18
|
+
private tableName: string;
|
|
19
|
+
private sessionSalt: string;
|
|
20
|
+
private partitionKey: string;
|
|
21
|
+
private sortKey: string;
|
|
22
|
+
private ddbDocumentClient: DynamoDBDocumentClient;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
{
|
|
26
|
+
tableName, tableRegion,
|
|
27
|
+
partitionKey, sortKey,
|
|
28
|
+
sessionSalt,
|
|
29
|
+
}: {
|
|
30
|
+
tableName: string, tableRegion: string,
|
|
31
|
+
partitionKey: string, sortKey: string,
|
|
32
|
+
sessionSalt: string,
|
|
33
|
+
}
|
|
34
|
+
){
|
|
35
|
+
this.tableName = tableName;
|
|
36
|
+
this.sessionSalt = sessionSalt;
|
|
37
|
+
this.partitionKey = partitionKey;
|
|
38
|
+
this.sortKey = sortKey;
|
|
39
|
+
|
|
40
|
+
const ddbClient = new DynamoDBClient({ region: tableRegion });
|
|
41
|
+
this.ddbDocumentClient = DynamoDBDocumentClient.from(ddbClient);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private sessionUserKeyHasher(password:string){
|
|
45
|
+
return crypto.createHash("sha256")
|
|
46
|
+
.update(`${password}${this.sessionSalt}`)
|
|
47
|
+
.digest("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async ddbGetItem<T=any>(key:Record<string,string|number>){
|
|
51
|
+
const response = await this.ddbDocumentClient.send(
|
|
52
|
+
new GetCommand({ TableName: this.tableName, Key: key, ConsistentRead: true })
|
|
53
|
+
);
|
|
54
|
+
if(response.Item) return response.Item as T;
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
private async ddbPutItem(item: Record<string,any>){
|
|
58
|
+
return await this.ddbDocumentClient.send(
|
|
59
|
+
new PutCommand({ TableName: this.tableName, Item: item, })
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
private async ddbDeleteItem(key:Record<string,string|number>){
|
|
64
|
+
return await this.ddbDocumentClient.send(
|
|
65
|
+
new DeleteCommand({ TableName: this.tableName, Key: key, })
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
private async ddbQueryAllByPartitionKey (partitionValue: string){
|
|
70
|
+
const params: any = {
|
|
71
|
+
TableName: this.tableName,
|
|
72
|
+
KeyConditionExpression: "#pk = :pv",
|
|
73
|
+
ExpressionAttributeNames:{ "#pk": this.partitionKey },
|
|
74
|
+
ExpressionAttributeValues: { ":pv": partitionValue },
|
|
75
|
+
}
|
|
76
|
+
const queryResults: any[] = [];
|
|
77
|
+
do{
|
|
78
|
+
const {Items, LastEvaluatedKey} = await this.ddbDocumentClient.send(new QueryCommand(params));
|
|
79
|
+
if(Items) queryResults.push(...Items);
|
|
80
|
+
params.ExclusiveStartKey = LastEvaluatedKey;
|
|
81
|
+
if(typeof LastEvaluatedKey == "undefined") return queryResults;
|
|
82
|
+
// eslint-disable-next-line no-constant-condition
|
|
83
|
+
} while (true);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
private async ddbDeleteAllByPartitionKey(partitionValue: string){
|
|
87
|
+
const queryResults = await this.ddbQueryAllByPartitionKey(partitionValue);
|
|
88
|
+
for(const item of queryResults){
|
|
89
|
+
await this.ddbDocumentClient.send(new DeleteCommand({
|
|
90
|
+
TableName: this.tableName,
|
|
91
|
+
Key: { [this.partitionKey]: partitionValue, [this.sortKey]: item[this.sortKey] }
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async createSession(
|
|
97
|
+
userKey: string,
|
|
98
|
+
data: any = {},
|
|
99
|
+
ttlInSeconds:number = 30*24*60*60
|
|
100
|
+
): Promise<LambderSessionContext> {
|
|
101
|
+
const userKeyHash = this.sessionUserKeyHasher(userKey);
|
|
102
|
+
const sessionSortKey = crypto.randomBytes(32).toString("hex");
|
|
103
|
+
const sessionToken = `${userKeyHash}:${sessionSortKey}`;
|
|
104
|
+
const csrfToken = crypto.randomBytes(8).toString("hex");
|
|
105
|
+
const createdAt = Math.floor(Date.now()/1000);
|
|
106
|
+
const expiresAt = Number(createdAt) + Number(ttlInSeconds);
|
|
107
|
+
|
|
108
|
+
const session = {
|
|
109
|
+
[this.partitionKey]: userKeyHash,
|
|
110
|
+
[this.sortKey]: sessionSortKey,
|
|
111
|
+
sessionToken, csrfToken,
|
|
112
|
+
userKey, data,
|
|
113
|
+
createdAt, expiresAt, ttlInSeconds
|
|
114
|
+
};
|
|
115
|
+
await this.ddbPutItem(session);
|
|
116
|
+
return session;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public async getSession(sessionToken: string): Promise<LambderSessionContext|null>{
|
|
120
|
+
const [ userKeyHash, sessionSortKey ] = sessionToken.split(":");
|
|
121
|
+
if(!userKeyHash || !sessionSortKey) return null;
|
|
122
|
+
try{
|
|
123
|
+
let session = await this.ddbGetItem({
|
|
124
|
+
[this.partitionKey]: userKeyHash,
|
|
125
|
+
[this.sortKey]: sessionSortKey
|
|
126
|
+
});
|
|
127
|
+
if(!session) throw "Session not found";
|
|
128
|
+
if(!session.sessionToken || session.sessionToken !== sessionToken) throw "Not found: session.sessionToken";
|
|
129
|
+
if(!session.csrfToken) throw "Not found: session.csrfToken";
|
|
130
|
+
if(!session.userKey) throw "Not found: session.userKey";
|
|
131
|
+
if(!session.createdAt) throw "Not found: session.createdAt";
|
|
132
|
+
if(!session.expiresAt || session.expiresAt < Date.now()/1000) throw "Not found: session.expiresAt";
|
|
133
|
+
return session;
|
|
134
|
+
}catch(err){
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
public isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck = false){
|
|
140
|
+
if(!session) return false;
|
|
141
|
+
if(!sessionToken || typeof sessionToken !== "string") return false;
|
|
142
|
+
if(session.sessionToken !== sessionToken) return false;
|
|
143
|
+
if(!session.csrfToken) return false;
|
|
144
|
+
if(!session.userKey) return false;
|
|
145
|
+
if(!session.createdAt) return false;
|
|
146
|
+
if(!session.expiresAt || session.expiresAt < Date.now()/1000) return false;
|
|
147
|
+
if(!skipCsrfTokenCheck){
|
|
148
|
+
if(!csrfToken || typeof csrfToken !== "string") return false;
|
|
149
|
+
if(session.csrfToken !== csrfToken) return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public async deleteSession(session: Record<string, any>): Promise<boolean>{
|
|
155
|
+
await this.ddbDeleteItem({
|
|
156
|
+
[this.partitionKey]: session[this.partitionKey],
|
|
157
|
+
[this.sortKey]: session[this.sortKey],
|
|
158
|
+
});
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
public async deleteSessionAll (session: Record<string, any>): Promise<boolean>{
|
|
163
|
+
await this.ddbDeleteAllByPartitionKey(session[this.partitionKey])
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
};
|