diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index 189976ed..e70371ca 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -1,6 +1,5 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' - +import { z } from 'zod' import { PrivateUser, STARTING_BALANCE, @@ -18,83 +17,79 @@ import { isWhitelisted } from '../../common/envs/constants' import { DEFAULT_CATEGORIES } from '../../common/categories' import { track } from './analytics' +import { APIError, newEndpoint, validate } from './api' -export const createUser = functions - .runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] }) - .https.onCall(async (data: { deviceToken?: string }, context) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + deviceToken: z.string().optional(), +}) - const preexistingUser = await getUser(userId) - if (preexistingUser) - return { - status: 'error', - message: 'User already created', - user: preexistingUser, - } +const opts = { secrets: ['MAILGUN_KEY'] } - const fbUser = await admin.auth().getUser(userId) +export const createuser = newEndpoint(opts, async (req, auth) => { + const { deviceToken } = validate(bodySchema, req.body) + const preexistingUser = await getUser(auth.uid) + if (preexistingUser) + throw new APIError(400, 'User already exists', { user: preexistingUser }) - const email = fbUser.email - if (!isWhitelisted(email)) { - return { status: 'error', message: `${email} is not whitelisted` } - } - const emailName = email?.replace(/@.*$/, '') + const fbUser = await admin.auth().getUser(auth.uid) - const rawName = fbUser.displayName || emailName || 'User' + randomString(4) - const name = cleanDisplayName(rawName) - let username = cleanUsername(name) + const email = fbUser.email + if (!isWhitelisted(email)) { + throw new APIError(400, `${email} is not whitelisted`) + } + const emailName = email?.replace(/@.*$/, '') - const sameNameUser = await getUserByUsername(username) - if (sameNameUser) { - username += randomString(4) - } + const rawName = fbUser.displayName || emailName || 'User' + randomString(4) + const name = cleanDisplayName(rawName) + let username = cleanUsername(name) - const avatarUrl = fbUser.photoURL + const sameNameUser = await getUserByUsername(username) + if (sameNameUser) { + username += randomString(4) + } - const { deviceToken } = data - const deviceUsedBefore = - !deviceToken || (await isPrivateUserWithDeviceToken(deviceToken)) + const avatarUrl = fbUser.photoURL + const deviceUsedBefore = + !deviceToken || (await isPrivateUserWithDeviceToken(deviceToken)) - const ipAddress = context.rawRequest.ip - const ipCount = ipAddress ? await numberUsersWithIp(ipAddress) : 0 + const ipCount = req.ip ? await numberUsersWithIp(req.ip) : 0 - const balance = - deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE + const balance = + deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE - const user: User = { - id: userId, - name, - username, - avatarUrl, - balance, - totalDeposits: balance, - createdTime: Date.now(), - profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, - creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, - followerCountCached: 0, - followedCategories: DEFAULT_CATEGORIES, - } + const user: User = { + id: auth.uid, + name, + username, + avatarUrl, + balance, + totalDeposits: balance, + createdTime: Date.now(), + profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, + creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, + followerCountCached: 0, + followedCategories: DEFAULT_CATEGORIES, + } - await firestore.collection('users').doc(userId).create(user) - console.log('created user', username, 'firebase id:', userId) + await firestore.collection('users').doc(auth.uid).create(user) + console.log('created user', username, 'firebase id:', auth.uid) - const privateUser: PrivateUser = { - id: userId, - username, - email, - initialIpAddress: ipAddress, - initialDeviceToken: deviceToken, - } + const privateUser: PrivateUser = { + id: auth.uid, + username, + email, + initialIpAddress: req.ip, + initialDeviceToken: deviceToken, + } - await firestore.collection('private-users').doc(userId).create(privateUser) + await firestore.collection('private-users').doc(auth.uid).create(privateUser) - await sendWelcomeEmail(user, privateUser) + await sendWelcomeEmail(user, privateUser) - await track(userId, 'create user', { username }, { ip: ipAddress }) + await track(auth.uid, 'create user', { username }, { ip: req.ip }) - return { status: 'success', user } - }) + return user +}) const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index 380e4f93..e5ae78ec 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 './create-user' export * from './on-create-bet' export * from './on-create-comment-on-contract' export * from './on-view' @@ -27,6 +26,7 @@ export * from './on-create-txn' 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' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 94da9f09..fc1e78bd 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -58,6 +58,10 @@ export function transact(params: any) { return call(getFunctionUrl('transact'), 'POST', params) } +export function createUser(params: any) { + return call(getFunctionUrl('createuser'), '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 deleted file mode 100644 index 2f299aea..00000000 --- a/web/lib/firebase/fn-call.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { httpsCallable } from 'firebase/functions' -import { User } from 'common/user' -import { randomString } from 'common/util/random' -import './init' -import { functions } from './init' -import { safeLocalStorage } from '../util/local' - -export const cloudFunction = (name: string) => - httpsCallable(functions, name) - -export const createUser: () => Promise = () => { - const local = safeLocalStorage() - let deviceToken = local?.getItem('device-token') - if (!deviceToken) { - deviceToken = randomString() - local?.setItem('device-token', deviceToken) - } - - return cloudFunction('createUser')({ deviceToken }) - .then((r) => (r.data as any)?.user || null) - .catch(() => null) -} diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 7f007031..d2e1ee04 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -20,11 +20,10 @@ import { GoogleAuthProvider, signInWithPopup, } from 'firebase/auth' -import { throttle, zip } from 'lodash' - +import { zip } from 'lodash' import { app, db } from './init' import { PortfolioMetrics, PrivateUser, User } from 'common/user' -import { createUser } from './fn-call' +import { createUser } from './api-call' import { coll, getValue, @@ -38,6 +37,7 @@ import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' import { addUserToGroupViaSlug } from 'web/lib/firebase/groups' import { removeUndefinedProps } from 'common/util/object' +import { randomString } from 'common/util/random' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' dayjs.extend(utc) @@ -101,11 +101,6 @@ const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY' const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY' const CACHED_REFERRAL_GROUP_SLUG_KEY = 'CACHED_REFERRAL_GROUP_KEY' -// used to avoid weird race condition -let createUserPromise: Promise | undefined = undefined - -const warmUpCreateUser = throttle(createUser, 5000 /* ms */) - export function writeReferralInfo( defaultReferrerUsername: string, contractId?: string, @@ -183,22 +178,29 @@ async function setCachedReferralInfoForUser(user: User | null) { local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY) } +// used to avoid weird race condition +let createUserPromise: Promise | undefined = undefined + export function listenForLogin(onUser: (user: User | null) => void) { const local = safeLocalStorage() const cachedUser = local?.getItem(CACHED_USER_KEY) onUser(cachedUser && JSON.parse(cachedUser)) - if (!cachedUser) warmUpCreateUser() - return onAuthStateChanged(auth, async (fbUser) => { if (fbUser) { let user: User | null = await getUser(fbUser.uid) if (!user) { - if (!createUserPromise) { - createUserPromise = createUser() + if (createUserPromise == null) { + const local = safeLocalStorage() + let deviceToken = local?.getItem('device-token') + if (!deviceToken) { + deviceToken = randomString() + local?.setItem('device-token', deviceToken) + } + createUserPromise = createUser({ deviceToken }).then((r) => r as User) } - user = (await createUserPromise) || null + user = await createUserPromise } onUser(user) @@ -211,7 +213,6 @@ export function listenForLogin(onUser: (user: User | null) => void) { // User logged out; reset to null onUser(null) local?.removeItem(CACHED_USER_KEY) - createUserPromise = undefined } }) }