Skip to content

Commit af641a5

Browse files
committedDec 5, 2022
Migrate API from Heroku to Cloudflare workers
1 parent adc8330 commit af641a5

15 files changed

+603
-86
lines changed
 

‎Procfile

-1
This file was deleted.

‎package-lock.json

+428
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
"pwa": "npm run build && npm run serve",
1010
"lint": "eslint . --ext .ts",
1111
"prepare": "husky install",
12+
"cloudflare:login": "wrangler login",
13+
"api": "cd src/api && wrangler dev",
14+
"api:deploy": "cd src/api && wrangler publish",
1215
"cypress": "cypress open",
1316
"e2e": "cypress run"
1417
},
@@ -41,6 +44,7 @@
4144
"uuid": "^3.4.0"
4245
},
4346
"devDependencies": {
47+
"@cloudflare/wrangler": "^1.20.0",
4448
"@emotion/babel-plugin": "^11.9.5",
4549
"@types/react": "^17.0.1",
4650
"@types/react-dom": "^17.0.0",

‎src/3-github/gh-utils.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getQueryParameter } from '../0-dom/getQueryParameter';
22
import { Note } from '../2-entities/Note';
33
import {
4+
API_ORIGIN,
45
AUTH_ENDPOINT,
56
CLIENT_ID_DEV,
67
CLIENT_ID_PROD,
@@ -10,6 +11,7 @@ import {
1011
} from '../config.json';
1112
import { GithubToken } from './GithubAuth';
1213
import { GithubUsername } from './models/GHApiUser';
14+
1315
const GH_API = 'https://api.github.com';
1416

1517
export const ghRepository = getQueryParameter('repo', 'pensieve-data');
@@ -34,10 +36,11 @@ export const appOrigin = getOrigin();
3436
export const ghScope = GH_SCOPE;
3537
export const ghClientId = isLocalHost ? CLIENT_ID_DEV : CLIENT_ID_PROD;
3638

37-
const endpointOrigin = isLocalHost ? VALID_ORIGINS[0] : appOrigin;
39+
// TODO: we can't proxy the API calls since we moved to Cloudflare Workers
40+
// const endpointOrigin = isLocalHost ? VALID_ORIGINS[0] : appOrigin;
3841

39-
export const ghAuthEndpoint = `${endpointOrigin}${AUTH_ENDPOINT}`;
40-
export const ghCommitEndpoint = `${endpointOrigin}${COMMIT_ENDPOINT}`;
42+
export const ghAuthEndpoint = `${API_ORIGIN}${AUTH_ENDPOINT}`;
43+
export const ghCommitEndpoint = `${API_ORIGIN}${COMMIT_ENDPOINT}`;
4144

4245
function getOrigin() {
4346
const { origin } = location;

‎src/4-storage/middleware/ResilientOnlineStore.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { debugMethods } from '../../util/debugMethods';
44
import { AsyncStore } from '../AsyncStore';
55
import { WriteOptions } from '../helpers/WriteOptions';
66

7+
const MAX_ATTEMPTS = 2;
8+
79
interface Command<T extends keyof AsyncStore = keyof AsyncStore> {
810
method: T;
911
params: Parameters<AsyncStore[T]>;
@@ -99,7 +101,7 @@ export class ResilientOnlineStore implements AsyncStore {
99101
}
100102

101103
for (const { method, params, attempts } of retrievePending()) {
102-
if (attempts < 5) {
104+
if (attempts < MAX_ATTEMPTS) {
103105
this.command(method, params, attempts);
104106
} else {
105107
console.warn(`Command ${method} failed ${attempts} times`, ...params);

‎src/api/.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"root": true,
3+
"sourceType": "module",
34
"parserOptions": { "ecmaVersion": 2020 },
45
"env": { "es6": true, "node": true },
56
"extends": ["eslint:recommended"],

‎src/api/.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/target
2+
/dist
3+
**/*.rs.bk
4+
Cargo.lock
5+
bin/
6+
pkg/
7+
wasm-pack.log
8+
worker/
9+
node_modules/
10+
.cargo-ok

‎src/api/_proxyAxiosToExpress.js

-23
This file was deleted.

‎src/api/auth.js

+13-17
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1-
const axios = require('axios');
2-
const pipeAxiosToExpress = require('./_proxyAxiosToExpress');
1+
import { CLIENT_ID_DEV, CLIENT_ID_PROD } from '../config.json';
32

4-
const { CLIENT_ID_PROD, CLIENT_ID_DEV } = require('../config.json');
5-
const { CLIENT_SECRET_PROD, CLIENT_SECRET_DEV } = process.env;
6-
7-
module.exports = async (req, res) => {
8-
const { code, redirect_uri, state } = req.query;
3+
/**
4+
* @param {Request} request
5+
* @returns {Promise<Response>}
6+
*/
7+
export default async request => {
8+
const { searchParams } = new URL(request.url);
9+
const redirect_uri = searchParams.get('redirect_uri');
910
const isDev = redirect_uri.startsWith('http://localhost');
1011

11-
const params = {
12+
const url = 'https://github.com/login/oauth/access_token';
13+
const params = new URLSearchParams({
1214
client_id: isDev ? CLIENT_ID_DEV : CLIENT_ID_PROD,
1315
client_secret: isDev ? CLIENT_SECRET_DEV : CLIENT_SECRET_PROD,
1416
redirect_uri,
15-
state,
16-
code,
17-
};
18-
19-
console.log(params);
20-
21-
const response = axios.get('https://github.com/login/oauth/access_token', {
22-
params,
17+
state: searchParams.get('state'),
18+
code: searchParams.get('code'),
2319
});
2420

25-
return pipeAxiosToExpress(response, req, res);
21+
return fetch(`${url}?${params}`);
2622
};

‎src/api/commit.js

+54-24
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,64 @@
1-
const axios = require('axios');
2-
const pipeAxiosToExpress = require('./_proxyAxiosToExpress');
3-
const { GH_API } = require('../config.json');
4-
5-
module.exports = async (req, res) => {
6-
const { token, owner, repo, branch, files, message } = req.body;
7-
const promise = githubCommit({ token, owner, repo, branch, files, message });
8-
pipeAxiosToExpress(promise, req, res);
1+
import { GH_API } from '../config.json';
2+
3+
/**
4+
* @param {Request} request
5+
* @returns {Promise<Response>}
6+
*/
7+
export default async request => {
8+
const body = await request.json();
9+
const userAgent = request.headers.get('User-Agent');
10+
const { token, owner, repo, branch, files, message } = body;
11+
12+
await githubCommit({
13+
token,
14+
owner,
15+
repo,
16+
branch,
17+
files,
18+
message,
19+
userAgent,
20+
});
921
};
1022

11-
async function githubCommit({ token, owner, repo, branch, files, message }) {
23+
async function githubCommit({
24+
token,
25+
owner,
26+
repo,
27+
branch,
28+
files,
29+
message,
30+
userAgent,
31+
}) {
1232
const key = `${owner}/${repo}:${branch}`;
33+
const baseURL = `${GH_API}/repos/${owner}/${repo}`;
34+
const headers = { Authorization: `token ${token}`, 'User-Agent': userAgent };
1335

1436
console.log(`${key} - Commit requests`);
1537

16-
const request = axios.create({
17-
baseURL: `${GH_API}/repos/${owner}/${repo}`,
18-
headers: { Authorization: `token ${token}` },
19-
});
38+
async function request(method, url, json) {
39+
const response = await fetch(`${baseURL}${url}`, {
40+
method,
41+
headers,
42+
body: JSON.stringify(json),
43+
});
44+
45+
try {
46+
return await response.json();
47+
} catch (err) {
48+
console.log(`[Error from Github] ${await response.text()}`);
49+
throw err;
50+
}
51+
}
2052

2153
const items = prepareFilesForRequest(files);
2254

2355
console.log(`${key} - Requesting ref...`);
24-
const { data: ref } = await request.get(`/git/refs/heads/${branch}`);
56+
const ref = await request('GET', `/git/refs/heads/${branch}`);
2557
console.log(`${key} - Creating tree...`);
2658

2759
// Create tree
2860
// https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-tree
29-
const { data: tree } = await request.post(`/git/trees`, {
61+
const tree = await request('POST', `/git/trees`, {
3062
tree: items,
3163
base_tree: ref.object.sha,
3264
});
@@ -35,7 +67,7 @@ async function githubCommit({ token, owner, repo, branch, files, message }) {
3567

3668
// Create commit
3769
// https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-commit
38-
const { data: commit } = await request.post(`/git/commits`, {
70+
const commit = await request('POST', `/git/commits`, {
3971
message,
4072
tree: tree.sha,
4173
parents: [ref.object.sha],
@@ -45,14 +77,12 @@ async function githubCommit({ token, owner, repo, branch, files, message }) {
4577

4678
// Update a reference
4779
// https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
48-
return request
49-
.post(`/git/refs/heads/${branch}`, {
50-
sha: commit.sha,
51-
force: true,
52-
})
53-
.finally(() => {
54-
console.log(`${key} - Complete`);
55-
});
80+
return request('POST', `/git/refs/heads/${branch}`, {
81+
sha: commit.sha,
82+
force: true,
83+
}).finally(() => {
84+
console.log(`${key} - Complete`);
85+
});
5686
}
5787

5888
function prepareFilesForRequest(files) {

‎src/api/index.js

+60-17
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,67 @@
1-
const express = require('express');
2-
const app = express();
3-
const bodyParser = require('body-parser');
4-
const cors = require('cors');
1+
import { VALID_ORIGINS } from '../config.json';
2+
import auth from './auth.js';
3+
import commit from './commit.js';
54

6-
const port = process.env.PORT;
5+
const handlers = { auth, commit };
76

8-
if (!port) {
9-
throw new Error('Environment variables not set');
7+
addEventListener('fetch', event =>
8+
event.respondWith(handleCorsRequest(event.request)),
9+
);
10+
11+
/**
12+
* @param {Request} request
13+
* @returns {Promise<Response>}
14+
*/
15+
async function handleCorsRequest(request) {
16+
const method = request.method;
17+
const { origin } = new URL(request.url);
18+
19+
console.log({ origin });
20+
21+
const result = isValidOrigin(origin)
22+
? await handleRequest(request).catch(handleError)
23+
: new Response('Who the fck are you?', { status: 403 });
24+
25+
const response =
26+
result instanceof Response
27+
? new Response(result.body, result)
28+
: new Response(JSON.stringify(result), { status: 200 });
29+
30+
response.headers.set('Access-Control-Allow-Origin', '*');
31+
response.headers.set('Access-Control-Allow-Methods', method);
32+
response.headers.set('Access-Control-Max-Age', '86400');
33+
34+
return response;
1035
}
1136

12-
app.use(cors());
37+
/**
38+
* @param {Request} request
39+
* @returns {Promise<Response>}
40+
*/
41+
async function handleRequest(request) {
42+
const { pathname } = new URL(request.url);
43+
const [, firstPart] = pathname.split('/');
44+
const handler = handlers[firstPart];
1345

14-
app.post('/auth', require('./auth'));
46+
console.log(
47+
`Request to ${pathname} - ${handler ? handler.name : 'No handler'}`,
48+
);
1549

16-
app.post(
17-
'/commit',
18-
bodyParser.json({ limit: '50mb', type: 'text/plain' }),
19-
require('./commit'),
20-
);
50+
if (!handler) {
51+
return new Response(`Unknown route ${pathname}`, { status: 404 });
52+
}
2153

22-
app.listen(port, () =>
23-
console.log(`Hello world app listening on port ${port}!`),
24-
);
54+
return handler(request);
55+
}
56+
57+
function isValidOrigin(origin) {
58+
return (
59+
origin === 'localhost' ||
60+
origin === 'https://pensieve-api.amatiasq.workers.dev' ||
61+
VALID_ORIGINS.includes(origin)
62+
);
63+
}
64+
65+
function handleError(err) {
66+
return new Response(err.stack, { status: 500 });
67+
}

‎src/api/package-lock.json

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/api/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "pensieve-api",
3+
"version": "1.0.0",
4+
"main": "index.js"
5+
}

‎src/api/wrangler.toml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name = "pensieve-api"
2+
type = "webpack"
3+
4+
account_id = "a030a3e94508b912c6b31b84fd4b4041"
5+
workers_dev = true
6+
compatibility_date = "2022-12-05"

‎src/config.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"COMMIT_ENDPOINT": "/commit",
66
"CLIENT_ID_DEV": "c55d2c46c215f9a4c3cb",
77
"CLIENT_ID_PROD": "09580e62d66243b6b09d",
8+
"API_ORIGIN": "https://pensieve-api.amatiasq.workers.dev",
89
"VALID_ORIGINS": ["https://pensieve.amatiasq.com"]
910
}

0 commit comments

Comments
 (0)
Please sign in to comment.