diff --git a/backend/middleware/validateUserProfile.js b/backend/middleware/validateUserProfile.js new file mode 100644 index 0000000..610fd2f --- /dev/null +++ b/backend/middleware/validateUserProfile.js @@ -0,0 +1,21 @@ +import { body, validationResult } from "express-validator"; + +export const validateUserProfile = [ + body("name").optional().isString().withMessage("Name must be a string"), + body("bio").optional().isString().withMessage("Bio must be a string"), + body("location") + .optional() + .isString() + .withMessage("Location must be a string"), + body("avatar").optional().isURL().withMessage("Avatar must be a valid URL"), + + (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + next(); + }, +]; + +// Using express-validator to validate user profile fields diff --git a/backend/models/userModel.js b/backend/models/userModel.js index 7e3e012..4285bd9 100644 --- a/backend/models/userModel.js +++ b/backend/models/userModel.js @@ -1,24 +1,90 @@ import mongoose from "mongoose"; -const userSchema = new mongoose.Schema({ - name: { type: String, default: "" }, // Optional name field. It can be updated from frontend - email: { type: String, required: true, unique: true }, - password: { type: String, required: true }, // Password should be hashed before saving to the database - bio: { type: String, default: "" }, // Optional bio field - location: { type: String, default: "" }, // Optional location field - avatar: { type: String, default: "" }, // Optional avatar field - // This field can be used to store the URL of the user's avatar image - createdAt: { type: Date, default: Date.now }, // Timestamp of user creation - updatedAt: { type: Date, default: Date.now }, // Timestamp of last update - role: { - type: String, - enum: ["admin", "user"], - default: "user", +const userSchema = new mongoose.Schema( + { + name: { type: String, default: "" }, // Optional name field. It can be updated from frontend + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, // Password should be hashed before saving to the database + bio: { type: String, default: "" }, // Optional bio field + location: { type: String, default: "" }, // Optional location field + avatar: { + type: String, + default: "https://avatars.githubusercontent.com/u/165805964?v=4", + }, // Optional avatar field (my github avatar URL) + // This field can be used to store the URL of the user's avatar image + createdAt: { type: Date, default: Date.now }, // Timestamp of user creation + updatedAt: { type: Date, default: Date.now }, // Timestamp of last update + role: { + type: String, + enum: ["admin", "user"], + default: "user", + }, }, -}); + { timestamps: true } // Add createdAt and updatedAt fields automatically +); const User = mongoose.model("User", userSchema); export default User; // Next, I am going jump and create an auth route inside app.js. This route will be responsible for authenticating users. I split it when i have more time and more routes to create. + +// Full version of the user model with regex to check the valid email, hashed password before saving, compare password and time stamp for createdAt and updatedAt fields, and role-based access control. + +{ + /* + import mongoose from "mongoose"; +import bcrypt from "bcrypt"; + +const userSchema = new mongoose.Schema( + { + name: { type: String, default: "" }, // Optional name field + email: { + type: String, + required: true, + unique: true, + validate: { + validator: function (v) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); // Regex for email validation + }, + message: (props) => `${props.value} is not a valid email address!`, + }, + }, + password: { type: String, required: true }, // Password should be hashed before saving + bio: { type: String, default: "" }, // Optional bio field + location: { type: String, default: "" }, // Optional location field + avatar: { + type: String, + default: "https://avatars.githubusercontent.com/u/165805964?v=4", // Default avatar URL + }, + role: { + type: String, + enum: ["admin", "user"], + default: "user", + }, + }, + { timestamps: true } // Automatically adds createdAt and updatedAt fields +); + +// Hash password before saving +userSchema.pre("save", async function (next) { + if (!this.isModified("password")) return next(); + try { + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + next(); + } catch (err) { + next(err); + } +}); + +// Method to compare passwords +userSchema.methods.comparePassword = async function (candidatePassword) { + return await bcrypt.compare(candidatePassword, this.password); +}; + +const User = mongoose.model("User", userSchema); + +export default User; + */ +} diff --git a/backend/routes/userProfile.js b/backend/routes/userProfile.js index c6b27a3..29ff209 100644 --- a/backend/routes/userProfile.js +++ b/backend/routes/userProfile.js @@ -2,6 +2,7 @@ import express from "express"; import authenticateToken from "../middleware/authMiddleware.js"; import User from "../models/userModel.js"; +import { validateUserProfile } from "../middleware/validateUserProfile.js"; const router = express.Router(); console.log("userProfile routes is loaded"); @@ -28,28 +29,24 @@ router.get("/", authenticateToken, async (req, res) => { // PUT /profile — update user profile fields! // Change to PATCH method to allow partial updates -router.patch("/", authenticateToken, async (req, res) => { +// Additional validation middleware to ensure correct data types (validateUserProfile) +router.patch("/", authenticateToken, validateUserProfile, async (req, res) => { console.log("Updating user profile:", req.body); try { const { name, bio, location, avatar } = req.body; - // Optional validation - if (name && typeof name !== "string") { - return res.status(400).json({ error: "Name must be a string" }); - } - if (bio && typeof bio !== "string") { - return res.status(400).json({ error: "Bio must be a string" }); - } - if (location && typeof location !== "string") { - return res.status(400).json({ error: "Location must be a string" }); - } - if (avatar && typeof avatar !== "string") { - return res.status(400).json({ error: "Avatar must be a string (URL)" }); - } - const user = await User.findById(req.user._id); if (!user) return res.status(404).json({ error: "User not found" }); + // Validate that at least one field is provided + if (!name && !bio && !location && !avatar) { + return res + .status(400) + .json({ error: "At least one field must be provided" }); + } + + // user profile validation moved to validateUserProfile middleware! + // Conditionally update only if provided if (name !== undefined) user.name = name; if (bio !== undefined) user.bio = bio; diff --git a/frontend/src/components/views/UserProfile.jsx b/frontend/src/components/views/UserProfile.jsx index 4a99eb4..398f15d 100644 --- a/frontend/src/components/views/UserProfile.jsx +++ b/frontend/src/components/views/UserProfile.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useUserProfileFetcher } from "../hooks/userProfileFetcher"; import PropTypes from "prop-types"; import { apiRequest } from "../utils/api"; @@ -8,24 +8,20 @@ import BtnNeoGradient from "../buttons/BtnNeonGradient"; import Button from "../buttons/Button"; import ButtonGradient from "../buttons/ButtonGradient"; -const UserProfile = ({ returnToInfo }) => { +const UserProfile = ({ returnToInfo, onUpdate }) => { const { profile, setProfile, loading, message, setMessage, refetch } = useUserProfileFetcher(); const [saving, setSaving] = useState(false); - useEffect(() => { - refetch(); // Force refetch when the component mounts - }, []); - const { name, bio, location, avatar } = profile; - // Detect if any field changed - Updated from name only to include all fields + const isUnchanged = name === "" && bio === "" && location === "" && avatar === ""; const handleChange = (e) => { setProfile({ ...profile, [e.target.name]: e.target.value }); }; - // Handle update + const handleSubmit = async (e) => { e.preventDefault(); setSaving(true); @@ -41,9 +37,17 @@ const UserProfile = ({ returnToInfo }) => { setMessage("Profile updated!"); console.log("Profile updated successfully:", name); - // Refetch the profile data from the server to ensure it's up-to-date - await refetch(); - console.log("Refetched profile data:", profile); + await refetch(); // Refetch the profile data to ensure it's up-to-date + + if (onUpdate) { + onUpdate({ + name: payload.name, + bio: payload.bio, + location: payload.location, + avatar: payload.avatar, + }); + console.log("onUpdate callback called with:", payload); + } } catch (err) { setMessage(err.message); } finally { @@ -51,26 +55,32 @@ const UserProfile = ({ returnToInfo }) => { } }; - // If loading, show a loading message if (loading) { return
Loading profile...
; } - console.log("Rendering UserProfile with profile:", profile); - return (