Expressification of cloud functions
This commit is contained in:
parent
4b7ac9abae
commit
af3779b8be
|
@ -1,6 +1,7 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { logger } from 'firebase-functions/v2'
|
||||
import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https'
|
||||
import { Request, RequestHandler, Response } from 'express'
|
||||
import { error } from 'firebase-functions/logger'
|
||||
import { HttpsOptions } from 'firebase-functions/v2/https'
|
||||
import { log } from './utils'
|
||||
import { z } from 'zod'
|
||||
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) }
|
||||
} catch (err) {
|
||||
// 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.')
|
||||
}
|
||||
case 'Key':
|
||||
|
@ -83,6 +84,11 @@ export const zTimestamp = () => {
|
|||
}, z.date())
|
||||
}
|
||||
|
||||
export type EndpointDefinition = {
|
||||
opts: EndpointOptions & { method: string }
|
||||
handler: RequestHandler
|
||||
}
|
||||
|
||||
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
|
||||
const result = schema.safeParse(val)
|
||||
if (!result.success) {
|
||||
|
@ -114,26 +120,28 @@ const DEFAULT_OPTS = {
|
|||
|
||||
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
||||
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
|
||||
return onRequest(opts, async (req, res) => {
|
||||
log('Request processing started.')
|
||||
try {
|
||||
if (opts.method !== req.method) {
|
||||
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
|
||||
}
|
||||
const authedUser = await lookupUser(await parseCredentials(req))
|
||||
log('User credentials processed.')
|
||||
res.status(200).json(await fn(req, authedUser))
|
||||
} catch (e) {
|
||||
if (e instanceof APIError) {
|
||||
const output: { [k: string]: unknown } = { message: e.message }
|
||||
if (e.details != null) {
|
||||
output.details = e.details
|
||||
return {
|
||||
opts,
|
||||
handler: async (req: Request, res: Response) => {
|
||||
log('Request processing started.')
|
||||
try {
|
||||
if (opts.method !== req.method) {
|
||||
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
|
||||
}
|
||||
const authedUser = await lookupUser(await parseCredentials(req))
|
||||
res.status(200).json(await fn(req, authedUser))
|
||||
} catch (e) {
|
||||
if (e instanceof APIError) {
|
||||
const output: { [k: string]: unknown } = { message: e.message }
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { onRequest } from 'firebase-functions/v2/https'
|
||||
import { EndpointDefinition } from './api'
|
||||
|
||||
admin.initializeApp()
|
||||
|
||||
|
@ -25,20 +27,63 @@ export * from './on-delete-group'
|
|||
export * from './score-contracts'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
export * from './transact'
|
||||
export * from './change-user-info'
|
||||
export * from './create-user'
|
||||
export * from './create-answer'
|
||||
export * from './place-bet'
|
||||
export * from './cancel-bet'
|
||||
export * from './sell-bet'
|
||||
export * from './sell-shares'
|
||||
export * from './claim-manalink'
|
||||
export * from './create-contract'
|
||||
export * from './add-liquidity'
|
||||
export * from './withdraw-liquidity'
|
||||
export * from './create-group'
|
||||
export * from './resolve-market'
|
||||
export * from './unsubscribe'
|
||||
export * from './stripe'
|
||||
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 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,
|
||||
}
|
||||
|
|
|
@ -66,10 +66,18 @@ export const getServiceAccountCredentials = (env?: string) => {
|
|||
}
|
||||
|
||||
export const initAdmin = (env?: string) => {
|
||||
const serviceAccount = getServiceAccountCredentials(env)
|
||||
console.log(`Initializing connection to ${serviceAccount.project_id}...`)
|
||||
return admin.initializeApp({
|
||||
projectId: serviceAccount.project_id,
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
})
|
||||
try {
|
||||
const serviceAccount = getServiceAccountCredentials(env)
|
||||
console.log(
|
||||
`Initializing connection to ${serviceAccount.project_id} Firebase...`
|
||||
)
|
||||
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
59
functions/src/serve.ts
Normal 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}.`)
|
|
@ -1,7 +1,7 @@
|
|||
import { onRequest } from 'firebase-functions/v2/https'
|
||||
import * as admin from 'firebase-admin'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
import { EndpointDefinition } from './api'
|
||||
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
||||
import { sendThankYouEmail } from './emails'
|
||||
import { track } from './analytics'
|
||||
|
@ -42,9 +42,9 @@ const manticDollarStripePrice = isProd()
|
|||
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
|
||||
}
|
||||
|
||||
export const createcheckoutsession = onRequest(
|
||||
{ minInstances: 1, secrets: ['STRIPE_APIKEY'] },
|
||||
async (req, res) => {
|
||||
export const createcheckoutsession: EndpointDefinition = {
|
||||
opts: { method: 'POST', minInstances: 1, secrets: ['STRIPE_APIKEY'] },
|
||||
handler: async (req, res) => {
|
||||
const userId = req.query.userId?.toString()
|
||||
|
||||
const manticDollarQuantity = req.query.manticDollarQuantity?.toString()
|
||||
|
@ -86,21 +86,23 @@ export const createcheckoutsession = onRequest(
|
|||
})
|
||||
|
||||
res.redirect(303, session.url || '')
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const stripewebhook = onRequest(
|
||||
{
|
||||
export const stripewebhook: EndpointDefinition = {
|
||||
opts: {
|
||||
method: 'POST',
|
||||
minInstances: 1,
|
||||
secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'],
|
||||
},
|
||||
async (req, res) => {
|
||||
handler: async (req, res) => {
|
||||
const stripe = initStripe()
|
||||
let event
|
||||
|
||||
try {
|
||||
console.log(typeof req.body, req.body)
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.rawBody,
|
||||
req.body,
|
||||
req.headers['stripe-signature'] as string,
|
||||
process.env.STRIPE_WEBHOOKSECRET as string
|
||||
)
|
||||
|
@ -116,8 +118,8 @@ export const stripewebhook = onRequest(
|
|||
}
|
||||
|
||||
res.status(200).send('success')
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const issueMoneys = async (session: StripeSession) => {
|
||||
const { id: sessionId } = session
|
||||
|
|
|
@ -1,66 +1,72 @@
|
|||
import { onRequest } from 'firebase-functions/v2/https'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { EndpointDefinition } from './api'
|
||||
import { getUser } from './utils'
|
||||
import { PrivateUser } from '../../common/user'
|
||||
|
||||
export const unsubscribe = onRequest({ minInstances: 1 }, async (req, res) => {
|
||||
const id = req.query.id as string
|
||||
let type = req.query.type as string
|
||||
if (!id || !type) {
|
||||
res.status(400).send('Empty id or type parameter.')
|
||||
return
|
||||
}
|
||||
export const unsubscribe: EndpointDefinition = {
|
||||
opts: { method: 'GET', minInstances: 1 },
|
||||
handler: async (req, res) => {
|
||||
const id = req.query.id as string
|
||||
let type = req.query.type as string
|
||||
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 (
|
||||
!['market-resolve', 'market-comment', 'market-answer', 'generic'].includes(
|
||||
type
|
||||
)
|
||||
) {
|
||||
res.status(400).send('Invalid type parameter.')
|
||||
return
|
||||
}
|
||||
if (
|
||||
![
|
||||
'market-resolve',
|
||||
'market-comment',
|
||||
'market-answer',
|
||||
'generic',
|
||||
].includes(type)
|
||||
) {
|
||||
res.status(400).send('Invalid type parameter.')
|
||||
return
|
||||
}
|
||||
|
||||
const user = await getUser(id)
|
||||
const user = await getUser(id)
|
||||
|
||||
if (!user) {
|
||||
res.send('This user is not currently subscribed or does not exist.')
|
||||
return
|
||||
}
|
||||
if (!user) {
|
||||
res.send('This user is not currently subscribed or does not exist.')
|
||||
return
|
||||
}
|
||||
|
||||
const { name } = user
|
||||
const { name } = user
|
||||
|
||||
const update: Partial<PrivateUser> = {
|
||||
...(type === 'market-resolve' && {
|
||||
unsubscribedFromResolutionEmails: true,
|
||||
}),
|
||||
...(type === 'market-comment' && {
|
||||
unsubscribedFromCommentEmails: true,
|
||||
}),
|
||||
...(type === 'market-answer' && {
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
...(type === 'generic' && {
|
||||
unsubscribedFromGenericEmails: true,
|
||||
}),
|
||||
}
|
||||
const update: Partial<PrivateUser> = {
|
||||
...(type === 'market-resolve' && {
|
||||
unsubscribedFromResolutionEmails: true,
|
||||
}),
|
||||
...(type === 'market-comment' && {
|
||||
unsubscribedFromCommentEmails: true,
|
||||
}),
|
||||
...(type === 'market-answer' && {
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
...(type === 'generic' && {
|
||||
unsubscribedFromGenericEmails: true,
|
||||
}),
|
||||
}
|
||||
|
||||
await firestore.collection('private-users').doc(id).update(update)
|
||||
await firestore.collection('private-users').doc(id).update(update)
|
||||
|
||||
if (type === 'market-resolve')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-comment')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-answer')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
|
||||
)
|
||||
else res.send(`${name}, you have been unsubscribed.`)
|
||||
})
|
||||
if (type === 'market-resolve')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-comment')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-answer')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
|
||||
)
|
||||
else res.send(`${name}, you have been unsubscribed.`)
|
||||
},
|
||||
}
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
|
Loading…
Reference in New Issue
Block a user