diff --git a/common/envs/dev.ts b/common/envs/dev.ts index 9b82f3c0..ec8f40b5 100644 --- a/common/envs/dev.ts +++ b/common/envs/dev.ts @@ -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', diff --git a/common/envs/prod.ts b/common/envs/prod.ts index 8d3a014f..07218167 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -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', diff --git a/common/envs/theoremone.ts b/common/envs/theoremone.ts index ebe504d9..9a5f4ee6 100644 --- a/common/envs/theoremone.ts +++ b/common/envs/theoremone.ts @@ -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', diff --git a/web/lib/api/cors.ts b/web/lib/api/cors.ts index 976a0ffc..ae04e482 100644 --- a/web/lib/api/cors.ts +++ b/web/lib/api/cors.ts @@ -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) => { diff --git a/web/lib/api/proxy.ts b/web/lib/api/proxy.ts new file mode 100644 index 00000000..6fa66873 --- /dev/null +++ b/web/lib/api/proxy.ts @@ -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) + } +} diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 61a32021..3fa93004 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -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) } diff --git a/web/package.json b/web/package.json index 28e06e2e..d2fbfa98 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/pages/api/v0/bets/index.ts b/web/pages/api/v0/bets/index.ts new file mode 100644 index 00000000..a7154824 --- /dev/null +++ b/web/pages/api/v0/bets/index.ts @@ -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.' }) + } +} diff --git a/web/pages/api/v0/market/index.ts b/web/pages/api/v0/market/index.ts new file mode 100644 index 00000000..6053247a --- /dev/null +++ b/web/pages/api/v0/market/index.ts @@ -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.' }) + } +} diff --git a/yarn.lock b/yarn.lock index 2da70cae..fc3b5de9 100644 --- a/yarn.lock +++ b/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"