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..55fac600 --- /dev/null +++ b/functions/src/create-user.ts @@ -0,0 +1,54 @@ +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 { slugify } from '../../common/util/slugify' +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' } + + 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(), + } + + 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 slugify(name.replace(/\s+/g, '')) +} + +const firestore = admin.firestore() 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/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 3d541291..4871259b 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -1,4 +1,5 @@ import { getFunctions, httpsCallable } from 'firebase/functions' +import { User } from '../../../common/user' const functions = getFunctions() @@ -11,3 +12,8 @@ export const placeBet = cloudFunction('placeBet') export const resolveMarket = cloudFunction('resolveMarket') export const sellBet = cloudFunction('sellBet') + +export const createUser = () => + cloudFunction('createUser')({}).then( + (r) => (r.data as any)?.user as User | undefined + ) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index e4a07406..109359fc 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -21,10 +21,9 @@ import { import { User } from '../../../common/user' import { listenForValues } from './utils' +import { createUser } from './api-call' export type { User } -export const STARTING_BALANCE = 1000 - const db = getFirestore(app) export const auth = getAuth(app) @@ -54,31 +53,22 @@ export function listenForUser(userId: string, setUser: (user: User) => void) { } const CACHED_USER_KEY = 'CACHED_USER_KEY' + export function listenForLogin(onUser: (user: User | null) => void) { const cachedUser = localStorage.getItem(CACHED_USER_KEY) onUser(cachedUser ? JSON.parse(cachedUser) : null) + if (!cachedUser) createUser().catch(() => {}) // warm up cloud function + return onAuthStateChanged(auth, async (fbUser) => { if (fbUser) { - let user = await getUser(fbUser.uid) + let user: User | null = await getUser(fbUser.uid) + if (!user) { // User just created an account; save them to our database. - user = { - id: fbUser.uid, - name: fbUser.displayName || 'Default Name', - username: - fbUser.displayName?.replace(/\s+/g, '') || 'DefaultUsername', - avatarUrl: fbUser.photoURL || '', - email: fbUser.email || 'default@blah.com', - balance: STARTING_BALANCE, - // TODO: use Firestore timestamp? - createdTime: Date.now(), - lastUpdatedTime: Date.now(), - totalPnLCached: 0, - creatorVolumeCached: 0, - } - await setUser(fbUser.uid, user) + user = (await createUser()) || null } + onUser(user) // Persist to local storage, to reduce login blink next time.