@flink-app/generic-request-plugin 0.12.1-alpha.45 → 0.12.1-alpha.47
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/spec/GenericRequestPlugin.spec.d.ts +1 -0
- package/dist/spec/GenericRequestPlugin.spec.js +494 -0
- package/dist/{index.d.ts → src/index.d.ts} +6 -0
- package/dist/src/index.js +94 -0
- package/package.json +7 -4
- package/readme.md +230 -3
- package/spec/GenericRequestPlugin.spec.ts +459 -0
- package/spec/helpers/reporter.ts +22 -0
- package/spec/support/jasmine.json +7 -0
- package/src/index.ts +52 -29
- package/tsconfig.json +4 -2
- package/dist/index.js +0 -31
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
var index_1 = require("../src/index");
|
|
40
|
+
describe("GenericRequestPlugin", function () {
|
|
41
|
+
var mockApp;
|
|
42
|
+
var mockExpressApp;
|
|
43
|
+
var mockAuth;
|
|
44
|
+
var registeredRoutes;
|
|
45
|
+
beforeEach(function () {
|
|
46
|
+
registeredRoutes = new Map();
|
|
47
|
+
// Mock Express app
|
|
48
|
+
mockExpressApp = {
|
|
49
|
+
get: jasmine.createSpy("get").and.callFake(function (path, handler) {
|
|
50
|
+
registeredRoutes.set("GET:".concat(path), handler);
|
|
51
|
+
}),
|
|
52
|
+
post: jasmine.createSpy("post").and.callFake(function (path, handler) {
|
|
53
|
+
registeredRoutes.set("POST:".concat(path), handler);
|
|
54
|
+
}),
|
|
55
|
+
put: jasmine.createSpy("put").and.callFake(function (path, handler) {
|
|
56
|
+
registeredRoutes.set("PUT:".concat(path), handler);
|
|
57
|
+
}),
|
|
58
|
+
delete: jasmine.createSpy("delete").and.callFake(function (path, handler) {
|
|
59
|
+
registeredRoutes.set("DELETE:".concat(path), handler);
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
// Mock auth plugin
|
|
63
|
+
mockAuth = {
|
|
64
|
+
authenticateRequest: jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(true)),
|
|
65
|
+
createToken: jasmine.createSpy("createToken").and.returnValue(Promise.resolve("token")),
|
|
66
|
+
};
|
|
67
|
+
// Mock FlinkApp
|
|
68
|
+
mockApp = {
|
|
69
|
+
expressApp: mockExpressApp,
|
|
70
|
+
auth: mockAuth,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
describe("Plugin Registration", function () {
|
|
74
|
+
it("should create a valid plugin", function () {
|
|
75
|
+
var plugin = (0, index_1.genericRequestPlugin)({
|
|
76
|
+
path: "/test",
|
|
77
|
+
method: index_1.HttpMethod.get,
|
|
78
|
+
handler: function () { },
|
|
79
|
+
});
|
|
80
|
+
expect(plugin).toBeDefined();
|
|
81
|
+
expect(plugin.id).toBe("genericRequestPlugin");
|
|
82
|
+
expect(plugin.init).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
it("should throw error if Express app is not initialized", function () {
|
|
85
|
+
var appWithoutExpress = { expressApp: null };
|
|
86
|
+
var plugin = (0, index_1.genericRequestPlugin)({
|
|
87
|
+
path: "/test",
|
|
88
|
+
method: index_1.HttpMethod.get,
|
|
89
|
+
handler: function () { },
|
|
90
|
+
});
|
|
91
|
+
expect(function () { return plugin.init(appWithoutExpress); }).toThrowError("Express app not initialized");
|
|
92
|
+
});
|
|
93
|
+
it("should register GET route", function () {
|
|
94
|
+
var plugin = (0, index_1.genericRequestPlugin)({
|
|
95
|
+
path: "/test",
|
|
96
|
+
method: index_1.HttpMethod.get,
|
|
97
|
+
handler: function () { },
|
|
98
|
+
});
|
|
99
|
+
plugin.init(mockApp);
|
|
100
|
+
expect(mockExpressApp.get).toHaveBeenCalledWith("/test", jasmine.any(Function));
|
|
101
|
+
});
|
|
102
|
+
it("should register POST route", function () {
|
|
103
|
+
var plugin = (0, index_1.genericRequestPlugin)({
|
|
104
|
+
path: "/webhook",
|
|
105
|
+
method: index_1.HttpMethod.post,
|
|
106
|
+
handler: function () { },
|
|
107
|
+
});
|
|
108
|
+
plugin.init(mockApp);
|
|
109
|
+
expect(mockExpressApp.post).toHaveBeenCalledWith("/webhook", jasmine.any(Function));
|
|
110
|
+
});
|
|
111
|
+
it("should register PUT route", function () {
|
|
112
|
+
var plugin = (0, index_1.genericRequestPlugin)({
|
|
113
|
+
path: "/update",
|
|
114
|
+
method: index_1.HttpMethod.put,
|
|
115
|
+
handler: function () { },
|
|
116
|
+
});
|
|
117
|
+
plugin.init(mockApp);
|
|
118
|
+
expect(mockExpressApp.put).toHaveBeenCalledWith("/update", jasmine.any(Function));
|
|
119
|
+
});
|
|
120
|
+
it("should register DELETE route", function () {
|
|
121
|
+
var plugin = (0, index_1.genericRequestPlugin)({
|
|
122
|
+
path: "/remove",
|
|
123
|
+
method: index_1.HttpMethod.delete,
|
|
124
|
+
handler: function () { },
|
|
125
|
+
});
|
|
126
|
+
plugin.init(mockApp);
|
|
127
|
+
expect(mockExpressApp.delete).toHaveBeenCalledWith("/remove", jasmine.any(Function));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("Handler Execution - No Permissions", function () {
|
|
131
|
+
it("should call handler directly when no permissions are set", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
132
|
+
var handlerSpy, plugin, routeHandler, mockReq, mockRes;
|
|
133
|
+
return __generator(this, function (_a) {
|
|
134
|
+
switch (_a.label) {
|
|
135
|
+
case 0:
|
|
136
|
+
handlerSpy = jasmine.createSpy("handler");
|
|
137
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
138
|
+
path: "/public",
|
|
139
|
+
method: index_1.HttpMethod.get,
|
|
140
|
+
handler: handlerSpy,
|
|
141
|
+
});
|
|
142
|
+
plugin.init(mockApp);
|
|
143
|
+
routeHandler = registeredRoutes.get("GET:/public");
|
|
144
|
+
if (!routeHandler) {
|
|
145
|
+
fail("Route handler not registered");
|
|
146
|
+
return [2 /*return*/];
|
|
147
|
+
}
|
|
148
|
+
mockReq = {};
|
|
149
|
+
mockRes = {};
|
|
150
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
151
|
+
case 1:
|
|
152
|
+
_a.sent();
|
|
153
|
+
expect(handlerSpy).toHaveBeenCalledWith(mockReq, mockRes, mockApp);
|
|
154
|
+
expect(mockAuth.authenticateRequest).not.toHaveBeenCalled();
|
|
155
|
+
return [2 /*return*/];
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}); });
|
|
159
|
+
it("should pass req, res, and app to handler", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
160
|
+
var capturedReq, capturedRes, capturedApp, handler, plugin, routeHandler, mockReq, mockRes;
|
|
161
|
+
return __generator(this, function (_a) {
|
|
162
|
+
switch (_a.label) {
|
|
163
|
+
case 0:
|
|
164
|
+
handler = function (req, res, app) {
|
|
165
|
+
capturedReq = req;
|
|
166
|
+
capturedRes = res;
|
|
167
|
+
capturedApp = app;
|
|
168
|
+
};
|
|
169
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
170
|
+
path: "/test",
|
|
171
|
+
method: index_1.HttpMethod.get,
|
|
172
|
+
handler: handler,
|
|
173
|
+
});
|
|
174
|
+
plugin.init(mockApp);
|
|
175
|
+
routeHandler = registeredRoutes.get("GET:/test");
|
|
176
|
+
if (!routeHandler) {
|
|
177
|
+
fail("Route handler not registered");
|
|
178
|
+
return [2 /*return*/];
|
|
179
|
+
}
|
|
180
|
+
mockReq = { path: "/test" };
|
|
181
|
+
mockRes = { status: function () { return ({ json: function () { } }); } };
|
|
182
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
183
|
+
case 1:
|
|
184
|
+
_a.sent();
|
|
185
|
+
expect(capturedReq).toBe(mockReq);
|
|
186
|
+
expect(capturedRes).toBe(mockRes);
|
|
187
|
+
expect(capturedApp).toBe(mockApp);
|
|
188
|
+
return [2 /*return*/];
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}); });
|
|
192
|
+
});
|
|
193
|
+
describe("Permission Validation", function () {
|
|
194
|
+
it("should validate permissions when set", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
195
|
+
var handlerSpy, plugin, routeHandler, mockReq, mockRes;
|
|
196
|
+
return __generator(this, function (_a) {
|
|
197
|
+
switch (_a.label) {
|
|
198
|
+
case 0:
|
|
199
|
+
handlerSpy = jasmine.createSpy("handler");
|
|
200
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
201
|
+
path: "/protected",
|
|
202
|
+
method: index_1.HttpMethod.get,
|
|
203
|
+
permissions: "read",
|
|
204
|
+
handler: handlerSpy,
|
|
205
|
+
});
|
|
206
|
+
plugin.init(mockApp);
|
|
207
|
+
routeHandler = registeredRoutes.get("GET:/protected");
|
|
208
|
+
if (!routeHandler) {
|
|
209
|
+
fail("Route handler not registered");
|
|
210
|
+
return [2 /*return*/];
|
|
211
|
+
}
|
|
212
|
+
mockReq = {};
|
|
213
|
+
mockRes = {};
|
|
214
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
215
|
+
case 1:
|
|
216
|
+
_a.sent();
|
|
217
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read");
|
|
218
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
219
|
+
return [2 /*return*/];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}); });
|
|
223
|
+
it("should validate multiple permissions", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
224
|
+
var handlerSpy, plugin, routeHandler, mockReq, mockRes;
|
|
225
|
+
return __generator(this, function (_a) {
|
|
226
|
+
switch (_a.label) {
|
|
227
|
+
case 0:
|
|
228
|
+
handlerSpy = jasmine.createSpy("handler");
|
|
229
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
230
|
+
path: "/admin",
|
|
231
|
+
method: index_1.HttpMethod.post,
|
|
232
|
+
permissions: ["read", "write", "admin"],
|
|
233
|
+
handler: handlerSpy,
|
|
234
|
+
});
|
|
235
|
+
plugin.init(mockApp);
|
|
236
|
+
routeHandler = registeredRoutes.get("POST:/admin");
|
|
237
|
+
if (!routeHandler) {
|
|
238
|
+
fail("Route handler not registered");
|
|
239
|
+
return [2 /*return*/];
|
|
240
|
+
}
|
|
241
|
+
mockReq = {};
|
|
242
|
+
mockRes = {};
|
|
243
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
244
|
+
case 1:
|
|
245
|
+
_a.sent();
|
|
246
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, ["read", "write", "admin"]);
|
|
247
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
248
|
+
return [2 /*return*/];
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}); });
|
|
252
|
+
it("should return 401 when authentication fails", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
253
|
+
var handlerSpy, plugin, routeHandler, mockReq, mockJsonSpy, mockRes;
|
|
254
|
+
return __generator(this, function (_a) {
|
|
255
|
+
switch (_a.label) {
|
|
256
|
+
case 0:
|
|
257
|
+
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(false));
|
|
258
|
+
handlerSpy = jasmine.createSpy("handler");
|
|
259
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
260
|
+
path: "/protected",
|
|
261
|
+
method: index_1.HttpMethod.get,
|
|
262
|
+
permissions: "read",
|
|
263
|
+
handler: handlerSpy,
|
|
264
|
+
});
|
|
265
|
+
plugin.init(mockApp);
|
|
266
|
+
routeHandler = registeredRoutes.get("GET:/protected");
|
|
267
|
+
if (!routeHandler) {
|
|
268
|
+
fail("Route handler not registered");
|
|
269
|
+
return [2 /*return*/];
|
|
270
|
+
}
|
|
271
|
+
mockReq = {};
|
|
272
|
+
mockJsonSpy = jasmine.createSpy("json");
|
|
273
|
+
mockRes = {
|
|
274
|
+
status: jasmine.createSpy("status").and.returnValue({ json: mockJsonSpy }),
|
|
275
|
+
};
|
|
276
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
277
|
+
case 1:
|
|
278
|
+
_a.sent();
|
|
279
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read");
|
|
280
|
+
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
281
|
+
expect(mockJsonSpy).toHaveBeenCalledWith({
|
|
282
|
+
status: 401,
|
|
283
|
+
error: {
|
|
284
|
+
title: "Unauthorized",
|
|
285
|
+
detail: "Authentication required or insufficient permissions",
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
expect(handlerSpy).not.toHaveBeenCalled();
|
|
289
|
+
return [2 /*return*/];
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}); });
|
|
293
|
+
it("should not call handler when authentication fails", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
294
|
+
var handlerSpy, plugin, routeHandler, mockReq, mockRes;
|
|
295
|
+
return __generator(this, function (_a) {
|
|
296
|
+
switch (_a.label) {
|
|
297
|
+
case 0:
|
|
298
|
+
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(false));
|
|
299
|
+
handlerSpy = jasmine.createSpy("handler");
|
|
300
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
301
|
+
path: "/protected",
|
|
302
|
+
method: index_1.HttpMethod.post,
|
|
303
|
+
permissions: "write",
|
|
304
|
+
handler: handlerSpy,
|
|
305
|
+
});
|
|
306
|
+
plugin.init(mockApp);
|
|
307
|
+
routeHandler = registeredRoutes.get("POST:/protected");
|
|
308
|
+
if (!routeHandler) {
|
|
309
|
+
fail("Route handler not registered");
|
|
310
|
+
return [2 /*return*/];
|
|
311
|
+
}
|
|
312
|
+
mockReq = {};
|
|
313
|
+
mockRes = {
|
|
314
|
+
status: function () { return ({ json: function () { } }); },
|
|
315
|
+
};
|
|
316
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
317
|
+
case 1:
|
|
318
|
+
_a.sent();
|
|
319
|
+
expect(handlerSpy).not.toHaveBeenCalled();
|
|
320
|
+
return [2 /*return*/];
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}); });
|
|
324
|
+
it("should throw error if permissions are set but no auth plugin is configured", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
325
|
+
var appWithoutAuth, plugin, routeHandler, mockReq, mockRes;
|
|
326
|
+
return __generator(this, function (_a) {
|
|
327
|
+
switch (_a.label) {
|
|
328
|
+
case 0:
|
|
329
|
+
appWithoutAuth = {
|
|
330
|
+
expressApp: mockExpressApp,
|
|
331
|
+
auth: null,
|
|
332
|
+
};
|
|
333
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
334
|
+
path: "/protected",
|
|
335
|
+
method: index_1.HttpMethod.get,
|
|
336
|
+
permissions: "read",
|
|
337
|
+
handler: function () { },
|
|
338
|
+
});
|
|
339
|
+
plugin.init(appWithoutAuth);
|
|
340
|
+
routeHandler = registeredRoutes.get("GET:/protected");
|
|
341
|
+
if (!routeHandler) {
|
|
342
|
+
fail("Route handler not registered");
|
|
343
|
+
return [2 /*return*/];
|
|
344
|
+
}
|
|
345
|
+
mockReq = {};
|
|
346
|
+
mockRes = {};
|
|
347
|
+
return [4 /*yield*/, expectAsync(routeHandler(mockReq, mockRes)).toBeRejectedWithError("Route GET /protected requires permissions but no auth plugin is configured")];
|
|
348
|
+
case 1:
|
|
349
|
+
_a.sent();
|
|
350
|
+
return [2 /*return*/];
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}); });
|
|
354
|
+
it("should call handler after successful authentication", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
355
|
+
var handlerSpy, plugin, routeHandler, mockReq, mockRes;
|
|
356
|
+
return __generator(this, function (_a) {
|
|
357
|
+
switch (_a.label) {
|
|
358
|
+
case 0:
|
|
359
|
+
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.callFake(function (req) {
|
|
360
|
+
req.user = { id: "123", username: "testuser" };
|
|
361
|
+
return Promise.resolve(true);
|
|
362
|
+
});
|
|
363
|
+
handlerSpy = jasmine.createSpy("handler");
|
|
364
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
365
|
+
path: "/protected",
|
|
366
|
+
method: index_1.HttpMethod.get,
|
|
367
|
+
permissions: "read",
|
|
368
|
+
handler: handlerSpy,
|
|
369
|
+
});
|
|
370
|
+
plugin.init(mockApp);
|
|
371
|
+
routeHandler = registeredRoutes.get("GET:/protected");
|
|
372
|
+
if (!routeHandler) {
|
|
373
|
+
fail("Route handler not registered");
|
|
374
|
+
return [2 /*return*/];
|
|
375
|
+
}
|
|
376
|
+
mockReq = {};
|
|
377
|
+
mockRes = {};
|
|
378
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
379
|
+
case 1:
|
|
380
|
+
_a.sent();
|
|
381
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalled();
|
|
382
|
+
expect(mockReq.user).toEqual({ id: "123", username: "testuser" });
|
|
383
|
+
expect(handlerSpy).toHaveBeenCalledWith(mockReq, mockRes, mockApp);
|
|
384
|
+
return [2 /*return*/];
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}); });
|
|
388
|
+
});
|
|
389
|
+
describe("Wildcard Permission", function () {
|
|
390
|
+
it("should validate wildcard permission for any authenticated user", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
391
|
+
var handlerSpy, plugin, routeHandler, mockReq, mockRes;
|
|
392
|
+
return __generator(this, function (_a) {
|
|
393
|
+
switch (_a.label) {
|
|
394
|
+
case 0:
|
|
395
|
+
handlerSpy = jasmine.createSpy("handler");
|
|
396
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
397
|
+
path: "/authenticated",
|
|
398
|
+
method: index_1.HttpMethod.get,
|
|
399
|
+
permissions: "*",
|
|
400
|
+
handler: handlerSpy,
|
|
401
|
+
});
|
|
402
|
+
plugin.init(mockApp);
|
|
403
|
+
routeHandler = registeredRoutes.get("GET:/authenticated");
|
|
404
|
+
if (!routeHandler) {
|
|
405
|
+
fail("Route handler not registered");
|
|
406
|
+
return [2 /*return*/];
|
|
407
|
+
}
|
|
408
|
+
mockReq = {};
|
|
409
|
+
mockRes = {};
|
|
410
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
411
|
+
case 1:
|
|
412
|
+
_a.sent();
|
|
413
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "*");
|
|
414
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
415
|
+
return [2 /*return*/];
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}); });
|
|
419
|
+
});
|
|
420
|
+
describe("Real-world Scenarios", function () {
|
|
421
|
+
it("should handle webhook with permissions", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
422
|
+
var webhookData, webhookHandler, plugin, routeHandler, mockReq, mockJsonSpy, mockRes;
|
|
423
|
+
return __generator(this, function (_a) {
|
|
424
|
+
switch (_a.label) {
|
|
425
|
+
case 0:
|
|
426
|
+
webhookHandler = function (req, res) {
|
|
427
|
+
webhookData = req.body;
|
|
428
|
+
res.json({ received: true });
|
|
429
|
+
};
|
|
430
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
431
|
+
path: "/webhook/stripe",
|
|
432
|
+
method: index_1.HttpMethod.post,
|
|
433
|
+
permissions: "webhook:stripe",
|
|
434
|
+
handler: webhookHandler,
|
|
435
|
+
});
|
|
436
|
+
plugin.init(mockApp);
|
|
437
|
+
routeHandler = registeredRoutes.get("POST:/webhook/stripe");
|
|
438
|
+
if (!routeHandler) {
|
|
439
|
+
fail("Route handler not registered");
|
|
440
|
+
return [2 /*return*/];
|
|
441
|
+
}
|
|
442
|
+
mockReq = { body: { event: "payment.success" } };
|
|
443
|
+
mockJsonSpy = jasmine.createSpy("json");
|
|
444
|
+
mockRes = { json: mockJsonSpy };
|
|
445
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
446
|
+
case 1:
|
|
447
|
+
_a.sent();
|
|
448
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "webhook:stripe");
|
|
449
|
+
expect(webhookData).toEqual({ event: "payment.success" });
|
|
450
|
+
expect(mockJsonSpy).toHaveBeenCalledWith({ received: true });
|
|
451
|
+
return [2 /*return*/];
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}); });
|
|
455
|
+
it("should handle file download with permissions", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
456
|
+
var fileHandler, plugin, routeHandler, mockReq, mockSetHeaderSpy, mockEndSpy, mockRes;
|
|
457
|
+
return __generator(this, function (_a) {
|
|
458
|
+
switch (_a.label) {
|
|
459
|
+
case 0:
|
|
460
|
+
fileHandler = function (req, res) {
|
|
461
|
+
res.setHeader("Content-Type", "application/pdf");
|
|
462
|
+
res.end("file-data");
|
|
463
|
+
};
|
|
464
|
+
plugin = (0, index_1.genericRequestPlugin)({
|
|
465
|
+
path: "/download/:fileId",
|
|
466
|
+
method: index_1.HttpMethod.get,
|
|
467
|
+
permissions: "file:download",
|
|
468
|
+
handler: fileHandler,
|
|
469
|
+
});
|
|
470
|
+
plugin.init(mockApp);
|
|
471
|
+
routeHandler = registeredRoutes.get("GET:/download/:fileId");
|
|
472
|
+
if (!routeHandler) {
|
|
473
|
+
fail("Route handler not registered");
|
|
474
|
+
return [2 /*return*/];
|
|
475
|
+
}
|
|
476
|
+
mockReq = { params: { fileId: "123" } };
|
|
477
|
+
mockSetHeaderSpy = jasmine.createSpy("setHeader");
|
|
478
|
+
mockEndSpy = jasmine.createSpy("end");
|
|
479
|
+
mockRes = {
|
|
480
|
+
setHeader: mockSetHeaderSpy,
|
|
481
|
+
end: mockEndSpy,
|
|
482
|
+
};
|
|
483
|
+
return [4 /*yield*/, routeHandler(mockReq, mockRes)];
|
|
484
|
+
case 1:
|
|
485
|
+
_a.sent();
|
|
486
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "file:download");
|
|
487
|
+
expect(mockSetHeaderSpy).toHaveBeenCalledWith("Content-Type", "application/pdf");
|
|
488
|
+
expect(mockEndSpy).toHaveBeenCalledWith("file-data");
|
|
489
|
+
return [2 /*return*/];
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}); });
|
|
493
|
+
});
|
|
494
|
+
});
|
|
@@ -18,5 +18,11 @@ export type GenericRequestOptions = {
|
|
|
18
18
|
* Http method for this request
|
|
19
19
|
*/
|
|
20
20
|
method: HttpMethod;
|
|
21
|
+
/**
|
|
22
|
+
* Optional permission(s) required to access this route.
|
|
23
|
+
* If set, the auth plugin will validate the request before calling the handler.
|
|
24
|
+
* Requires an auth plugin (e.g., jwt-auth-plugin) to be configured in FlinkApp.
|
|
25
|
+
*/
|
|
26
|
+
permissions?: string | string[];
|
|
21
27
|
};
|
|
22
28
|
export declare const genericRequestPlugin: (options: GenericRequestOptions) => FlinkPlugin;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.genericRequestPlugin = exports.HttpMethod = void 0;
|
|
43
|
+
var node_color_log_1 = __importDefault(require("node-color-log"));
|
|
44
|
+
var HttpMethod;
|
|
45
|
+
(function (HttpMethod) {
|
|
46
|
+
HttpMethod["get"] = "get";
|
|
47
|
+
HttpMethod["post"] = "post";
|
|
48
|
+
HttpMethod["put"] = "put";
|
|
49
|
+
HttpMethod["delete"] = "delete";
|
|
50
|
+
})(HttpMethod || (exports.HttpMethod = HttpMethod = {}));
|
|
51
|
+
var genericRequestPlugin = function (options) {
|
|
52
|
+
return {
|
|
53
|
+
id: "genericRequestPlugin",
|
|
54
|
+
init: function (app) { return init(app, options); },
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
exports.genericRequestPlugin = genericRequestPlugin;
|
|
58
|
+
function init(app, options) {
|
|
59
|
+
var _this = this;
|
|
60
|
+
var expressApp = app.expressApp;
|
|
61
|
+
if (!expressApp) {
|
|
62
|
+
throw new Error("Express app not initialized");
|
|
63
|
+
}
|
|
64
|
+
expressApp[options.method](options.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
65
|
+
var authenticated;
|
|
66
|
+
return __generator(this, function (_a) {
|
|
67
|
+
switch (_a.label) {
|
|
68
|
+
case 0:
|
|
69
|
+
if (!options.permissions) return [3 /*break*/, 2];
|
|
70
|
+
if (!app.auth) {
|
|
71
|
+
throw new Error("Route ".concat(options.method.toUpperCase(), " ").concat(options.path, " requires permissions but no auth plugin is configured"));
|
|
72
|
+
}
|
|
73
|
+
return [4 /*yield*/, app.auth.authenticateRequest(req, options.permissions)];
|
|
74
|
+
case 1:
|
|
75
|
+
authenticated = _a.sent();
|
|
76
|
+
if (!authenticated) {
|
|
77
|
+
return [2 /*return*/, res.status(401).json({
|
|
78
|
+
status: 401,
|
|
79
|
+
error: {
|
|
80
|
+
title: "Unauthorized",
|
|
81
|
+
detail: "Authentication required or insufficient permissions",
|
|
82
|
+
},
|
|
83
|
+
})];
|
|
84
|
+
}
|
|
85
|
+
_a.label = 2;
|
|
86
|
+
case 2:
|
|
87
|
+
// Call the handler
|
|
88
|
+
options.handler(req, res, app);
|
|
89
|
+
return [2 /*return*/];
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}); });
|
|
93
|
+
node_color_log_1.default.info("Registered genericRequest route ".concat(options.method, " ").concat(options.path));
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/generic-request-plugin",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.47",
|
|
4
4
|
"description": "Flink plugin that makes it possible to override default request handlers and handle specific requests manually",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "
|
|
6
|
+
"test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
|
|
7
7
|
"prepare": "tsc"
|
|
8
8
|
},
|
|
9
9
|
"author": "johan@frost.se",
|
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
"node-color-log": "^5.3.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@flink-app/flink": "^0.12.1-alpha.
|
|
21
|
+
"@flink-app/flink": "^0.12.1-alpha.47",
|
|
22
22
|
"@types/express": "4.17.11",
|
|
23
|
+
"@types/jasmine": "^3.7.1",
|
|
23
24
|
"@types/node": "22.13.10",
|
|
25
|
+
"jasmine": "^3.7.0",
|
|
26
|
+
"jasmine-spec-reporter": "^7.0.0",
|
|
24
27
|
"ts-node": "^9.1.1",
|
|
25
28
|
"typescript": "5.4.5"
|
|
26
29
|
},
|
|
27
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "a98a0af7f11e4a97f68da4d0d67677df7d2a2749"
|
|
28
31
|
}
|