Expressification of cloud functions

This commit is contained in:
Marshall Polaris 2022-07-12 09:53:15 -07:00
parent 4b7ac9abae
commit af3779b8be
6 changed files with 238 additions and 110 deletions

View File

@ -1,6 +1,7 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { logger } from 'firebase-functions/v2' import { Request, RequestHandler, Response } from 'express'
import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https' import { error } from 'firebase-functions/logger'
import { HttpsOptions } from 'firebase-functions/v2/https'
import { log } from './utils' import { log } from './utils'
import { z } from 'zod' import { z } from 'zod'
import { APIError } from '../../common/api' import { APIError } from '../../common/api'
@ -45,7 +46,7 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
return { kind: 'jwt', data: await auth.verifyIdToken(payload) } return { kind: 'jwt', data: await auth.verifyIdToken(payload) }
} catch (err) { } catch (err) {
// This is somewhat suspicious, so get it into the firebase console // This is somewhat suspicious, so get it into the firebase console
logger.error('Error verifying Firebase JWT: ', err) error('Error verifying Firebase JWT: ', err)
throw new APIError(403, 'Error validating token.') throw new APIError(403, 'Error validating token.')
} }
case 'Key': case 'Key':
@ -83,6 +84,11 @@ export const zTimestamp = () => {
}, z.date()) }, z.date())
} }
export type EndpointDefinition = {
opts: EndpointOptions & { method: string }
handler: RequestHandler
}
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => { export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
const result = schema.safeParse(val) const result = schema.safeParse(val)
if (!result.success) { if (!result.success) {
@ -114,26 +120,28 @@ const DEFAULT_OPTS = {
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts) const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
return onRequest(opts, async (req, res) => { return {
log('Request processing started.') opts,
try { handler: async (req: Request, res: Response) => {
if (opts.method !== req.method) { log('Request processing started.')
throw new APIError(405, `This endpoint supports only ${opts.method}.`) try {
} if (opts.method !== req.method) {
const authedUser = await lookupUser(await parseCredentials(req)) throw new APIError(405, `This endpoint supports only ${opts.method}.`)
log('User credentials processed.') }
res.status(200).json(await fn(req, authedUser)) const authedUser = await lookupUser(await parseCredentials(req))
} catch (e) { res.status(200).json(await fn(req, authedUser))
if (e instanceof APIError) { } catch (e) {
const output: { [k: string]: unknown } = { message: e.message } if (e instanceof APIError) {
if (e.details != null) { const output: { [k: string]: unknown } = { message: e.message }
output.details = e.details if (e.details != null) {
output.details = e.details
}
res.status(e.code).json(output)
} else {
error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
} }
res.status(e.code).json(output)
} else {
logger.error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
} }
} },
}) } as EndpointDefinition
} }

View File

@ -1,4 +1,6 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { onRequest } from 'firebase-functions/v2/https'
import { EndpointDefinition } from './api'
admin.initializeApp() admin.initializeApp()
@ -25,20 +27,63 @@ export * from './on-delete-group'
export * from './score-contracts' export * from './score-contracts'
// v2 // v2
export * from './health' import { health } from './health'
export * from './transact' import { transact } from './transact'
export * from './change-user-info' import { changeuserinfo } from './change-user-info'
export * from './create-user' import { createuser } from './create-user'
export * from './create-answer' import { createanswer } from './create-answer'
export * from './place-bet' import { placebet } from './place-bet'
export * from './cancel-bet' import { cancelbet } from './cancel-bet'
export * from './sell-bet' import { sellbet } from './sell-bet'
export * from './sell-shares' import { sellshares } from './sell-shares'
export * from './claim-manalink' import { claimmanalink } from './claim-manalink'
export * from './create-contract' import { createmarket } from './create-contract'
export * from './add-liquidity' import { addliquidity } from './add-liquidity'
export * from './withdraw-liquidity' import { withdrawliquidity } from './withdraw-liquidity'
export * from './create-group' import { creategroup } from './create-group'
export * from './resolve-market' import { resolvemarket } from './resolve-market'
export * from './unsubscribe' import { unsubscribe } from './unsubscribe'
export * from './stripe' import { stripewebhook, createcheckoutsession } from './stripe'
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
onRequest(opts, handler as any)
}
const healthFunction = toCloudFunction(health)
const transactFunction = toCloudFunction(transact)
const changeUserInfoFunction = toCloudFunction(changeuserinfo)
const createUserFunction = toCloudFunction(createuser)
const createAnswerFunction = toCloudFunction(createanswer)
const placeBetFunction = toCloudFunction(placebet)
const cancelBetFunction = toCloudFunction(cancelbet)
const sellBetFunction = toCloudFunction(sellbet)
const sellSharesFunction = toCloudFunction(sellshares)
const claimManalinkFunction = toCloudFunction(claimmanalink)
const createMarketFunction = toCloudFunction(createmarket)
const addLiquidityFunction = toCloudFunction(addliquidity)
const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity)
const createGroupFunction = toCloudFunction(creategroup)
const resolveMarketFunction = toCloudFunction(resolvemarket)
const unsubscribeFunction = toCloudFunction(unsubscribe)
const stripeWebhookFunction = toCloudFunction(stripewebhook)
const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
export {
healthFunction as health,
transactFunction as transact,
changeUserInfoFunction as changeuserinfo,
createUserFunction as createuser,
createAnswerFunction as createanswer,
placeBetFunction as placebet,
cancelBetFunction as cancelbet,
sellBetFunction as sellbet,
sellSharesFunction as sellshares,
claimManalinkFunction as claimmanalink,
createMarketFunction as createmarket,
addLiquidityFunction as addliquidity,
withdrawLiquidityFunction as withdrawliquidity,
createGroupFunction as creategroup,
resolveMarketFunction as resolvemarket,
unsubscribeFunction as unsubscribe,
stripeWebhookFunction as stripewebhook,
createCheckoutSessionFunction as createcheckoutsession,
}

View File

@ -66,10 +66,18 @@ export const getServiceAccountCredentials = (env?: string) => {
} }
export const initAdmin = (env?: string) => { export const initAdmin = (env?: string) => {
const serviceAccount = getServiceAccountCredentials(env) try {
console.log(`Initializing connection to ${serviceAccount.project_id}...`) const serviceAccount = getServiceAccountCredentials(env)
return admin.initializeApp({ console.log(
projectId: serviceAccount.project_id, `Initializing connection to ${serviceAccount.project_id} Firebase...`
credential: admin.credential.cert(serviceAccount), )
}) return admin.initializeApp({
projectId: serviceAccount.project_id,
credential: admin.credential.cert(serviceAccount),
})
} catch (err) {
console.error(err)
console.log(`Initializing connection to default Firebase...`)
return admin.initializeApp()
}
} }

59
functions/src/serve.ts Normal file
View File

@ -0,0 +1,59 @@
import * as cors from 'cors'
import * as express from 'express'
import { Express } from 'express'
import { EndpointDefinition } from './api'
const PORT = 8080
import { initAdmin } from './scripts/script-init'
initAdmin()
import { health } from './health'
import { transact } from './transact'
import { changeuserinfo } from './change-user-info'
import { createuser } from './create-user'
import { createanswer } from './create-answer'
import { placebet } from './place-bet'
import { cancelbet } from './cancel-bet'
import { sellbet } from './sell-bet'
import { sellshares } from './sell-shares'
import { claimmanalink } from './claim-manalink'
import { createmarket } from './create-contract'
import { addliquidity } from './add-liquidity'
import { withdrawliquidity } from './withdraw-liquidity'
import { creategroup } from './create-group'
import { resolvemarket } from './resolve-market'
import { unsubscribe } from './unsubscribe'
import { stripewebhook, createcheckoutsession } from './stripe'
const app = express()
const addEndpointRoute = (name: string, endpoint: EndpointDefinition) => {
const method = endpoint.opts.method.toLowerCase() as keyof Express
const corsMiddleware = cors({ origin: endpoint.opts.cors })
const middleware = [express.json(), corsMiddleware]
app.options(name, corsMiddleware) // preflight requests
app[method](name, ...middleware, endpoint.handler)
}
addEndpointRoute('/health', health)
addEndpointRoute('/transact', transact)
addEndpointRoute('/changeuserinfo', changeuserinfo)
addEndpointRoute('/createuser', createuser)
addEndpointRoute('/createanswer', createanswer)
addEndpointRoute('/placebet', placebet)
addEndpointRoute('/cancelbet', cancelbet)
addEndpointRoute('/sellbet', sellbet)
addEndpointRoute('/sellshares', sellshares)
addEndpointRoute('/claimmanalink', claimmanalink)
addEndpointRoute('/createmarket', createmarket)
addEndpointRoute('/addliquidity', addliquidity)
addEndpointRoute('/withdrawliquidity', withdrawliquidity)
addEndpointRoute('/creategroup', creategroup)
addEndpointRoute('/resolvemarket', resolvemarket)
addEndpointRoute('/unsubscribe', unsubscribe)
addEndpointRoute('/stripewebhook', stripewebhook)
addEndpointRoute('/createcheckoutsession', createcheckoutsession)
app.listen(PORT)
console.log(`Serving functions on port ${PORT}.`)

View File

@ -1,7 +1,7 @@
import { onRequest } from 'firebase-functions/v2/https'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import Stripe from 'stripe' import Stripe from 'stripe'
import { EndpointDefinition } from './api'
import { getPrivateUser, getUser, isProd, payUser } from './utils' import { getPrivateUser, getUser, isProd, payUser } from './utils'
import { sendThankYouEmail } from './emails' import { sendThankYouEmail } from './emails'
import { track } from './analytics' import { track } from './analytics'
@ -42,9 +42,9 @@ const manticDollarStripePrice = isProd()
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
} }
export const createcheckoutsession = onRequest( export const createcheckoutsession: EndpointDefinition = {
{ minInstances: 1, secrets: ['STRIPE_APIKEY'] }, opts: { method: 'POST', minInstances: 1, secrets: ['STRIPE_APIKEY'] },
async (req, res) => { handler: async (req, res) => {
const userId = req.query.userId?.toString() const userId = req.query.userId?.toString()
const manticDollarQuantity = req.query.manticDollarQuantity?.toString() const manticDollarQuantity = req.query.manticDollarQuantity?.toString()
@ -86,21 +86,23 @@ export const createcheckoutsession = onRequest(
}) })
res.redirect(303, session.url || '') res.redirect(303, session.url || '')
} },
) }
export const stripewebhook = onRequest( export const stripewebhook: EndpointDefinition = {
{ opts: {
method: 'POST',
minInstances: 1, minInstances: 1,
secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'], secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'],
}, },
async (req, res) => { handler: async (req, res) => {
const stripe = initStripe() const stripe = initStripe()
let event let event
try { try {
console.log(typeof req.body, req.body)
event = stripe.webhooks.constructEvent( event = stripe.webhooks.constructEvent(
req.rawBody, req.body,
req.headers['stripe-signature'] as string, req.headers['stripe-signature'] as string,
process.env.STRIPE_WEBHOOKSECRET as string process.env.STRIPE_WEBHOOKSECRET as string
) )
@ -116,8 +118,8 @@ export const stripewebhook = onRequest(
} }
res.status(200).send('success') res.status(200).send('success')
} },
) }
const issueMoneys = async (session: StripeSession) => { const issueMoneys = async (session: StripeSession) => {
const { id: sessionId } = session const { id: sessionId } = session

View File

@ -1,66 +1,72 @@
import { onRequest } from 'firebase-functions/v2/https'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { EndpointDefinition } from './api'
import { getUser } from './utils' import { getUser } from './utils'
import { PrivateUser } from '../../common/user' import { PrivateUser } from '../../common/user'
export const unsubscribe = onRequest({ minInstances: 1 }, async (req, res) => { export const unsubscribe: EndpointDefinition = {
const id = req.query.id as string opts: { method: 'GET', minInstances: 1 },
let type = req.query.type as string handler: async (req, res) => {
if (!id || !type) { const id = req.query.id as string
res.status(400).send('Empty id or type parameter.') let type = req.query.type as string
return if (!id || !type) {
} res.status(400).send('Empty id or type parameter.')
return
}
if (type === 'market-resolved') type = 'market-resolve' if (type === 'market-resolved') type = 'market-resolve'
if ( if (
!['market-resolve', 'market-comment', 'market-answer', 'generic'].includes( ![
type 'market-resolve',
) 'market-comment',
) { 'market-answer',
res.status(400).send('Invalid type parameter.') 'generic',
return ].includes(type)
} ) {
res.status(400).send('Invalid type parameter.')
return
}
const user = await getUser(id) const user = await getUser(id)
if (!user) { if (!user) {
res.send('This user is not currently subscribed or does not exist.') res.send('This user is not currently subscribed or does not exist.')
return return
} }
const { name } = user const { name } = user
const update: Partial<PrivateUser> = { const update: Partial<PrivateUser> = {
...(type === 'market-resolve' && { ...(type === 'market-resolve' && {
unsubscribedFromResolutionEmails: true, unsubscribedFromResolutionEmails: true,
}), }),
...(type === 'market-comment' && { ...(type === 'market-comment' && {
unsubscribedFromCommentEmails: true, unsubscribedFromCommentEmails: true,
}), }),
...(type === 'market-answer' && { ...(type === 'market-answer' && {
unsubscribedFromAnswerEmails: true, unsubscribedFromAnswerEmails: true,
}), }),
...(type === 'generic' && { ...(type === 'generic' && {
unsubscribedFromGenericEmails: true, unsubscribedFromGenericEmails: true,
}), }),
} }
await firestore.collection('private-users').doc(id).update(update) await firestore.collection('private-users').doc(id).update(update)
if (type === 'market-resolve') if (type === 'market-resolve')
res.send( res.send(
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.` `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
) )
else if (type === 'market-comment') else if (type === 'market-comment')
res.send( res.send(
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.` `${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
) )
else if (type === 'market-answer') else if (type === 'market-answer')
res.send( res.send(
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.` `${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
) )
else res.send(`${name}, you have been unsubscribed.`) else res.send(`${name}, you have been unsubscribed.`)
}) },
}
const firestore = admin.firestore() const firestore = admin.firestore()