Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"markdown-it": "^14.1.0",
"marked": "^15.0.7",
"normalize.css": "^8.0.1",
"pino": "^9.6.0",
"prismjs": "^1.29.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
Expand Down
55 changes: 29 additions & 26 deletions pages-common/apiHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {NextApiRequest, NextApiResponse} from "next";
import {handleExceptions} from "./handleExceptions";
import {AuthContext, readJWTBearer} from "./auth";
import {wrapAPILogging} from "./logger";

type APIHandler = (req: NextApiRequest, res: NextApiResponse) => Promise<void>;

Expand All @@ -12,6 +13,7 @@ export function createAPIHandler(methodHandlers: {
PATCH?: MethodHandler;
PUT?: MethodHandler;
DELETE?: MethodHandler;
_all?: MethodHandler;
}): APIHandler {
const handlers: {[key: string]: {handler: APIHandler; noAuth: boolean}} = {};
for (const [method, handler] of Object.entries(methodHandlers)) {
Expand All @@ -21,34 +23,35 @@ export function createAPIHandler(methodHandlers: {
: {...handler, noAuth: handler.noAuth ?? false};
}

return async (req: NextApiRequest, res: NextApiResponse) => {
const methodHandler = handlers[req.method!];
return async (req: NextApiRequest, res: NextApiResponse) =>
wrapAPILogging(req, res, async () => {
const methodHandler = handlers[req.method!] ?? handlers._all;

if (!methodHandler) {
res.status(405).end(`method ${req.method} not allowed`);
return;
}

// Auth can be disabled on a per method basis because some admin api
// routes can still be public:
// for example those that return the list of agreements (GET),
// even though cherry-picking endpoints that require authentication is
// not the best developer's UX.
if (!methodHandler.noAuth) {
/**
* Requires an authenticated user for an API request. Inspects request
* headers to look for a valid JWT Bearer token. Stops request handling
* and returns 401 for unauthorized users.
*/
try {
const user = await readJWTBearer(req);
(req as unknown as AuthContext).user = user;
} catch (err: any) {
res.status(401).end(err);
if (!methodHandler) {
res.status(405).end(`method ${req.method} not allowed`);
return;
}
}

await handleExceptions(res, () => methodHandler.handler(req, res));
};
// Auth can be disabled on a per method basis because some admin api
// routes can still be public:
// for example those that return the list of agreements (GET),
// even though cherry-picking endpoints that require authentication is
// not the best developer's UX.
if (!methodHandler.noAuth) {
/**
* Requires an authenticated user for an API request. Inspects request
* headers to look for a valid JWT Bearer token. Stops request handling
* and returns 401 for unauthorized users.
*/
try {
const user = await readJWTBearer(req);
(req as unknown as AuthContext).user = user;
} catch (err: any) {
res.status(401).end(err);
return;
}
}

await handleExceptions(res, () => methodHandler.handler(req, res));
});
}
53 changes: 53 additions & 0 deletions pages-common/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {NextApiRequest, NextApiResponse} from "next";
import pino from "pino";

const logger = pino({
timestamp: pino.stdTimeFunctions.isoTime,
});

const reqContext = new AsyncLocalStorage();

export async function wrapAPILogging(
req: NextApiRequest,
res: NextApiResponse,
handler: () => Promise<void>
): Promise<void> {
const logData: object = {};
const startTime = performance.now();
try {
await reqContext.run(logData, handler);
logger.info({
method: req.method,
url: req.url,
duration_ms: performance.now() - startTime,
status_code: res.statusCode,
...logData,
});
} catch (err) {
logger.error({
method: req.method,
url: req.url,
duration_ms: performance.now() - startTime,
status_code: 500,
...(err instanceof Error
? {
error_name: err.name,
message: err.message,
cause: err.cause,
stack: err.stack,
}
: {
message: String(err),
}),
...logData,
});
res.status(500).end("internal server error");
}
}

export function addLogContext(data: object): void {
const ctxData = reqContext.getStore();
if (ctxData) {
Object.assign(ctxData, data);
}
}
14 changes: 10 additions & 4 deletions pages/api/health.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import {NextApiRequest, NextApiResponse} from "next";
import {NextApiResponse} from "next";
import {createAPIHandler} from "../../pages-common/apiHandler";

interface AliveContract {
alive: boolean;
timestamp: Date;
}

export default (req: NextApiRequest, res: NextApiResponse<AliveContract>) => {
res.status(200).json({alive: true, timestamp: new Date()});
};
export default createAPIHandler({
_all: {
handler: async (req, res: NextApiResponse<AliveContract>) => {
res.status(200).json({alive: true, timestamp: new Date()});
},
noAuth: true,
},
});
6 changes: 6 additions & 0 deletions pages/api/pullrequesthook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as bodyParser from "body-parser";
import * as httpErrors from "http-errors";
import {NextHandleFunction} from "connect";
import {createAPIHandler} from "../../../pages-common/apiHandler";
import {addLogContext} from "../../../pages-common/logger";

export const config = {
api: {
Expand Down Expand Up @@ -113,6 +114,8 @@ async function _handler(
// Next.js enforces lowercase header names
const event = req.headers["x-github-event"];

addLogContext({github_event: event});

if (!event) {
res.status(400).end("Missing X-GitHub-Event header");
return;
Expand All @@ -127,6 +130,7 @@ async function _handler(
const {body} = req;

const action = body.action;
addLogContext({github_pr_action: action});
if (["opened", "reopened", "synchronize"].indexOf(action) === -1) {
res
.status(200)
Expand Down Expand Up @@ -195,6 +199,8 @@ async function _handler(
},
};

addLogContext({cla_check_input: input});

await claHandler.checkCla(input);
res.status(200).end("OK");
return;
Expand Down
83 changes: 83 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,11 @@ assertion-error@^2.0.1:
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==

atomic-sleep@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==

babel-plugin-macros@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
Expand Down Expand Up @@ -1682,6 +1687,11 @@ eventemitter3@^4.0.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==

fast-redact@^3.1.1:
version "3.5.0"
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4"
integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==

fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
Expand Down Expand Up @@ -2579,6 +2589,11 @@ object-keys@~0.4.0:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
integrity sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==

on-exit-leak-free@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8"
integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==

[email protected]:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
Expand Down Expand Up @@ -2713,6 +2728,35 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==

pino-abstract-transport@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60"
integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==
dependencies:
split2 "^4.0.0"

pino-std-serializers@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b"
integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==

pino@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/pino/-/pino-9.6.0.tgz#6bc628159ba0cc81806d286718903b7fc6b13169"
integrity sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==
dependencies:
atomic-sleep "^1.0.0"
fast-redact "^3.1.1"
on-exit-leak-free "^2.1.0"
pino-abstract-transport "^2.0.0"
pino-std-serializers "^7.0.0"
process-warning "^4.0.0"
quick-format-unescaped "^4.0.3"
real-require "^0.2.0"
safe-stable-stringify "^2.3.1"
sonic-boom "^4.0.1"
thread-stream "^3.0.0"

pkg-dir@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
Expand Down Expand Up @@ -2800,6 +2844,11 @@ process-on-spawn@^1.0.0:
dependencies:
fromentries "^1.2.0"

process-warning@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb"
integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==

prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
Expand Down Expand Up @@ -2838,6 +2887,11 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==

quick-format-unescaped@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==

randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
Expand Down Expand Up @@ -2948,6 +3002,11 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"

real-require@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==

redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
Expand Down Expand Up @@ -3038,6 +3097,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==

safe-stable-stringify@^2.3.1:
version "2.5.0"
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==

"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
Expand Down Expand Up @@ -3205,6 +3269,13 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"

sonic-boom@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d"
integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==
dependencies:
atomic-sleep "^1.0.0"

"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
Expand Down Expand Up @@ -3232,6 +3303,11 @@ spawn-wrap@^2.0.0:
signal-exit "^3.0.2"
which "^2.0.1"

split2@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==

sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
Expand Down Expand Up @@ -3364,6 +3440,13 @@ test-exclude@^6.0.0:
glob "^7.1.4"
minimatch "^3.0.4"

thread-stream@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1"
integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==
dependencies:
real-require "^0.2.0"

through2@~0.4.1:
version "0.4.2"
resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b"
Expand Down