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 (
+
+ );
+};
+
+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 ? (
+
+ ) : (
+
+ {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 */}
- {/* ✅ Main Section */}
-
+ {/* Main Section */}
+
{/* Top Actions */}
- Your Meetings
+ Meeting Repository
- {/* ✅ Meetings List */}
-
- {/* Placeholder cards (replace with actual meeting data later) */}
-
-
-
-
AI Strategy Discussion
-
-
- Recorded on Oct 28, 2025 • Duration: 45 min
-
-
-
-
-
-
-
Team Review Meeting
-
-
- Recorded on Oct 25, 2025 • Duration: 1 hr
-
-
-
-
-
-
-
Faculty Roundtable
-
-
- 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;