Skip to content
This repository was archived by the owner on Mar 26, 2023. It is now read-only.

Commit ea0d480

Browse files
feat(koa): Koa-ize sugar (#2)
* Koa-ize sugar * You shall not pass * Fix typo in console.log * Code review * Use @koa/router * Go back to using ctx.request.body * Apply suggestions from code review (ctx.headers) Co-authored-by: Adrian Paschkowski <[email protected]> Co-authored-by: Adrian Paschkowski <[email protected]>
1 parent 5f48363 commit ea0d480

File tree

13 files changed

+468
-424
lines changed

13 files changed

+468
-424
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/dist/
22
/node_modules/
33
/src/config.json
4-
/src/lastUpdate.json
4+
/src/lastUpdate.json
5+
yarn-error.log

package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@
1010
"plugin:you-dont-need-momentjs/recommended"
1111
],
1212
"dependencies": {
13+
"@koa/router": "^10.1.1",
1314
"@types/chai": "^4.2.22",
1415
"chai": "^4.3.4",
1516
"chai-as-promised": "^7.1.1",
1617
"chargebee-typescript": "^2.3.0",
1718
"dayjs": "^1.10.7",
1819
"eslint": "^8.2.0",
19-
"express": "^4.17.1",
20-
"fs": "^0.0.1-security",
21-
"helmet": "^4.6.0",
20+
"koa": "^2.13.4",
21+
"koa-bodyparser": "^4.3.0",
22+
"koa-helmet": "^6.1.0",
2223
"mocha": "^9.1.3",
2324
"pg": "^8.7.1",
2425
"ts-node": "^10.4.0",
@@ -34,7 +35,10 @@
3435
"test": "mocha --require ts-node/register --timeout 10000 test/**/*.spec.ts"
3536
},
3637
"devDependencies": {
37-
"@types/express": "^4.17.13",
38+
"@types/koa": "^2.13.4",
39+
"@types/koa-bodyparser": "^4.3.5",
40+
"@types/koa-helmet": "^6.0.4",
41+
"@types/koa__router": "^8.0.11",
3842
"@types/node": "^16.11.7",
3943
"@types/pg": "^8.6.1",
4044
"@typescript-eslint/eslint-plugin": "^5.4.0",

src/app.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
1-
import express from "express";
2-
import helmet from "helmet";
1+
import Koa from "koa";
2+
import helmet from "koa-helmet";
3+
import bodyParser from "koa-bodyparser";
34
import { HOOK_PORT } from "./config.json";
4-
import hookRoutes from "./hook/routes";
5-
import errorHandler from "./middlewares/errorHandler";
5+
import hookRoute from "./hook/route";
66
import notFoundHandler from "./middlewares/notFoundHandler";
77

8-
const app = express();
8+
const app = new Koa();
99
const port = HOOK_PORT;
10+
app.proxy = true;
1011

1112
app.use(helmet());
12-
app.use(express.json());
13-
app.set("trust proxy", true);
14-
15-
app.use("/", hookRoutes);
16-
13+
app.use(bodyParser());
1714
app.use(notFoundHandler);
18-
app.use(errorHandler);
15+
16+
app.use(hookRoute.routes());
1917

2018
app.listen(HOOK_PORT, () => {
21-
console.log(`Hook listening on port ${port}`);
19+
console.log(`Server running on port ${port}`);
2220
});

src/hook/controller.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Event } from "chargebee-typescript/lib/resources";
2+
import { RouterContext } from "@koa/router";
3+
4+
const allowed_events: Event["event_type"][] = [
5+
"subscription_activated",
6+
"subscription_started",
7+
"subscription_paused",
8+
"subscription_cancelled",
9+
];
10+
11+
export const hookController = async (ctx: RouterContext) => {
12+
const { event_type: eventType } = ctx.request.body as Event;
13+
console.log("Received hookController event", {
14+
body: ctx.request.body,
15+
eventType: eventType,
16+
});
17+
18+
if (!allowed_events.includes(eventType || "")) {
19+
console.log("process request found an unknown event", {
20+
event: eventType,
21+
});
22+
23+
ctx.status = 404;
24+
ctx.body = {};
25+
return;
26+
}
27+
28+
// TODO: Handle events
29+
console.log("Valid event: " + eventType);
30+
31+
ctx.status = 200;
32+
ctx.body = {};
33+
};

src/hook/controllers/hook.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/hook/requestAuthenticator.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fs from "fs";
2-
import { RequestHandler, Request } from "express";
2+
import { Context, Middleware } from "koa";
33
import {
44
HOOK_KEY,
55
BASIC_AUTH_USERNAME,
@@ -17,44 +17,40 @@ const isAllowedOrigin = (hostname: string) => {
1717
return false;
1818
};
1919

20-
export const authenticateRequest: RequestHandler = async (req, res, next) => {
21-
if (
22-
!isAllowedOrigin(req.hostname) &&
23-
(!req.headers["user-agent"] || req.headers["user-agent"] !== "ChargeBee")
24-
) {
20+
export const authenticateRequest: Middleware = async (ctx, next) => {
21+
if (!isAllowedOrigin(ctx.hostname) && ctx.get("user-agent") !== "ChargeBee") {
2522
console.log("requestAuthenticator error - UNEXPECTED REQUEST ORIGIN", {
26-
userAgent: req.headers["user-agent"],
27-
ip: req.ip,
28-
hostname: req.hostname,
23+
userAgent: ctx.get("user-agent"),
24+
ip: ctx.ip,
25+
hostname: ctx.hostname,
2926
});
3027

31-
res.status(401).json({ error: "UNEXPECTED REQUEST ORIGIN" });
28+
ctx.status = 401;
29+
ctx.body = { error: "UNEXPECTED REQUEST ORIGIN" };
3230
return;
3331
}
34-
if (!canAttemptAuthentication(req)) {
32+
if (!canAttemptAuthentication(ctx)) {
3533
console.log("requestAuthenticator error - MAX ATTEMPTS REACHED FOR IP", {
36-
ip: req.ip,
34+
ip: ctx.ip,
3735
});
3836

39-
return res
40-
.status(401)
41-
.json({ error: "MAX ATTEMPTS REACHED FOR " + req.ip });
37+
ctx.status = 401;
38+
ctx.body = { error: "MAX ATTEMPTS REACHED FOR " + ctx.ip };
39+
return;
4240
}
43-
if (!req.query.key || req.query.key !== HOOK_KEY) {
41+
if (!ctx.query.key || ctx.query.key !== HOOK_KEY) {
4442
console.log("requestAuthenticator error - INVALID KEY", {
45-
key: req.query.key?.toString().slice(-5),
43+
key: ctx.query.key?.toString().slice(-5),
4644
});
4745

48-
onAuthenticationFailure(req);
49-
return res.status(401).json({ error: "INVALID KEY" });
46+
onAuthenticationFailure(ctx);
47+
ctx.status = 401;
48+
return;
5049
}
51-
if (
52-
req.headers.authorization &&
53-
req.headers.authorization.startsWith("Basic ")
54-
) {
55-
const base64authorization: string = req.headers.authorization.substring(
56-
"Basic ".length
57-
);
50+
if (ctx.get("authorization")?.startsWith("Basic ")) {
51+
const base64authorization: string = ctx
52+
.get("authorization")
53+
.substring("Basic ".length);
5854
const authorization: string[] = Buffer.from(base64authorization, "base64")
5955
.toString("utf8")
6056
.split(":", 2);
@@ -66,17 +62,19 @@ export const authenticateRequest: RequestHandler = async (req, res, next) => {
6662
password: (password || "").slice(-5),
6763
});
6864

69-
onAuthenticationFailure(req);
70-
return res.status(401).json({ error: "INVALID CREDENTIALS" });
65+
onAuthenticationFailure(ctx);
66+
ctx.status = 401;
67+
return;
7168
}
7269
return next();
7370
} else {
7471
console.log("requestAuthenticator error - MISSING AUTHENTICATION HEADER", {
75-
authHeader: req.headers.authorization?.slice(0, 5),
72+
authHeader: ctx.get("authorization")?.slice(0, 5),
7673
});
7774

78-
onAuthenticationFailure(req);
79-
return res.status(401).json({ error: "MISSING AUTHENTICATION HEADER" });
75+
onAuthenticationFailure(ctx);
76+
ctx.status = 401;
77+
return;
8078
}
8179
};
8280

@@ -122,8 +120,8 @@ class IpAuthData {
122120

123121
const authCache: { [ip: string]: IpAuthData } = {};
124122

125-
const onAuthenticationFailure = (req: Request): void => {
126-
const ip: string = getIp(req);
123+
const onAuthenticationFailure = (ctx: Context): void => {
124+
const ip: string = getIp(ctx);
127125
let ipAuthData = authCache[ip];
128126
if (!ipAuthData) {
129127
authCache[ip] = new IpAuthData(false);
@@ -151,8 +149,8 @@ const onAuthenticationFailure = (req: Request): void => {
151149
}
152150
};
153151

154-
const canAttemptAuthentication = (req: Request): boolean => {
155-
const ip: string = getIp(req);
152+
const canAttemptAuthentication = (ctx: Context): boolean => {
153+
const ip: string = getIp(ctx);
156154
const ipAuthData = authCache[ip];
157155
if (ipAuthData) {
158156
if (ipAuthData.isBlocked()) {
@@ -170,13 +168,13 @@ const canAttemptAuthentication = (req: Request): boolean => {
170168
return true;
171169
};
172170

173-
const getIp = (req: Request): string => {
174-
const cfIp: string | string[] | undefined = req.headers["cf-connecting-ip"];
171+
const getIp = (ctx: Context): string => {
172+
const cfIp: string | string[] | undefined = ctx.get("cf-connecting-ip");
175173
const ip: string = cfIp
176174
? typeof cfIp === "string"
177175
? cfIp
178176
: cfIp[0]
179-
: req.ip;
177+
: ctx.ip;
180178
return ip;
181179
};
182180

src/hook/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Router from "@koa/router";
2+
import { hookController } from "./controller";
3+
import { authenticateRequest } from "./requestAuthenticator";
4+
5+
const router = new Router();
6+
7+
router.post("/hook", authenticateRequest, hookController);
8+
9+
export default router;

src/hook/routes/hook.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/hook/routes/index.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/middlewares/errorHandler.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)