diff --git a/client/package-lock.json b/client/package-lock.json index 24ca143..8c12b36 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2284,7 +2284,7 @@ "version": "19.1.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", - "dev": true, + "devOptional": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2385,6 +2385,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2828,6 +2840,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dompurify": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", @@ -3375,6 +3393,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4330,6 +4357,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.30.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", @@ -4360,6 +4434,28 @@ "react-dom": ">=16.8" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/recharts": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.0.2.tgz", @@ -4729,6 +4825,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", diff --git a/client/src/components/Header.jsx b/client/src/components/Header.jsx index 71b4e90..a09c072 100644 --- a/client/src/components/Header.jsx +++ b/client/src/components/Header.jsx @@ -1,60 +1,65 @@ -import { useState, useRef, useEffect } from "react" -import { Link } from "react-router-dom" -import { FaUser, FaSignOutAlt, FaChevronDown, FaBars, FaTimes } from "react-icons/fa" -import { useAuth } from "../context/AuthContext" -import React from "react" +import { useState, useRef, useEffect } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { FaUser, FaSignOutAlt, FaChevronDown, FaBars, FaTimes } from "react-icons/fa"; +import { useAuth } from "../context/AuthContext"; +import React from "react"; export default function Header() { - const { user, isLoggedIn, logout } = useAuth() - const [isDropdownOpen, setIsDropdownOpen] = useState(false) - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) - const [showLogoutConfirm, setShowLogoutConfirm] = useState(false) - const dropdownRef = useRef(null) + const { user, isLoggedIn, logout } = useAuth(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [showLogoutConfirm, setShowLogoutConfirm] = useState(false); + const dropdownRef = useRef(null); + const navigate = useNavigate(); useEffect(() => { function handleClickOutside(event) { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { - setIsDropdownOpen(false) + setIsDropdownOpen(false); } } - document.addEventListener("mousedown", handleClickOutside) + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener("mousedown", handleClickOutside) - } - }, []) + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); const handleLogout = () => { - setShowLogoutConfirm(true) - setIsDropdownOpen(false) - } + setShowLogoutConfirm(true); + setIsDropdownOpen(false); + }; + + const confirmLogout = async () => { + // If you have a delete endpoint, replace this with axios call + // await axios.delete("/api/user/delete", { headers: { Authorization: `Bearer ${user.token}` } }); - const confirmLogout = () => { - logout() - setShowLogoutConfirm(false) - } + logout(); // Clears auth state + setShowLogoutConfirm(false); + navigate("/login"); + }; const cancelLogout = () => { - setShowLogoutConfirm(false) - } + setShowLogoutConfirm(false); + }; const toggleDropdown = () => { - setIsDropdownOpen(!isDropdownOpen) - } + setIsDropdownOpen(!isDropdownOpen); + }; const toggleMobileMenu = () => { - setIsMobileMenuOpen(!isMobileMenuOpen) - } + setIsMobileMenuOpen(!isMobileMenuOpen); + }; const getInitials = (name) => { - if (!name) return "U" + if (!name) return "U"; return name .split(" ") .map((n) => n[0]) .join("") .toUpperCase() - .slice(0, 2) - } + .slice(0, 2); + }; const NavLinks = () => (
@@ -68,7 +73,7 @@ export default function Header() { )} Resources
- ) + ); return ( <> @@ -96,7 +101,7 @@ export default function Header() {
- {!isLoggedIn ? ( + {!user || !isLoggedIn ? ( <> Log in Sign up @@ -144,7 +149,7 @@ export default function Header() { {isMobileMenuOpen && (
- {!isLoggedIn ? ( + {!user || !isLoggedIn ? (
Log in Sign up @@ -183,5 +188,5 @@ export default function Header() {
)} - ) -} + ); +} \ No newline at end of file diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx index 4700a70..6c63428 100644 --- a/client/src/context/AuthContext.jsx +++ b/client/src/context/AuthContext.jsx @@ -11,6 +11,20 @@ export const AuthProvider = ({ children }) => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [loading, setLoading] = useState(true); + const refreshUser = async () => { + const current = auth.currentUser; + if (current) { + const token = await current.getIdToken(); + setUser({ + uid: current.uid, + email: current.email, + name: current.displayName || "", + avatar: current.photoURL || "", + token, + }); + } + }; + const logout = () => { auth.signOut() .then(() => { @@ -25,7 +39,16 @@ export const AuthProvider = ({ children }) => { useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => { if (firebaseUser) { - setUser(firebaseUser); + const token = await firebaseUser.getIdToken(); + + setUser({ + uid: firebaseUser.uid, + email: firebaseUser.email, + name: firebaseUser.displayName || "", + avatar: firebaseUser.photoURL || "", + token, + }); + setIsLoggedIn(true); } else { setUser(null); @@ -38,8 +61,19 @@ export const AuthProvider = ({ children }) => { }, []); return ( - + {children} ); -}; +}; \ No newline at end of file diff --git a/client/src/pages/ProfilePage.jsx b/client/src/pages/ProfilePage.jsx new file mode 100644 index 0000000..0831435 --- /dev/null +++ b/client/src/pages/ProfilePage.jsx @@ -0,0 +1,205 @@ +import { useState } from "react"; +import { useAuth } from "../context/AuthContext"; +import axios from "axios"; +import Header from "../components/Header"; +import Footer from "../components/Footer"; +import { FaPen } from "react-icons/fa"; +import { useNavigate } from "react-router-dom"; + +export default function ProfilePage() { + const { user, isLoggedIn, refreshUser } = useAuth(); + const [name, setName] = useState(user?.name || ""); + const [email] = useState(user?.email || ""); + const [editName, setEditName] = useState(false); + const [saving, setSaving] = useState(false); + const [deleting, setDeleting] = useState(false); + const [message, setMessage] = useState(""); + const [error, setError] = useState(""); + const navigate = useNavigate(); + + const isEdited = editName && name.trim() !== user.name; + + const recentActivity = [ + { id: 1, action: "Completed React quiz", date: "Aug 1, 2025" }, + { id: 2, action: "Commented on State Management article", date: "Jul 30, 2025" }, + { id: 3, action: "Joined Practice Room", date: "Jul 28, 2025" }, + ]; + + const preferences = { + theme: "Light Mode", + notifications: true, + language: "English", + }; + + const updateProfile = async () => { + setSaving(true); + setMessage(""); + setError(""); + + try { + await axios.put(`${import.meta.env.VITE_API_URL}/api/auth/update-profile`, { name }, { + headers: { Authorization: `Bearer ${user.token}` }, + withCredentials: true, + }); + + await refreshUser(); + setMessage("Profile updated successfully."); + setEditName(false); + } catch (err) { + console.error(err); + setError("Failed to update profile."); + } finally { + setSaving(false); + } + }; + + const deleteAccount = async () => { + if (!window.confirm("Are you sure you want to delete your account? This action is irreversible.")) return; + + setDeleting(true); + setMessage(""); + setError(""); + + try { + await axios.delete(`${import.meta.env.VITE_API_URL}/api/auth/delete`, { + headers: { Authorization: `Bearer ${user.token}` }, + withCredentials: true, + }); + + setMessage("Account deleted. Redirecting..."); + setTimeout(() => navigate("/login"), 1500); + } catch (err) { + console.error(err); + setError("Account deletion failed."); + } finally { + setDeleting(false); + } + }; + + if (!isLoggedIn || !user) { + return ( +
+
+

Please log in to view your profile.

+
+
+ ); + } + + return ( +
+
+
+
+
+
+
+ +

Your Profile

+ + {/* Avatar */} +
+ {user.avatar ? ( + User Avatar + ) : ( +
+ {user.name?.charAt(0) || "U"} +
+ )} +
+ + {/* Editable Info */} +
+ {/* Name */} +
+ + {!editName ? ( +
+

{name}

+ +
+ ) : ( + setName(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring focus:ring-indigo-300" + /> + )} +
+ + {/* Email */} +
+ +

{email}

+
+ + {/* Preferences & Activity */} +
+
+

Preferences

+
    +
  • 🌈 Theme: {preferences.theme}
  • +
  • 🔔 Notifications: {preferences.notifications ? "Enabled" : "Disabled"}
  • +
  • 🗣️ Language: {preferences.language}
  • +
+
+ +
+

Recent Activity

+
    + {recentActivity.map((item) => ( +
  • + {item.action} + {item.date} +
  • + ))} +
+
+
+
+ + {/* Buttons */} +
+ {isEdited && ( + + )} + +
+ + {/* Feedback */} + {(message || error) && ( +
+ {message &&

{message}

} + {error &&

{error}

} +
+ )} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/routes.jsx b/client/src/routes.jsx index 279c579..c4ddb04 100644 --- a/client/src/routes.jsx +++ b/client/src/routes.jsx @@ -11,6 +11,7 @@ import Resources from "./pages/Resources"; import About from "./pages/About"; import Contact from "./pages/Contact"; import Error404 from "./pages/404"; +import ProfilePage from "./pages/ProfilePage"; const router = createBrowserRouter([ { path: "/", element: }, @@ -23,6 +24,7 @@ const router = createBrowserRouter([ { path: "/resources", element: }, { path: "/about", element: }, { path: "/contact", element: }, + { path: "/profile", element: }, { path: "*", element: }, ]); diff --git a/server/config/db.js b/server/config/db.js index 0b54af6..2d37000 100644 --- a/server/config/db.js +++ b/server/config/db.js @@ -2,10 +2,7 @@ import mongoose from 'mongoose'; const connectDB = async() => { try { - await mongoose.connect(process.env.MONGO_URL, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); + await mongoose.connect(process.env.MONGO_URL); } catch (error) { process.exit(1); } diff --git a/server/index.js b/server/index.js index 016aa75..88bff24 100644 --- a/server/index.js +++ b/server/index.js @@ -13,8 +13,19 @@ import multer from "multer"; dotenv.config(); const app = express(); const upload = multer({ dest: "uploads/" }); +const allowedOrigins = [ + 'http://localhost:5173', + 'https://prepedgeai.vercel.app' +]; + app.use(cors({ - origin: 'https://prepedgeai.vercel.app', + origin: function (origin, callback) { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, credentials: true, })); app.use(express.json()); diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index 262ebbf..797dc16 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -6,6 +6,7 @@ import express from "express"; import User from "../models/UserModel.js"; import firebaseAuthMiddleware from "../middleware/firebaseAuthMiddleware.js"; +import admin from "firebase-admin"; const router = express.Router(); router.use(express.json()); @@ -44,4 +45,48 @@ router.post("/login", firebaseAuthMiddleware, async (req, res) => { } }); +router.put("/update-profile", firebaseAuthMiddleware, async (req, res) => { + const { uid } = req.firebaseUser; + const { name, avatar } = req.body; + + try { + const user = await User.findOne({ firebase_user_id: uid }); + if (!user) return res.status(404).json({ error: "User not found" }); + + if (name) user.name = name; + if (avatar) user.avatar = avatar; + + await user.save(); + res.status(200).json({ message: "Profile updated successfully", user }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +router.delete("/delete", firebaseAuthMiddleware, async (req, res) => { + const { uid } = req.firebaseUser; + + try { + const user = await User.findOneAndDelete({ firebase_user_id: uid }); + if (!user) return res.status(404).json({ error: "User not found" }); + await admin.auth().deleteUser(uid); // Delete Firebase user + res.status(200).json({ message: "User deleted successfully" }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +router.get("/me", firebaseAuthMiddleware, async (req, res) => { + const { uid } = req.firebaseUser; + + try { + const user = await User.findOne({ firebase_user_id: uid }); + if (!user) return res.status(404).json({ error: "User not found" }); + + res.status(200).json({ user }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + export default router;