From 908c8c03e6ce6c774c5d63c97a4b342c05814577 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Tue, 18 Jan 2022 01:18:38 -0600 Subject: [PATCH] Create user cloud function (#31) * createUser cloud function; change User object * initial commit * listenForLogin: avoid race condition * createUser: allow capital letters in username * remove debugging * leaderboard: empty url for undefined avatar image --- common/user.ts | 10 +++--- common/util/random.ts | 5 ++- functions/src/create-user.ts | 62 +++++++++++++++++++++++++++++++++ functions/src/emails.ts | 8 ++++- functions/src/index.ts | 1 + functions/src/utils.ts | 9 +++++ web/components/profile-menu.tsx | 4 ++- web/components/user-page.tsx | 1 - web/lib/firebase/api-call.ts | 6 ++++ web/lib/firebase/users.ts | 34 ++++++++---------- web/pages/leaderboards.tsx | 2 +- 11 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 functions/src/create-user.ts diff --git a/common/user.ts b/common/user.ts index f35cbdaf..0ba23010 100644 --- a/common/user.ts +++ b/common/user.ts @@ -1,12 +1,14 @@ export type User = { id: string - email: string + createdTime: number + name: string username: string - avatarUrl: string + avatarUrl?: string + balance: number - createdTime: number - lastUpdatedTime: number totalPnLCached: number creatorVolumeCached: number } + +export const STARTING_BALANCE = 1000 diff --git a/common/util/random.ts b/common/util/random.ts index 59ab5ba9..ce2bc54a 100644 --- a/common/util/random.ts +++ b/common/util/random.ts @@ -1,4 +1,7 @@ -export const randomString = () => Math.random().toString(16).substr(2, 14) +export const randomString = (length = 12) => + Math.random() + .toString(16) + .substring(2, length + 2) export function createRNG(seed: string) { // https://stackoverflow.com/a/47593316/1592933 diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts new file mode 100644 index 00000000..aba7b8e8 --- /dev/null +++ b/functions/src/create-user.ts @@ -0,0 +1,62 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { STARTING_BALANCE, User } from '../../common/user' +import { getUser, getUserByUsername } from './utils' +import { randomString } from '../../common/util/random' + +export const createUser = functions + .runWith({ minInstances: 1 }) + .https.onCall(async (_, context) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } + + const preexistingUser = await getUser(userId) + if (preexistingUser) + return { + status: 'error', + message: 'User already created', + user: preexistingUser, + } + + const fbUser = await admin.auth().getUser(userId) + + const email = fbUser.email + const emailName = email?.replace(/@.*$/, '') + + const name = fbUser.displayName || emailName || 'User' + randomString(4) + let username = cleanUsername(name) + + const sameNameUser = await getUserByUsername(username) + if (sameNameUser) { + username += randomString(4) + } + + const avatarUrl = fbUser.photoURL + + const user: User = { + id: userId, + name, + username, + avatarUrl, + balance: STARTING_BALANCE, + createdTime: Date.now(), + totalPnLCached: 0, + creatorVolumeCached: 0, + } + + await firestore.collection('users').doc(userId).create(user) + console.log('created user', username, 'firebase id:', userId) + + return { status: 'success', user } + }) + +const cleanUsername = (name: string) => { + return name + .replace(/\s+/g, '') + .normalize('NFD') // split an accented letter in the base letter and the acent + .replace(/[\u0300-\u036f]/g, '') // remove all previously split accents + .replace(/[^A-Za-z0-9_]/g, '') // remove all chars not letters, numbers and underscores +} + +const firestore = admin.firestore() diff --git a/functions/src/emails.ts b/functions/src/emails.ts index d7318caa..3dd0873f 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,3 +1,5 @@ +import * as admin from 'firebase-admin' + import { Contract } from '../../common/contract' import { User } from '../../common/user' import { sendTemplateEmail } from './send-email' @@ -22,6 +24,10 @@ export const sendMarketResolutionEmail = async ( const user = await getUser(userId) if (!user) return + const fbUser = await admin.auth().getUser(userId) + const email = fbUser.email + if (!email) return + const outcome = toDisplayResolution[resolution] const subject = `Resolved ${outcome}: ${contract.question}` @@ -39,7 +45,7 @@ export const sendMarketResolutionEmail = async ( // https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial // Mailgun username: james@mantic.markets - await sendTemplateEmail(user.email, subject, 'market-resolved', templateData) + await sendTemplateEmail(email, subject, 'market-resolved', templateData) } const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' } diff --git a/functions/src/index.ts b/functions/src/index.ts index bc769ea2..534e72cf 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -9,5 +9,6 @@ export * from './resolve-market' export * from './stripe' export * from './sell-bet' export * from './create-contract' +export * from './create-user' export * from './update-contract-metrics' export * from './update-user-metrics' diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 887d602c..e6ecd7cf 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -22,6 +22,15 @@ export const getUser = (userId: string) => { return getValue('users', userId) } +export const getUserByUsername = async (username: string) => { + const snap = await firestore + .collection('users') + .where('username', '==', username) + .get() + + return snap.empty ? undefined : (snap.docs[0].data() as User) +} + const firestore = admin.firestore() const updateUserBalance = (userId: string, delta: number) => { diff --git a/web/components/profile-menu.tsx b/web/components/profile-menu.tsx index b732d059..e97a337e 100644 --- a/web/components/profile-menu.tsx +++ b/web/components/profile-menu.tsx @@ -76,7 +76,9 @@ function ProfileSummary(props: { user: User }) { return (
- + {user.avatarUrl && ( + + )}
{user.name}
diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index f75dcf7e..4784c295 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -43,7 +43,6 @@ function UserCard(props: { user: User; showPrivateInfo?: boolean }) { {showPrivateInfo && ( <> -

{user?.email}

{formatMoney(user?.balance)}