diff --git a/functions/src/index.ts b/functions/src/index.ts index 34fceaa7..35f29954 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import * as admin from 'firebase-admin' admin.initializeApp() // v1 -export * from './transact' export * from './stripe' export * from './create-user' export * from './on-create-bet' @@ -28,6 +27,7 @@ export * from './on-create-txn' // v2 export * from './health' +export * from './transact' export * from './change-user-info' export * from './create-answer' export * from './place-bet' diff --git a/functions/src/transact.ts b/functions/src/transact.ts index cd091b83..113afc0b 100644 --- a/functions/src/transact.ts +++ b/functions/src/transact.ts @@ -1,40 +1,40 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { User } from '../../common/user' import { Txn } from '../../common/txn' import { removeUndefinedProps } from '../../common/util/object' +import { APIError, newEndpoint } from './api' export type TxnData = Omit -export const transact = functions - .runWith({ minInstances: 1 }) - .https.onCall(async (data: TxnData, context) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +// TODO: We totally fail to validate most of the input to this function, +// so anyone can spam our database with malformed transactions. - const { amount, fromType, fromId } = data +export const transact = newEndpoint({}, async (req, auth) => { + const data = req.body + const { amount, fromType, fromId } = data - if (fromType !== 'USER') - return { - status: 'error', - message: "From type is only implemented for type 'user'.", - } + if (fromType !== 'USER') + throw new APIError(400, "From type is only implemented for type 'user'.") - if (fromId !== userId) - return { - status: 'error', - message: 'Must be authenticated with userId equal to specified fromId.', - } + if (fromId !== auth.uid) + throw new APIError( + 403, + 'Must be authenticated with userId equal to specified fromId.' + ) - if (isNaN(amount) || !isFinite(amount)) - return { status: 'error', message: 'Invalid amount' } + if (isNaN(amount) || !isFinite(amount)) + throw new APIError(400, 'Invalid amount') - // Run as transaction to prevent race conditions. - return await firestore.runTransaction(async (transaction) => { - await runTxn(transaction, data) - }) + // Run as transaction to prevent race conditions. + return await firestore.runTransaction(async (transaction) => { + const result = await runTxn(transaction, data) + if (result.status == 'error') { + throw new APIError(500, result.message ?? 'An unknown error occurred.') + } + return result }) +}) export async function runTxn( fbTransaction: admin.firestore.Transaction, diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 6f7dfbcb..e4b6580f 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -11,7 +11,7 @@ import { debounce, sum } from 'lodash' import { useEffect, useRef, useState } from 'react' import { CommentTips } from 'web/hooks/use-tip-txns' import { useUser } from 'web/hooks/use-user' -import { transact } from 'web/lib/firebase/fn-call' +import { transact } from 'web/lib/firebase/api-call' import { track } from 'web/lib/service/analytics' import { Row } from './layout/row' import { Tooltip } from './tooltip' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index d65a44f3..7882d9ba 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -54,6 +54,10 @@ export function createAnswer(params: any) { return call(getFunctionUrl('createanswer'), 'POST', params) } +export function transact(params: any) { + return call(getFunctionUrl('transact'), 'POST', params) +} + export function changeUserInfo(params: any) { return call(getFunctionUrl('changeuserinfo'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index 27a5e8f3..2f299aea 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -1,5 +1,4 @@ import { httpsCallable } from 'firebase/functions' -import { Txn } from 'common/txn' import { User } from 'common/user' import { randomString } from 'common/util/random' import './init' @@ -9,11 +8,6 @@ import { safeLocalStorage } from '../util/local' export const cloudFunction = (name: string) => httpsCallable(functions, name) -export const transact = cloudFunction< - Omit, - { status: 'error' | 'success'; message?: string; txn?: Txn } ->('transact') - export const createUser: () => Promise = () => { const local = safeLocalStorage() let deviceToken = local?.getItem('device-token') diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index 7c3ce51b..c3e0912a 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -10,7 +10,7 @@ import { Spacer } from 'web/components/layout/spacer' import { User } from 'common/user' import { useUser } from 'web/hooks/use-user' import { Linkify } from 'web/components/linkify' -import { transact } from 'web/lib/firebase/fn-call' +import { transact } from 'web/lib/firebase/api-call' import { charities, Charity } from 'common/charity' import { useRouter } from 'next/router' import Custom404 from '../404'