tutorbook
Version:
Web app connecting students with expert mentors and tutors.
129 lines (122 loc) • 5.22 kB
text/typescript
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<User> {
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<UserRecord, FirebaseError>(
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<UpdateUserRes>
): Promise<void> {
/* 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 */
}