diff --git a/common/comment.ts b/common/comment.ts index 8fe25688..fe18346b 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -1,10 +1,14 @@ // Currently, comments are created after the bet, not atomically with the bet. // They're uniquely identified by the pair contractId/betId. export type Comment = { + id: string contractId: string betId: string + userId: string + text: string createdTime: number + // Denormalized, for rendering comments userName?: string userUsername?: string diff --git a/common/util/clean-username.ts b/common/util/clean-username.ts new file mode 100644 index 00000000..5c295aeb --- /dev/null +++ b/common/util/clean-username.ts @@ -0,0 +1,12 @@ +export const cleanUsername = (name: string, maxLength = 25) => { + 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 + .substring(0, maxLength) +} + +export const cleanDisplayName = (displayName: string, maxLength = 25) => { + return displayName.replace(/\s+/g, ' ').substring(0, maxLength).trim() +} diff --git a/functions/src/change-user-info.ts b/functions/src/change-user-info.ts new file mode 100644 index 00000000..de8a8d6a --- /dev/null +++ b/functions/src/change-user-info.ts @@ -0,0 +1,101 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { getUser, removeUndefinedProps } from './utils' +import { Contract } from '../../common/contract' +import { Comment } from '../../common/comment' +import { User } from '../../common/user' +import { cleanUsername } from '../../common/util/clean-username' + +export const changeUserInfo = functions + .runWith({ minInstances: 1 }) + .https.onCall( + async ( + data: { + username?: string + name?: string + avatarUrl?: string + }, + context + ) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } + + const user = await getUser(userId) + if (!user) return { status: 'error', message: 'User not found' } + + const { username, name, avatarUrl } = data + + return await changeUser(user, { username, name, avatarUrl }) + .then(() => { + console.log('succesfully changed', user.username, 'to', data) + return { status: 'success' } + }) + .catch((e) => { + console.log('Error', e.message) + return { status: 'error', message: e.message } + }) + } + ) + +export const changeUser = async ( + user: User, + update: { + username?: string + name?: string + avatarUrl?: string + } +) => { + return await firestore.runTransaction(async (transaction) => { + if (update.username) { + update.username = cleanUsername(update.username) + if (!update.username) { + throw new Error('Invalid username') + } + + const sameNameUser = await transaction.get( + firestore.collection('users').where('username', '==', update.username) + ) + if (!sameNameUser.empty) { + throw new Error('Username already exists') + } + } + + const userRef = firestore.collection('users').doc(user.id) + const userUpdate: Partial = removeUndefinedProps(update) + + const contractsRef = firestore + .collection('contracts') + .where('creatorId', '==', user.id) + + const contracts = await transaction.get(contractsRef) + + const contractUpdate: Partial = removeUndefinedProps({ + creatorName: update.name, + creatorUsername: update.username, + creatorAvatarUrl: update.avatarUrl, + }) + + const commentSnap = await transaction.get( + firestore + .collectionGroup('comments') + .where('userUsername', '==', user.username) + ) + + const commentUpdate: Partial = removeUndefinedProps({ + userName: update.name, + userUsername: update.username, + userAvatarUrl: update.avatarUrl, + }) + + await transaction.update(userRef, userUpdate) + + await Promise.all( + commentSnap.docs.map((d) => transaction.update(d.ref, commentUpdate)) + ) + + await contracts.docs.map((d) => transaction.update(d.ref, contractUpdate)) + }) +} + +const firestore = admin.firestore() diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index cd6f92b2..2b672358 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -9,6 +9,10 @@ import { } from '../../common/user' import { getUser, getUserByUsername } from './utils' import { randomString } from '../../common/util/random' +import { + cleanDisplayName, + cleanUsername, +} from '../../common/util/clean-username' export const createUser = functions .runWith({ minInstances: 1 }) @@ -29,7 +33,8 @@ export const createUser = functions const email = fbUser.email const emailName = email?.replace(/@.*$/, '') - const name = fbUser.displayName || emailName || 'User' + randomString(4) + const rawName = fbUser.displayName || emailName || 'User' + randomString(4) + const name = cleanDisplayName(rawName) let username = cleanUsername(name) const sameNameUser = await getUserByUsername(username) @@ -77,14 +82,6 @@ export const createUser = functions 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() const isPrivateUserWithDeviceToken = async (deviceToken: string) => { diff --git a/functions/src/index.ts b/functions/src/index.ts index df9ec9ab..f46e72a8 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -17,3 +17,4 @@ export * from './unsubscribe' export * from './update-contract-metrics' export * from './update-user-metrics' export * from './backup-db' +export * from './change-user-info' diff --git a/functions/src/scripts/change-user-info.ts b/functions/src/scripts/change-user-info.ts new file mode 100644 index 00000000..2fdc3d1b --- /dev/null +++ b/functions/src/scripts/change-user-info.ts @@ -0,0 +1,50 @@ +import * as admin from 'firebase-admin' +import * as _ from 'lodash' + +// Generate your own private key, and set the path below: +// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk + +// const serviceAccount = require('../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json') +const serviceAccount = require('../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json') + +admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), +}) + +import { getUserByUsername } from '../utils' +import { changeUser } from '../change-user-info' + +async function main() { + const username = process.argv[2] + const name = process.argv[3] + const newUsername = process.argv[4] + const avatarUrl = process.argv[5] + + if (process.argv.length < 4) { + console.log( + 'syntax: node change-user-info.js [current username] [new name] [new username] [new avatar]' + ) + return + } + + const user = await getUserByUsername(username) + if (!user) { + console.log('username', username, 'could not be found') + return + } + + await changeUser(user, { username: newUsername, name, avatarUrl }) + .then(() => + console.log( + 'succesfully changed', + user.username, + 'to', + name, + avatarUrl, + newUsername + ) + ) + .catch((e) => console.log(e.message)) +} + +if (require.main === module) main().then(() => process.exit()) diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 9f3777e8..95881870 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -77,3 +77,13 @@ export const chargeUser = (userId: string, charge: number) => { return updateUserBalance(userId, -charge) } + +export const removeUndefinedProps = (obj: T): T => { + let newObj: any = {} + + for (let key of Object.keys(obj)) { + if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key] + } + + return newObj +} diff --git a/web/components/add-funds-button.tsx b/web/components/add-funds-button.tsx index 65b716a6..3903f079 100644 --- a/web/components/add-funds-button.tsx +++ b/web/components/add-funds-button.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useUser } from '../hooks/use-user' import { checkoutURL } from '../lib/service/stripe' @@ -13,6 +13,8 @@ export function AddFundsButton(props: { className?: string }) { 500 | 1000 | 2500 | 10000 >(500) + const location = useLocation() + return ( <>