import { NextApiRequest, NextApiResponse } from 'next'; import { User, UserJSON } from '@tutorbook/model'; import to from 'await-to-js'; import error from './helpers/error'; import verify from './helpers/verify'; import { db, auth, UserRecord, FirebaseError, DocumentReference, } from './helpers/firebase'; /** * Don't let the user delete past known info (e.g. if the old `userRecord` has * data that this new `User` doesn't; we don't just get rid of data). * * Note that this function merely performs side-effects on the given user * object (and thus does not return anything). */ function preventDataLoss( user: User, userRecord: UserRecord, updatedRecord: UserRecord ): void { /* eslint-disable no-param-reassign */ user.name = updatedRecord.displayName || userRecord.displayName || ''; user.photo = updatedRecord.photoURL || userRecord.photoURL || ''; user.email = updatedRecord.email || userRecord.email || ''; user.phone = updatedRecord.phoneNumber || userRecord.phoneNumber || ''; /* eslint-enable no-param-reassign */ } /** * Helper function that's called when a user with the given email already * exists. This function will address the issue cleanly by: * 1. Fetching that user's existing Firebase `UserRecord`. * 2. (Optional) If that existing `UserRecord` doesn't match the given `user`, * we'll update it (**without** losing any user data). * 3. Finally, we'll update the given `User`'s properties to be in sync with * that stored in the latest Firebase `UserRecord`. * * Note that this function **will not** erase any data; if a piece of data (e.g. * the user's `phoneNumber`) is found in the Firebase `UserRecord` but not on * the given `User` object, we'll just add the value found in Firebase to the * given `User` object. * * @todo Enable users to remove their phone numbers and other sensitive PII as * they please (see above note for more info on why that's not working now). * * @todo Send the `auth/phone-number-already-exists` error code back to the * client and show the user a warning message (in the form of a confirmation * dialog to ensure they're not accidentally creating duplicate accounts). I'm * guessing that this happens primarily b/c someone is registering themself as * both the parent and the pupil at the pupil signup form. */ async function updateUser(updatedUser: User): Promise { console.log('[DEBUG] Updating Firebase Authorization account...'); const user: User = new User(updatedUser); const userRecord: UserRecord = await auth.getUser(user.id); const userNeedsToBeUpdated: boolean = (!!user.name && userRecord.displayName !== user.name) || (!!user.photo && userRecord.photoURL !== user.photo) || (!!user.email && userRecord.displayName !== user.email) || (!!user.phone && userRecord.phoneNumber !== user.phone); user.id = userRecord.uid; if (userNeedsToBeUpdated) { const [err, updatedRecord] = await to( auth.updateUser(user.id, { displayName: user.name, photoURL: user.photo ? user.photo : undefined, email: user.email ? user.email : undefined, phoneNumber: user.phone ? user.phone : undefined, }) ); if (err && err.code === 'auth/email-already-exists') { /* eslint-disable-next-line no-shadow */ const updatedRecord = await auth.updateUser(user.id, { displayName: user.name, photoURL: user.photo ? user.photo : undefined, phoneNumber: user.phone ? user.phone : undefined, }); preventDataLoss(user, userRecord, updatedRecord); } else if (err && err.code === 'auth/phone-number-already-exists') { /* eslint-disable-next-line no-shadow */ const updatedRecord = await auth.updateUser(user.id, { displayName: user.name, photoURL: user.photo ? user.photo : undefined, email: user.email ? user.email : undefined, }); preventDataLoss(user, userRecord, updatedRecord); } else if (err) { const msg = `${err.name} updating ${user.toString()}: ${err.message}`; throw new Error(msg); } else { preventDataLoss(user, userRecord, updatedRecord as UserRecord); } } console.log('[DEBUG] Updated Firebase Authorization account.'); const userRef: DocumentReference = db.collection('users').doc(user.id); console.log('[DEBUG] Updating profile document...'); await userRef.update(user.toFirestore()); console.log(`[DEBUG] Updated ${user.name}'s profile document (${user.id}).`); return user; } export type UpdateUserRes = UserJSON; export default async function updateUserEndpoint( req: NextApiRequest, res: NextApiResponse ): Promise { /* eslint-disable @typescript-eslint/no-unsafe-member-access */ if (!req.body) { error(res, 'You must provide a request body.'); } else if (typeof req.body.id !== 'string') { error(res, 'Your request body must contain a valid user ID.'); } else { const user: User = User.fromJSON(req.body); await verify(req, res, user, async () => { await updateUser(user); res.status(200).json(user.toJSON()); }); } /* eslint-enable @typescript-eslint/no-unsafe-member-access */ }