From 9fbf86dd6957e6480b2132306786b90a540504c0 Mon Sep 17 00:00:00 2001 From: ouckah Date: Wed, 17 Apr 2024 14:05:17 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Backend=20Validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Profile.js | 135 ++---------------------- server/controllers/profileController.js | 43 +++++++- 2 files changed, 50 insertions(+), 128 deletions(-) diff --git a/client/src/pages/Profile.js b/client/src/pages/Profile.js index e0a5be6..c20db2c 100644 --- a/client/src/pages/Profile.js +++ b/client/src/pages/Profile.js @@ -19,10 +19,6 @@ function Profile() { const { id } = useParams() const [profile, setProfile] = useState({}) - // for searching through profiles that have - // id as a username instead of id - const [profiles, setProfiles] = useState([]) - const { user } = useAuthContext() const [loading, setLoading] = useState(false) @@ -30,15 +26,13 @@ function Profile() { const [pfp, setPfp] = useState('') const [username, setUsername] = useState('') - const [usernameErrorMessage, setUsernameErrorMessage] = useState('') + const [errorMessage, setErrorMessage] = useState('') const [linkedin, setLinkedin] = useState('') - const [linkedinErrorMessage, setLinkedinErrorMessage] = useState('') const [school, setSchool] = useState('') const [location, setLocation] = useState('') - const hasError = - usernameErrorMessage.length !== 0 && linkedinErrorMessage.length !== 0 + const hasError = errorMessage.length !== 0 const fetchProfile = async () => { try { @@ -83,24 +77,6 @@ function Profile() { } } - const fetchProfiles = async () => { - setLoading(true) - - try { - const data = await fetchWithAuth({ - url: `${HOST}/api/profile/`, - method: 'GET', - }) - - // If fetchWithAuth doesn't throw, it means the response was ok - setProfiles(data) - } catch (error) { - console.error('Error:', error.message) - } finally { - setLoading(false) - } - } - const getCurrentExperience = () => { if (!profile || !profile.pipeline) return @@ -151,67 +127,6 @@ function Profile() { } } - const validateUsername = async (username) => { - const isValidUsername = async (username) => { - const mongodbConflict = await isMongoDBId(username) - const usernameRegex = - /^[a-zA-Z0-9_](?!.*[._]{2})[a-zA-Z0-9_.]{1,30}[a-zA-Z0-9_]$/ - return !mongodbConflict && usernameRegex.test(username) - } - - const isAvailable = (username) => { - const filteredProfiles = profiles.filter( - (profile) => - profile.username.toLowerCase() === username.toLowerCase() - ) - - // check if the one profile there is the current user's - if (filteredProfiles.length === 1) { - const filteredProfile = filteredProfiles[0] - - return profile.username === filteredProfile.username - } - - return filteredProfiles.length === 0 - } - - // blank username - if (username.length === 0) { - setUsernameErrorMessage('Invalid username.') - return false - } else if (username.indexOf('/') !== -1) { - // contains '/' - setUsernameErrorMessage('Invalid username.') - return false - } else if (await isValidUsername(username)) { - // invalid regex - setUsernameErrorMessage('Invalid username.') - return false - } else if (!isAvailable(username)) { - // taken username - setUsernameErrorMessage('Username already taken.') - return false - } else { - // valid username - setUsernameErrorMessage('') - return true - } - } - - const validateLinkedin = async (linkedin) => { - // Regular expression for a basic LinkedIn username check - const regex = /^[a-z0-9-]+$/i - - // Check if the username matches the pattern - if (!regex.test(linkedin)) { - setLinkedinErrorMessage('Invalid Linkedin username.') - return false - } else { - setLinkedinErrorMessage('') - return true - } - } - const extractLinkedinUsername = (linkedin) => { const regex = /https:\/\/linkedin\.com\/in\/([^/]+)/ const match = linkedin.match(regex) @@ -225,17 +140,12 @@ function Profile() { return null } - const buildLinkedinUrl = (username) => { - const baseLinkedinUrl = 'https://linkedin.com/in/' - return `${baseLinkedinUrl}${username}` - } - const handleUsernameChange = async (e) => { // change -> saveable progress setSaveable(true) // remove previous errors - setUsernameErrorMessage('') + setErrorMessage('') const value = e.target.value setUsername(value) @@ -252,43 +162,19 @@ function Profile() { const handleEditProfile = async () => { const updatedProfile = { username, - linkedin: buildLinkedinUrl(linkedin), location, } - // check all fields are filled out - if (!username || username.length === 0) { - setUsernameErrorMessage('All fields must be filled.') - return - } - - if (!linkedin || linkedin.length === 0) { - // clear linkedin if user left blank / deleted - updatedProfile.linkedin = '' - } - - if (!location || location.length === 0) { - // clear location if user left blank / deleted - updatedProfile.location = '' - } - - // field validation - if (!validateUsername(username)) { - return - } - - if (!validateLinkedin(linkedin)) { - return - } - try { await fetchWithAuth({ - url: `${HOST}/api/profile/get/${user.profileId}`, + url: `${HOST}/api/profile/${user.profileId}`, method: 'PATCH', data: updatedProfile, }) } catch (error) { - console.error('Error:', error.message) + const message = error.message + console.error('Error:', message) + setErrorMessage(message) } setSaveable(false) @@ -297,7 +183,6 @@ function Profile() { useEffect(() => { const fetchInfo = async () => { await fetchProfile() - await fetchProfiles() } fetchInfo() @@ -349,9 +234,9 @@ function Profile() { value={username} onChange={handleUsernameChange} /> - {usernameErrorMessage && ( + {errorMessage && (

- {usernameErrorMessage} + {errorMessage}

)} @@ -376,7 +261,7 @@ function Profile() {

Anonymous

) : (

diff --git a/server/controllers/profileController.js b/server/controllers/profileController.js index a477d1a..5794c9f 100644 --- a/server/controllers/profileController.js +++ b/server/controllers/profileController.js @@ -143,12 +143,49 @@ const updateProfile = async (req, res) => { return res.status(404).json({ error: "No such Profile." }); } + // * PROFILE PAGE VALIDATION + // username validation + const username = profile.username; + if (username) { + const isValidUsername = (username) => { + const mongodbConflict = mongoose.Types.ObjectId.isValid(username); + const usernameRegex = + /^[a-zA-Z0-9_](?!.*[._]{2})[a-zA-Z0-9_.]{1,30}[a-zA-Z0-9_]$/; + console.log("Mongo Conflict:", mongodbConflict); + console.log("Regex Conflict:", usernameRegex.test(username)); + return !mongodbConflict && usernameRegex.test(username); + }; + + const isAvailable = async (username) => { + // Check if any profile already has the given username + const existingProfile = await Profile.findOne({ + username: username.toLowerCase(), + }); + return !existingProfile; + }; + + // blank username + if (username.length === 0) + return res.status(404).json({ error: "Invalid username." }); + // contains '/' + else if (username.indexOf("/") !== -1) + return res.status(404).json({ error: "Invalid username." }); + // invalid regex + else if (!isValidUsername(username)) + return res.status(404).json({ error: "Invalid username." }); + // taken username + else if (!isAvailable(username)) + return res.status(404).json({ error: "Username already taken." }); + } + + // * PIPELINE / EXPERIENCE VALIDATION // check if a pipeline change is within the req.body - if (profile.pipeline) { + const pipeline = profile.pipeline; + if (pipeline) { // if there is a pipeline change // make sure display names are added vs. raw names - for (let i = 0; i < profile.pipeline.length; i++) { - const query_name = profile.pipeline[i].companyName; + for (let i = 0; i < pipeline.length; i++) { + const query_name = pipeline[i].companyName; const company = await Company.findOne({ name: query_name }); if (company) { From 770d2356a4d66b3704192733c1a9614e13e06f27 Mon Sep 17 00:00:00 2001 From: ouckah Date: Wed, 17 Apr 2024 14:34:22 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9A=80=20Username=20Profile=20Routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/ProfilePicture.js | 5 +-- client/src/pages/Profile.js | 43 ++++++++++++++++++++----- server/controllers/profileController.js | 38 ++++++++++++++++++++++ server/routes/profiles.js | 4 +++ 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/client/src/components/ProfilePicture.js b/client/src/components/ProfilePicture.js index cfbe739..d0bf71b 100644 --- a/client/src/components/ProfilePicture.js +++ b/client/src/components/ProfilePicture.js @@ -1,18 +1,15 @@ import { useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' import { HOST } from '../util/apiRoutes' import { fetchWithAuth } from '../util/fetchUtils' export const ProfilePicture = ({ profile, setPfp }) => { - const { id } = useParams() - const [fetchedPfp, setFetchedPfp] = useState(null) const fetchPfp = async () => { try { const data = await fetchWithAuth({ - url: `${HOST}/api/pfp/${id}`, + url: `${HOST}/api/pfp/${profile._id}`, method: 'GET', }) diff --git a/client/src/pages/Profile.js b/client/src/pages/Profile.js index c20db2c..8fa10e8 100644 --- a/client/src/pages/Profile.js +++ b/client/src/pages/Profile.js @@ -37,12 +37,6 @@ function Profile() { const fetchProfile = async () => { try { setLoading(true) - const isValidId = await isMongoDBId(id) - - if (!isValidId) { - setProfile(null) - throw new Error('Invalid MongoDB ID') - } const data = await fetchWithAuth({ url: `${HOST}/api/profile/get/${id}`, @@ -65,6 +59,31 @@ function Profile() { } } + const fetchProfileByUsername = async () => { + try { + setLoading(true) + + const data = await fetchWithAuth({ + url: `${HOST}/api/profile/getBy?username=${id}`, + method: 'GET', + }) + + if (data.school) fetchSchool(data.school) + + // Successful fetch and data extraction + setProfile(data) + setUsername(data.username) + setLinkedin(extractLinkedinUsername(data.linkedin)) + setLocation(data.location) + setPfp(data.pfp) + } catch (error) { + console.error('Error:', error.message) + setProfile(null) + } finally { + setLoading(false) + } + } + const fetchSchool = async (id) => { try { const data = await fetchWithAuth({ @@ -182,13 +201,21 @@ function Profile() { useEffect(() => { const fetchInfo = async () => { - await fetchProfile() + const isValidId = await isMongoDBId(id) + + if (!isValidId.response) { + await fetchProfileByUsername() + } else { + await fetchProfile() + } } fetchInfo() }, [id]) - const admin = user && (user.profileId === id || user.username === id) + // establish if the user viewing the profile + // has admin privileges of the viewed profile + const admin = user && profile && user.profileId === profile._id const currentExperienceInfo = // {'Incoming / Currently / Previously', Work Title, Company Name} profile && profile.pipeline && profile.pipeline.length > 0 diff --git a/server/controllers/profileController.js b/server/controllers/profileController.js index 5794c9f..ac48239 100644 --- a/server/controllers/profileController.js +++ b/server/controllers/profileController.js @@ -53,6 +53,43 @@ const getProfile = async (req, res) => { res.status(200).json(profile); }; +// GET a single profile by username +const getProfileByUsername = async (req, res) => { + const { username } = req.query; + + // Search for profile by username + const profile = await Profile.findOne({ username }); + + if (!profile) { + return res.status(404).json({ error: "No such Profile." }); + } + + // check if profile is anonymous + if (profile.anonymous) { + // Convert the Mongoose document to a plain JavaScript object + let anonymousProfile = profile.toObject(); + + // Use object destructuring to exclude certain properties + const { linkedin, pfp, location, lastName, ...rest } = anonymousProfile; + + // Create a new profile object with the properties you want to retain and modify + anonymousProfile = { + ...rest, + firstName: "Anonymous", + + // set unwanted properties an empty string + linkedin: "", + pfp: "", + location: "", + lastName: "", + }; + + return res.status(200).json(anonymousProfile); + } + + res.status(200).json(profile); +}; + // GET a certain amount of random profiles const getRandomProfiles = async (req, res) => { try { @@ -221,6 +258,7 @@ const updateProfile = async (req, res) => { module.exports = { getProfiles, getProfile, + getProfileByUsername, getRandomProfiles, deleteProfile, updateProfile, diff --git a/server/routes/profiles.js b/server/routes/profiles.js index e6eed86..5a63ead 100644 --- a/server/routes/profiles.js +++ b/server/routes/profiles.js @@ -2,6 +2,7 @@ const express = require("express"); const { getProfiles, getProfile, + getProfileByUsername, getRandomProfiles, deleteProfile, updateProfile, @@ -16,6 +17,9 @@ read.get("/", getProfiles); // GET a single profile read.get("/get/:id", getProfile); +// GET a single profile by username +read.get("/getBy", getProfileByUsername); + // GET a certain amount of random profiles read.get("/random", getRandomProfiles);