Expressification of cloud functions
This commit is contained in:
parent
4b7ac9abae
commit
af3779b8be
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
@ -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
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 * 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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user