From 5ea14c701515138e715d57486d748f5074655a28 Mon Sep 17 00:00:00 2001 From: Mrunmayee Kokitkar Date: Tue, 7 Jul 2026 09:31:10 +0530 Subject: [PATCH] feat: Implement Meeting Repository & History Dashboard - Create MeetingRepository component for browsing, searching, and managing meetings - Add MeetingCard component with quick actions (view, download, rename, delete) - Implement MeetingSearch component for real-time search functionality - Add MeetingFilters component with status, type, date range, and sorting options - Create Pagination component for handling large meeting lists - Add EmptyState component for no meetings and no search results scenarios - Update MeetingListPage to use MeetingRepository component - Add updateMeeting controller for renaming meetings - Add PUT route for meeting updates - Create rateLimiter middleware with three tiers (api, write, upload) - Apply rate limiting to all meeting routes for security --- client/src/components/meetings/EmptyState.jsx | 48 +++ .../src/components/meetings/MeetingCard.jsx | 190 ++++++++++ .../components/meetings/MeetingFilters.jsx | 135 +++++++ .../components/meetings/MeetingRepository.jsx | 351 ++++++++++++++++++ .../src/components/meetings/MeetingSearch.jsx | 30 ++ client/src/components/meetings/Pagination.jsx | 68 ++++ client/src/pages/MeetingListPage.jsx | 58 +-- server/controllers/meetingController.js | 63 +++- server/middleware/rateLimiter.js | 37 ++ server/routes/meetingRoutes.js | 20 +- 10 files changed, 945 insertions(+), 55 deletions(-) create mode 100644 client/src/components/meetings/EmptyState.jsx create mode 100644 client/src/components/meetings/MeetingCard.jsx create mode 100644 client/src/components/meetings/MeetingFilters.jsx create mode 100644 client/src/components/meetings/MeetingRepository.jsx create mode 100644 client/src/components/meetings/MeetingSearch.jsx create mode 100644 client/src/components/meetings/Pagination.jsx create mode 100644 server/middleware/rateLimiter.js diff --git a/client/src/components/meetings/EmptyState.jsx b/client/src/components/meetings/EmptyState.jsx new file mode 100644 index 0000000..3517322 --- /dev/null +++ b/client/src/components/meetings/EmptyState.jsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Calendar, Search, FileText } from "lucide-react"; + +const EmptyState = ({ type }) => { + const states = { + noMeetings: { + icon: , + title: "No Meetings Yet", + description: "You haven't uploaded any meetings yet. Start by uploading your first meeting to see it here.", + actionText: "Upload Your First Meeting", + actionLink: "/upload-meeting", + }, + noResults: { + icon: , + title: "No Meetings Found", + description: "We couldn't find any meetings matching your search or filters. Try adjusting your criteria.", + actionText: "Clear Filters", + actionLink: null, + }, + noScheduled: { + icon: , + title: "No Scheduled Meetings", + description: "You don't have any scheduled meetings. Schedule a new meeting to get started.", + actionText: "Schedule a Meeting", + actionLink: "/create-meeting", + }, + }; + + const state = states[type] || states.noMeetings; + + return ( +
+
{state.icon}
+

{state.title}

+

{state.description}

+ {state.actionLink && ( + + {state.actionText} + + )} +
+ ); +}; + +export default EmptyState; diff --git a/client/src/components/meetings/MeetingCard.jsx b/client/src/components/meetings/MeetingCard.jsx new file mode 100644 index 0000000..70150fd --- /dev/null +++ b/client/src/components/meetings/MeetingCard.jsx @@ -0,0 +1,190 @@ +import React, { useState } from "react"; +import { Calendar, Clock, FileText, Tag, Trash2, Download, Edit2, Eye, MoreVertical } from "lucide-react"; + +const MeetingCard = ({ meeting, onDelete, onRename, onDownload }) => { + const [showMenu, setShowMenu] = useState(false); + const [isRenaming, setIsRenaming] = useState(false); + const [newTitle, setNewTitle] = useState(meeting.title); + + const handleRenameSubmit = (e) => { + e.preventDefault(); + if (newTitle.trim() && newTitle !== meeting.title) { + onRename(meeting._id, newTitle.trim()); + } + setIsRenaming(false); + setNewTitle(meeting.title); + }; + + const formatDate = (dateString) => { + if (!dateString) return "N/A"; + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + }; + + const getStatusColor = (status) => { + switch (status?.toLowerCase()) { + case "completed": + return "bg-green-100 text-green-700"; + case "processing": + return "bg-yellow-100 text-yellow-700"; + case "failed": + return "bg-red-100 text-red-700"; + default: + return "bg-gray-100 text-gray-700"; + } + }; + + return ( +
+ {/* Card Header */} +
+
+ {isRenaming ? ( +
+ setNewTitle(e.target.value)} + onBlur={() => { + setIsRenaming(false); + setNewTitle(meeting.title); + }} + autoFocus + className="w-full px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-400 outline-none" + /> +
+ ) : ( +

+ {meeting.title || "Untitled Meeting"} +

+ )} +
+ + {showMenu && ( +
+ + + +
+ )} +
+
+
+ + {meeting.status || "Unknown"} + + {meeting.meetingType && ( + + {meeting.meetingType} + + )} +
+
+ + {/* Card Body */} +
+ {/* Date and Duration */} +
+
+ + {formatDate(meeting.date || meeting.createdAt)} +
+ {meeting.duration && ( +
+ + {meeting.duration} min +
+ )} +
+ + {/* Summary Preview */} + {meeting.summary && ( +
+ +

+ {meeting.summary} +

+
+ )} + + {/* Tags */} + {meeting.tags && meeting.tags.length > 0 && ( +
+ +
+ {meeting.tags.slice(0, 3).map((tag, index) => ( + + {tag} + + ))} + {meeting.tags.length > 3 && ( + + +{meeting.tags.length - 3} + + )} +
+
+ )} + + {/* Participants */} + {meeting.participants && meeting.participants.length > 0 && ( +
+ {meeting.participants.length} + participant(s) +
+ )} +
+ + {/* Card Footer */} +
+ + Created {formatDate(meeting.createdAt)} + + +
+
+ ); +}; + +export default MeetingCard; diff --git a/client/src/components/meetings/MeetingFilters.jsx b/client/src/components/meetings/MeetingFilters.jsx new file mode 100644 index 0000000..4557a06 --- /dev/null +++ b/client/src/components/meetings/MeetingFilters.jsx @@ -0,0 +1,135 @@ +import React from "react"; +import { Filter, ChevronDown, X } from "lucide-react"; + +const MeetingFilters = ({ + filters, + onFilterChange, + onClearFilters, +}) => { + const [showDropdown, setShowDropdown] = React.useState(false); + + const filterOptions = [ + { + key: "status", + label: "Processing Status", + options: [ + { value: "all", label: "All Statuses" }, + { value: "completed", label: "Completed" }, + { value: "processing", label: "Processing" }, + { value: "failed", label: "Failed" }, + ], + }, + { + key: "meetingType", + label: "Meeting Type", + options: [ + { value: "all", label: "All Types" }, + { value: "conference", label: "Conference" }, + { value: "policy", label: "Policy" }, + { value: "event", label: "Event" }, + { value: "internal", label: "Internal" }, + ], + }, + { + key: "dateRange", + label: "Date Range", + options: [ + { value: "all", label: "All Time" }, + { value: "today", label: "Today" }, + { value: "week", label: "Last 7 Days" }, + { value: "month", label: "Last 30 Days" }, + { value: "year", label: "Last Year" }, + ], + }, + { + key: "sortBy", + label: "Sort By", + options: [ + { value: "createdAt-desc", label: "Newest First" }, + { value: "createdAt-asc", label: "Oldest First" }, + { value: "date-desc", label: "Meeting Date (Newest)" }, + { value: "date-asc", label: "Meeting Date (Oldest)" }, + { value: "title-asc", label: "Title (A-Z)" }, + { value: "title-desc", label: "Title (Z-A)" }, + ], + }, + ]; + + const hasActiveFilters = + filters.status !== "all" || + filters.meetingType !== "all" || + filters.dateRange !== "all" || + filters.sortBy !== "createdAt-desc"; + + return ( +
+ + + {showDropdown && ( +
+
+

Filter Meetings

+ {hasActiveFilters && ( + + )} +
+ +
+ {filterOptions.map((filter) => ( +
+ + +
+ ))} +
+ + +
+ )} +
+ ); +}; + +export default MeetingFilters; diff --git a/client/src/components/meetings/MeetingRepository.jsx b/client/src/components/meetings/MeetingRepository.jsx new file mode 100644 index 0000000..64a1aa9 --- /dev/null +++ b/client/src/components/meetings/MeetingRepository.jsx @@ -0,0 +1,351 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { toast } from "react-toastify"; +import { Trash2, Download, Edit2, Eye, Search, Filter } from "lucide-react"; +import AppContent from "../../context/AppContent"; +import MeetingCard from "./MeetingCard.jsx"; +import MeetingSearch from "./MeetingSearch.jsx"; +import MeetingFilters from "./MeetingFilters.jsx"; +import Pagination from "./Pagination.jsx"; +import EmptyState from "./EmptyState.jsx"; + +const MeetingRepository = () => { + const { backendUrl } = React.useContext(AppContent); + const [meetings, setMeetings] = useState([]); + const [filteredMeetings, setFilteredMeetings] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [filters, setFilters] = useState({ + status: "all", + meetingType: "all", + dateRange: "all", + sortBy: "createdAt-desc", + }); + + // Pagination + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 9; + + // Fetch meetings + const fetchMeetings = async () => { + setLoading(true); + setError(null); + try { + const response = await axios.get(`${backendUrl}/api/meetings/all`, { + withCredentials: true, + }); + + if (response.data?.success) { + setMeetings(response.data.meetings || []); + } else { + setError("Failed to fetch meetings"); + } + } catch (err) { + console.error("Error fetching meetings:", err); + setError(err.response?.data?.message || "Failed to load meetings"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchMeetings(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Apply filters and search + useEffect(() => { + let filtered = [...meetings]; + + // Apply search + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter( + (meeting) => + meeting.title?.toLowerCase().includes(query) || + meeting.summary?.toLowerCase().includes(query) || + meeting.transcript?.toLowerCase().includes(query) || + meeting.tags?.some((tag) => tag.toLowerCase().includes(query)), + ); + } + + // Apply status filter + if (filters.status !== "all") { + filtered = filtered.filter( + (meeting) => meeting.status === filters.status, + ); + } + + // Apply meeting type filter + if (filters.meetingType !== "all") { + filtered = filtered.filter( + (meeting) => meeting.meetingType === filters.meetingType, + ); + } + + // Apply date range filter + if (filters.dateRange !== "all") { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + filtered = filtered.filter((meeting) => { + const meetingDate = new Date(meeting.date || meeting.createdAt); + + switch (filters.dateRange) { + case "today": + return meetingDate >= today; + case "week": { + const weekAgo = new Date(today); + weekAgo.setDate(weekAgo.getDate() - 7); + return meetingDate >= weekAgo; + } + case "month": { + const monthAgo = new Date(today); + monthAgo.setMonth(monthAgo.getMonth() - 1); + return meetingDate >= monthAgo; + } + case "year": { + const yearAgo = new Date(today); + yearAgo.setFullYear(yearAgo.getFullYear() - 1); + return meetingDate >= yearAgo; + } + default: + return true; + } + }); + } + + // Apply sorting + const [sortBy, sortOrder] = filters.sortBy.split("-"); + filtered.sort((a, b) => { + let comparison = 0; + + switch (sortBy) { + case "createdAt": + comparison = new Date(a.createdAt) - new Date(b.createdAt); + break; + case "date": + comparison = new Date(a.date || a.createdAt) - new Date(b.date || b.createdAt); + break; + case "title": + comparison = a.title.localeCompare(b.title); + break; + default: + comparison = 0; + } + + return sortOrder === "asc" ? comparison : -comparison; + }); + + setFilteredMeetings(filtered); + setCurrentPage(1); // Reset to first page when filters change + }, [meetings, searchQuery, filters]); + + // Pagination logic + const totalPages = Math.ceil(filteredMeetings.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentMeetings = filteredMeetings.slice(startIndex, endIndex); + + // Meeting actions + const handleDelete = async (meetingId) => { + if (!window.confirm("Are you sure you want to delete this meeting?")) { + return; + } + + try { + const response = await axios.delete( + `${backendUrl}/api/meetings/delete/${meetingId}`, + { withCredentials: true }, + ); + + if (response.data?.success) { + toast.success("Meeting deleted successfully"); + fetchMeetings(); + } else { + toast.error(response.data?.message || "Failed to delete meeting"); + } + } catch (err) { + console.error("Error deleting meeting:", err); + toast.error(err.response?.data?.message || "Failed to delete meeting"); + } + }; + + const handleRename = async (meetingId, newTitle) => { + try { + const response = await axios.put( + `${backendUrl}/api/meetings/${meetingId}`, + { title: newTitle }, + { withCredentials: true }, + ); + + if (response.data?.success) { + toast.success("Meeting renamed successfully"); + fetchMeetings(); + } else { + toast.error(response.data?.message || "Failed to rename meeting"); + } + } catch (err) { + console.error("Error renaming meeting:", err); + toast.error(err.response?.data?.message || "Failed to rename meeting"); + } + }; + + const handleDownload = (meeting) => { + // Create a text file with meeting details + const content = ` +Meeting: ${meeting.title} +Date: ${meeting.date ? new Date(meeting.date).toLocaleDateString() : "N/A"} +Type: ${meeting.meetingType || "N/A"} +Status: ${meeting.status || "N/A"} + +Summary: +${meeting.summary || "No summary available"} + +Transcript: +${meeting.transcript || "No transcript available"} + `.trim(); + + const blob = new Blob([content], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${meeting.title.replace(/[^a-z0-9]/gi, "_")}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success("Meeting downloaded"); + }; + + const handleFilterChange = (key, value) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }; + + const handleClearFilters = () => { + setFilters({ + status: "all", + meetingType: "all", + dateRange: "all", + sortBy: "createdAt-desc", + }); + setSearchQuery(""); + }; + + const hasActiveFilters = + filters.status !== "all" || + filters.meetingType !== "all" || + filters.dateRange !== "all" || + searchQuery.trim() !== ""; + + if (loading) { + return ( +
+
+
+

Loading meetings...

+
+
+ ); + } + + if (error) { + return ( +
+
+
⚠️
+

{error}

+ +
+
+ ); + } + + return ( +
+ {/* Search and Filters */} +
+ + +
+ + {/* Active Filters Display */} + {hasActiveFilters && ( +
+ Active filters: + {searchQuery && ( + + Search: "{searchQuery}" + + )} + {filters.status !== "all" && ( + + Status: {filters.status} + + )} + {filters.meetingType !== "all" && ( + + Type: {filters.meetingType} + + )} + {filters.dateRange !== "all" && ( + + Date: {filters.dateRange} + + )} + +
+ )} + + {/* Meeting Grid */} + {currentMeetings.length === 0 ? ( + + ) : ( + <> +
+ {currentMeetings.map((meeting) => ( + + ))} +
+ + {/* Pagination */} + {totalPages > 1 && ( + + )} + + )} +
+ ); +}; + +export default MeetingRepository; diff --git a/client/src/components/meetings/MeetingSearch.jsx b/client/src/components/meetings/MeetingSearch.jsx new file mode 100644 index 0000000..1644591 --- /dev/null +++ b/client/src/components/meetings/MeetingSearch.jsx @@ -0,0 +1,30 @@ +import React from "react"; +import { Search, X } from "lucide-react"; + +const MeetingSearch = ({ searchQuery, onSearchChange }) => { + return ( +
+ + onSearchChange(e.target.value)} + className="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition-all" + /> + {searchQuery && ( + + )} +
+ ); +}; + +export default MeetingSearch; diff --git a/client/src/components/meetings/Pagination.jsx b/client/src/components/meetings/Pagination.jsx new file mode 100644 index 0000000..c0390ac --- /dev/null +++ b/client/src/components/meetings/Pagination.jsx @@ -0,0 +1,68 @@ +import React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +const Pagination = ({ currentPage, totalPages, onPageChange }) => { + if (totalPages <= 1) return null; + + const getPageNumbers = () => { + const pages = []; + const maxVisible = 5; + + if (totalPages <= maxVisible) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + if (currentPage <= 3) { + pages.push(1, 2, 3, 4, "...", totalPages); + } else if (currentPage >= totalPages - 2) { + pages.push(1, "...", totalPages - 3, totalPages - 2, totalPages - 1, totalPages); + } else { + pages.push(1, "...", currentPage - 1, currentPage, currentPage + 1, "...", totalPages); + } + } + + return pages; + }; + + return ( +
+ + + {getPageNumbers().map((page, index) => ( + + {page === "..." ? ( + ... + ) : ( + + )} + + ))} + + +
+ ); +}; + +export default Pagination; diff --git a/client/src/pages/MeetingListPage.jsx b/client/src/pages/MeetingListPage.jsx index 8eb1f5f..699f4d8 100644 --- a/client/src/pages/MeetingListPage.jsx +++ b/client/src/pages/MeetingListPage.jsx @@ -2,7 +2,8 @@ import React, { useContext } from "react"; import { useNavigate } from "react-router-dom"; import AppContent from "../context/AppContent"; import Navbar from "../components/Navbar.jsx"; -import { PlusCircle, Calendar, Video, ClipboardList } from "lucide-react"; +import MeetingRepository from "../components/meetings/MeetingRepository.jsx"; +import { PlusCircle, Calendar } from "lucide-react"; const MeetingListPage = () => { const { userData } = useContext(AppContent); @@ -14,10 +15,10 @@ const MeetingListPage = () => { return (
- {/* ✅ Navbar */} + {/* Navbar */} - {/* ✅ Page Header */} + {/* Page Header */}

Welcome,{" "} @@ -27,17 +28,17 @@ const MeetingListPage = () => { !

- Here are your upcoming and past meetings. + Browse, search, and manage all your uploaded meetings

- {/* ✅ Main Section */} -
+ {/* Main Section */} +
{/* Top Actions */}

- Your Meetings + Meeting Repository

- {/* ✅ Meetings List */} -
- {/* Placeholder cards (replace with actual meeting data later) */} -
-
-
-

- Recorded on Oct 28, 2025 • Duration: 45 min -

-
- -
-
- -

Team Review Meeting

-
-

- Recorded on Oct 25, 2025 • Duration: 1 hr -

-
- -
-
-
-

- Recorded on Oct 20, 2025 • Duration: 30 min -

-
-
- - {/* ✅ Empty State (optional) */} - {/* Uncomment this when dynamic meetings are integrated */} - {/* {meetings.length === 0 && ( -

- You have no meetings scheduled. Click “Create New Meeting” to start one! -

- )} */} + {/* Meeting Repository Component */} +
); diff --git a/server/controllers/meetingController.js b/server/controllers/meetingController.js index 0695c77..31166c2 100644 --- a/server/controllers/meetingController.js +++ b/server/controllers/meetingController.js @@ -610,7 +610,68 @@ export const deleteMeeting = async (req, res) => { }; /* ========================================================= - 6. NEW: VOICE/TEXT SEARCH (searchMeetingsByText) + 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.findById(req.params.id); + if (!meeting) { + return res.status(404).json({ success: false, message: "Meeting not found" }); + } + + // 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" }); + } + + // 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; + if (date) meeting.date = new Date(date); + if (time !== undefined) meeting.time = time; + if (duration !== undefined) meeting.duration = duration; + if (location !== undefined) meeting.location = location; + if (venue !== undefined) meeting.venue = venue; + if (tags) meeting.tags = tags; + + await meeting.save(); + + // Re-index meeting for search + try { + await indexMeeting(meeting); + } catch (idxErr) { + console.error("⚠️ indexMeeting error (continuing):", idxErr.message); + } + + return res.status(200).json({ + success: true, + message: "Meeting updated successfully", + meeting: { + _id: meeting._id, + title: meeting.title, + description: meeting.description, + meetingType: meeting.meetingType, + date: meeting.date, + } + }); + } catch (error) { + console.error("❌ updateMeeting Error:", error.message); + return res.status(500).json({ success: false, message: "Failed to update meeting" }); + } +}; + +/* ========================================================= + 8. NEW: VOICE/TEXT SEARCH (searchMeetingsByText) - Accepts either: { query: "text to search" } or diff --git a/server/middleware/rateLimiter.js b/server/middleware/rateLimiter.js new file mode 100644 index 0000000..ec55bac --- /dev/null +++ b/server/middleware/rateLimiter.js @@ -0,0 +1,37 @@ +import rateLimit from "express-rate-limit"; + +// General rate limiter for API routes +export const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per windowMs + message: { + success: false, + message: "Too many requests from this IP, please try again later.", + }, + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +}); + +// Stricter rate limiter for write operations (create, update, delete) +export const writeLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 30, // Limit each IP to 30 write requests per windowMs + message: { + success: false, + message: "Too many write requests from this IP, please try again later.", + }, + standardHeaders: true, + legacyHeaders: false, +}); + +// Rate limiter for file uploads (stricter due to resource usage) +export const uploadLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // Limit each IP to 10 upload requests per windowMs + message: { + success: false, + message: "Too many upload requests from this IP, please try again later.", + }, + standardHeaders: true, + legacyHeaders: false, +}); diff --git a/server/routes/meetingRoutes.js b/server/routes/meetingRoutes.js index d88473f..7e225a5 100644 --- a/server/routes/meetingRoutes.js +++ b/server/routes/meetingRoutes.js @@ -1,6 +1,7 @@ import express from "express"; import multer from "multer"; import userAuth from "../middleware/userAuth.js"; +import { apiLimiter, writeLimiter, uploadLimiter } from "../middleware/rateLimiter.js"; import { createMeeting, // NEW: Schedule meetings from CreateMeeting form uploadMeeting, // EXISTING: Upload audio and transcribe @@ -8,35 +9,42 @@ import { summarizeMeeting, // EXISTING: Generate AI summary/MOM getAllMeetings, deleteMeeting, // EXISTING: Delete meeting - searchMeetingsByText // 🆕 NEW: Voice/Text Search + searchMeetingsByText, // 🆕 NEW: Voice/Text Search + updateMeeting // 🆕 NEW: Update meeting (rename, etc.) } from "../controllers/meetingController.js"; const router = express.Router(); const upload = multer({ dest: "uploads/" }); // temporary upload directory +// Apply rate limiting to all routes +router.use(apiLimiter); + // ========== EXISTING ROUTES (Working) ========== // ✅ Upload & Transcribe Meeting (from UploadMeetings page) -router.post("/upload", userAuth, upload.single("file"), uploadMeeting); +router.post("/upload", userAuth, uploadLimiter, upload.single("file"), uploadMeeting); // ✅ Summarize Transcript (send meetingId or transcript) -router.post("/summarize", userAuth, summarizeMeeting); +router.post("/summarize", userAuth, writeLimiter, summarizeMeeting); // ✅ Fetch All Meetings (for Summaries Page) router.get("/all", userAuth, getAllMeetings); // ✅ Delete Meeting -router.delete("/delete/:id", userAuth, deleteMeeting); +router.delete("/delete/:id", userAuth, writeLimiter, deleteMeeting); // ========== NEW ROUTES (for CreateMeeting.jsx) ========== // ✅ Create/Schedule Meeting (from CreateMeeting Schedule section) -router.post("/create", userAuth, createMeeting); +router.post("/create", userAuth, writeLimiter, createMeeting); // ✅ Upload Audio for existing meeting (from CreateMeeting Upload section) -router.post("/upload-audio", userAuth, upload.single("audio"), uploadAudioForMeeting); +router.post("/upload-audio", userAuth, uploadLimiter, upload.single("audio"), uploadAudioForMeeting); // 🆕 ✅ Voice/Text Search Route (Frontend: Summaries.jsx or Live Search) router.post("/search", userAuth, searchMeetingsByText); +// 🆕 ✅ Update Meeting Route (Frontend: Meeting Repository - rename, etc.) +router.put("/:id", userAuth, writeLimiter, updateMeeting); + export default router;