From e9b4db101b4f1fac8a175ee3243263532658c1ce Mon Sep 17 00:00:00 2001 From: Mrunmayee Kokitkar Date: Tue, 7 Jul 2026 13:16:26 +0530 Subject: [PATCH 1/2] feat: Add Team Members & Organization Directory page - Add API endpoint GET /api/organization/members to fetch organization members - Create TeamMembers page with member list, search, and filters - Implement member details modal with profile information - Add role-based filtering (Admin/Member) - Add search functionality for name, email, and role - Add sorting by name, join date, and role - Implement loading, empty, and error states - Add quick actions (copy email) - Add Team Members link to navigation bar - Follow MeetOnMemory design language with responsive layout --- client/src/App.jsx | 10 + client/src/components/Navbar.jsx | 28 +- client/src/pages/TeamMembers.jsx | 442 +++++++++++++++++++ server/controllers/organizationController.js | 55 ++- server/routes/organizationRoutes.js | 4 + 5 files changed, 531 insertions(+), 8 deletions(-) create mode 100644 client/src/pages/TeamMembers.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index d4eaa63..97026e3 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -24,6 +24,7 @@ import Reports from "./pages/Reports.jsx"; import AiSearch from "./pages/AiSearch.jsx"; import MeetingRoom from "./pages/MeetingRoom.jsx"; import MeetingDetails from "./pages/MeetingDetails.jsx"; +import TeamMembers from "./pages/TeamMembers.jsx"; // --- Components --- import ProtectedRoute from "./components/ProtectedRoute.jsx"; @@ -220,6 +221,15 @@ const App = () => { } /> + + + + } + /> + {/* ✅ Fallback route — send unknown routes to Home */} } /> diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index 4dfe9fe..44f6bec 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -16,6 +16,7 @@ import { LogOut, ChevronDown, Sparkles, + Users, } from "lucide-react"; const NAV_LINKS = [ @@ -198,6 +199,7 @@ const Navbar = () => { const appLinks = [ { label: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, { label: "Meetings", href: "/meetings", icon: Calendar }, + { label: "Team Members", href: "/team-members", icon: Users }, { label: "Organizations", href: "/select-role", icon: Building2 }, { label: "AI Search", href: "/ai-search", icon: Search }, ]; @@ -212,7 +214,7 @@ const Navbar = () => { >
- {/* Logo */} + {/* Logo */}
navigate("/")} @@ -223,15 +225,31 @@ const Navbar = () => { >
{/* Clean Extra Large Native Option A Infinity Symbol with scale only */} - + - + - + diff --git a/client/src/pages/TeamMembers.jsx b/client/src/pages/TeamMembers.jsx new file mode 100644 index 0000000..cd2d585 --- /dev/null +++ b/client/src/pages/TeamMembers.jsx @@ -0,0 +1,442 @@ +import React, { useContext, useState, useEffect, useCallback } from "react"; +import axios from "axios"; +import AppContent from "../context/AppContent"; +import Navbar from "../components/Navbar.jsx"; +import { + Users, + Search, + Filter, + Mail, + Calendar, + Shield, + Copy, + X, + ChevronDown, + Clock, + CheckCircle, + XCircle, +} from "lucide-react"; +import { toast } from "react-toastify"; + +const ROLE_STYLES = { + admin: "bg-violet-50 text-violet-700 border-violet-200", + member: "bg-sky-50 text-sky-700 border-sky-200", +}; + +const ROLE_LABELS = { + admin: "Admin", + member: "Member", +}; + +const TeamMembers = () => { + const { backendUrl } = useContext(AppContent); + + const [members, setMembers] = useState([]); + const [filteredMembers, setFilteredMembers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedMember, setSelectedMember] = useState(null); + const [showFilters, setShowFilters] = useState(false); + const [sortBy] = useState("name"); + const [sortOrder] = useState("asc"); + const [roleFilter, setRoleFilter] = useState("all"); + + const fetchMembers = useCallback(async () => { + try { + setLoading(true); + setError(null); + const { data } = await axios.get( + `${backendUrl}/api/organization/members`, + { + withCredentials: true, + }, + ); + + if (data.success) { + setMembers(data.members); + setFilteredMembers(data.members); + } else { + setError(data.message || "Failed to fetch members"); + } + } catch (err) { + console.error("Error fetching members:", err); + setError("Failed to fetch members. Please try again."); + } finally { + setLoading(false); + } + }, [backendUrl]); + + const applyFiltersAndSort = useCallback(() => { + let result = [...members]; + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + result = result.filter( + (member) => + member.name?.toLowerCase().includes(query) || + member.email?.toLowerCase().includes(query) || + member.role?.toLowerCase().includes(query) + ); + } + + // Apply role filter + if (roleFilter !== "all") { + result = result.filter((member) => member.role === roleFilter); + } + + // Apply sorting + result.sort((a, b) => { + let comparison = 0; + switch (sortBy) { + case "name": + comparison = (a.name || "").localeCompare(b.name || ""); + break; + case "role": + comparison = (a.role || "").localeCompare(b.role || ""); + break; + case "joined": + comparison = new Date(a.createdAt || 0) - new Date(b.createdAt || 0); + break; + default: + comparison = 0; + } + return sortOrder === "asc" ? comparison : -comparison; + }); + + setFilteredMembers(result); + }, [members, searchQuery, roleFilter, sortBy, sortOrder]); + + const handleCopyEmail = (email) => { + navigator.clipboard.writeText(email); + toast.success("Email copied to clipboard"); + }; + + const getInitials = (name) => { + if (!name) return "U"; + return name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + const formatDate = (dateString) => { + if (!dateString) return "N/A"; + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; + + useEffect(() => { + fetchMembers(); + }, [fetchMembers]); + + useEffect(() => { + applyFiltersAndSort(); + }, [applyFiltersAndSort]); + + if (loading) { + return ( +
+ +
+
+
+
+
+
+ ); + } + + if (error) { + return ( +
+ +
+
+ +

+ Error Loading Members +

+

{error}

+ +
+
+
+ ); + } + + return ( +
+ + +
+ {/* Header */} +
+
+
+ +
+
+

+ Team Members +

+

+ {filteredMembers.length}{" "} + {filteredMembers.length === 1 ? "member" : "members"} +

+
+
+
+ + {/* Search and Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-12 pr-4 py-3 rounded-xl border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" + /> +
+ +
+ + + {showFilters && ( +
+ +
+ )} +
+
+ + {/* Members List */} + {filteredMembers.length === 0 ? ( +
+ +

+ No members found +

+

+ {searchQuery || roleFilter !== "all" + ? "Try adjusting your search or filters" + : "Your organization has no members yet"} +

+
+ ) : ( +
+ {filteredMembers.map((member) => ( +
setSelectedMember(member)} + className="group relative flex items-center gap-4 p-4 rounded-xl border border-slate-200 bg-white hover:border-slate-300 hover:shadow-md transition-all cursor-pointer" + > + {/* Avatar */} +
+ {getInitials(member.name)} +
+ + {/* Member Info */} +
+
+

+ {member.name || "Unknown"} +

+ {member.isAccountVerified && ( + + )} +
+

+ {member.email} +

+
+ + {/* Role Badge */} + + + {ROLE_LABELS[member.role?.toLowerCase()] || + member.role || + "Member"} + + + {/* Join Date */} +
+ + {formatDate(member.createdAt)} +
+ + {/* Quick Actions */} +
+ +
+
+ ))} +
+ )} +
+ + {/* Member Details Modal */} + {selectedMember && ( +
setSelectedMember(null)} + > +
e.stopPropagation()} + > + {/* Close Button */} + + + {/* Modal Content */} +
+ {/* Avatar */} +
+
+ {getInitials(selectedMember.name)} +
+

+ {selectedMember.name || "Unknown"} +

+

{selectedMember.email}

+
+ + {/* Details */} +
+
+
+ + + Role + +
+ + {ROLE_LABELS[selectedMember.role?.toLowerCase()] || + selectedMember.role || + "Member"} + +
+ +
+
+ + + Email + +
+ +
+ +
+
+ + + Joined + +
+ + {formatDate(selectedMember.createdAt)} + +
+ +
+
+ + + Status + +
+ + {selectedMember.isAccountVerified ? ( + <> + + Verified + + ) : ( + <> + + Pending + + )} + +
+
+ + {/* Actions */} +
+ +
+
+
+
+ )} +
+ ); +}; + +export default TeamMembers; diff --git a/server/controllers/organizationController.js b/server/controllers/organizationController.js index 1a4d34b..34878a8 100644 --- a/server/controllers/organizationController.js +++ b/server/controllers/organizationController.js @@ -23,7 +23,10 @@ export const createOrJoinOrganization = async (req, res) => { if (!name || !name.trim()) { return res .status(400) - .json({ success: false, message: "Please provide an organization name." }); + .json({ + success: false, + message: "Please provide an organization name.", + }); } const userId = req.user.id; @@ -39,7 +42,7 @@ export const createOrJoinOrganization = async (req, res) => { if (organization) { // --- Join existing organization --- const alreadyMember = organization.members.some( - (m) => m.toString() === userId.toString() + (m) => m.toString() === userId.toString(), ); if (!alreadyMember) { @@ -153,7 +156,7 @@ export const joinOrganization = async (req, res) => { } const alreadyMember = organization.members.some( - (m) => m.toString() === userId.toString() + (m) => m.toString() === userId.toString(), ); if (!alreadyMember) { @@ -182,3 +185,49 @@ export const joinOrganization = async (req, res) => { res.status(500).json({ success: false, message: "Server error" }); } }; + +/** + * ✅ Get organization members + * Returns: { success: true, members: [...] } + */ +export const getOrganizationMembers = async (req, res) => { + try { + if (!req.user || !req.user.id) { + return res + .status(401) + .json({ success: false, message: "Authentication failed." }); + } + + const user = await userModel.findById(req.user.id); + if (!user || !user.organization) { + return res + .status(400) + .json({ + success: false, + message: "User is not part of an organization.", + }); + } + + const organization = await Organization.findById( + user.organization, + ).populate({ + path: "members", + select: "name email role createdAt isAccountVerified", + }); + + if (!organization) { + return res + .status(404) + .json({ success: false, message: "Organization not found." }); + } + + res.status(200).json({ + success: true, + members: organization.members, + organizationName: organization.name, + }); + } catch (error) { + console.error("❌ Error fetching organization members:", error); + res.status(500).json({ success: false, message: "Server error" }); + } +}; diff --git a/server/routes/organizationRoutes.js b/server/routes/organizationRoutes.js index 075b1f8..3a7986b 100644 --- a/server/routes/organizationRoutes.js +++ b/server/routes/organizationRoutes.js @@ -4,6 +4,7 @@ import { createOrJoinOrganization, getAllOrganizations, joinOrganization, + getOrganizationMembers, } from "../controllers/organizationController.js"; import userAuth from "../middleware/userAuth.js"; @@ -18,4 +19,7 @@ router.post("/join", userAuth, joinOrganization); // Fetch all organizations (list) - usable for the join UI router.get("/", userAuth, getAllOrganizations); +// Fetch organization members +router.get("/members", userAuth, getOrganizationMembers); + export default router; From 72de9e99374338852458972f06d89435fbfacd98 Mon Sep 17 00:00:00 2001 From: Mrunmayee Kokitkar Date: Tue, 7 Jul 2026 13:28:19 +0530 Subject: [PATCH 2/2] fix: Add rate limiting to organization routes and fix duplicate function declarations - Add apiLimiter to organizationRoutes.js to fix CodeQL security alerts - Fix duplicate updateMeeting function declaration in meetingController.js - Fix duplicate updateMeeting import in meetingRoutes.js - Fix malformed getMeetingById function in meetingController.js --- server/controllers/meetingController.js | 22 ++-------------------- server/routes/meetingRoutes.js | 1 - server/routes/organizationRoutes.js | 4 ++++ 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/server/controllers/meetingController.js b/server/controllers/meetingController.js index 8236afa..7e4a9b7 100644 --- a/server/controllers/meetingController.js +++ b/server/controllers/meetingController.js @@ -615,22 +615,12 @@ export const deleteMeeting = async (req, res) => { - Used by: MeetingDetails.jsx ========================================================= */ export const getMeetingById = async (req, res) => { - 7. NEW: UPDATE MEETING (updateMeeting) - - Updates meeting fields like title, description, etc. - - Used by: MeetingRepository component - ========================================================= */ -export const updateMeeting = async (req, res) => { try { const userId = req.user?.id || req.user?._id; if (!userId) { return res.status(401).json({ success: false, message: "Unauthorized" }); } - const meeting = await Meeting.findOne({ - _id: req.params.id, - uploadedBy: userId, - }); - const meeting = await Meeting.findById(req.params.id); if (!meeting) { return res.status(404).json({ success: false, message: "Meeting not found" }); @@ -664,14 +654,6 @@ export const updateMeeting = async (req, res) => { return res.status(404).json({ success: false, message: "Meeting not found" }); } - const { title } = req.body; - if (title) { - meeting.title = title.trim(); - } - - await meeting.save(); - - return res.status(200).json({ success: true, message: "Meeting updated successfully", meeting }); // Check if user owns the meeting if (meeting.uploadedBy.toString() !== userId.toString()) { return res.status(403).json({ success: false, message: "You don't have permission to update this meeting" }); @@ -679,7 +661,7 @@ export const updateMeeting = async (req, res) => { // Update allowed fields const { title, description, meetingType, date, time, duration, location, venue, tags } = req.body; - + if (title) meeting.title = title.trim(); if (description !== undefined) meeting.description = description; if (meetingType) meeting.meetingType = meetingType; @@ -708,7 +690,7 @@ export const updateMeeting = async (req, res) => { description: meeting.description, meetingType: meeting.meetingType, date: meeting.date, - } + }, }); } catch (error) { console.error("❌ updateMeeting Error:", error.message); diff --git a/server/routes/meetingRoutes.js b/server/routes/meetingRoutes.js index 2889e15..5322959 100644 --- a/server/routes/meetingRoutes.js +++ b/server/routes/meetingRoutes.js @@ -12,7 +12,6 @@ import { updateMeeting, // NEW: Update meeting (rename) deleteMeeting, // EXISTING: Delete meeting searchMeetingsByText, // 🆕 NEW: Voice/Text Search - updateMeeting // 🆕 NEW: Update meeting (rename, etc.) } from "../controllers/meetingController.js"; const router = express.Router(); diff --git a/server/routes/organizationRoutes.js b/server/routes/organizationRoutes.js index 3a7986b..b850ce7 100644 --- a/server/routes/organizationRoutes.js +++ b/server/routes/organizationRoutes.js @@ -7,9 +7,13 @@ import { getOrganizationMembers, } from "../controllers/organizationController.js"; import userAuth from "../middleware/userAuth.js"; +import { apiLimiter } from "../middleware/rateLimiter.js"; const router = express.Router(); +// Apply rate limiting to all routes +router.use(apiLimiter); + // Unified endpoint: handles both "create new" and "join existing" organizations router.post("/create-or-join", userAuth, createOrJoinOrganization);