Skip to content

Commit

Permalink
feat: implement ping endpoint and documentation
Browse files Browse the repository at this point in the history
- use typebox to verify inputs ardatan/feTS#902 (comment)
  • Loading branch information
MrOrz committed Nov 17, 2024
1 parent c79b3a7 commit 26b9f35
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 34 deletions.
39 changes: 21 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"description": "GraphQL API server for clients like rumors-site and rumors-line-bot",
"main": "index.js",
"repository": {
"url": "[email protected]:MrOrz/rumors-api.git",
"url": "[email protected]:cofacts/rumors-api.git",
"type": "git"
},
"author": "MrOrz <[email protected]>",
"author": "Cofacts WG <[email protected]>",
"license": "MIT",
"scripts": {
"dev": "pm2-dev start --timestamp process-dev.json",
Expand All @@ -31,6 +31,7 @@
"@google-cloud/vision": "^3.1.4",
"@grpc/grpc-js": "^1.6.7",
"@grpc/proto-loader": "^0.5.0",
"@sinclair/typebox": "^0.33.22",
"apollo-server-koa": "^2.11.0",
"cli-progress": "^3.9.1",
"dataloader": "^2.0.0",
Expand Down
17 changes: 17 additions & 0 deletions src/adm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Cofacts Admin API

Welcome! You have been granted access to this API by the Cofacts Work Group.

To access the API programmatically, you need to use [Service Tokens](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access).
Please contact Cofacts Work Group to get your `CLIENT_ID` and `CLIENT_SECRET`.

Here are some examples with `curl` command (`<current host>` denotes the host name of this document page, i.e. URL without "/docs".):
```sh
# Get OpenAPI schema via curl command
curl -H "CF-Access-Client-Id: <CLIENT_ID>" -H "CF-Access-Client-Secret: <CLIENT_SECRET>" <current host>/openapi.json

# Call POST /ping and get pong + echo
curl -XPOST -H "CF-Access-Client-Id: <CLIENT_ID>" -H "CF-Access-Client-Secret: <CLIENT_SECRET>" -d '{"echo": "foo"}' <current host>/ping
```

The response would attach a cookie named `CF_Authorization` that you may use for [subsequent requests](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#subsequent-requests).
4 changes: 4 additions & 0 deletions src/adm/handlers/ping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Input and output types are manually aligned to the actual API */
export default function pingHandler({ echo }: { echo: string }): string {
return `pong ${echo}`;
}
46 changes: 36 additions & 10 deletions src/adm/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
import 'dotenv/config';

import { createServer } from 'node:http';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { createRouter, Response } from 'fets';
import { Type } from '@sinclair/typebox';

import { useAuditLog } from './util';

import pingHandler from './handlers/ping';

const shouldAuth = true; // process.env.NODE_ENV === 'production';

const [titleLine, ...readmeLines] = readFileSync(
path.resolve(__dirname, 'README.md'),
'utf-8'
).split('\n');

const router = createRouter({
openAPI: {
info: {
title: titleLine.replace(/^#+\s+/, ''),
version: '1.0.0',
description: shouldAuth ? readmeLines.join('\n').trim() : '',
},
},

// Include audit log plugin and block non-cloudflare requests only in production
//
plugins: process.env.NODE_ENV === 'production' ? [useAuditLog()] : [],
plugins: shouldAuth ? [useAuditLog()] : [],
}).route({
method: 'GET',
path: '/greetings',
method: 'POST',
path: '/ping',
description:
'Please use this harmless endpoint to test if your connection with API is wired up correctly.',
schemas: {
responses: {
200: {
type: 'object',
properties: {
hello: { type: 'string' },
request: {
json: Type.Object(
{
echo: Type.String({
description: 'Text that will be included in response message',
}),
},
},
{ additionalProperties: false }
),
},
responses: { 200: { type: 'string' } },
},
handler: () => Response.json({ hello: 'world' }),
handler: async (request) => Response.json(pingHandler(await request.json())),
});

createServer(router).listen(process.env.ADM_PORT, () => {
Expand Down
20 changes: 16 additions & 4 deletions src/adm/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,15 @@ export function useAuditLog(): RouterPlugin<any, any> {
})
).payload;
} catch (e) {
logger.error(e, 'Cloudflare Access token verification failed');
logger.error(e, 'Cloudflare access token verification failed');
throw new HTTPError(403, 'Unauthorized', {});
}

const shouldIncludeBody =
'common_name' in payload /* Called via service tokens */ ||
request.headers.get('content-type') ===
'application/json'; /* Probably from Swagger UI */

logger.info(
{
...requestInfo,
Expand All @@ -85,20 +90,27 @@ export function useAuditLog(): RouterPlugin<any, any> {
TOKEN_KEYS_TO_INCLUDE.has(key)
)
),
req: shouldIncludeBody ? await request.json() : undefined,
},
'Cloudflare Access token verified'
'Req start'
);
},

async onResponse({ request, response }) {
const shouldIncludeBody =
response.ok &&
(response.headers.get('content-type') ?? '').startsWith(
'application/json'
);

logger.info(
{
id: request.headers.get('cf-ray'),
url: request.url,
status: response.status,
res: response.ok ? await response.json() : undefined,
res: shouldIncludeBody ? await response.json() : undefined,
},
'Request completed'
'Req end'
);
},
};
Expand Down

0 comments on commit 26b9f35

Please sign in to comment.