From 259ed03ee49306fe3916442a1a97191e2be3d673 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 26 Mar 2024 16:13:39 +0000 Subject: [PATCH] Solved a bug where users could change their e-mail to another user's email --- src/constants/errors.ts | 3 +- src/pages/api/register.ts | 252 +++++++++++++++++----------------- src/pages/api/users/update.ts | 12 +- src/pages/profile.tsx | 215 +++++++++++++++-------------- 4 files changed, 244 insertions(+), 238 deletions(-) diff --git a/src/constants/errors.ts b/src/constants/errors.ts index 82484137..87d33db1 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -1,4 +1,4 @@ -export type Error = "E001" | "E002"; +export type Error = "E001" | "E002" | "E003"; export interface ErrorMessage { error: Error; message: string; @@ -7,4 +7,5 @@ export interface ErrorMessage { export const errorMessages: {[key in Error]: string} = { E001: "Wrong password!", E002: "Invalid e-mail", + E003: "E-mail already in use!", }; diff --git a/src/pages/api/register.ts b/src/pages/api/register.ts index 80a2da9a..773bfbd6 100644 --- a/src/pages/api/register.ts +++ b/src/pages/api/register.ts @@ -1,24 +1,13 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { createUserWithEmailAndPassword, getAuth } from "firebase/auth"; -import { app } from "@/firebase"; -import { sessionOptions } from "@/lib/session"; -import { withIronSessionApiRoute } from "iron-session/next"; -import { - getFirestore, - doc, - setDoc, - query, - collection, - where, - getDocs, -} from "firebase/firestore"; -import { - CorporateInformation, - DemographicInformation, - Type, -} from "@/interfaces/user"; -import { addUserToGroupOnCreation } from "@/utils/registration"; +import {NextApiRequest, NextApiResponse} from "next"; +import {createUserWithEmailAndPassword, getAuth} from "firebase/auth"; +import {app} from "@/firebase"; +import {sessionOptions} from "@/lib/session"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {getFirestore, doc, setDoc, query, collection, where, getDocs} from "firebase/firestore"; +import {CorporateInformation, DemographicInformation, Group, Type} from "@/interfaces/user"; +import {addUserToGroupOnCreation} from "@/utils/registration"; import moment from "moment"; +import {v4} from "uuid"; const auth = getAuth(app); const db = getFirestore(app); @@ -26,140 +15,145 @@ const db = getFirestore(app); export default withIronSessionApiRoute(register, sessionOptions); const DEFAULT_DESIRED_LEVELS = { - reading: 9, - listening: 9, - writing: 9, - speaking: 9, + reading: 9, + listening: 9, + writing: 9, + speaking: 9, }; const DEFAULT_LEVELS = { - reading: 0, - listening: 0, - writing: 0, - speaking: 0, + reading: 0, + listening: 0, + writing: 0, + speaking: 0, }; async function register(req: NextApiRequest, res: NextApiResponse) { - const { type } = req.body as { - type: "individual" | "corporate"; - }; + const {type} = req.body as { + type: "individual" | "corporate"; + }; - if (type === "individual") return registerIndividual(req, res); - if (type === "corporate") return registerCorporate(req, res); + if (type === "individual") return registerIndividual(req, res); + if (type === "corporate") return registerCorporate(req, res); } async function registerIndividual(req: NextApiRequest, res: NextApiResponse) { - const { email, passport_id, password, code } = req.body as { - email: string; - passport_id?: string; - password: string; - code?: string; - }; + const {email, passport_id, password, code} = req.body as { + email: string; + passport_id?: string; + password: string; + code?: string; + }; - const codeQuery = query(collection(db, "codes"), where("code", "==", code)); - const codeDocs = (await getDocs(codeQuery)).docs.filter( - (x) => !Object.keys(x.data()).includes("userId"), - ); + const codeQuery = query(collection(db, "codes"), where("code", "==", code)); + const codeDocs = (await getDocs(codeQuery)).docs.filter((x) => !Object.keys(x.data()).includes("userId")); - if (code && code.length > 0 && codeDocs.length === 0) { - res.status(400).json({ error: "Invalid Code!" }); - return; - } + if (code && code.length > 0 && codeDocs.length === 0) { + res.status(400).json({error: "Invalid Code!"}); + return; + } - const codeData = - codeDocs.length > 0 - ? (codeDocs[0].data() as { - code: string; - type: Type; - creator?: string; - expiryDate: Date | null; - }) - : undefined; + const codeData = + codeDocs.length > 0 + ? (codeDocs[0].data() as { + code: string; + type: Type; + creator?: string; + expiryDate: Date | null; + }) + : undefined; - createUserWithEmailAndPassword(auth, email.toLowerCase(), password) - .then(async (userCredentials) => { - const userId = userCredentials.user.uid; - delete req.body.password; + createUserWithEmailAndPassword(auth, email.toLowerCase(), password) + .then(async (userCredentials) => { + const userId = userCredentials.user.uid; + delete req.body.password; - const user = { - ...req.body, - email: email.toLowerCase(), - desiredLevels: DEFAULT_DESIRED_LEVELS, - levels: DEFAULT_LEVELS, - bio: "", - isFirstLogin: codeData ? codeData.type === "student" : true, - focus: "academic", - type: email.endsWith("@ecrop.dev") - ? "developer" - : codeData - ? codeData.type - : "student", - subscriptionExpirationDate: codeData - ? codeData.expiryDate - : moment().subtract(1, "days").toISOString(), - ...(passport_id ? { demographicInformation: { passport_id } } : {}), - registrationDate: new Date().toISOString(), - status: code ? "active" : "paymentDue", - }; + const user = { + ...req.body, + email: email.toLowerCase(), + desiredLevels: DEFAULT_DESIRED_LEVELS, + levels: DEFAULT_LEVELS, + bio: "", + isFirstLogin: codeData ? codeData.type === "student" : true, + focus: "academic", + type: email.endsWith("@ecrop.dev") ? "developer" : codeData ? codeData.type : "student", + subscriptionExpirationDate: codeData ? codeData.expiryDate : moment().subtract(1, "days").toISOString(), + ...(passport_id ? {demographicInformation: {passport_id}} : {}), + registrationDate: new Date().toISOString(), + status: code ? "active" : "paymentDue", + }; - await setDoc(doc(db, "users", userId), user); + await setDoc(doc(db, "users", userId), user); - if (codeDocs.length > 0 && codeData) { - await setDoc(codeDocs[0].ref, { userId: userId }, { merge: true }); - if (codeData.creator) - await addUserToGroupOnCreation( - userId, - codeData.type, - codeData.creator, - ); - } + if (codeDocs.length > 0 && codeData) { + await setDoc(codeDocs[0].ref, {userId: userId}, {merge: true}); + if (codeData.creator) await addUserToGroupOnCreation(userId, codeData.type, codeData.creator); + } - req.session.user = { ...user, id: userId }; - await req.session.save(); + req.session.user = {...user, id: userId}; + await req.session.save(); - res.status(200).json({ user: { ...user, id: userId } }); - }) - .catch((error) => { - console.log(error); - res.status(401).json({ error }); - }); + res.status(200).json({user: {...user, id: userId}}); + }) + .catch((error) => { + console.log(error); + res.status(401).json({error}); + }); } async function registerCorporate(req: NextApiRequest, res: NextApiResponse) { - const { email, password } = req.body as { - email: string; - password: string; - corporateInformation: CorporateInformation; - }; + const {email, password} = req.body as { + email: string; + password: string; + corporateInformation: CorporateInformation; + }; - createUserWithEmailAndPassword(auth, email.toLowerCase(), password) - .then(async (userCredentials) => { - const userId = userCredentials.user.uid; - delete req.body.password; + createUserWithEmailAndPassword(auth, email.toLowerCase(), password) + .then(async (userCredentials) => { + const userId = userCredentials.user.uid; + delete req.body.password; - const user = { - ...req.body, - email: email.toLowerCase(), - desiredLevels: DEFAULT_DESIRED_LEVELS, - levels: DEFAULT_LEVELS, - bio: "", - isFirstLogin: false, - focus: "academic", - type: "corporate", - subscriptionExpirationDate: req.body.subscriptionExpirationDate || null, - status: "paymentDue", - registrationDate: new Date().toISOString(), - }; + const user = { + ...req.body, + email: email.toLowerCase(), + desiredLevels: DEFAULT_DESIRED_LEVELS, + levels: DEFAULT_LEVELS, + bio: "", + isFirstLogin: false, + focus: "academic", + type: "corporate", + subscriptionExpirationDate: req.body.subscriptionExpirationDate || null, + status: "paymentDue", + registrationDate: new Date().toISOString(), + }; - await setDoc(doc(db, "users", userId), user); + const defaultTeachersGroup: Group = { + admin: userId, + id: v4(), + name: "Teachers", + participants: [], + disableEditing: true, + }; - req.session.user = { ...user, id: userId }; - await req.session.save(); + const defaultStudentsGroup: Group = { + admin: userId, + id: v4(), + name: "Students", + participants: [], + disableEditing: true, + }; - res.status(200).json({ user: { ...user, id: userId } }); - }) - .catch((error) => { - console.log(error); - res.status(401).json({ error }); - }); + await setDoc(doc(db, "users", userId), user); + await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup); + await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup); + + req.session.user = {...user, id: userId}; + await req.session.save(); + + res.status(200).json({user: {...user, id: userId}}); + }) + .catch((error) => { + console.log(error); + res.status(401).json({error}); + }); } diff --git a/src/pages/api/users/update.ts b/src/pages/api/users/update.ts index 0813c91e..b8091f50 100644 --- a/src/pages/api/users/update.ts +++ b/src/pages/api/users/update.ts @@ -12,7 +12,7 @@ import moment from "moment"; import ShortUniqueId from "short-unique-id"; import {Payment} from "@/interfaces/paypal"; import {toFixedNumber} from "@/utils/number"; -import { propagateStatusChange } from '@/utils/propagate.user.changes'; +import {propagateStatusChange} from "@/utils/propagate.user.changes"; const db = getFirestore(app); const auth = getAuth(app); @@ -85,7 +85,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const user = await setDoc(userRef, updatedUser, {merge: true}); await managePaymentRecords(updatedUser, updatedUser.id); - if(updatedUser.status) { + if (updatedUser.status) { // there's no await as this does not affect the user propagateStatusChange(queryId, updatedUser.status); } @@ -117,6 +117,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (updatedUser.email !== req.session.user.email && updatedUser.password) { try { + const usersWithSameEmail = await getDocs(query(collection(db, "users"), where("email", "==", updatedUser.email.toLowerCase()))); + if (usersWithSameEmail.docs.length > 0) { + res.status(400).json({error: "E003", message: errorMessages.E003}); + return; + } + const credential = await signInWithEmailAndPassword(auth, req.session.user.email, updatedUser.password); await updateEmail(credential.user, updatedUser.email); @@ -142,7 +148,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } } - if(updatedUser.status) { + if (updatedUser.status) { // there's no await as this does not affect the user propagateStatusChange(req.session.user.id, updatedUser.status); } diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 8646b66c..f5dcc27b 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -64,6 +64,8 @@ interface Props { mutateUser: Function; } +const DoubleColumnRow = ({children}: {children: ReactNode}) =>
{children}
; + function UserProfile({user, mutateUser}: Props) { const [bio, setBio] = useState(user.bio || ""); const [name, setName] = useState(user.name || ""); @@ -150,106 +152,45 @@ function UserProfile({user, mutateUser}: Props) { } } - const request = await axios.post("/api/users/update", { - bio, - name, - email, - password, - newPassword, - profilePicture, - desiredLevels, - preferredGender, - preferredTopics, - demographicInformation: { - phone, - country, - employment: user?.type === "corporate" ? undefined : employment, - position: user?.type === "corporate" ? position : undefined, - gender, - passport_id, - timezone, - }, - ...(user.type === "corporate" ? {corporateInformation} : {}), - }); - if (request.status === 200) { - toast.success("Your profile has been updated!"); - mutateUser((request.data as {user: User}).user); - setIsLoading(false); - return; - } - - toast.error((request.data as ErrorMessage).message); - setIsLoading(false); + axios + .post("/api/users/update", { + bio, + name, + email, + password, + newPassword, + profilePicture, + desiredLevels, + preferredGender, + preferredTopics, + demographicInformation: { + phone, + country, + employment: user?.type === "corporate" ? undefined : employment, + position: user?.type === "corporate" ? position : undefined, + gender, + passport_id, + timezone, + }, + ...(user.type === "corporate" ? {corporateInformation} : {}), + }) + .then((response) => { + if (response.status === 200) { + toast.success("Your profile has been updated!"); + mutateUser((response.data as {user: User}).user); + setIsLoading(false); + return; + } + }) + .catch((error) => { + console.log(error); + toast.error((error.response.data as ErrorMessage).message); + }) + .finally(() => { + setIsLoading(false); + }); }; - const DoubleColumnRow = ({children}: {children: ReactNode}) =>
{children}
; - - const PasswordInput = () => ( - - setPassword(e)} - placeholder="Enter your password" - required - /> - setNewPassword(e)} - placeholder="Enter your new password (optional)" - /> - - ); - - const NameInput = () => ( - setName(e)} placeholder="Enter your name" defaultValue={name} required /> - ); - - const AgentInformationInput = () => ( -
- null} - placeholder="Enter corporate name" - defaultValue={companyName} - disabled - /> - null} - placeholder="Enter commercial registration" - defaultValue={commercialRegistration} - disabled - /> -
- ); - - const CountryInput = () => ( -
- - -
- ); - - const PhoneInput = () => ( - setPhone(e)} - placeholder="Enter phone number" - defaultValue={phone} - required - /> - ); - const ExpirationDate = () => (
@@ -276,7 +217,7 @@ function UserProfile({user, mutateUser}: Props) {
); - const manualDownloadLink = ['student', 'teacher', 'corporate'].includes(user.type) ? `/manuals/${user.type}.pdf` : ''; + const manualDownloadLink = ["student", "teacher", "corporate"].includes(user.type) ? `/manuals/${user.type}.pdf` : ""; return ( @@ -288,7 +229,15 @@ function UserProfile({user, mutateUser}: Props) {
{user.type !== "corporate" ? ( - + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + /> ) : ( - - {user.type === "agent" && } + + setPassword(e)} + placeholder="Enter your password" + required + /> + setNewPassword(e)} + placeholder="Enter your new password (optional)" + /> + + {user.type === "agent" && ( +
+ null} + placeholder="Enter corporate name" + defaultValue={companyName} + disabled + /> + null} + placeholder="Enter commercial registration" + defaultValue={commercialRegistration} + disabled + /> +
+ )} - - +
+ + +
+ setPhone(e)} + placeholder="Enter phone number" + defaultValue={phone} + required + />
{user.type === "student" ? ( @@ -426,7 +423,15 @@ function UserProfile({user, mutateUser}: Props) { <> - + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + />