Add Vercel routes for public write API (#280)

* Improve typing of client CORS helper

* Take node-fetch as a dependency

* Add explicit Firebase region into config

* Add new Vercel proxy API routes that talk to backend

* Call Vercel proxy routes from `api-call` module

* Tweak import to try to get Vercel happy

* Tidy up a tiny bit
This commit is contained in:
Marshall Polaris 2022-05-23 14:16:56 -07:00 committed by GitHub
parent fb33f829cc
commit 3987baa11b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 156 additions and 8 deletions

View File

@ -6,6 +6,7 @@ export const DEV_CONFIG: EnvConfig = {
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw', apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
authDomain: 'dev-mantic-markets.firebaseapp.com', authDomain: 'dev-mantic-markets.firebaseapp.com',
projectId: 'dev-mantic-markets', projectId: 'dev-mantic-markets',
region: 'us-central1',
storageBucket: 'dev-mantic-markets.appspot.com', storageBucket: 'dev-mantic-markets.appspot.com',
messagingSenderId: '134303100058', messagingSenderId: '134303100058',
appId: '1:134303100058:web:27f9ea8b83347251f80323', appId: '1:134303100058:web:27f9ea8b83347251f80323',

View File

@ -18,6 +18,7 @@ type FirebaseConfig = {
apiKey: string apiKey: string
authDomain: string authDomain: string
projectId: string projectId: string
region: string
storageBucket: string storageBucket: string
messagingSenderId: string messagingSenderId: string
appId: string appId: string
@ -30,6 +31,7 @@ export const PROD_CONFIG: EnvConfig = {
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw', apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
authDomain: 'mantic-markets.firebaseapp.com', authDomain: 'mantic-markets.firebaseapp.com',
projectId: 'mantic-markets', projectId: 'mantic-markets',
region: 'us-central1',
storageBucket: 'mantic-markets.appspot.com', storageBucket: 'mantic-markets.appspot.com',
messagingSenderId: '128925704902', messagingSenderId: '128925704902',
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',

View File

@ -6,6 +6,7 @@ export const THEOREMONE_CONFIG: EnvConfig = {
apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M', apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M',
authDomain: 'theoremone-manifold.firebaseapp.com', authDomain: 'theoremone-manifold.firebaseapp.com',
projectId: 'theoremone-manifold', projectId: 'theoremone-manifold',
region: 'us-central1',
storageBucket: 'theoremone-manifold.appspot.com', storageBucket: 'theoremone-manifold.appspot.com',
messagingSenderId: '698012149198', messagingSenderId: '698012149198',
appId: '1:698012149198:web:b342af75662831aa84b79f', appId: '1:698012149198:web:b342af75662831aa84b79f',

View File

@ -4,7 +4,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
export function applyCorsHeaders( export function applyCorsHeaders(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
params: object params: Cors.CorsOptions
) { ) {
// This cors module is made as express.js middleware, so it's easier to promisify it for ourselves. // This cors module is made as express.js middleware, so it's easier to promisify it for ourselves.
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

62
web/lib/api/proxy.ts Normal file
View File

@ -0,0 +1,62 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { FIREBASE_CONFIG } from 'common/envs/constants'
import { promisify } from 'util'
import { pipeline } from 'stream'
import fetch, { Headers, Response } from 'node-fetch'
function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) {
const result = new Headers()
for (let name of whitelist) {
const v = req.headers[name.toLowerCase()]
if (Array.isArray(v)) {
for (let vv of v) {
result.append(name, vv)
}
} else if (v != null) {
result.append(name, v)
}
}
result.append('X-Forwarded-For', req.socket.remoteAddress || '')
result.append('Via', 'Vercel public API')
return result
}
function getProxiedResponseHeaders(res: Response, whitelist: string[]) {
const result: { [k: string]: string } = {}
for (let name of whitelist) {
const v = res.headers.get(name)
if (v != null) {
result[name] = v
}
}
return result
}
export const fetchBackend = (req: NextApiRequest, endpoint: string) => {
const { projectId, region } = FIREBASE_CONFIG
const url = `https://${region}-${projectId}.cloudfunctions.net/${endpoint}`
const headers = getProxiedRequestHeaders(req, [
'Authorization',
'Content-Length',
'Content-Type',
'Origin',
])
return fetch(url, { headers, method: req.method, body: req })
}
export const forwardResponse = async (
res: NextApiResponse,
backendRes: Response
) => {
const headers = getProxiedResponseHeaders(backendRes, [
'Access-Control-Allow-Origin',
'Content-Type',
'Cache-Control',
'ETag',
'Vary',
])
res.writeHead(backendRes.status, headers)
if (backendRes.body != null) {
return await promisify(pipeline)(backendRes.body, res)
}
}

View File

@ -1,5 +1,4 @@
import { auth } from './users' import { auth } from './users'
import { app, functions } from './init'
export class APIError extends Error { export class APIError extends Error {
code: number code: number
@ -10,15 +9,12 @@ export class APIError extends Error {
} }
} }
export async function call(name: string, method: string, params: any) { export async function call(url: string, method: string, params: any) {
const user = auth.currentUser const user = auth.currentUser
if (user == null) { if (user == null) {
throw new Error('Must be signed in to make API calls.') throw new Error('Must be signed in to make API calls.')
} }
const token = await user.getIdToken() const token = await user.getIdToken()
const region = functions.region
const projectId = app.options.projectId
const url = `https://${region}-${projectId}.cloudfunctions.net/${name}`
const req = new Request(url, { const req = new Request(url, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
@ -37,9 +33,9 @@ export async function call(name: string, method: string, params: any) {
} }
export function createContract(params: any) { export function createContract(params: any) {
return call('createContract', 'POST', params) return call('/api/v0/market', 'POST', params)
} }
export function placeBet(params: any) { export function placeBet(params: any) {
return call('placeBet', 'POST', params) return call('/api/v0/bets', 'POST', params)
} }

View File

@ -30,6 +30,7 @@
"gridjs-react": "5.0.2", "gridjs-react": "5.0.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"next": "12.1.2", "next": "12.1.2",
"node-fetch": "3.2.4",
"react": "17.0.2", "react": "17.0.2",
"react-confetti": "6.0.1", "react-confetti": "6.0.1",
"react-dom": "17.0.2", "react-dom": "17.0.2",

View File

@ -0,0 +1,23 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
CORS_ORIGIN_MANIFOLD,
CORS_ORIGIN_LOCALHOST,
} from 'common/envs/constants'
import { applyCorsHeaders } from 'web/lib/api/cors'
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
export const config = { api: { bodyParser: false } }
export default async function route(req: NextApiRequest, res: NextApiResponse) {
await applyCorsHeaders(req, res, {
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
methods: 'POST',
})
try {
const backendRes = await fetchBackend(req, 'placeBet')
await forwardResponse(res, backendRes)
} catch (err) {
console.error('Error talking to cloud function: ', err)
res.status(500).json({ message: 'Error communicating with backend.' })
}
}

View File

@ -0,0 +1,23 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
CORS_ORIGIN_MANIFOLD,
CORS_ORIGIN_LOCALHOST,
} from 'common/envs/constants'
import { applyCorsHeaders } from 'web/lib/api/cors'
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
export const config = { api: { bodyParser: false } }
export default async function route(req: NextApiRequest, res: NextApiResponse) {
await applyCorsHeaders(req, res, {
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
methods: 'POST',
})
try {
const backendRes = await fetchBackend(req, 'createContract')
await forwardResponse(res, backendRes)
} catch (err) {
console.error('Error talking to cloud function: ', err)
res.status(500).json({ message: 'Error communicating with backend.' })
}
}

View File

@ -1974,6 +1974,11 @@ data-uri-to-buffer@1:
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
integrity sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ== integrity sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==
data-uri-to-buffer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
date-fns@^2.16.1: date-fns@^2.16.1:
version "2.28.0" version "2.28.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
@ -2650,6 +2655,14 @@ faye-websocket@0.11.4:
dependencies: dependencies:
websocket-driver ">=0.5.1" websocket-driver ">=0.5.1"
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.1.5"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.5.tgz#0077bf5f3fcdbd9d75a0b5362f77dbb743489863"
integrity sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==
dependencies:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
fetch@1.1.0: fetch@1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e" resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e"
@ -2786,6 +2799,13 @@ form-data@^2.3.3, form-data@^2.5.0:
combined-stream "^1.0.6" combined-stream "^1.0.6"
mime-types "^2.1.12" mime-types "^2.1.12"
formdata-polyfill@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
dependencies:
fetch-blob "^3.1.2"
forwarded@0.2.0: forwarded@0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@ -3862,6 +3882,11 @@ next@12.1.2:
"@next/swc-win32-ia32-msvc" "12.1.2" "@next/swc-win32-ia32-msvc" "12.1.2"
"@next/swc-win32-x64-msvc" "12.1.2" "@next/swc-win32-x64-msvc" "12.1.2"
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@2.6.5: node-fetch@2.6.5:
version "2.6.5" version "2.6.5"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
@ -3869,6 +3894,15 @@ node-fetch@2.6.5:
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
node-fetch@3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.4.tgz#3fbca2d8838111048232de54cb532bd3cf134947"
integrity sha512-WvYJRN7mMyOLurFR2YpysQGuwYrJN+qrrpHjJDuKMcSPdfFccRUla/kng2mz6HWSBxJcqPbvatS6Gb4RhOzCJw==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-fetch@^2.6.1, node-fetch@^2.6.7: node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.6.7" version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
@ -5208,6 +5242,11 @@ vary@^1, vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
web-streams-polyfill@^3.0.3:
version "3.2.1"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
webidl-conversions@^3.0.0: webidl-conversions@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"