Skip to content

Commit 5b7634b

Browse files
authored
Merge pull request #8 from TVATDCI/development
Development
2 parents 5675a83 + b821e95 commit 5b7634b

File tree

4 files changed

+146
-60
lines changed

4 files changed

+146
-60
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { body, validationResult } from "express-validator";
2+
3+
export const validateUserProfile = [
4+
body("name").optional().isString().withMessage("Name must be a string"),
5+
body("bio").optional().isString().withMessage("Bio must be a string"),
6+
body("location")
7+
.optional()
8+
.isString()
9+
.withMessage("Location must be a string"),
10+
body("avatar").optional().isURL().withMessage("Avatar must be a valid URL"),
11+
12+
(req, res, next) => {
13+
const errors = validationResult(req);
14+
if (!errors.isEmpty()) {
15+
return res.status(400).json({ errors: errors.array() });
16+
}
17+
next();
18+
},
19+
];
20+
21+
// Using express-validator to validate user profile fields

backend/models/userModel.js

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,90 @@
11
import mongoose from "mongoose";
22

3-
const userSchema = new mongoose.Schema({
4-
name: { type: String, default: "" }, // Optional name field. It can be updated from frontend
5-
email: { type: String, required: true, unique: true },
6-
password: { type: String, required: true }, // Password should be hashed before saving to the database
7-
bio: { type: String, default: "" }, // Optional bio field
8-
location: { type: String, default: "" }, // Optional location field
9-
avatar: { type: String, default: "" }, // Optional avatar field
10-
// This field can be used to store the URL of the user's avatar image
11-
createdAt: { type: Date, default: Date.now }, // Timestamp of user creation
12-
updatedAt: { type: Date, default: Date.now }, // Timestamp of last update
13-
role: {
14-
type: String,
15-
enum: ["admin", "user"],
16-
default: "user",
3+
const userSchema = new mongoose.Schema(
4+
{
5+
name: { type: String, default: "" }, // Optional name field. It can be updated from frontend
6+
email: { type: String, required: true, unique: true },
7+
password: { type: String, required: true }, // Password should be hashed before saving to the database
8+
bio: { type: String, default: "" }, // Optional bio field
9+
location: { type: String, default: "" }, // Optional location field
10+
avatar: {
11+
type: String,
12+
default: "https://avatars.githubusercontent.com/u/165805964?v=4",
13+
}, // Optional avatar field (my github avatar URL)
14+
// This field can be used to store the URL of the user's avatar image
15+
createdAt: { type: Date, default: Date.now }, // Timestamp of user creation
16+
updatedAt: { type: Date, default: Date.now }, // Timestamp of last update
17+
role: {
18+
type: String,
19+
enum: ["admin", "user"],
20+
default: "user",
21+
},
1722
},
18-
});
23+
{ timestamps: true } // Add createdAt and updatedAt fields automatically
24+
);
1925

2026
const User = mongoose.model("User", userSchema);
2127

2228
export default User;
2329

2430
// 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.
31+
32+
// 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.
33+
34+
{
35+
/*
36+
import mongoose from "mongoose";
37+
import bcrypt from "bcrypt";
38+
39+
const userSchema = new mongoose.Schema(
40+
{
41+
name: { type: String, default: "" }, // Optional name field
42+
email: {
43+
type: String,
44+
required: true,
45+
unique: true,
46+
validate: {
47+
validator: function (v) {
48+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); // Regex for email validation
49+
},
50+
message: (props) => `${props.value} is not a valid email address!`,
51+
},
52+
},
53+
password: { type: String, required: true }, // Password should be hashed before saving
54+
bio: { type: String, default: "" }, // Optional bio field
55+
location: { type: String, default: "" }, // Optional location field
56+
avatar: {
57+
type: String,
58+
default: "https://avatars.githubusercontent.com/u/165805964?v=4", // Default avatar URL
59+
},
60+
role: {
61+
type: String,
62+
enum: ["admin", "user"],
63+
default: "user",
64+
},
65+
},
66+
{ timestamps: true } // Automatically adds createdAt and updatedAt fields
67+
);
68+
69+
// Hash password before saving
70+
userSchema.pre("save", async function (next) {
71+
if (!this.isModified("password")) return next();
72+
try {
73+
const salt = await bcrypt.genSalt(10);
74+
this.password = await bcrypt.hash(this.password, salt);
75+
next();
76+
} catch (err) {
77+
next(err);
78+
}
79+
});
80+
81+
// Method to compare passwords
82+
userSchema.methods.comparePassword = async function (candidatePassword) {
83+
return await bcrypt.compare(candidatePassword, this.password);
84+
};
85+
86+
const User = mongoose.model("User", userSchema);
87+
88+
export default User;
89+
*/
90+
}

backend/routes/userProfile.js

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import express from "express";
33
import authenticateToken from "../middleware/authMiddleware.js";
44
import User from "../models/userModel.js";
5+
import { validateUserProfile } from "../middleware/validateUserProfile.js";
56

67
const router = express.Router();
78
console.log("userProfile routes is loaded");
@@ -28,28 +29,24 @@ router.get("/", authenticateToken, async (req, res) => {
2829

2930
// PUT /profile — update user profile fields!
3031
// Change to PATCH method to allow partial updates
31-
router.patch("/", authenticateToken, async (req, res) => {
32+
// Additional validation middleware to ensure correct data types (validateUserProfile)
33+
router.patch("/", authenticateToken, validateUserProfile, async (req, res) => {
3234
console.log("Updating user profile:", req.body);
3335
try {
3436
const { name, bio, location, avatar } = req.body;
3537

36-
// Optional validation
37-
if (name && typeof name !== "string") {
38-
return res.status(400).json({ error: "Name must be a string" });
39-
}
40-
if (bio && typeof bio !== "string") {
41-
return res.status(400).json({ error: "Bio must be a string" });
42-
}
43-
if (location && typeof location !== "string") {
44-
return res.status(400).json({ error: "Location must be a string" });
45-
}
46-
if (avatar && typeof avatar !== "string") {
47-
return res.status(400).json({ error: "Avatar must be a string (URL)" });
48-
}
49-
5038
const user = await User.findById(req.user._id);
5139
if (!user) return res.status(404).json({ error: "User not found" });
5240

41+
// Validate that at least one field is provided
42+
if (!name && !bio && !location && !avatar) {
43+
return res
44+
.status(400)
45+
.json({ error: "At least one field must be provided" });
46+
}
47+
48+
// user profile validation moved to validateUserProfile middleware!
49+
5350
// Conditionally update only if provided
5451
if (name !== undefined) user.name = name;
5552
if (bio !== undefined) user.bio = bio;

frontend/src/components/views/UserProfile.jsx

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from "react";
1+
import { useState } from "react";
22
import { useUserProfileFetcher } from "../hooks/userProfileFetcher";
33
import PropTypes from "prop-types";
44
import { apiRequest } from "../utils/api";
@@ -8,24 +8,20 @@ import BtnNeoGradient from "../buttons/BtnNeonGradient";
88
import Button from "../buttons/Button";
99
import ButtonGradient from "../buttons/ButtonGradient";
1010

11-
const UserProfile = ({ returnToInfo }) => {
11+
const UserProfile = ({ returnToInfo, onUpdate }) => {
1212
const { profile, setProfile, loading, message, setMessage, refetch } =
1313
useUserProfileFetcher();
1414
const [saving, setSaving] = useState(false);
1515

16-
useEffect(() => {
17-
refetch(); // Force refetch when the component mounts
18-
}, []);
19-
2016
const { name, bio, location, avatar } = profile;
21-
// Detect if any field changed - Updated from name only to include all fields
17+
2218
const isUnchanged =
2319
name === "" && bio === "" && location === "" && avatar === "";
2420

2521
const handleChange = (e) => {
2622
setProfile({ ...profile, [e.target.name]: e.target.value });
2723
};
28-
// Handle update
24+
2925
const handleSubmit = async (e) => {
3026
e.preventDefault();
3127
setSaving(true);
@@ -41,36 +37,50 @@ const UserProfile = ({ returnToInfo }) => {
4137
setMessage("Profile updated!");
4238
console.log("Profile updated successfully:", name);
4339

44-
// Refetch the profile data from the server to ensure it's up-to-date
45-
await refetch();
46-
console.log("Refetched profile data:", profile);
40+
await refetch(); // Refetch the profile data to ensure it's up-to-date
41+
42+
if (onUpdate) {
43+
onUpdate({
44+
name: payload.name,
45+
bio: payload.bio,
46+
location: payload.location,
47+
avatar: payload.avatar,
48+
});
49+
console.log("onUpdate callback called with:", payload);
50+
}
4751
} catch (err) {
4852
setMessage(err.message);
4953
} finally {
5054
setSaving(false);
5155
}
5256
};
5357

54-
// If loading, show a loading message
5558
if (loading) {
5659
return <p className="text-yellow-500">Loading profile...</p>;
5760
}
5861

59-
console.log("Rendering UserProfile with profile:", profile);
60-
6162
return (
6263
<div className="bg-neutral-800/20 backdrop-blur-sm p-4 rounded-lg shadow-lg max-w-xs">
63-
<h2 className="text-2xl text-red-600 p-2 font-bold mb-2">
64-
Edit Your Profile
65-
</h2>
64+
<h3 className="text-2xl text-red-600 p-2 font-bold mb-1 text-center">
65+
{name || "User Profile"}
66+
</h3>
6667
<form onSubmit={handleSubmit}>
68+
{avatar && (
69+
<div className="mb-1 flex justify-center">
70+
<img
71+
src={avatar}
72+
alt="Avatar Preview"
73+
className="rounded-full w-24 h-24 object-cover"
74+
/>
75+
</div>
76+
)}
6777
<label className="block mb-2">
6878
Name:
6979
<input
7080
name="name"
7181
className="border p-2 w-full"
7282
type="text"
73-
value={profile.name}
83+
value={name}
7484
onChange={handleChange}
7585
disabled={loading}
7686
placeholder="Enter your user name"
@@ -82,7 +92,7 @@ const UserProfile = ({ returnToInfo }) => {
8292
<textarea
8393
name="bio"
8494
className="border p-2 w-full"
85-
value={profile.bio}
95+
value={bio}
8696
onChange={handleChange}
8797
disabled={loading}
8898
rows="3"
@@ -96,7 +106,7 @@ const UserProfile = ({ returnToInfo }) => {
96106
name="location"
97107
className="border p-2 w-full"
98108
type="text"
99-
value={profile.location}
109+
value={location}
100110
onChange={handleChange}
101111
disabled={loading}
102112
placeholder="Where are you located?"
@@ -109,22 +119,13 @@ const UserProfile = ({ returnToInfo }) => {
109119
name="avatar"
110120
className="border p-2 w-full"
111121
type="text"
112-
value={profile.avatar}
122+
value={avatar}
113123
onChange={handleChange}
114124
disabled={loading}
115125
placeholder="Enter your avatar image URL"
116126
/>
117127
</label>
118128

119-
{avatar && (
120-
<div className="mb-2">
121-
<img
122-
src={avatar}
123-
alt="Avatar Preview"
124-
className="rounded-full w-20 h-20 object-cover"
125-
/>
126-
</div>
127-
)}
128129
<BtnNeoGradient />
129130
<SpaceBtn
130131
type="submit"
@@ -144,6 +145,7 @@ const UserProfile = ({ returnToInfo }) => {
144145
</div>
145146
);
146147
};
148+
147149
UserProfile.propTypes = {
148150
returnToInfo: PropTypes.func.isRequired,
149151
onUpdate: PropTypes.func.isRequired,

0 commit comments

Comments
 (0)