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:
parent
fb33f829cc
commit
3987baa11b
|
@ -6,6 +6,7 @@ export const DEV_CONFIG: EnvConfig = {
|
|||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||
projectId: 'dev-mantic-markets',
|
||||
region: 'us-central1',
|
||||
storageBucket: 'dev-mantic-markets.appspot.com',
|
||||
messagingSenderId: '134303100058',
|
||||
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
||||
|
|
|
@ -18,6 +18,7 @@ type FirebaseConfig = {
|
|||
apiKey: string
|
||||
authDomain: string
|
||||
projectId: string
|
||||
region: string
|
||||
storageBucket: string
|
||||
messagingSenderId: string
|
||||
appId: string
|
||||
|
@ -30,6 +31,7 @@ export const PROD_CONFIG: EnvConfig = {
|
|||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
authDomain: 'mantic-markets.firebaseapp.com',
|
||||
projectId: 'mantic-markets',
|
||||
region: 'us-central1',
|
||||
storageBucket: 'mantic-markets.appspot.com',
|
||||
messagingSenderId: '128925704902',
|
||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||
|
|
|
@ -6,6 +6,7 @@ export const THEOREMONE_CONFIG: EnvConfig = {
|
|||
apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M',
|
||||
authDomain: 'theoremone-manifold.firebaseapp.com',
|
||||
projectId: 'theoremone-manifold',
|
||||
region: 'us-central1',
|
||||
storageBucket: 'theoremone-manifold.appspot.com',
|
||||
messagingSenderId: '698012149198',
|
||||
appId: '1:698012149198:web:b342af75662831aa84b79f',
|
||||
|
|
|
@ -4,7 +4,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||
export function applyCorsHeaders(
|
||||
req: NextApiRequest,
|
||||
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.
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
62
web/lib/api/proxy.ts
Normal file
62
web/lib/api/proxy.ts
Normal 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)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { auth } from './users'
|
||||
import { app, functions } from './init'
|
||||
|
||||
export class APIError extends Error {
|
||||
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
|
||||
if (user == null) {
|
||||
throw new Error('Must be signed in to make API calls.')
|
||||
}
|
||||
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, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
@ -37,9 +33,9 @@ export async function call(name: string, method: string, params: any) {
|
|||
}
|
||||
|
||||
export function createContract(params: any) {
|
||||
return call('createContract', 'POST', params)
|
||||
return call('/api/v0/market', 'POST', params)
|
||||
}
|
||||
|
||||
export function placeBet(params: any) {
|
||||
return call('placeBet', 'POST', params)
|
||||
return call('/api/v0/bets', 'POST', params)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"gridjs-react": "5.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"next": "12.1.2",
|
||||
"node-fetch": "3.2.4",
|
||||
"react": "17.0.2",
|
||||
"react-confetti": "6.0.1",
|
||||
"react-dom": "17.0.2",
|
||||
|
|
23
web/pages/api/v0/bets/index.ts
Normal file
23
web/pages/api/v0/bets/index.ts
Normal 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.' })
|
||||
}
|
||||
}
|
23
web/pages/api/v0/market/index.ts
Normal file
23
web/pages/api/v0/market/index.ts
Normal 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.' })
|
||||
}
|
||||
}
|
39
yarn.lock
39
yarn.lock
|
@ -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"
|
||||
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:
|
||||
version "2.28.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||
|
@ -2650,6 +2655,14 @@ faye-websocket@0.11.4:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
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:
|
||||
version "0.2.0"
|
||||
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-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:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
|
||||
|
@ -3869,6 +3894,15 @@ node-fetch@2.6.5:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.6.7"
|
||||
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"
|
||||
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:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
|
|
Loading…
Reference in New Issue
Block a user