+ );
+}
\ No newline at end of file
diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx
new file mode 100644
index 000000000..aac97bff5
--- /dev/null
+++ b/client/src/pages/Home.jsx
@@ -0,0 +1,377 @@
+import { Link, useNavigate } from "react-router-dom";
+import { useState, useEffect } from "react";
+import { getTownSuggestions } from "../services/venueService";
+import { usePageTitle } from "../hooks/usePageTitle";
+import { useAuth } from "../context/AuthContext";
+function Home() {
+ usePageTitle("Home");
+ const navigate = useNavigate();
+ const { user } = useAuth();
+
+ const [district, setDistrict] = useState("");
+ const [town, setTown] = useState("");
+ const [category, setCategory] = useState("");
+ const [eventDate, setEventDate] = useState("");
+ const [listVenueMessage, setListVenueMessage] = useState("");
+ const [locationLoading, setLocationLoading] = useState(false);
+ const [locationError, setLocationError] = useState("");
+ const [townSuggestions, setTownSuggestions] = useState([]);
+ const [showTownSuggestions, setShowTownSuggestions] = useState(false);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadTownSuggestions = async () => {
+ const keyword = town.trim();
+
+ if (keyword.length < 1) {
+ setTownSuggestions([]);
+ setShowTownSuggestions(false);
+ return;
+ }
+
+ try {
+ const result = await getTownSuggestions({
+ district,
+ keyword,
+ });
+
+ if (isMounted) {
+ setTownSuggestions(result.data || []);
+ setShowTownSuggestions((result.data || []).length > 0);
+ }
+ } catch {
+ if (isMounted) {
+ setTownSuggestions([]);
+ setShowTownSuggestions(false);
+ }
+ }
+ };
+
+ loadTownSuggestions();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [district, town]);
+
+ const handleSelectTown = (selectedTown) => {
+ setTown(selectedTown);
+ setTownSuggestions([]);
+ setShowTownSuggestions(false);
+ };
+
+ const getTodayDate = () => {
+ const today = new Date();
+ const timezoneOffset = today.getTimezoneOffset() * 60000;
+ return new Date(today.getTime() - timezoneOffset)
+ .toISOString()
+ .split("T")[0];
+ };
+
+ const handleManualSearch = (e) => {
+ e.preventDefault();
+
+ const queryParams = new URLSearchParams();
+
+ if (district) queryParams.set("district", district);
+ if (town) queryParams.set("town", town);
+ if (category) queryParams.set("category", category);
+ if (eventDate) queryParams.set("eventDate", eventDate);
+
+ navigate(`/venues?${queryParams.toString()}`);
+ };
+
+ const handleUseCurrentLocation = () => {
+ setLocationError("");
+
+ if (!navigator.geolocation) {
+ setLocationError("Geolocation is not supported by your browser.");
+ return;
+ }
+
+ setLocationLoading(true);
+
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const { latitude, longitude } = position.coords;
+
+ const queryParams = new URLSearchParams();
+
+ queryParams.set("lat", latitude);
+ queryParams.set("lng", longitude);
+
+ if (category) queryParams.set("category", category);
+ if (eventDate) queryParams.set("eventDate", eventDate);
+
+ navigate(`/venues/nearby?${queryParams.toString()}`);
+
+ setLocationLoading(false);
+ },
+ () => {
+ setLocationError("Unable to access your location. Use manual search.");
+ setLocationLoading(false);
+ }
+ );
+ };
+
+ const handleListVenue = () => {
+ if (!user) {
+ navigate("/register?role=owner");
+ return;
+ }
+
+ if (user.role === "owner") {
+ navigate("/owner/venues/new");
+ return;
+ }
+
+ if (user.role === "admin") {
+ navigate("/admin/dashboard");
+ return;
+ }
+
+ setListVenueMessage("Please use an owner account to list venues.");
+ };
+
+ return (
+
+ );
+}
+
+export default Home;
diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx
new file mode 100644
index 000000000..010e7f95b
--- /dev/null
+++ b/client/src/pages/Login.jsx
@@ -0,0 +1,163 @@
+import { useState } from "react";
+import { useNavigate, Link } from "react-router-dom";
+import { MapPin } from "lucide-react";
+
+import { useAuth } from "../context/AuthContext";
+import { usePageTitle } from "../hooks/usePageTitle";
+function Login() {
+ usePageTitle("Login");
+ const navigate = useNavigate();
+ const { login, getDashboardPathByRole } = useAuth();
+
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ try {
+ setLoading(true);
+ setError("");
+
+ const result = await login(formData);
+
+ const role = result.user?.role || "user";
+
+ navigate(getDashboardPathByRole(role), {
+ replace: true,
+ });
+ } catch (err) {
+ setError(err.response?.data?.message || "Login failed");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default Login;
diff --git a/client/src/pages/OwnerDashboard.jsx b/client/src/pages/OwnerDashboard.jsx
new file mode 100644
index 000000000..b7e3e59a2
--- /dev/null
+++ b/client/src/pages/OwnerDashboard.jsx
@@ -0,0 +1,642 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import {
+ CheckCircle,
+ XCircle,
+ Clock,
+ Calendar,
+ X,
+ Phone,
+ Mail,
+ Users,
+ MapPin,
+ MessageSquare,
+ Building2,
+ IndianRupee,
+} from "lucide-react";
+
+import {
+ getOwnerBookingInquiries,
+ updateBookingInquiryStatus,
+} from "../services/bookingInquiryService.js";
+
+import { usePageTitle } from "../hooks/usePageTitle.js";
+
+function OwnerDashboard() {
+ usePageTitle("Owner Dashboard")
+ const [bookings, setBookings] = useState([]);
+ const [selectedBooking, setSelectedBooking] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [actionLoadingId, setActionLoadingId] = useState("");
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadBookings = async () => {
+ try {
+ const result = await getOwnerBookingInquiries();
+
+ if (isMounted) {
+ setBookings(result.data || []);
+ }
+ } catch (err) {
+ if (isMounted) {
+ setError(
+ err.response?.data?.message || "Failed to fetch booking inquiries"
+ );
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadBookings();
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ const updateBookingStatus = async (id, status) => {
+ try {
+ setActionLoadingId(id);
+ setError("");
+
+ const result = await updateBookingInquiryStatus(id, status);
+
+ setBookings((prevBookings) =>
+ prevBookings.map((booking) =>
+ booking._id === id ? result.data : booking
+ )
+ );
+
+ setSelectedBooking((prev) =>
+ prev && prev._id === id ? result.data : prev
+ );
+ } catch (err) {
+ setError(
+ err.response?.data?.message || "Failed to update booking status"
+ );
+ } finally {
+ setActionLoadingId("");
+ }
+ };
+
+ const stats = useMemo(() => {
+ const total = bookings.length;
+ const accepted = bookings.filter((b) => b.status === "accepted").length;
+ const pending = bookings.filter((b) => b.status === "pending").length;
+ const rejected = bookings.filter((b) => b.status === "rejected").length;
+
+ return {
+ total,
+ accepted,
+ pending,
+ rejected,
+ };
+ }, [bookings]);
+
+ const formatStatus = (status = "") => {
+ return status.charAt(0).toUpperCase() + status.slice(1);
+ };
+
+ const getStatusClass = (status) => {
+ if (status === "accepted") return "bg-green-100 text-green-700";
+ if (status === "rejected") return "bg-red-100 text-red-700";
+ if (status === "cancelled") return "bg-gray-200 text-gray-700";
+ return "bg-yellow-100 text-yellow-700";
+ };
+
+ const formatPricingModel = (pricingModel = "") => {
+ return pricingModel.replace("per_", "Per ");
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ Owner Dashboard
+
+
+ Manage your venues, bookings and inquiries.
+
+
+
+
+
+ My Venues
+
+
+
+ + Submit Venue
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
Total Inquiries
+
+ {stats.total}
+
+
+
+
+
+
+
+
+
+
+
+
+
Accepted
+
+ {stats.accepted}
+
+
+
+
+
+
+
+
+
+
+
+
+
Pending
+
+ {stats.pending}
+
+
+
+
+
+
+
+
+
+
+
+
+
Rejected
+
+ {stats.rejected}
+
+
+
+
+
+
+
+
+
+
+
+
+ Booking Inquiry Requests
+
+
+ {bookings.length === 0 ? (
+
No booking inquiries found.
+ ) : (
+
+
+
+
+ | Customer |
+ Venue |
+ Event Date |
+ Guests |
+ Status |
+ Action |
+
+
+
+
+ {bookings.map((booking) => (
+
+
+
+
+ {booking.customerName}
+
+
+ {booking.customerPhone}
+
+
+ |
+
+
+
+ 
+
+
+
+ {booking.venue?.name || "Venue unavailable"}
+
+
+ {booking.venue?.town}, {booking.venue?.district}
+
+
+
+ |
+
+ {booking.eventDate} |
+
+ {booking.guestCount} |
+
+
+
+ {formatStatus(booking.status)}
+
+ |
+
+
+
+
+
+ {booking.status === "pending" && (
+ <>
+
+
+
+ >
+ )}
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+ {selectedBooking && (
+
+
setSelectedBooking(null)}
+ />
+
+
+
+
+
+

+
+
+
+
+ {formatStatus(selectedBooking.status)}
+
+
+
+ Booking Inquiry
+
+
+
+ For {selectedBooking.venue?.name || "Venue unavailable"}
+
+
+
+
+
+
+
+
+
+ Customer Details
+
+
+
+
+
Customer Name
+
+ {selectedBooking.customerName}
+
+
+
+
+
Phone Number
+
+
+ {selectedBooking.customerPhone}
+
+
+
+
+
Email
+
+
+ {selectedBooking.customerEmail || "Not provided"}
+
+
+
+
+
Logged-in User
+
+ {selectedBooking.user?.name || "Guest Inquiry"}
+
+
+
+
+
+
+
+ Event Details
+
+
+
+
+
Event Date
+
+
+ {selectedBooking.eventDate}
+
+
+
+
+
Guest Count
+
+
+ {selectedBooking.guestCount} guests
+
+
+
+
+
Message
+
+
+ {selectedBooking.message || "No message provided."}
+
+
+
+
+
+
+
+ Venue Details
+
+
+
+
+
+
+
+
+ {selectedBooking.venue?.name || "Venue unavailable"}
+
+
+
+ {selectedBooking.venue?.description ||
+ "No venue description available."}
+
+
+
+
+ {selectedBooking.venue?.address ||
+ `${selectedBooking.venue?.town}, ${selectedBooking.venue?.district}`}
+
+
+
+
+
+
+ {selectedBooking.venue?.amenities?.length > 0 && (
+
+
+ Venue Amenities
+
+
+
+ {selectedBooking.venue.amenities.map((amenity) => (
+
+ {amenity}
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ Inquiry Summary
+
+
+
+
+ Status:{" "}
+ {formatStatus(selectedBooking.status)}
+
+
+
+ Submitted On:{" "}
+ {new Date(selectedBooking.createdAt).toLocaleDateString()}
+
+
+
+ Last Updated:{" "}
+ {new Date(selectedBooking.updatedAt).toLocaleDateString()}
+
+
+
+
+
+
+ Venue Summary
+
+
+
+
+ Category:{" "}
+ {selectedBooking.venue?.category || "N/A"}
+
+
+
+ Capacity: Up to{" "}
+ {selectedBooking.venue?.capacity || "N/A"} guests
+
+
+
+
+
+ Starting Price:{" "}
+ ₹{selectedBooking.venue?.pricing?.basePrice || "N/A"}
+
+
+
+
+ Pricing Model:{" "}
+ {formatPricingModel(
+ selectedBooking.venue?.pricing?.pricingModel ||
+ "per_day"
+ )}
+
+
+
+
+
+
+ Venue Contact
+
+
+
+
+
+ {selectedBooking.venue?.contactInfo?.phone ||
+ "Not provided"}
+
+
+
+
+ {selectedBooking.venue?.contactInfo?.email ||
+ "Not provided"}
+
+
+
+
+ {selectedBooking.status === "pending" && (
+
+
+
+
+
+ )}
+
+ {selectedBooking.status === "accepted" && (
+
+ This inquiry is accepted. The venue will be treated as
+ booked for this event date.
+
+ )}
+
+ {selectedBooking.status === "rejected" && (
+
+ This inquiry has been rejected.
+
+ )}
+
+
+
+
+ )}
+
+ );
+}
+
+export default OwnerDashboard;
\ No newline at end of file
diff --git a/client/src/pages/OwnerVenueEdit.jsx b/client/src/pages/OwnerVenueEdit.jsx
new file mode 100644
index 000000000..a06efc5f8
--- /dev/null
+++ b/client/src/pages/OwnerVenueEdit.jsx
@@ -0,0 +1,604 @@
+import { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+import {
+ getOwnerVenueById,
+ updateOwnerVenue,
+} from "../services/ownerVenueService";
+
+import { uploadVenueImages } from "../services/uploadService";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+function OwnerVenueEdit() {
+ usePageTitle("Update Venue");
+ const { id } = useParams();
+ const navigate = useNavigate();
+
+ const [formData, setFormData] = useState({
+ name: "",
+ description: "",
+ category: "",
+ district: "",
+ town: "",
+ address: "",
+ latitude: "",
+ longitude: "",
+ capacity: "",
+ basePrice: "",
+ pricingModel: "per_day",
+ amenities: "",
+ phone: "",
+ email: "",
+ website: "",
+ });
+
+ const [existingImages, setExistingImages] = useState([]);
+ const [newImageFiles, setNewImageFiles] = useState([]);
+ const [newImagePreviews, setNewImagePreviews] = useState([]);
+
+ const [loading, setLoading] = useState(true);
+ const [submitLoading, setSubmitLoading] = useState(false);
+ const [uploadingImages, setUploadingImages] = useState(false);
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadVenue = async () => {
+ try {
+ const result = await getOwnerVenueById(id);
+ const venue = result.data;
+
+ if (isMounted) {
+ setFormData({
+ name: venue.name || "",
+ description: venue.description || "",
+ category: venue.category || "",
+ district: venue.district || "",
+ town: venue.town || "",
+ address: venue.address || "",
+ latitude: venue.location?.coordinates?.[1] || "",
+ longitude: venue.location?.coordinates?.[0] || "",
+ capacity: venue.capacity || "",
+ basePrice: venue.pricing?.basePrice || "",
+ pricingModel: venue.pricing?.pricingModel || "per_day",
+ amenities: venue.amenities?.join(", ") || "",
+ phone: venue.contactInfo?.phone || "",
+ email: venue.contactInfo?.email || "",
+ website: venue.contactInfo?.website || "",
+ });
+
+ setExistingImages(venue.images || []);
+ }
+ } catch (err) {
+ if (isMounted) {
+ setError(err.response?.data?.message || "Failed to fetch venue");
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadVenue();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [id]);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const convertCommaTextToArray = (text) => {
+ return text
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean);
+ };
+
+ const handleNewImageChange = (e) => {
+ const files = Array.from(e.target.files);
+
+ if (files.length > 5) {
+ setError("You can upload a maximum of 5 new images.");
+ return;
+ }
+
+ const hasLargeFile = files.some((file) => file.size > 5 * 1024 * 1024);
+
+ if (hasLargeFile) {
+ setError("Each image must be less than 5 MB.");
+ return;
+ }
+
+ setError("");
+ setNewImageFiles(files);
+
+ const previews = files.map((file) => URL.createObjectURL(file));
+ setNewImagePreviews(previews);
+ };
+
+ const removeExistingImage = (imageUrl) => {
+ setExistingImages((prev) => prev.filter((image) => image !== imageUrl));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ try {
+ setSubmitLoading(true);
+ setError("");
+
+ const latitude = Number(formData.latitude);
+ const longitude = Number(formData.longitude);
+ const capacity = Number(formData.capacity);
+ const basePrice = Number(formData.basePrice);
+
+ if (Number.isNaN(latitude) || Number.isNaN(longitude)) {
+ setError("Latitude and longitude must be valid numbers.");
+ return;
+ }
+
+ if (Number.isNaN(capacity) || capacity < 1) {
+ setError("Capacity must be a valid number.");
+ return;
+ }
+
+ if (Number.isNaN(basePrice) || basePrice < 0) {
+ setError("Starting price must be a valid number.");
+ return;
+ }
+
+ let uploadedNewImageUrls = [];
+
+ if (newImageFiles.length > 0) {
+ setUploadingImages(true);
+
+ const uploadResult = await uploadVenueImages(newImageFiles);
+
+ uploadedNewImageUrls = uploadResult.data.map((image) => image.url);
+
+ setUploadingImages(false);
+ }
+
+ const finalImages = [...existingImages, ...uploadedNewImageUrls];
+
+ const venueData = {
+ name: formData.name,
+ description: formData.description,
+ category: formData.category,
+ district: formData.district,
+ town: formData.town,
+ address: formData.address,
+
+ location: {
+ type: "Point",
+ coordinates: [longitude, latitude],
+ },
+
+ capacity,
+
+ pricing: {
+ basePrice,
+ currency: "INR",
+ pricingModel: formData.pricingModel,
+ },
+
+ amenities: convertCommaTextToArray(formData.amenities),
+
+ images: finalImages,
+
+ contactInfo: {
+ phone: formData.phone,
+ email: formData.email,
+ website: formData.website,
+ },
+ };
+
+ await updateOwnerVenue(id, venueData);
+
+ navigate("/owner/venues", {
+ replace: true,
+ });
+ } catch (err) {
+ setError(err.response?.data?.message || "Failed to update venue");
+ } finally {
+ setSubmitLoading(false);
+ setUploadingImages(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
Loading venue details...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ Update Venue
+
+
+
+ After updating, the venue will be sent for admin approval again.
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default OwnerVenueEdit;
\ No newline at end of file
diff --git a/client/src/pages/Register.jsx b/client/src/pages/Register.jsx
new file mode 100644
index 000000000..724ade5b9
--- /dev/null
+++ b/client/src/pages/Register.jsx
@@ -0,0 +1,139 @@
+import { useState } from "react";
+import { Link, useNavigate, useSearchParams } from "react-router-dom";
+
+import { useAuth } from "../context/AuthContext";
+import { usePageTitle } from "../hooks/usePageTitle";
+function Register() {
+ usePageTitle("Register");
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const { register, getDashboardPathByRole } = useAuth();
+ const initialRole = searchParams.get("role") === "owner" ? "owner" : "user";
+
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ password: "",
+ role: initialRole,
+ });
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ try {
+ setLoading(true);
+ setError("");
+
+ const result = await register(formData);
+
+ const role = result.user?.role || formData.role;
+
+ navigate(getDashboardPathByRole(role), {
+ replace: true,
+ });
+ } catch (err) {
+ setError(err.response?.data?.message || "Registration failed");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Create Account
+
+
+
+ Join BookMyVenue and start booking venues
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ Already have an account?{" "}
+
+ Login
+
+
+
+
+ );
+}
+
+export default Register;
diff --git a/client/src/pages/Unauthorized.jsx b/client/src/pages/Unauthorized.jsx
new file mode 100644
index 000000000..b4475d904
--- /dev/null
+++ b/client/src/pages/Unauthorized.jsx
@@ -0,0 +1,11 @@
+function Unauthorized() {
+ return (
+
+
+ Access Denied
+
+
+ );
+}
+
+export default Unauthorized;
\ No newline at end of file
diff --git a/client/src/pages/VenueDetails.jsx b/client/src/pages/VenueDetails.jsx
new file mode 100644
index 000000000..1cca77a2b
--- /dev/null
+++ b/client/src/pages/VenueDetails.jsx
@@ -0,0 +1,203 @@
+import { useParams, useNavigate } from "react-router-dom";
+import { useEffect, useState } from "react";
+import {
+ MapPin,
+ Star,
+ Users,
+ Wifi,
+ Car,
+ Music,
+ Camera,
+ Phone,
+ Mail,
+} from "lucide-react";
+
+import { getVenueById } from "../services/venueService.js";
+import { usePageTitle } from "../hooks/usePageTitle.js";
+
+function VenueDetails() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+
+ const [venue, setVenue] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ usePageTitle(venue ? venue.name : "Venue Details");
+ useEffect(() => {
+ const fetchVenue = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const result = await getVenueById(id);
+
+ setVenue(result.data);
+ } catch (err) {
+ setError(err.response?.data?.message || "Failed to fetch venue");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchVenue();
+ }, [id]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !venue) {
+ return (
+
+
+ {error || "Venue Not Found"}
+
+
+ );
+ }
+
+ const mainImage = venue.images?.[0] || "/placeholder-venue.jpg";
+
+ return (
+
+
+

+
+
+
+
{venue.name}
+
+
+
+
+ {venue.town}, {venue.district}
+
+
+ {venue.rating && (
+
+
+ {venue.rating}
+
+ )}
+
+
+
+
+
+
+
+
+
About Venue
+
+
+ {venue.description}
+
+
+
+
+
Amenities
+
+
+
+
+ {venue.amenities?.map((item) => (
+
+ ✓ {item}
+
+ ))}
+
+
+
+
+
Gallery
+
+
+ {venue.images?.map((img, index) => (
+

+ ))}
+
+
+
+
+
+
+
+ ₹{venue.pricing?.basePrice}
+
+ /{venue.pricing?.pricingModel?.replace("per_", "") || "event"}
+
+
+
+
+
+
+ Capacity: Up to {venue.capacity} Guests
+
+
+ {venue.contactInfo?.phone && (
+
+
+ {venue.contactInfo.phone}
+
+ )}
+
+ {venue.contactInfo?.email && (
+
+
+ {venue.contactInfo.email}
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default VenueDetails;
\ No newline at end of file
diff --git a/client/src/pages/Venues.jsx b/client/src/pages/Venues.jsx
new file mode 100644
index 000000000..3c14438d1
--- /dev/null
+++ b/client/src/pages/Venues.jsx
@@ -0,0 +1,597 @@
+import { useEffect, useState } from "react";
+import {
+ Link,
+ useLocation,
+ useNavigate,
+ useSearchParams,
+} from "react-router-dom";
+
+import { getNearbyVenues, getVenues, getTownSuggestions } from "../services/venueService";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+function VenuePage() {
+ usePageTitle("Explore Venues")
+ const [searchParams, setSearchParams] = useSearchParams();
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ const [venues, setVenues] = useState([]);
+ const [filters, setFilters] = useState({
+ district: "",
+ town: "",
+ category: "",
+ eventDate: "",
+ minCapacity: "",
+ maxPrice: "",
+ });
+
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [locationLoading, setLocationLoading] = useState(false);
+ const [locationError, setLocationError] = useState("");
+ const [townSuggestions, setTownSuggestions] = useState([]);
+ const [showTownSuggestions, setShowTownSuggestions] = useState(false);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadTownSuggestions = async () => {
+ const keyword = filters.town.trim();
+
+ if (keyword.length < 1) {
+ setTownSuggestions([]);
+ setShowTownSuggestions(false);
+ return;
+ }
+
+ try {
+ const result = await getTownSuggestions({
+ district: filters.district,
+ keyword,
+ });
+
+ if (isMounted) {
+ setTownSuggestions(result.data || []);
+ setShowTownSuggestions((result.data || []).length > 0);
+ }
+ } catch {
+ if (isMounted) {
+ setTownSuggestions([]);
+ setShowTownSuggestions(false);
+ }
+ }
+ };
+
+ loadTownSuggestions();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [filters.district, filters.town]);
+
+ const handleSelectTown = (selectedTown) => {
+ setFilters((prev) => ({
+ ...prev,
+ town: selectedTown,
+ }));
+
+ setTownSuggestions([]);
+ setShowTownSuggestions(false);
+ };
+
+ const isNearbySearch =
+ location.pathname.includes("/nearby") &&
+ searchParams.has("lat") &&
+ searchParams.has("lng");
+
+ const getTodayDate = () => {
+ const today = new Date();
+ const timezoneOffset = today.getTimezoneOffset() * 60000;
+
+ return new Date(today.getTime() - timezoneOffset)
+ .toISOString()
+ .split("T")[0];
+ };
+
+ useEffect(() => {
+ setFilters({
+ district: searchParams.get("district") || "",
+ town: searchParams.get("town") || "",
+ category: searchParams.get("category") || "",
+ eventDate: searchParams.get("eventDate") || "",
+ minCapacity: searchParams.get("minCapacity") || "",
+ maxPrice: searchParams.get("maxPrice") || "",
+ });
+ }, [searchParams]);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadVenues = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const queryFilters = Object.fromEntries(searchParams.entries());
+
+ const result = isNearbySearch
+ ? await getNearbyVenues(queryFilters)
+ : await getVenues(queryFilters);
+
+ if (isMounted) {
+ setVenues(result.data || []);
+ }
+ } catch (err) {
+ if (isMounted) {
+ setError(err.response?.data?.message || "Failed to fetch venues");
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadVenues();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [searchParams, isNearbySearch]);
+
+ const handleFilterChange = (e) => {
+ const { name, value } = e.target;
+
+ setFilters((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const buildFilterQueryParams = (includeNearbyCoordinates = false) => {
+ const queryParams = new URLSearchParams();
+
+ if (includeNearbyCoordinates) {
+ const lat = searchParams.get("lat");
+ const lng = searchParams.get("lng");
+
+ if (lat && lng) {
+ queryParams.set("lat", lat);
+ queryParams.set("lng", lng);
+ }
+ }
+
+ Object.entries(filters).forEach(([key, value]) => {
+ if (value) {
+ queryParams.set(key, value);
+ }
+ });
+
+ return queryParams;
+ };
+
+ const handleUseCurrentLocation = () => {
+ setLocationError("");
+
+ if (!navigator.geolocation) {
+ setLocationError("Geolocation is not supported by your browser.");
+ return;
+ }
+
+ setLocationLoading(true);
+
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const { latitude, longitude } = position.coords;
+ const queryParams = buildFilterQueryParams(false);
+
+ queryParams.set("lat", latitude);
+ queryParams.set("lng", longitude);
+
+ navigate(`/venues/nearby?${queryParams.toString()}`);
+ setLocationLoading(false);
+ },
+ () => {
+ setLocationError("Unable to access your location. Use manual filters.");
+ setLocationLoading(false);
+ }
+ );
+ };
+
+ const handleRemoveCurrentLocation = () => {
+ const queryParams = buildFilterQueryParams(false);
+ const queryString = queryParams.toString();
+
+ navigate(queryString ? `/venues?${queryString}` : "/venues");
+ };
+
+ const handleApplyFilters = (e) => {
+ e.preventDefault();
+
+ const queryParams = buildFilterQueryParams(isNearbySearch);
+
+ if (isNearbySearch) {
+ setSearchParams(queryParams);
+ return;
+ }
+
+ const queryString = queryParams.toString();
+
+ navigate(queryString ? `/venues?${queryString}` : "/venues");
+ };
+
+ const handleClearFilters = () => {
+ setFilters({
+ district: "",
+ town: "",
+ category: "",
+ eventDate: "",
+ minCapacity: "",
+ maxPrice: "",
+ });
+
+ navigate("/venues");
+ };
+
+ return (
+
+
+
+
+ Explore Venues
+
+
+
+ Find and compare venues based on location, date, capacity and price.
+
+
+
+
+
+
+
+
+
+
+ {isNearbySearch ? "Nearby Venues" : "Available Venues"}
+
+
+
+ {loading
+ ? "Loading venues..."
+ : `${venues.length} venue${venues.length === 1 ? "" : "s"
+ } found`}
+
+
+
+ {filters.eventDate && (
+
+ Showing availability for {filters.eventDate}
+
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading ? (
+
+ ) : venues.length === 0 ? (
+
+
+ No venues found
+
+
+
+ Try changing your filters or clearing them.
+
+
+
+
+ ) : (
+
+ {venues.map((venue) => (
+
+
+

+
+ {venue.isBooked && (
+
+ Unavailable
+
+ )}
+
+ {venue.distanceKm !== undefined && (
+
+ {venue.distanceKm} km away
+
+ )}
+
+
+
+
+
+
+ {venue.name}
+
+
+
+ {venue.town}, {venue.district}
+
+
+
+
+
+ Category: {venue.category}
+
+
+
+ Starting from ₹{venue.pricing?.basePrice || "N/A"}
+
+
+
+ Capacity: Up to {venue.capacity || "N/A"} Guests
+
+
+ {venue.isBooked ? (
+
+ ) : (
+
+ View Details
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
+
+export default VenuePage;
diff --git a/client/src/pages/ownerVenueCreate.jsx b/client/src/pages/ownerVenueCreate.jsx
new file mode 100644
index 000000000..78ccc2799
--- /dev/null
+++ b/client/src/pages/ownerVenueCreate.jsx
@@ -0,0 +1,496 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { uploadVenueImages } from "../services/uploadService.js";
+import { createOwnerVenue } from "../services/ownerVenueService.js";
+import { usePageTitle } from "../hooks/usePageTitle.js";
+
+function OwnerVenueCreate() {
+ usePageTitle("Submit Venue");
+ const navigate = useNavigate();
+
+ const [formData, setFormData] = useState({
+ name: "",
+ description: "",
+ category: "",
+ district: "",
+ town: "",
+ address: "",
+ latitude: "",
+ longitude: "",
+ capacity: "",
+ basePrice: "",
+ pricingModel: "per_day",
+ amenities: "",
+ phone: "",
+ email: "",
+ website: "",
+ });
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [imageFiles, setImageFiles] = useState([]);
+ const [imagePreviews, setImagePreviews] = useState([]);
+ const [uploadingImages, setUploadingImages] = useState(false);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+ const handleImageChange = (e) => {
+ const files = Array.from(e.target.files);
+
+ if (files.length > 5) {
+ setError("You can upload a maximum of 5 images.");
+ return;
+ }
+
+ const hasLargeFile = files.some((file) => file.size > 5 * 1024 * 1024);
+
+ if (hasLargeFile) {
+ setError("Each image must be less than 5 MB.");
+ return;
+ }
+
+ setError("");
+ setImageFiles(files);
+
+ const previews = files.map((file) => URL.createObjectURL(file));
+ setImagePreviews(previews);
+ };
+
+ const convertCommaTextToArray = (text) => {
+ return text
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean);
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ try {
+ setLoading(true);
+ setUploadingImages(true);
+ setError("");
+
+ const latitude = Number(formData.latitude);
+ const longitude = Number(formData.longitude);
+
+ if (Number.isNaN(latitude) || Number.isNaN(longitude)) {
+ setError("Latitude and longitude must be valid numbers.");
+ return;
+ }
+
+ let uploadedImageUrls = [];
+
+ if (imageFiles.length > 0) {
+ const uploadResult = await uploadVenueImages(imageFiles);
+
+ uploadedImageUrls = uploadResult.data.map((image) => image.url);
+ }
+
+ setUploadingImages(false);
+
+ const venueData = {
+ name: formData.name,
+ description: formData.description,
+ category: formData.category,
+ district: formData.district,
+ town: formData.town,
+ address: formData.address,
+
+ location: {
+ type: "Point",
+ coordinates: [longitude, latitude],
+ },
+
+ capacity: Number(formData.capacity),
+
+ pricing: {
+ basePrice: Number(formData.basePrice),
+ currency: "INR",
+ pricingModel: formData.pricingModel,
+ },
+
+ amenities: convertCommaTextToArray(formData.amenities),
+ images: uploadedImageUrls,
+
+ contactInfo: {
+ phone: formData.phone,
+ email: formData.email,
+ website: formData.website,
+ },
+ };
+
+ await createOwnerVenue(venueData);
+
+ navigate("/owner/venues", {
+ replace: true,
+ });
+ } catch (err) {
+ setError(err.response?.data?.message || "Failed to submit venue");
+ } finally {
+ setLoading(false);
+ setUploadingImages(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Submit Your Venue
+
+
+
+ Add your venue details. Admin approval is required before it becomes public.
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default OwnerVenueCreate;
\ No newline at end of file
diff --git a/client/src/pages/ownerVenues.jsx b/client/src/pages/ownerVenues.jsx
new file mode 100644
index 000000000..55687813a
--- /dev/null
+++ b/client/src/pages/ownerVenues.jsx
@@ -0,0 +1,166 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+
+import { getOwnerVenues } from "../services/ownerVenueService";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+function OwnerVenues() {
+ usePageTitle("My Venues");
+ const [venues, setVenues] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadVenues = async () => {
+ try {
+ const result = await getOwnerVenues();
+
+ if (isMounted) {
+ setVenues(result.data || []);
+ }
+ } catch (err) {
+ if (isMounted) {
+ setError(err.response?.data?.message || "Failed to fetch your venues");
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadVenues();
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ const formatStatus = (status) => {
+ return status.charAt(0).toUpperCase() + status.slice(1);
+ };
+
+ if (loading) {
+ return (
+
+
Loading your venues...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ My Venues
+
+
+ View the venues you submitted and track approval status.
+
+
+
+
+ + Submit New Venue
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {venues.length === 0 ? (
+
+
You have not submitted any venues yet.
+
+
+ Submit Your First Venue
+
+
+ ) : (
+
+ {venues.map((venue) => (
+
+

+
+
+
+
+ {venue.name}
+
+
+
+ {formatStatus(venue.status)}
+
+
+
+
+ {venue.town}, {venue.district}
+
+
+
+ Category: {venue.category}
+
+
+
+ ₹{venue.pricing?.basePrice}
+
+
+
+ Capacity: Up to {venue.capacity} Guests
+
+
+
+
+ Edit
+
+
+ {venue.status === "approved" && (
+
+ View Public
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export default OwnerVenues;
\ No newline at end of file
diff --git a/client/src/routes/index.jsx b/client/src/routes/index.jsx
new file mode 100644
index 000000000..6cb0a90c1
--- /dev/null
+++ b/client/src/routes/index.jsx
@@ -0,0 +1,65 @@
+import { createBrowserRouter } from "react-router-dom";
+import App from "../App";
+import Home from "../pages/Home";
+import Register from "../pages/Register";
+import Login from "../pages/Login";
+import VenuePage from "../pages/Venues";
+import VenueDetails from "../pages/VenueDetails";
+import BookingInquiry from "../pages/BookingInquiry";
+import OwnerDashboard from "../pages/OwnerDashboard";
+import AdminDashboard from "../pages/AdminDashboard";
+import ProtectedRoute from "../components/ProtectedRoute";
+import OwnerVenueCreate from "../pages/ownerVenueCreate";
+import OwnerVenues from "../pages/ownerVenues";
+import OwnerVenueEdit from "../pages/OwnerVenueEdit";
+import CheckStatus from "../pages/CheckStatus";
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element:
,
+ children: [
+ { index: true, element:
},
+ { path: "register", element:
},
+ { path: "login", element:
},
+ { path: "venues", element:
},
+ { path: "venues/nearby", element:
},
+ { path: "venues/:id", element:
},
+ { path: "booking/:venueId", element:
},
+ {path: "check-status", element:
},
+ {
+ element:
,
+ children: [
+ {
+ path: "owner/dashboard",
+ element:
+ },
+ {
+ path: "owner/venues",
+ element:
+
+ },
+ {
+ path: "owner/venues/new",
+ element:
+ },
+ {
+ path: "owner/venues/:id/edit",
+ element:
+ }
+ ]
+ },
+ {
+ element:
,
+ children: [
+ {
+ path: "admin/dashboard",
+ element:
+ }
+ ]
+ }
+ ]
+ }
+])
+
+export default router;
\ No newline at end of file
diff --git a/client/src/services/adminVenueService.js b/client/src/services/adminVenueService.js
new file mode 100644
index 000000000..558c59034
--- /dev/null
+++ b/client/src/services/adminVenueService.js
@@ -0,0 +1,17 @@
+import apiClient from "./apiClient";
+
+export const getAdminVenues = async (filters = {}) => {
+ const response = await apiClient.get("/admin/venues", {
+ params: filters,
+ });
+
+ return response.data;
+};
+
+export const updateVenueApprovalStatus = async (venueId, status) => {
+ const response = await apiClient.patch(`/admin/venues/${venueId}/status`, {
+ status,
+ });
+
+ return response.data;
+};
\ No newline at end of file
diff --git a/client/src/services/apiClient.js b/client/src/services/apiClient.js
new file mode 100644
index 000000000..6c0e9a37d
--- /dev/null
+++ b/client/src/services/apiClient.js
@@ -0,0 +1,37 @@
+import axios from "axios";
+
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api";
+const apiClient = axios.create({
+ baseURL: API_BASE_URL,
+ withCredentials: true,
+});
+
+const refreshClient = axios.create({
+ baseURL: API_BASE_URL,
+ withCredentials: true,
+});
+
+apiClient.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ const originalRequest = error.config;
+
+ const isUnauthorized = error.response?.status === 401;
+ const isRefreshRequest = originalRequest?.url?.includes("/auth/refresh");
+
+ if (isUnauthorized && !originalRequest._retry && !isRefreshRequest) {
+ originalRequest._retry = true;
+
+ try {
+ await refreshClient.post("/auth/refresh");
+ return apiClient(originalRequest);
+ } catch (refreshError) {
+ return Promise.reject(refreshError);
+ }
+ }
+
+ return Promise.reject(error);
+ }
+);
+
+export default apiClient;
\ No newline at end of file
diff --git a/client/src/services/authService.js b/client/src/services/authService.js
new file mode 100644
index 000000000..f4eb6c114
--- /dev/null
+++ b/client/src/services/authService.js
@@ -0,0 +1,38 @@
+import apiClient from "./apiClient";
+
+const extractUser = (responseData) => {
+ return responseData?.data?.user || responseData?.data || responseData?.user || null;
+};
+
+export const registerUser = async (userData) => {
+ const response = await apiClient.post("/auth/register", userData);
+
+ return {
+ ...response.data,
+ user: extractUser(response.data),
+ };
+};
+
+export const loginUser = async (credentials) => {
+ const response = await apiClient.post("/auth/login", credentials);
+
+ return {
+ ...response.data,
+ user: extractUser(response.data),
+ };
+};
+
+export const getCurrentUser = async () => {
+ const response = await apiClient.get("/auth/me");
+
+ return {
+ ...response.data,
+ user: extractUser(response.data),
+ };
+};
+
+export const logoutUser = async () => {
+ const response = await apiClient.post("/auth/logout");
+ return response.data;
+};
+
diff --git a/client/src/services/bookingInquiryService.js b/client/src/services/bookingInquiryService.js
new file mode 100644
index 000000000..95131892a
--- /dev/null
+++ b/client/src/services/bookingInquiryService.js
@@ -0,0 +1,37 @@
+import apiClient from "./apiClient";
+
+export const createBookingInquiry = async (bookingData) => {
+ const response = await apiClient.post("/booking-inquiries", bookingData);
+ return response.data;
+};
+
+export const getOwnerBookingInquiries = async () => {
+ const response = await apiClient.get("/booking-inquiries/owner");
+ return response.data;
+};
+
+export const updateBookingInquiryStatus = async (inquiryId, status) => {
+ const response = await apiClient.patch(`/booking-inquiries/${inquiryId}/status`, {
+ status,
+ });
+
+ return response.data;
+};
+
+export const checkBookingInquiryStatus = async (statusData) => {
+ const response = await apiClient.post(
+ "/booking-inquiries/check-status",
+ statusData
+ );
+
+ return response.data;
+};
+
+export const cancelBookingInquiry = async (cancelData) => {
+ const response = await apiClient.patch(
+ "/booking-inquiries/cancel",
+ cancelData
+ );
+
+ return response.data;
+};
\ No newline at end of file
diff --git a/client/src/services/ownerVenueService.js b/client/src/services/ownerVenueService.js
new file mode 100644
index 000000000..5a5471d51
--- /dev/null
+++ b/client/src/services/ownerVenueService.js
@@ -0,0 +1,21 @@
+import apiClient from "./apiClient";
+
+export const createOwnerVenue = async (venueData) => {
+ const response = await apiClient.post("/owner/venues", venueData);
+ return response.data;
+};
+
+export const getOwnerVenues = async () => {
+ const response = await apiClient.get("/owner/venues");
+ return response.data;
+};
+
+export const getOwnerVenueById = async (venueId) => {
+ const response = await apiClient.get(`/owner/venues/${venueId}`);
+ return response.data;
+};
+
+export const updateOwnerVenue = async (venueId, venueData) => {
+ const response = await apiClient.patch(`/owner/venues/${venueId}`, venueData);
+ return response.data;
+};
\ No newline at end of file
diff --git a/client/src/services/uploadService.js b/client/src/services/uploadService.js
new file mode 100644
index 000000000..b3774d982
--- /dev/null
+++ b/client/src/services/uploadService.js
@@ -0,0 +1,13 @@
+import apiClient from "./apiClient";
+
+export const uploadVenueImages = async (files) => {
+ const formData = new FormData();
+
+ files.forEach((file) => {
+ formData.append("images", file);
+ });
+
+ const response = await apiClient.post("/uploads/venue-images", formData);
+
+ return response.data;
+};
\ No newline at end of file
diff --git a/client/src/services/venueService.js b/client/src/services/venueService.js
new file mode 100644
index 000000000..fd86fc62a
--- /dev/null
+++ b/client/src/services/venueService.js
@@ -0,0 +1,31 @@
+import apiClient from "./apiClient.js";
+
+export const getVenues = async (filters = {}) => {
+ const response = await apiClient.get("/venues", {
+ params: filters,
+ });
+
+ return response.data;
+};
+
+export const getVenueById = async (venueId) => {
+ const response = await apiClient.get(`/venues/${venueId}`);
+
+ return response.data;
+};
+
+export const getNearbyVenues = async (filters = {}) => {
+ const response = await apiClient.get("/venues/nearby", {
+ params: filters,
+ });
+
+ return response.data;
+};
+
+export const getTownSuggestions = async (filters = {}) => {
+ const response = await apiClient.get("/venues/town-suggestions", {
+ params: filters,
+ });
+
+ return response.data;
+};
\ No newline at end of file
diff --git a/client/src/utils/.gitkeep b/client/src/utils/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
new file mode 100644
index 000000000..d8f08734d
--- /dev/null
+++ b/client/tailwind.config.js
@@ -0,0 +1,7 @@
+export default {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/client/vite.config.js b/client/vite.config.js
new file mode 100644
index 000000000..8b0f57b91
--- /dev/null
+++ b/client/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/docs/git-workflow.md b/docs/git-workflow.md
new file mode 100644
index 000000000..acd8b88f6
--- /dev/null
+++ b/docs/git-workflow.md
@@ -0,0 +1,419 @@
+# BookMyVenue Git Workflow Guide
+
+This guide explains the Git workflow every BookMyVenue team member should follow during Phase 1 MVP development.
+
+---
+
+## 1. Branch Strategy
+
+We use three main branch types:
+
+```txt
+main = stable MVP branch
+dev = integration branch
+feature/* = individual task branches
+```
+
+| Branch | Purpose | Direct push rule |
+|---|---|---|
+| `main` | Stable, submission-ready MVP | No direct push |
+| `dev` | Integration branch for tested work | No direct push preferred |
+| `feature/*`, `fix/*`, `docs/*`, `chore/*` | Individual work branches | Members push to their own branches |
+
+All work should be done in a separate branch and merged through a Pull Request.
+
+---
+
+## 2. Starting a New Task
+
+Before creating any new branch, always update your local `dev` branch first:
+
+```bash
+git checkout dev
+git pull origin dev
+git checkout -b feature/your-task-name
+```
+
+Example:
+
+```bash
+git checkout dev
+git pull origin dev
+git checkout -b feature/venue-model
+```
+
+This makes sure your new branch starts from the latest team code.
+
+---
+
+## 3. Daily Work Commands
+
+Check your current branch:
+
+```bash
+git branch
+```
+
+Check changed files:
+
+```bash
+git status
+```
+
+Stage and commit your changes:
+
+```bash
+git add .
+git commit -m "Add venue model"
+```
+
+Push your branch:
+
+```bash
+git push origin feature/venue-model
+```
+
+After pushing, go to GitHub and click Compare & pull request for your branch.
+
+Set the PR direction correctly:
+
+base: dev
+compare: feature/your-feature-name
+
+This means:
+
+feature/your-feature-name → dev
+
+Then add a clear PR title and description, and create the Pull Request.
+
+---
+
+## 4. Branch Naming Rules
+
+Use clear branch names based on the type of work.
+
+### Feature branches
+
+```txt
+feature/backend-setup
+feature/venue-model
+feature/venue-api
+feature/nearby-search
+feature/home-page
+feature/venue-listing-ui
+feature/auth-ui
+feature/owner-dashboard
+feature/admin-dashboard
+```
+
+### Bug fix branches
+
+```txt
+fix/venue-filter-bug
+fix/login-error-handling
+fix/navbar-mobile-layout
+```
+
+### Documentation branches
+
+```txt
+docs/api-contract
+docs/readme-update
+docs/setup-guide
+```
+
+### Chore branches
+
+```txt
+chore/env-example
+chore/deployment-config
+chore/package-cleanup
+```
+
+Avoid vague names:
+
+```txt
+new-branch
+my-work
+changes
+update
+final-code
+```
+
+---
+
+## 5. Commit Message Rules
+
+Write small, meaningful commit messages.
+
+Good examples:
+
+```txt
+Add Express server setup
+Add MongoDB connection config
+Add Venue model
+Add venue listing page
+Connect venue listing to API
+Fix venue card responsive layout
+Update API contract for inquiry endpoints
+```
+
+Bad examples:
+
+```txt
+changes
+done
+final
+updated code
+my work
+bug fix
+```
+
+---
+
+## 6. If You Created a Branch Without Pulling Latest `dev`
+
+### Case 1: No changes made yet
+
+Delete and recreate the branch from latest `dev`:
+
+```bash
+git checkout dev
+git pull origin dev
+git branch -D feature/your-branch-name
+git checkout -b feature/your-branch-name
+```
+
+### Case 2: Changes already made
+
+Commit your work first:
+
+```bash
+git add .
+git commit -m "Work in progress"
+```
+
+Update `dev` and merge it into your feature branch:
+
+```bash
+git checkout dev
+git pull origin dev
+git checkout feature/your-branch-name
+git merge dev
+```
+
+Resolve conflicts if Git asks you to.
+
+---
+
+## 7. Keeping an Existing Feature Branch Updated
+
+If another PR was merged into `dev` while you are still working, update your feature branch before opening your PR:
+
+```bash
+git checkout dev
+git pull origin dev
+git checkout feature/your-branch-name
+git merge dev
+```
+
+Then test your code again.
+
+After testing:
+
+```bash
+git add .
+git commit -m "Merge latest dev into feature branch"
+git push origin feature/your-branch-name
+```
+
+---
+
+## 8. Handling Merge Conflicts
+
+If Git shows conflicts, run:
+
+```bash
+git status
+```
+
+Open the conflicted files and look for conflict markers:
+
+```txt
+<<<<<<< HEAD
+Your branch code
+=======
+Incoming dev code
+>>>>>>> dev
+```
+
+Manually choose the correct code, remove the conflict markers, then run:
+
+```bash
+git add .
+git commit -m "Resolve merge conflict with dev"
+```
+
+---
+
+## 9. Files Rule
+
+Never commit these files. Add these in .gitignore:
+
+```txt
+node_modules/
+.env
+.env.local
+.DS_Store
+dist/
+build/
+```
+
+Commit these when relevant:
+
+```txt
+package.json
+package-lock.json
+.env.example
+README.md
+CONTRIBUTING.md
+docs/api-contract.md
+```
+
+After pulling latest `dev`, run `npm install` only if `package.json` or `package-lock.json` changed in `client` or `server`.
+
+For frontend dependency changes:
+
+```bash
+cd client
+npm install
+```
+
+For backend dependency changes:
+
+```bash
+cd server
+npm install
+```
+
+You do not need to run `npm install` after every pull if dependencies did not change.
+
+---
+
+## 10. Environment Variable Rule
+
+Never commit `.env` to GitHub.
+
+Use `.env.example` to show required variables without exposing real secrets.
+
+Example:
+
+```env
+PORT=5000
+MONGO_URI=your_mongodb_connection_string_here
+JWT_SECRET=your_jwt_secret_here
+CLIENT_URL=http://localhost:5173
+```
+
+Each member should create their own `.env` file locally.
+
+---
+
+## 11. Pull Request Rules
+
+Every PR should explain:
+
+```txt
+What was added
+What was changed
+How it was tested
+Any pending issues
+```
+
+Example PR description:
+
+```md
+## What I did
+- Added Venue model
+- Added default venue status as pending
+
+## Testing
+- Checked model import
+- Server runs without errors
+
+## Notes
+- Seed data will be added in another PR
+```
+
+Open PRs to:
+
+```txt
+your-branch → dev
+```
+
+---
+
+## 12. PR Review Process
+
+1. Member opens PR to `dev`.
+2. Backend Lead / Integration Lead reviews the PR.
+3. Reviewer checks:
+
+```txt
+Does the code match the assigned task?
+Does the app still run?
+Are unrelated files changed?
+Are .env or node_modules committed by mistake?
+Are folder structures preserved?
+Does the API match docs/api-contract.md?
+```
+
+4. If changes are needed, reviewer comments on the PR.
+5. Member updates the same branch and pushes again.
+6. After approval, PR is merged into `dev`.
+7. Everyone pulls latest `dev` before starting new work or updating existing work.
+
+---
+
+## 13. Main Branch Rule
+
+Only stable code should go into `main`.
+
+Use this flow:
+
+```txt
+feature branch → dev → tested → main
+```
+
+Before merging `dev` into `main`, test the full MVP flow:
+
+```txt
+Owner signup/login
+Owner submits venue
+Admin approves venue
+User sees approved venue
+User sends booking inquiry
+Owner accepts/rejects inquiry
+```
+
+---
+
+## 14. Common Mistakes to Avoid
+
+Do not create a feature branch without first updating `dev`.
+
+Do not commit:
+
+```txt
+node_modules
+.env
+```
+Add them in .gitignore
+
+Do not edit another member's files unless discussed.
+
+Do not delete or restructure shared folders without team approval.
+
+Do not keep working for many days without pulling latest `dev`.
+
+Do not open huge PRs with unrelated changes.
diff --git a/docs/phase1-plan.md b/docs/phase1-plan.md
new file mode 100644
index 000000000..ce883b9a6
--- /dev/null
+++ b/docs/phase1-plan.md
@@ -0,0 +1,137 @@
+# BookMyVenue Phase 1 MVP Plan
+
+## MVP Goal
+
+Build a working MVP focused on nearby venue discovery.
+
+The user should be able to:
+- Use current location or manually select district and town/locality
+- View nearby venue listings
+- Filter venues by type, price range, capacity, etc..
+- Open venue details
+- Send a booking inquiry
+
+The owner should be able to:
+- Register/login
+- Submit a venue
+- View booking inquiries
+- Accept or reject inquiries
+
+The admin should be able to:
+- Login using seeded admin account
+- Approve or reject venue listings
+
+## Included in Phase 1
+
+- Nearby venue listings
+- Current location support
+- Manual district/town fallback
+- Venue listing page
+- Venue detail page
+- Booking inquiry form
+- Owner dashboard
+- Admin approval flow
+- Basic role-based auth
+- Deployment
+- README and demo credentials
+
+## Excluded from Phase 1
+
+- Payment
+- Refund
+- Google Maps UI
+- Reviews
+- Chat
+- Advanced analytics
+- Complex availability calendar
+- Email verification
+- Forgot password
+
+## Team Roles
+
+### Backend Lead
+Owner: Jibin
+
+Responsibilities:
+- Backend setup
+- API contract
+- Auth and role middleware
+- Nearby venue API
+- PR review
+- Deployment coordination
+
+### Backend Feature Developer
+Owner: Anirudh
+
+Responsibilities:
+- Venue model
+- Seed venue data
+- Venue APIs
+- Booking inquiry APIs
+- API testing
+
+### Frontend Discovery Developer
+Owner: Amal
+
+Responsibilities:
+- Home page
+- Location search UI
+- Venue listing page
+- Venue card
+- Venue detail page
+
+### Frontend Dashboard Developer
+Owner: Theresrose
+
+Responsibilities:
+- Login/register pages
+- Booking form
+- Owner dashboard
+- Admin dashboard
+
+## Week 1 Goal
+
+Foundation + nearby venue listing.
+
+By the end of Week 1:
+- Repo workflow should be stable
+- Backend server should be running
+- Venue model and seed data should be ready
+- Venue listing UI should be ready
+- Auth UI should be ready
+- GET venues API should work
+- Frontend should show venue data using dummy data or backend API
+
+## Week 2 Goal
+
+Complete the platform loop.
+
+By the end of Week 2:
+- Owner can submit venue
+- Admin can approve/reject venue
+- User can see approved venues
+- User can send booking inquiry
+- Owner can accept/reject inquiry
+
+## Week 3 Goal
+
+Integration, polish, and deployment.
+
+By the end of Week 3:
+- Frontend and backend integrated
+- Core bugs fixed
+- App deployed
+- README completed
+- Demo credentials ready
+- Full MVP flow tested
+
+## Final Submission Checklist
+
+- Deployed frontend link
+- Deployed backend link
+- GitHub repo link
+- Demo credentials
+- README setup instructions
+- Screenshots or demo video
+- Team contribution table
+- Working MVP flow
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..803d43245
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1479 @@
+{
+ "name": "bookmyvenue-backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "bookmyvenue-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "mongoose": "^8.4.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.3"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
+ "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
+ "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bson": {
+ "version": "6.10.4",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
+ "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.20.1"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
+ "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT"
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
+ "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^6.10.4",
+ "mongodb-connection-string-url": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=16.20.1"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.188.0",
+ "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
+ "gcp-metadata": "^5.2.0",
+ "kerberos": "^2.0.1",
+ "mongodb-client-encryption": ">=6.0.0 <7",
+ "snappy": "^7.3.2",
+ "socks": "^2.7.1"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
+ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^11.0.2",
+ "whatwg-url": "^14.1.0 || ^13.0.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "8.24.0",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.0.tgz",
+ "integrity": "sha512-EEZwOibDPZ5uZN3bFapfnRskEbdljAf6sP9ln6u+P4e5IfkOAh6Tqw2g8/Tag++KHOAJ095WXT/c0uqRq4Vckg==",
+ "license": "MIT",
+ "dependencies": {
+ "bson": "^6.10.4",
+ "kareem": "2.6.3",
+ "mongodb": "~6.20.0",
+ "mpath": "0.9.0",
+ "mquery": "5.0.0",
+ "ms": "2.1.3",
+ "sift": "17.1.3"
+ },
+ "engines": {
+ "node": ">=16.20.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/mongoose/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
+ "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4.x"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/mquery/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mquery/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sift": {
+ "version": "17.1.3",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
+ "license": "MIT"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..72ce8e0f8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "bookmyvenue-backend",
+ "version": "1.0.0",
+ "description": "Backend for BookMyVenue - Kerala venue discovery app",
+ "main": "server.js",
+ "scripts": {
+ "start": "npm start --prefix server",
+ "build": "npm install --prefix server && npm install --include=dev --prefix client && npm run build --prefix client",
+ "dev": "npm run dev --prefix server"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "mongoose": "^8.4.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.3"
+ }
+}
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 000000000..dfe987395
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,10 @@
+PORT=your_backend_running_port
+MONGODB_URI=your_mongodb_connection_string
+JWT_ACCESS_SECRET=your_access_token_secret
+JWT_REFRESH_SECRET=your_refresh_token_secret
+ADMIN_NAME=BookMyVenue Admin
+ADMIN_EMAIL=admin@example.com
+ADMIN_PASSWORD=your_admin_password_here
+CLOUDINARY_CLOUD_NAME=
+CLOUDINARY_API_KEY=
+CLOUDINARY_API_SECRET=
\ No newline at end of file
diff --git a/server/.gitkeep b/server/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 000000000..c7d94335a
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,1845 @@
+{
+ "name": "bookmyvenue-server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "bookmyvenue-server",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "bcryptjs": "^3.0.3",
+ "bookmyvenue-backend": "file:..",
+ "cloudinary": "^2.10.0",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^9.6.3",
+ "morgan": "^1.11.0",
+ "multer": "^2.2.0",
+ "streamifier": "^0.1.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
+ },
+ "..": {
+ "name": "bookmyvenue-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "mongoose": "^8.4.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.3"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
+ "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/bcryptjs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bookmyvenue-backend": {
+ "resolved": "..",
+ "link": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cloudinary": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.10.0.tgz",
+ "integrity": "sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.23"
+ },
+ "engines": {
+ "node": ">=9"
+ }
+ },
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-parser": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-parser/node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz",
+ "integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT"
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
+ "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.2.0",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "9.6.3",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.3.tgz",
+ "integrity": "sha512-vI6dTTlQnfMCyyQ5TrvhG0bCRs4dq5e1uFNPtOOWsOhn0fSg8AoIHjfyyCYr8aybyvPs845dRHGxsC3w/fHcBA==",
+ "license": "MIT",
+ "dependencies": {
+ "kareem": "3.3.0",
+ "mongodb": "~7.2",
+ "mpath": "0.9.0",
+ "mquery": "6.0.0",
+ "ms": "2.1.3",
+ "sift": "17.1.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
+ "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.4.1",
+ "on-headers": "~1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/morgan/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/morgan/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
+ "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
+ "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "type-is": "^1.6.18"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/multer/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
+ "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sift": {
+ "version": "17.1.3",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
+ "license": "MIT"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/streamifier": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz",
+ "integrity": "sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 000000000..3b5663d59
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "bookmyvenue-server",
+ "version": "1.0.0",
+ "description": "Backend API for BookMyVenue Phase 1 MVP",
+ "main": "src/server.js",
+ "type": "module",
+ "scripts": {
+ "dev": "nodemon src/server.js",
+ "start": "node src/server.js",
+ "seed:admin": "node src/seed/seedAdmin.js",
+ "seed:venues": "node src/utils/seedVenues.js",
+ "migrate:capacity": "node src/utils/migrateCapacityToNumber.js",
+ "migrate:booking-tracking": "node src/utils/migrateBookingInquiryTrackingCode.js",
+ "migrate:assign-specific-owners": "node src/utils/assignSpecificVenuesToOwners.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "bcryptjs": "^3.0.3",
+ "bookmyvenue-backend": "file:..",
+ "cloudinary": "^2.10.0",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^9.6.3",
+ "morgan": "^1.11.0",
+ "multer": "^2.2.0",
+ "streamifier": "^0.1.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
+}
diff --git a/server/src/app.js b/server/src/app.js
new file mode 100644
index 000000000..e0d667402
--- /dev/null
+++ b/server/src/app.js
@@ -0,0 +1,64 @@
+import express from "express";
+import "dotenv/config";
+import cors from "cors";
+import cookieParser from "cookie-parser";
+import path from "path";
+import { fileURLToPath } from "url";
+import venueRoutes from "./routes/venueRoutes.js"
+import authRoutes from "./routes/authRoutes.js";
+import bookingInquiryRoutes from "./routes/bookingInquiryRoutes.js"
+import ownerVenueRoutes from "./routes/ownerVenueRoutes.js";
+import adminVenueRoutes from "./routes/adminVenueRoutes.js";
+import uploadRoutes from "./routes/uploadRoutes.js";
+const app = express();
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+app.set("trust proxy", 1);
+
+const allowedOrigins = (process.env.CLIENT_URLS || process.env.CLIENT_URL || "http://localhost:5173")
+ .split(",")
+ .map((origin) => origin.trim())
+ .map((origin) => origin.replace(/\/$/, ""))
+ .filter(Boolean);
+
+app.use(cors({
+ origin: (origin, callback) => {
+ if (!origin || allowedOrigins.includes(origin.replace(/\/$/, ""))) {
+ return callback(null, true);
+ }
+
+ return callback(new Error(`Origin ${origin} is not allowed by CORS`));
+ },
+ credentials: true,
+ optionsSuccessStatus: 204
+}));
+
+app.use(express.json());
+app.use(cookieParser());
+
+app.get("/api/health", (req, res) => {
+ res.status(200).json({
+ success: true,
+ message: "BookMyVenue backend is running",
+ });
+});
+
+app.use("/api/auth", authRoutes);
+app.use("/api/venues", venueRoutes);
+app.use("/api/booking-inquiries", bookingInquiryRoutes);
+app.use("/api/owner/venues", ownerVenueRoutes);
+app.use("/api/admin/venues", adminVenueRoutes);
+app.use("/api/uploads", uploadRoutes);
+
+if (process.env.NODE_ENV === "production") {
+ const clientDistPath = path.resolve(__dirname, "../../client/dist");
+
+ app.use(express.static(clientDistPath));
+
+ app.get(/^\/(?!api).*/, (req, res) => {
+ res.sendFile(path.join(clientDistPath, "index.html"));
+ });
+}
+
+export default app;
diff --git a/server/src/config/cloudinary.js b/server/src/config/cloudinary.js
new file mode 100644
index 000000000..35923e10d
--- /dev/null
+++ b/server/src/config/cloudinary.js
@@ -0,0 +1,9 @@
+import { v2 as cloudinary } from "cloudinary";
+
+cloudinary.config({
+ cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
+ api_key: process.env.CLOUDINARY_API_KEY,
+ api_secret: process.env.CLOUDINARY_API_SECRET,
+});
+
+export default cloudinary;
\ No newline at end of file
diff --git a/server/src/config/db.js b/server/src/config/db.js
new file mode 100644
index 000000000..a58c076b5
--- /dev/null
+++ b/server/src/config/db.js
@@ -0,0 +1,14 @@
+import "dotenv/config";
+import mongoose from 'mongoose'
+
+const connectDB = async () => {
+ try {
+ const connect = await mongoose.connect(process.env.MONGODB_URI);
+ console.log("mongoDB connected..")
+ } catch (error) {
+ console.log(`Database connection failed: ${error.message}`);
+ process.exit(1);
+ }
+}
+
+export default connectDB;
\ No newline at end of file
diff --git a/server/src/controllers/adminVenueController.js b/server/src/controllers/adminVenueController.js
new file mode 100644
index 000000000..2168792b8
--- /dev/null
+++ b/server/src/controllers/adminVenueController.js
@@ -0,0 +1,41 @@
+import {
+ getAdminVenuesService,
+ updateVenueApprovalStatusService,
+} from "../services/adminVenueService.js";
+
+export const getAdminVenues = async (req, res) => {
+ try {
+ const venues = await getAdminVenuesService(req.query);
+
+ res.status(200).json({
+ success: true,
+ count: venues.length,
+ data: venues,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch venues",
+ });
+ }
+};
+
+export const updateVenueApprovalStatus = async (req, res) => {
+ try {
+ const venue = await updateVenueApprovalStatusService({
+ venueId: req.params.id,
+ status: req.body.status,
+ });
+
+ res.status(200).json({
+ success: true,
+ message: `Venue ${req.body.status} successfully`,
+ data: venue,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to update venue status",
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/src/controllers/authController.js b/server/src/controllers/authController.js
new file mode 100644
index 000000000..7835ed11d
--- /dev/null
+++ b/server/src/controllers/authController.js
@@ -0,0 +1,111 @@
+import { loginUser, refreshAccessToken, registerUser } from "../services/authService.js";
+
+const isProduction = process.env.NODE_ENV === "production";
+
+const baseCookieOptions = {
+ httpOnly: true,
+ secure: isProduction,
+ sameSite: process.env.COOKIE_SAME_SITE || "lax",
+ path: "/",
+};
+
+const accessTokenCookieOptions = {
+ ...baseCookieOptions,
+ maxAge: 15 * 60 * 1000
+};
+
+const refreshTokenCookieOptions = {
+ ...baseCookieOptions,
+ maxAge: 7 * 24 * 60 * 60 * 1000
+};
+
+const clearCookieOptions = baseCookieOptions;
+
+export const register = async (req, res) => {
+ try{
+ const {user, accessToken, refreshToken} = await registerUser(req.body);
+
+ res.cookie("accessToken", accessToken, accessTokenCookieOptions);
+ res.cookie("refreshToken", refreshToken, refreshTokenCookieOptions);
+
+
+ res.status(201).json({
+ success: true,
+ message: "User Registered Successfully",
+ data: {
+ _id: user.id,
+ name: user.name,
+ email: user.email,
+ role: user.role
+ },
+ });
+
+ } catch (error){
+ res.status(400).json({
+ success: false,
+ message: error.message,
+ });
+ }
+
+}
+
+export const login = async (req, res) => {
+ try {
+ const {user, accessToken, refreshToken} = await loginUser(req.body);
+
+ res.cookie("accessToken", accessToken, accessTokenCookieOptions);
+ res.cookie("refreshToken", refreshToken, refreshTokenCookieOptions);
+
+ res.status(201).json({
+ success: true,
+ message: "User Logged-In Successfully",
+ data: {
+ _id: user.id,
+ name: user.name,
+ email: user.email,
+ role: user.role
+ },
+ });
+
+ } catch (error) {
+ res.status(401).json({
+ success: false,
+ message: error.message,
+ });
+ }
+}
+
+export const refresh = async (req, res) => {
+ try {
+ const refreshToken = req.cookies.refreshToken;
+ const {accessToken} = await refreshAccessToken(refreshToken);
+
+ res.cookie("accessToken", accessToken, accessTokenCookieOptions);
+
+ res.status(201).json({
+ success: true,
+ message: "New access token generated successfully",
+ });
+
+ } catch (error) {
+ res.status(401).json({
+ success: false,
+ message: error.message,
+ });
+ }
+
+}
+
+export const logout = async (req, res) => {
+ res.clearCookie("accessToken", clearCookieOptions);
+ res.clearCookie("refreshToken", clearCookieOptions);
+
+ res.status(200).json({
+ success: true,
+ message: "Logged out successfully",
+ });
+};
+
+export const getMe = (req, res) => {
+ res.status(200).json({user : req.user});
+}
diff --git a/server/src/controllers/bookingInquiryController.js b/server/src/controllers/bookingInquiryController.js
new file mode 100644
index 000000000..5f59d7567
--- /dev/null
+++ b/server/src/controllers/bookingInquiryController.js
@@ -0,0 +1,98 @@
+import {
+ createBookingInquiryService,
+ getOwnerBookingInquiriesService,
+ updateBookingInquiryStatusService,
+ checkBookingInquiryStatusService,
+ cancelBookingInquiryService
+} from "../services/bookingInquiryService.js";
+
+export const createBookingInquiry = async (req, res) => {
+ try {
+ const bookingInquiry = await createBookingInquiryService({
+ ...req.body,
+ userId: req.user?._id || null,
+ });
+
+ res.status(201).json({
+ success: true,
+ message: "Booking inquiry submitted successfully",
+ data: bookingInquiry,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to create booking inquiry",
+ });
+ }
+};
+
+export const getOwnerBookingInquiries = async (req, res) => {
+ try {
+ const inquiries = await getOwnerBookingInquiriesService(req.user._id);
+
+ res.status(200).json({
+ success: true,
+ count: inquiries.length,
+ data: inquiries,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch booking inquiries",
+ });
+ }
+};
+
+export const updateBookingInquiryStatus = async (req, res) => {
+ try {
+ const inquiry = await updateBookingInquiryStatusService({
+ inquiryId: req.params.id,
+ ownerId: req.user._id,
+ status: req.body.status,
+ });
+
+ res.status(200).json({
+ success: true,
+ message: `Booking inquiry ${req.body.status} successfully`,
+ data: inquiry,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to update booking inquiry",
+ });
+ }
+};
+
+export const checkBookingInquiryStatus = async (req, res) => {
+ try {
+ const inquiry = await checkBookingInquiryStatusService(req.body);
+
+ res.status(200).json({
+ success: true,
+ data: inquiry,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to check booking inquiry status",
+ });
+ }
+};
+
+export const cancelBookingInquiry = async (req, res) => {
+ try {
+ const inquiry = await cancelBookingInquiryService(req.body);
+
+ res.status(200).json({
+ success: true,
+ message: "Booking inquiry cancelled successfully",
+ data: inquiry,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to cancel booking inquiry",
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/src/controllers/ownerVenueController.js b/server/src/controllers/ownerVenueController.js
new file mode 100644
index 000000000..9f4bf89ab
--- /dev/null
+++ b/server/src/controllers/ownerVenueController.js
@@ -0,0 +1,83 @@
+import {
+ createOwnerVenueService,
+ getOwnerVenuesService,
+ getOwnerVenueByIdService,
+ updateOwnerVenueService,
+} from "../services/ownerVenueService.js";
+
+export const createOwnerVenue = async (req, res) => {
+ try {
+ const venue = await createOwnerVenueService({
+ ownerId: req.user._id,
+ venueData: req.body,
+ });
+
+ res.status(201).json({
+ success: true,
+ message: "Venue submitted successfully. Waiting for admin approval.",
+ data: venue,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to submit venue",
+ });
+ }
+};
+
+export const getOwnerVenues = async (req, res) => {
+ try {
+ const venues = await getOwnerVenuesService(req.user._id);
+
+ res.status(200).json({
+ success: true,
+ count: venues.length,
+ data: venues,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch owner venues",
+ });
+ }
+};
+
+export const getOwnerVenueById = async (req, res) => {
+ try {
+ const venue = await getOwnerVenueByIdService({
+ ownerId: req.user._id,
+ venueId: req.params.id,
+ });
+
+ res.status(200).json({
+ success: true,
+ data: venue,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch venue",
+ });
+ }
+};
+
+export const updateOwnerVenue = async (req, res) => {
+ try {
+ const venue = await updateOwnerVenueService({
+ ownerId: req.user._id,
+ venueId: req.params.id,
+ venueData: req.body,
+ });
+
+ res.status(200).json({
+ success: true,
+ message: "Venue updated successfully. Waiting for admin approval.",
+ data: venue,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to update venue",
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/src/controllers/uploadController.js b/server/src/controllers/uploadController.js
new file mode 100644
index 000000000..5207ab678
--- /dev/null
+++ b/server/src/controllers/uploadController.js
@@ -0,0 +1,18 @@
+import { uploadVenueImagesService } from "../services/uploadService.js";
+
+export const uploadVenueImages = async (req, res) => {
+ try {
+ const uploadedImages = await uploadVenueImagesService(req.files);
+
+ res.status(201).json({
+ success: true,
+ message: "Images uploaded successfully",
+ data: uploadedImages,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Image upload failed",
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/src/controllers/venueController.js b/server/src/controllers/venueController.js
new file mode 100644
index 000000000..a795df8e2
--- /dev/null
+++ b/server/src/controllers/venueController.js
@@ -0,0 +1,68 @@
+import { getAvailableVenuesService, getNearbyVenuesService, getVenueByIdService, getTownSuggestionsService } from "../services/venueService.js";
+
+export const getVenues = async (req, res) => {
+ try {
+ const venues = await getAvailableVenuesService(req.query);
+
+ res.status(200).json({
+ success: true,
+ count: venues.length,
+ data: venues,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch venues",
+ });
+ }
+};
+
+export const getVenueById = async (req, res) => {
+ try {
+ const venue = await getVenueByIdService(req.params.id);
+
+ res.status(200).json({
+ success: true,
+ data: venue,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch venue",
+ });
+ }
+};
+
+export const getNearbyVenues = async (req, res) => {
+ try {
+ const venues = await getNearbyVenuesService(req.query);
+
+ res.status(200).json({
+ success: true,
+ count: venues.length,
+ data: venues,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch nearby venues",
+ });
+ }
+};
+
+export const getTownSuggestions = async (req, res) => {
+ try {
+ const towns = await getTownSuggestionsService(req.query);
+
+ res.status(200).json({
+ success: true,
+ count: towns.length,
+ data: towns,
+ });
+ } catch (error) {
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || "Failed to fetch town suggestions",
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/src/data/.gitkeep b/server/src/data/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/src/data/venues.js b/server/src/data/venues.js
new file mode 100644
index 000000000..9af10e108
--- /dev/null
+++ b/server/src/data/venues.js
@@ -0,0 +1,376 @@
+const venues = [
+ {
+ name: "Kochi Grand Convention Hall",
+ description:
+ "A spacious convention venue suitable for weddings, receptions, exhibitions, and corporate events.",
+ category: "convention center",
+ district: "Ernakulam",
+ town: "Kochi",
+ address: "MG Road, Kochi, Ernakulam, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.2673, 9.9312], // [longitude, latitude]
+ },
+ capacity: 400,
+ pricing: {
+ basePrice: 85000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["AC", "Parking", "Catering", "Stage", "Projector", "WiFi"],
+ images: ["https://images.unsplash.com/photo-1554118811-1e0d58224f24?auto=format&fit=crop&w=900&q=80"],
+ contactInfo: {
+ phone: "9000000001",
+ email: "kochi.grand@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Trivandrum Royal Auditorium",
+ description:
+ "A well-equipped auditorium for weddings, cultural events, and public gatherings.",
+ category: "auditorium",
+ district: "Thiruvananthapuram",
+ town: "Thiruvananthapuram",
+ address: "Pattom, Thiruvananthapuram, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.9366, 8.5241],
+ },
+ capacity: 600,
+ pricing: {
+ basePrice: 70000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["AC", "Parking", "Stage", "Dressing Room", "Generator"],
+ images: ["https://images.unsplash.com/photo-1511578314322-379afb476865?auto=format&fit=crop&w=900&q=80"],
+ contactInfo: {
+ phone: "9000000002",
+ email: "trivandrum.royal@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Kozhikode Marina Banquet Hall",
+ description:
+ "A modern banquet hall suitable for engagement ceremonies, birthdays, and private functions.",
+ category: "banquet hall",
+ district: "Kozhikode",
+ town: "Kozhikode",
+ address: "Mavoor Road, Kozhikode, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [75.7804, 11.2588],
+ },
+ capacity: 400,
+ pricing: {
+ basePrice: 45000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["AC", "Parking", "Catering", "WiFi"],
+ images: ["https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=900&q=80"],
+ contactInfo: {
+ phone: "9000000003",
+ email: "kozhikode.marina@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Thrissur Heritage Wedding Hall",
+ description:
+ "A traditional wedding hall designed for marriage functions, receptions, and family events.",
+ category: "wedding hall",
+ district: "Thrissur",
+ town: "Thrissur",
+ address: "Swaraj Round, Thrissur, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.2144, 10.5276],
+ },
+ capacity: 700,
+ pricing: {
+ basePrice: 60000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["Parking", "Catering", "Stage", "Dressing Room", "Generator"],
+ images: ["https://images.unsplash.com/photo-1519167758481-83f550bb49b3?auto=format&fit=crop&w=900&q=80"],
+ contactInfo: {
+ phone: "9000000004",
+ email: "thrissur.heritage@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Kottayam Lakeview Resort Venue",
+ description:
+ "A resort-style venue suitable for destination weddings, corporate retreats, and private events.",
+ category: "resort",
+ district: "Kottayam",
+ town: "Kumarakom",
+ address: "Kumarakom, Kottayam, Kerala",
+
+ location: {
+ type: "Point",
+ coordinates: [76.4292, 9.6175],
+ },
+ capacity: 100,
+ pricing: {
+ basePrice: 95000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["Parking", "Catering", "Lake View", "Outdoor Space", "WiFi"],
+ images: ["https://images.unsplash.com/photo-1497366412874-3415097a27e7?auto=format&fit=crop&w=900&q=80"],
+ contactInfo: {
+ phone: "9000000005",
+ email: "kottayam.lakeview@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Alappuzha Backwater Outdoor Venue",
+ description:
+ "An outdoor event space near the backwaters for weddings, receptions, and small gatherings.",
+ category: "outdoor",
+ district: "Alappuzha",
+ town: "Alappuzha",
+ address: "Finishing Point Road, Alappuzha, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.3388, 9.4981],
+ },
+ capacity: 100,
+ pricing: {
+ basePrice: 50000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["Outdoor Space", "Parking", "Catering", "Generator"],
+ images: ["https://images.unsplash.com/photo-1511795409834-ef04bbd61622?auto=format&fit=crop&w=900&q=80"],
+ contactInfo: {
+ phone: "9000000006",
+ email: "alappuzha.backwater@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Kollam Sapphire Community Hall",
+ description:
+ "A budget-friendly community hall suitable for local events, meetings, and small functions.",
+ category: "community hall",
+ district: "Kollam",
+ town: "Kollam",
+ address: "Chinnakada, Kollam, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.6141, 8.8932],
+ },
+ capacity: 200,
+ pricing: {
+ basePrice: 25000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["Parking", "Stage", "Fans", "Generator"],
+ images: ["https://images.unsplash.com/photo-1568530873454-e4103a0b3c71?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"],
+ contactInfo: {
+ phone: "9000000007",
+ email: "kollam.sapphire@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Palakkad Greenfield Party Hall",
+ description:
+ "A compact party hall suitable for birthdays, engagement functions, and family celebrations.",
+ category: "banquet hall",
+ district: "Palakkad",
+ town: "Palakkad",
+ address: "Stadium Bypass Road, Palakkad, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.6548, 10.7867],
+ },
+ capacity: 150,
+ pricing: {
+ basePrice: 35000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["AC", "Parking", "Catering", "WiFi"],
+ images: ["https://plus.unsplash.com/premium_photo-1661907977530-eb64ddbfb88a?q=80&w=1221&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"],
+ contactInfo: {
+ phone: "9000000008",
+ email: "palakkad.greenfield@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Kannur Coastal Rooftop Venue",
+ description:
+ "A rooftop venue suitable for small receptions, private parties, and evening gatherings.",
+ category: "rooftop",
+ district: "Kannur",
+ town: "Kannur",
+ address: "Payyambalam Road, Kannur, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [75.3704, 11.8745],
+ },
+ capacity: 180,
+ pricing: {
+ basePrice: 30000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["Sea View", "Parking", "Lighting", "Catering"],
+ images: ["https://images.unsplash.com/photo-1493246318656-5bfd4cfb29b8?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"],
+ contactInfo: {
+ phone: "9000000009",
+ email: "kannur.coastal@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Malappuram Crescent Wedding Hall",
+ description:
+ "A large wedding hall for marriage functions, receptions, and community gatherings.",
+ category: "wedding hall",
+ district: "Malappuram",
+ town: "Malappuram",
+ address: "Down Hill, Malappuram, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.074, 11.051],
+ },
+ capacity: "750",
+ pricing: {
+ basePrice: 55000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["Parking", "Catering", "Stage", "Dressing Room", "Generator"],
+ images: ["https://images.unsplash.com/photo-1542665952-14513db15293?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"],
+ contactInfo: {
+ phone: "9000000010",
+ email: "malappuram.crescent@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Wayanad Misty Outdoor Venue",
+ description:
+ "An outdoor venue surrounded by greenery, suitable for small destination weddings and retreats.",
+ category: "outdoor",
+ district: "Wayanad",
+ town: "Kalpetta",
+ address: "Kalpetta, Wayanad, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [76.0827, 11.6102],
+ },
+ capacity: 100,
+ pricing: {
+ basePrice: 65000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["Outdoor Space", "Parking", "Mountain View", "Catering"],
+ images: ["https://plus.unsplash.com/premium_photo-1673626579377-8dfda319246b?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"],
+ contactInfo: {
+ phone: "9000000011",
+ email: "wayanad.misty@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+
+ {
+ name: "Kasaragod Pearl Conference Room",
+ description:
+ "A small conference venue suitable for business meetings, workshops, and training sessions.",
+ category: "conference room",
+ district: "Kasaragod",
+ town: "Kasaragod",
+ address: "MG Road, Kasaragod, Kerala",
+ location: {
+ type: "Point",
+ coordinates: [74.99, 12.4996],
+ },
+ capacity: 250,
+ pricing: {
+ basePrice: 15000,
+ currency: "INR",
+ pricingModel: "per_day",
+ },
+ amenities: ["AC", "Projector", "WiFi", "Parking"],
+ images: ["https://images.unsplash.com/photo-1431540015161-0bf868a2d407?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"],
+ contactInfo: {
+ phone: "9000000012",
+ email: "kasaragod.pearl@example.com",
+ website: "",
+ },
+ status: "approved",
+ isActive: true,
+ isSeed: true,
+ owner: null,
+ },
+];
+
+export default venues;
\ No newline at end of file
diff --git a/server/src/middleware/AuthMiddleware.js b/server/src/middleware/AuthMiddleware.js
new file mode 100644
index 000000000..5c417b831
--- /dev/null
+++ b/server/src/middleware/AuthMiddleware.js
@@ -0,0 +1,34 @@
+import User from "../models/User.js";
+import { verifyAccessToken } from "../services/tokenService.js";
+
+export const authenticate = async (req, res, next) => {
+ try {
+ const token = req.cookies.accessToken;
+ if (!token) {
+ return res.status(401).json({
+ success: false,
+ message: "No access token provided",
+ });
+ }
+
+ const decoded = verifyAccessToken(token);
+
+ const user = await User.findById(decoded.id).select("-password");
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ message: "User not found",
+ });
+ }
+
+ req.user = user;
+ next();
+
+ } catch (error) {
+ return res.status(401).json({
+ success: false,
+ message: "Invalid or expired token",
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/src/middleware/UploadMiddleware.js b/server/src/middleware/UploadMiddleware.js
new file mode 100644
index 000000000..b096354b1
--- /dev/null
+++ b/server/src/middleware/UploadMiddleware.js
@@ -0,0 +1,20 @@
+import multer from "multer";
+
+const storage = multer.memoryStorage();
+
+const fileFilter = (req, file, cb) => {
+ if (file.mimetype.startsWith("image/")) {
+ cb(null, true);
+ } else {
+ cb(new Error("Only image files are allowed"), false);
+ }
+};
+
+export const uploadVenueImagesMiddleware = multer({
+ storage,
+ fileFilter,
+ limits: {
+ fileSize: 5 * 1024 * 1024,
+ files: 5,
+ },
+});
\ No newline at end of file
diff --git a/server/src/middleware/roleMiddleware.js b/server/src/middleware/roleMiddleware.js
new file mode 100644
index 000000000..0eaa11f17
--- /dev/null
+++ b/server/src/middleware/roleMiddleware.js
@@ -0,0 +1,19 @@
+export const authorize = (...roles) => {
+ return (req, res, next) => {
+ if (!req.user) {
+ return res.status(401).json({
+ success: false,
+ message: "Not authenticated",
+ });
+ }
+
+ if (!roles.includes(req.user.role)) {
+ return res.status(403).json({
+ success: false,
+ message: "Insufficient permissions",
+ });
+ }
+
+ next();
+ };
+};
\ No newline at end of file
diff --git a/server/src/models/.gitkeep b/server/src/models/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/src/models/BookingInquiry.js b/server/src/models/BookingInquiry.js
new file mode 100644
index 000000000..fadb64261
--- /dev/null
+++ b/server/src/models/BookingInquiry.js
@@ -0,0 +1,102 @@
+import mongoose from "mongoose";
+
+const bookingInquirySchema = new mongoose.Schema(
+ {
+ venue: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Venue",
+ required: [true, "Venue is required"],
+ },
+
+ // without forcing login. If only logged-in users can inquire, make this required.
+ user: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ default: null,
+ },
+
+ // Owner of the venue. Useful for owner dashboard inquiry management.
+ // For seed venues with no owner, this may be null unless you seed a demo owner.
+ owner: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ default: null,
+ },
+
+ customerName: {
+ type: String,
+ required: [true, "Customer name is required"],
+ trim: true,
+ },
+
+ customerPhone: {
+ type: String,
+ required: [true, "Phone number is required"],
+ trim: true,
+ },
+
+ customerEmail: {
+ type: String,
+ trim: true,
+ lowercase: true,
+ },
+
+ // Store as YYYY-MM-DD string to avoid timezone issues in MVP
+ eventDate: {
+ type: String,
+ required: [true, "Event date is required"],
+ validate: {
+ validator: function (value) {
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
+ },
+ message: "Event date must be in YYYY-MM-DD format",
+ },
+ },
+
+ guestCount: {
+ type: Number,
+ required: [true, "Guest count is required"],
+ min: [1, "Guest count must be at least 1"],
+ },
+
+ message: {
+ type: String,
+ trim: true,
+ default: "",
+ },
+ trackingCode: {
+ type: String,
+ trim: true,
+ uppercase: true,
+ unique: true,
+ sparse: true,
+ index: true,
+ },
+ status: {
+ type: String,
+ enum: ["pending", "accepted", "rejected", "cancelled"],
+ default: "pending",
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+// Helps venue availability filtering:
+// find accepted inquiries for a selected date
+bookingInquirySchema.index({ venue: 1, eventDate: 1, status: 1 });
+
+// Prevents double booking:
+// only one accepted inquiry is allowed for the same venue on the same date
+bookingInquirySchema.index(
+ { venue: 1, eventDate: 1 },
+ {
+ unique: true,
+ partialFilterExpression: { status: "accepted" },
+ }
+);
+
+const BookingInquiry = mongoose.model("BookingInquiry", bookingInquirySchema);
+
+export default BookingInquiry;
\ No newline at end of file
diff --git a/server/src/models/User.js b/server/src/models/User.js
new file mode 100644
index 000000000..4abf18436
--- /dev/null
+++ b/server/src/models/User.js
@@ -0,0 +1,33 @@
+import mongoose from "mongoose";
+
+const userSchema = new mongoose.Schema(
+ {
+ name: {
+ type: String,
+ required: true,
+ trim: true
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ lowercase: true,
+ trim: true
+ },
+ password: {
+ type: String,
+ required: true,
+ minLength: 6,
+ select: false
+ },
+ role: {
+ type: String,
+ enum: ['user', 'owner', 'admin'],
+ default: 'user'
+ },
+ },
+ { timestamps: true }
+);
+
+const User = mongoose.model('User', userSchema);
+export default User;
\ No newline at end of file
diff --git a/server/src/models/Venue.js b/server/src/models/Venue.js
new file mode 100644
index 000000000..3a0c76597
--- /dev/null
+++ b/server/src/models/Venue.js
@@ -0,0 +1,150 @@
+import mongoose from "mongoose";
+
+const venueSchema = new mongoose.Schema(
+ {
+ name: {
+ type: String,
+ required: [true, "Venue name is required"],
+ trim: true,
+ },
+
+ description: {
+ type: String,
+ trim: true,
+ },
+
+ category: {
+ type: String,
+ enum: [
+ "banquet hall",
+ "auditorium",
+ "convention center",
+ "outdoor",
+ "rooftop",
+ "resort",
+ "hotel ballroom",
+ "community hall",
+ "wedding hall",
+ "conference room",
+ ],
+ required: [true, "Venue category is required"],
+ },
+
+ district: {
+ type: String,
+ required: [true, "District is required"],
+ trim: true,
+ },
+
+ town: {
+ type: String,
+ required: [true, "Town/Area is required"],
+ trim: true,
+ },
+
+ address: {
+ type: String,
+ required: [true, "Address is required"],
+ trim: true,
+ },
+
+ location: {
+ type: {
+ type: String,
+ enum: ["Point"],
+ default: "Point",
+ },
+ coordinates: {
+ type: [Number],
+ required: [true, "Coordinates are required"],
+ validate: {
+ validator: function (value) {
+ return value.length === 2;
+ },
+ message: "Coordinates must contain longitude and latitude",
+ },
+ },
+ },
+
+ capacity: {
+ type: Number,
+ required: [true, "Capacity is required"],
+ min: [1, "Capacity must be at least 1"],
+ },
+
+ pricing: {
+ basePrice: {
+ type: Number,
+ required: [true, "Base price is required"],
+ min: [0, "Base price cannot be negative"],
+ },
+ currency: {
+ type: String,
+ default: "INR",
+ },
+ pricingModel: {
+ type: String,
+ enum: ["per_day", "per_hour", "per_event"],
+ default: "per_day",
+ },
+ },
+
+ amenities: {
+ type: [String],
+ default: [],
+ },
+
+ images: {
+ type: [String],
+ default: [],
+ },
+
+ contactInfo: {
+ phone: {
+ type: String,
+ trim: true,
+ },
+ email: {
+ type: String,
+ trim: true,
+ lowercase: true,
+ },
+ website: {
+ type: String,
+ trim: true,
+ },
+ },
+
+ status: {
+ type: String,
+ enum: ["pending", "approved", "rejected"],
+ default: "pending",
+ },
+
+ isActive: {
+ type: Boolean,
+ default: true,
+ },
+
+ isSeed: {
+ type: Boolean,
+ default: false,
+ },
+
+ owner: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ default: null,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+venueSchema.index({ location: "2dsphere" });
+venueSchema.index({ name: "text", town: "text", district: "text" });
+
+const Venue = mongoose.model("Venue", venueSchema);
+
+export default Venue;
\ No newline at end of file
diff --git a/server/src/routes/adminVenueRoutes.js b/server/src/routes/adminVenueRoutes.js
new file mode 100644
index 000000000..3e4a9a969
--- /dev/null
+++ b/server/src/routes/adminVenueRoutes.js
@@ -0,0 +1,19 @@
+import express from "express";
+
+import {
+ getAdminVenues,
+ updateVenueApprovalStatus,
+} from "../controllers/adminVenueController.js";
+
+import { authenticate } from "../middleware/AuthMiddleware.js";
+import { authorize } from "../middleware/roleMiddleware.js";
+
+const router = express.Router();
+
+router.use(authenticate);
+router.use(authorize("admin"));
+
+router.get("/", getAdminVenues);
+router.patch("/:id/status", updateVenueApprovalStatus);
+
+export default router;
\ No newline at end of file
diff --git a/server/src/routes/authRoutes.js b/server/src/routes/authRoutes.js
new file mode 100644
index 000000000..c96cf62cc
--- /dev/null
+++ b/server/src/routes/authRoutes.js
@@ -0,0 +1,15 @@
+import express from "express";
+import { register, login, refresh, logout, getMe } from "../controllers/authController.js";
+import { authenticate } from "../middleware/AuthMiddleware.js";
+import { authorize } from "../middleware/roleMiddleware.js";
+
+const router = express.Router();
+
+router.post("/register", register);
+router.post("/login", login);
+router.post("/refresh", refresh);
+router.post("/logout", logout)
+
+router.get("/me", authenticate, getMe);
+
+export default router;
\ No newline at end of file
diff --git a/server/src/routes/bookingInquiryRoutes.js b/server/src/routes/bookingInquiryRoutes.js
new file mode 100644
index 000000000..1726b767b
--- /dev/null
+++ b/server/src/routes/bookingInquiryRoutes.js
@@ -0,0 +1,34 @@
+import express from "express";
+
+import {
+ createBookingInquiry,
+ getOwnerBookingInquiries,
+ updateBookingInquiryStatus,
+ checkBookingInquiryStatus,
+ cancelBookingInquiry
+} from "../controllers/bookingInquiryController.js";
+
+import { authenticate } from "../middleware/AuthMiddleware.js";
+import { authorize } from "../middleware/roleMiddleware.js";
+
+const router = express.Router();
+
+router.post("/", createBookingInquiry);
+router.post("/check-status", checkBookingInquiryStatus);
+router.patch("/cancel", cancelBookingInquiry);
+
+router.get(
+ "/owner",
+ authenticate,
+ authorize("owner"),
+ getOwnerBookingInquiries
+);
+
+router.patch(
+ "/:id/status",
+ authenticate,
+ authorize("owner"),
+ updateBookingInquiryStatus
+);
+
+export default router;
\ No newline at end of file
diff --git a/server/src/routes/ownerVenueRoutes.js b/server/src/routes/ownerVenueRoutes.js
new file mode 100644
index 000000000..65b80cee7
--- /dev/null
+++ b/server/src/routes/ownerVenueRoutes.js
@@ -0,0 +1,23 @@
+import express from "express";
+
+import {
+ createOwnerVenue,
+ getOwnerVenues,
+ getOwnerVenueById,
+ updateOwnerVenue,
+} from "../controllers/ownerVenueController.js";
+
+import { authenticate } from "../middleware/AuthMiddleware.js";
+import { authorize } from "../middleware/roleMiddleware.js";
+
+const router = express.Router();
+
+router.use(authenticate);
+router.use(authorize("owner"));
+
+router.post("/", createOwnerVenue);
+router.get("/", getOwnerVenues);
+router.get("/:id", getOwnerVenueById);
+router.patch("/:id", updateOwnerVenue);
+
+export default router;
\ No newline at end of file
diff --git a/server/src/routes/uploadRoutes.js b/server/src/routes/uploadRoutes.js
new file mode 100644
index 000000000..8eb21b8ed
--- /dev/null
+++ b/server/src/routes/uploadRoutes.js
@@ -0,0 +1,18 @@
+import express from "express";
+
+import { uploadVenueImages } from "../controllers/uploadController.js";
+import { uploadVenueImagesMiddleware } from "../middleware/UploadMiddleware.js";
+import { authenticate } from "../middleware/AuthMiddleware.js";
+import { authorize } from "../middleware/roleMiddleware.js";
+
+const router = express.Router();
+
+router.post(
+ "/venue-images",
+ authenticate,
+ authorize("owner"),
+ uploadVenueImagesMiddleware.array("images", 5),
+ uploadVenueImages
+);
+
+export default router;
\ No newline at end of file
diff --git a/server/src/routes/venueRoutes.js b/server/src/routes/venueRoutes.js
new file mode 100644
index 000000000..e90099927
--- /dev/null
+++ b/server/src/routes/venueRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express';
+import { getVenues, getNearbyVenues, getVenueById, getTownSuggestions } from "../controllers/venueController.js";
+
+const router = express.Router();
+
+
+router.get("/", getVenues);
+router.get("/nearby", getNearbyVenues);
+router.get("/town-suggestions", getTownSuggestions);
+router.get("/:id", getVenueById);
+
+export default router;
\ No newline at end of file
diff --git a/server/src/seed/seedAdmin.js b/server/src/seed/seedAdmin.js
new file mode 100644
index 000000000..03357cb3d
--- /dev/null
+++ b/server/src/seed/seedAdmin.js
@@ -0,0 +1,44 @@
+import dotenv from 'dotenv';
+import connectDB from '../config/db.js';
+import User from '../models/User.js';
+import bcrypt from 'bcryptjs';
+dotenv.config();
+
+const seedAdmin = async () => {
+ try {
+ await connectDB();
+
+ const adminEmail = process.env.ADMIN_EMAIL;
+ const adminPassword = process.env.ADMIN_PASSWORD;
+ const adminName = process.env.ADMIN_NAME || "BookMyVenue Admin";
+
+ if(!adminEmail || !adminPassword){
+ console.error("admin email and password required in .env!");
+ process.exit(1);
+ }
+
+ const adminExist = await User.findOne({adminEmail});
+
+ if(adminExist){
+ console.log("Admin already exists!");
+ process.exit(0);
+ }
+
+ const hashedPassword = await bcrypt.hash(adminPassword, 10);
+
+ const admin = await User.create({
+ name: adminName,
+ email: adminEmail,
+ password: hashedPassword,
+ role: "admin",
+ });
+ console.log("Admin account seeded successfully..");
+ process.exit(0);
+
+ } catch (error) {
+ console.error("Failed to seed admin account!", error.message);
+ process.exit(1);
+ }
+}
+
+seedAdmin();
diff --git a/server/src/server.js b/server/src/server.js
new file mode 100644
index 000000000..3d227a29d
--- /dev/null
+++ b/server/src/server.js
@@ -0,0 +1,11 @@
+import "dotenv/config";
+import app from "./app.js";
+import connectDB from "./config/db.js";
+
+const port = process.env.PORT || 3000;
+
+connectDB();
+
+app.listen(port, () => {
+ console.log(`BookMyVenue backend listening on port ${port}`);
+});
\ No newline at end of file
diff --git a/server/src/services/adminVenueService.js b/server/src/services/adminVenueService.js
new file mode 100644
index 000000000..a6e22bd42
--- /dev/null
+++ b/server/src/services/adminVenueService.js
@@ -0,0 +1,46 @@
+import mongoose from "mongoose";
+import Venue from "../models/Venue.js";
+
+export const getAdminVenuesService = async (query = {}) => {
+ const { status } = query;
+
+ const filter = {};
+
+ if (status && ["pending", "approved", "rejected"].includes(status)) {
+ filter.status = status;
+ }
+
+ const venues = await Venue.find(filter)
+ .populate("owner", "name email")
+ .sort({ createdAt: -1 });
+
+ return venues;
+};
+
+export const updateVenueApprovalStatusService = async ({ venueId, status }) => {
+ if (!mongoose.Types.ObjectId.isValid(venueId)) {
+ const error = new Error("Invalid venue ID");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ if (!["approved", "rejected"].includes(status)) {
+ const error = new Error("Status must be approved or rejected");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const venue = await Venue.findById(venueId);
+
+ if (!venue) {
+ const error = new Error("Venue not found");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ venue.status = status;
+
+ await venue.save();
+
+ return venue;
+};
\ No newline at end of file
diff --git a/server/src/services/authService.js b/server/src/services/authService.js
new file mode 100644
index 000000000..b47a3cf44
--- /dev/null
+++ b/server/src/services/authService.js
@@ -0,0 +1,72 @@
+import User from "../models/User.js";
+import { generateAccessToken, verifyRefreshToken, generateRefreshToken } from "./tokenService.js";
+import bcrypt from 'bcryptjs'
+
+const allowedRoles = ["user", "owner"]
+export const registerUser = async ({ name, email, password, role }) => {
+ if (!allowedRoles.includes(role)) {
+ throw new Error("Invalid role for registration");
+ }
+
+ const userExist = await User.findOne({ email });
+ if (userExist) {
+ throw new Error("User already exists..");
+ }
+
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ const user = await User.create({
+ name,
+ email,
+ password: hashedPassword,
+ role
+ });
+ const accessToken = generateAccessToken(user);
+ const refreshToken = generateRefreshToken(user);
+
+ return {
+ user,
+ accessToken,
+ refreshToken
+ }
+}
+
+export const loginUser = async ({ email, password }) => {
+
+ const user = await User.findOne({ email }).select("+password");
+ if (!user) {
+ throw new Error("User not exist!");
+ }
+ const isMatch = await bcrypt.compare(password, user.password);
+
+ if (!isMatch) {
+ throw new Error("Invalid email or password!");
+ }
+
+ const accessToken = generateAccessToken(user);
+ const refreshToken = generateRefreshToken(user);
+
+ return {
+ user,
+ accessToken,
+ refreshToken
+ }
+}
+
+export const refreshAccessToken = async (refreshToken) => {
+ if (!refreshToken) {
+ throw new Error("Token is missing");
+ }
+
+ const decoded = verifyRefreshToken(refreshToken);
+ const user = await User.findById(decoded.id);
+
+ if (!user) {
+ throw new Error("User not found");
+ }
+ const newAccessToken = generateAccessToken(user);
+
+ return {
+ accessToken: newAccessToken
+ }
+}
\ No newline at end of file
diff --git a/server/src/services/bookingInquiryService.js b/server/src/services/bookingInquiryService.js
new file mode 100644
index 000000000..13b012180
--- /dev/null
+++ b/server/src/services/bookingInquiryService.js
@@ -0,0 +1,222 @@
+import mongoose from "mongoose";
+import BookingInquiry from "../models/BookingInquiry.js";
+import Venue from "../models/Venue.js";
+
+const isValidDateString = (date) => {
+ return /^\d{4}-\d{2}-\d{2}$/.test(date);
+};
+
+const generateTrackingCode = () => {
+ const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase();
+ return `BMV-${randomPart}`;
+};
+
+const isPastDate = (dateString) => {
+ const today = new Date();
+ const todayString = today.toISOString().split("T")[0];
+
+ return dateString < todayString;
+};
+
+export const createBookingInquiryService = async (data) => {
+ const {
+ venueId,
+ customerName,
+ customerPhone,
+ customerEmail,
+ eventDate,
+ guestCount,
+ message,
+ userId = null,
+ } = data;
+
+ if (!mongoose.Types.ObjectId.isValid(venueId)) {
+ const error = new Error("Invalid venue ID");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ if (!eventDate || !isValidDateString(eventDate)) {
+ const error = new Error("Event date must be in YYYY-MM-DD format");
+ error.statusCode = 400;
+ throw error;
+ }
+ if (isPastDate(eventDate)) {
+ const error = new Error("Event date cannot be in the past");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const venue = await Venue.findOne({
+ _id: venueId,
+ status: "approved",
+ isActive: true,
+ });
+
+ if (!venue) {
+ const error = new Error("Venue not found or not available");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ if (Number(guestCount) > venue.capacity) {
+ const error = new Error(
+ `Guest count exceeds venue capacity of ${venue.capacity}`
+ );
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const alreadyBooked = await BookingInquiry.findOne({
+ venue: venue._id,
+ eventDate,
+ status: "accepted",
+ });
+
+ if (alreadyBooked) {
+ const error = new Error("This venue is already booked for the selected date");
+ error.statusCode = 409;
+ throw error;
+ }
+
+ const bookingInquiry = await BookingInquiry.create({
+ venue: venue._id,
+ user: userId,
+ owner: venue.owner || null,
+ trackingCode: generateTrackingCode(),
+ customerName,
+ customerPhone,
+ customerEmail,
+ eventDate,
+ guestCount: Number(guestCount),
+ message,
+ });
+
+ return bookingInquiry;
+};
+
+export const getOwnerBookingInquiriesService = async (ownerId) => {
+ const inquiries = await BookingInquiry.find({
+ owner: ownerId,
+ })
+ .populate("venue")
+ .populate("user", "name email role")
+ .sort({ createdAt: -1 });
+
+ return inquiries;
+};
+
+export const updateBookingInquiryStatusService = async ({
+ inquiryId,
+ ownerId,
+ status,
+}) => {
+ if (!mongoose.Types.ObjectId.isValid(inquiryId)) {
+ const error = new Error("Invalid inquiry ID");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ if (!["accepted", "rejected"].includes(status)) {
+ const error = new Error("Status must be accepted or rejected");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const inquiry = await BookingInquiry.findOne({
+ _id: inquiryId,
+ owner: ownerId,
+ });
+
+ if (!inquiry) {
+ const error = new Error("Booking inquiry not found");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ if (inquiry.status !== "pending") {
+ const error = new Error(
+ "Only pending booking inquiries can be accepted or rejected"
+ );
+ error.statusCode = 400;
+ throw error;
+ }
+
+ if (status === "accepted") {
+ const existingAcceptedInquiry = await BookingInquiry.findOne({
+ _id: { $ne: inquiry._id },
+ venue: inquiry.venue,
+ eventDate: inquiry.eventDate,
+ status: "accepted",
+ });
+
+ if (existingAcceptedInquiry) {
+ const error = new Error("Another inquiry is already accepted for this venue and date");
+ error.statusCode = 409;
+ throw error;
+ }
+ }
+
+ inquiry.status = status;
+
+ await inquiry.save();
+
+ return inquiry;
+};
+
+export const checkBookingInquiryStatusService = async ({
+ trackingCode,
+ customerPhone,
+}) => {
+ if (!trackingCode || !customerPhone) {
+ const error = new Error("Tracking code and phone number are required");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const inquiry = await BookingInquiry.findOne({
+ trackingCode: trackingCode.trim().toUpperCase(),
+ customerPhone: customerPhone.trim(),
+ }).populate("venue", "name town district images");
+
+ if (!inquiry) {
+ const error = new Error("Booking inquiry not found");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ return inquiry;
+};
+
+export const cancelBookingInquiryService = async ({
+ trackingCode,
+ customerPhone,
+}) => {
+ if (!trackingCode || !customerPhone) {
+ const error = new Error("Tracking code and phone number are required");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const inquiry = await BookingInquiry.findOne({
+ trackingCode: trackingCode.trim().toUpperCase(),
+ customerPhone: customerPhone.trim(),
+ }).populate("venue", "name town district images");
+
+ if (!inquiry) {
+ const error = new Error("Booking inquiry not found");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ if (inquiry.status !== "pending") {
+ const error = new Error("Only pending booking inquiries can be cancelled");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ inquiry.status = "cancelled";
+ await inquiry.save();
+
+ return inquiry;
+};
\ No newline at end of file
diff --git a/server/src/services/ownerVenueService.js b/server/src/services/ownerVenueService.js
new file mode 100644
index 000000000..5dcbc31be
--- /dev/null
+++ b/server/src/services/ownerVenueService.js
@@ -0,0 +1,225 @@
+import mongoose from "mongoose";
+import Venue from "../models/Venue.js";
+
+const allowedCategoryValues = [
+ "banquet hall",
+ "auditorium",
+ "convention center",
+ "outdoor",
+ "rooftop",
+ "resort",
+ "hotel ballroom",
+ "community hall",
+ "wedding hall",
+ "conference room",
+];
+
+const allowedPricingModels = ["per_day", "per_hour", "per_event"];
+
+const validateObjectId = (id, message = "Invalid ID") => {
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ const error = new Error(message);
+ error.statusCode = 400;
+ throw error;
+ }
+};
+
+const buildVenueUpdateData = (venueData = {}) => {
+ const updateData = {};
+
+ if (venueData.name !== undefined) {
+ updateData.name = venueData.name;
+ }
+
+ if (venueData.description !== undefined) {
+ updateData.description = venueData.description;
+ }
+
+ if (venueData.category !== undefined) {
+ const category = venueData.category.trim().toLowerCase();
+
+ if (!allowedCategoryValues.includes(category)) {
+ const error = new Error("Invalid venue category");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ updateData.category = category;
+ }
+
+ if (venueData.district !== undefined) {
+ updateData.district = venueData.district;
+ }
+
+ if (venueData.town !== undefined) {
+ updateData.town = venueData.town;
+ }
+
+ if (venueData.address !== undefined) {
+ updateData.address = venueData.address;
+ }
+
+ if (venueData.location !== undefined) {
+ const coordinates = venueData.location?.coordinates;
+
+ if (
+ !Array.isArray(coordinates) ||
+ coordinates.length !== 2 ||
+ Number.isNaN(Number(coordinates[0])) ||
+ Number.isNaN(Number(coordinates[1]))
+ ) {
+ const error = new Error(
+ "Location coordinates must be [longitude, latitude]"
+ );
+ error.statusCode = 400;
+ throw error;
+ }
+
+ updateData.location = {
+ type: "Point",
+ coordinates: [Number(coordinates[0]), Number(coordinates[1])],
+ };
+ }
+
+ if (venueData.capacity !== undefined) {
+ const capacity = Number(venueData.capacity);
+
+ if (Number.isNaN(capacity) || capacity < 1) {
+ const error = new Error("Capacity must be a valid number");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ updateData.capacity = capacity;
+ }
+
+ if (venueData.pricing !== undefined) {
+ const basePrice = Number(venueData.pricing?.basePrice);
+ const pricingModel = venueData.pricing?.pricingModel || "per_day";
+
+ if (Number.isNaN(basePrice) || basePrice < 0) {
+ const error = new Error("Starting price must be a valid number");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ if (!allowedPricingModels.includes(pricingModel)) {
+ const error = new Error("Invalid pricing model");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ updateData.pricing = {
+ basePrice,
+ currency: venueData.pricing?.currency || "INR",
+ pricingModel,
+ };
+ }
+
+ if (venueData.amenities !== undefined) {
+ if (!Array.isArray(venueData.amenities)) {
+ const error = new Error("Amenities must be an array");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ updateData.amenities = venueData.amenities;
+ }
+
+ if (venueData.images !== undefined) {
+ if (!Array.isArray(venueData.images)) {
+ const error = new Error("Images must be an array");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ updateData.images = venueData.images;
+ }
+
+ if (venueData.contactInfo !== undefined) {
+ updateData.contactInfo = {
+ phone: venueData.contactInfo?.phone || "",
+ email: venueData.contactInfo?.email || "",
+ website: venueData.contactInfo?.website || "",
+ };
+ }
+
+ return updateData;
+};
+
+export const createOwnerVenueService = async ({ ownerId, venueData }) => {
+ const venue = await Venue.create({
+ ...venueData,
+ owner: ownerId,
+ status: "pending",
+ isActive: true,
+ isSeed: false,
+ });
+
+ return venue;
+};
+
+export const getOwnerVenuesService = async (ownerId) => {
+ const venues = await Venue.find({
+ owner: ownerId,
+ }).sort({ createdAt: -1 });
+
+ return venues;
+};
+
+export const getOwnerVenueByIdService = async ({ ownerId, venueId }) => {
+ validateObjectId(venueId, "Invalid venue ID");
+
+ const venue = await Venue.findOne({
+ _id: venueId,
+ owner: ownerId,
+ });
+
+ if (!venue) {
+ const error = new Error("Venue not found");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ return venue;
+};
+
+export const updateOwnerVenueService = async ({
+ ownerId,
+ venueId,
+ venueData,
+}) => {
+ validateObjectId(venueId, "Invalid venue ID");
+
+ const existingVenue = await Venue.findOne({
+ _id: venueId,
+ owner: ownerId,
+ });
+
+ if (!existingVenue) {
+ const error = new Error("Venue not found");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ const updateData = buildVenueUpdateData(venueData);
+
+ updateData.status = "pending";
+ updateData.isSeed = false;
+
+ const updatedVenue = await Venue.findOneAndUpdate(
+ {
+ _id: venueId,
+ owner: ownerId,
+ },
+ {
+ $set: updateData,
+ },
+ {
+ new: true,
+ runValidators: true,
+ }
+ );
+
+ return updatedVenue;
+};
\ No newline at end of file
diff --git a/server/src/services/tokenService.js b/server/src/services/tokenService.js
new file mode 100644
index 000000000..c7d8cbed8
--- /dev/null
+++ b/server/src/services/tokenService.js
@@ -0,0 +1,33 @@
+import jwt from "jsonwebtoken";
+
+export const generateAccessToken = (user) => {
+ return jwt.sign({
+ id: user.id,
+ role: user.role
+ },
+ process.env.JWT_ACCESS_SECRET,
+ {
+ expiresIn: "15m"
+ }
+)
+}
+
+export const generateRefreshToken = (user) => {
+ return jwt.sign({
+ id: user.id,
+ role: user.role
+ },
+ process.env.JWT_REFRESH_SECRET,
+ {
+ expiresIn: "7d"
+ }
+)
+}
+
+export const verifyAccessToken = (token) => {
+ return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
+}
+
+export const verifyRefreshToken = (token) => {
+ return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
+}
\ No newline at end of file
diff --git a/server/src/services/uploadService.js b/server/src/services/uploadService.js
new file mode 100644
index 000000000..fd77eacea
--- /dev/null
+++ b/server/src/services/uploadService.js
@@ -0,0 +1,51 @@
+import streamifier from "streamifier";
+import cloudinary from "../config/cloudinary.js";
+
+const uploadBufferToCloudinary = (fileBuffer, folder) => {
+ return new Promise((resolve, reject) => {
+ const uploadStream = cloudinary.uploader.upload_stream(
+ {
+ folder: "bookmyvenue/venue-images",
+ resource_type: "image",
+ transformation: [
+ {
+ width: 1200,
+ height: 800,
+ crop: "limit",
+ quality: "auto",
+ fetch_format: "auto",
+ },
+ ],
+ },
+ (error, result) => {
+ if (error) {
+ reject(error);
+ return;
+ }
+
+ resolve({
+ url: result.secure_url,
+ publicId: result.public_id,
+ });
+ }
+ );
+
+ streamifier.createReadStream(fileBuffer).pipe(uploadStream);
+ });
+};
+
+export const uploadVenueImagesService = async (files = []) => {
+ if (!files.length) {
+ const error = new Error("Please upload at least one image");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const uploadedImages = await Promise.all(
+ files.map((file) =>
+ uploadBufferToCloudinary(file.buffer, "bookmyvenue/venue-images")
+ )
+ );
+
+ return uploadedImages;
+};
\ No newline at end of file
diff --git a/server/src/services/venueService.js b/server/src/services/venueService.js
new file mode 100644
index 000000000..69ad999c7
--- /dev/null
+++ b/server/src/services/venueService.js
@@ -0,0 +1,303 @@
+import mongoose from "mongoose";
+import Venue from "../models/Venue.js";
+import BookingInquiry from "../models/BookingInquiry.js";
+
+/**
+ * Escapes special regex characters from user input.
+ * This prevents user search text from breaking regex queries.
+ */
+const escapeRegex = (value = "") => {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+};
+
+/**
+ * Checks whether eventDate follows YYYY-MM-DD format.
+ */
+const isValidDateString = (date) => {
+ return /^\d{4}-\d{2}-\d{2}$/.test(date);
+};
+
+const isPastDate = (dateString) => {
+ const today = new Date();
+ const todayString = today.toISOString().split("T")[0];
+
+ return dateString < todayString;
+};
+
+/**
+ * Finds venue IDs that are already booked for a selected event date.
+ */
+const getBookedVenueIdsByDate = async (eventDate) => {
+ if (!eventDate) return [];
+
+ if (!isValidDateString(eventDate)) {
+ const error = new Error("eventDate must be in YYYY-MM-DD format");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ if (isPastDate(eventDate)) {
+ const error = new Error("Event date cannot be in the past");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const bookedInquiries = await BookingInquiry.find({
+ eventDate,
+ status: "accepted",
+ }).select("venue");
+
+ return bookedInquiries.map((inquiry) => inquiry.venue);
+};
+
+const addAvailabilityToVenues = async (venues, eventDate) => {
+ if (!eventDate) {
+ return venues;
+ }
+
+ const bookedVenueIds = await getBookedVenueIdsByDate(eventDate);
+
+ const bookedVenueIdSet = new Set(
+ bookedVenueIds.map((id) => id.toString())
+ );
+
+ const venuesWithAvailability = venues.map((venue) => {
+ const venueObject =
+ typeof venue.toObject === "function" ? venue.toObject() : venue;
+
+ const isBooked = bookedVenueIdSet.has(venueObject._id.toString());
+
+ return {
+ ...venueObject,
+ isBooked,
+ isAvailable: !isBooked,
+ };
+ });
+
+ venuesWithAvailability.sort((a, b) => {
+ return Number(a.isBooked) - Number(b.isBooked);
+ });
+
+ return venuesWithAvailability;
+};
+
+/**
+ * Builds the MongoDB filter object for venue listing.
+ *
+ * If no filters are provided, this still returns:
+ * {
+ * status: "approved",
+ * isActive: true
+ * }
+ *
+ * So GET /api/venues returns all approved active venues.
+ */
+
+const buildVenueFilter = async (query = {}) => {
+ const {
+ district,
+ town,
+ category,
+ keyword,
+ eventDate,
+ minCapacity,
+ maxPrice,
+ } = query;
+
+ const filter = {
+ status: "approved",
+ isActive: true,
+ };
+
+ if (district) {
+ filter.district = {
+ $regex: `^${escapeRegex(district)}$`,
+ $options: "i",
+ };
+ }
+
+ if (town) {
+ filter.town = {
+ $regex: escapeRegex(town),
+ $options: "i",
+ };
+ }
+
+ if (category) {
+ filter.category = category;
+ }
+
+ if (minCapacity) {
+ const capacityNumber = Number(minCapacity);
+
+ if (Number.isNaN(capacityNumber)) {
+ const error = new Error("minCapacity must be a number");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ filter.capacity = {
+ $gte: capacityNumber,
+ };
+ }
+
+ if (maxPrice) {
+ const priceNumber = Number(maxPrice);
+
+ if (Number.isNaN(priceNumber)) {
+ const error = new Error("maxPrice must be a number");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ filter["pricing.basePrice"] = {
+ $lte: priceNumber,
+ };
+ }
+
+ if (keyword) {
+ const safeKeyword = escapeRegex(keyword);
+
+ filter.$or = [
+ { name: { $regex: safeKeyword, $options: "i" } },
+ { town: { $regex: safeKeyword, $options: "i" } },
+ { district: { $regex: safeKeyword, $options: "i" } },
+ { address: { $regex: safeKeyword, $options: "i" } },
+ ];
+ }
+
+ return filter;
+};
+
+
+/**
+ * Get all approved venues or filtered venues.
+ */
+export const getAvailableVenuesService = async (query = {}) => {
+ const filter = await buildVenueFilter(query);
+
+ const venues = await Venue.find(filter).sort({ createdAt: -1 });
+
+ return addAvailabilityToVenues(venues, query.eventDate);
+};
+/**
+ * Get one approved active venue by ID.
+ */
+export const getVenueByIdService = async (venueId) => {
+ if (!mongoose.Types.ObjectId.isValid(venueId)) {
+ const error = new Error("Invalid venue ID");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const venue = await Venue.findOne({
+ _id: venueId,
+ status: "approved",
+ isActive: true,
+ });
+
+ if (!venue) {
+ const error = new Error("Venue not found");
+ error.statusCode = 404;
+ throw error;
+ }
+
+ return venue;
+};
+
+/**
+ * Get nearby approved venues using user's current location.
+ *
+ * Frontend sends:
+ * lat = latitude
+ * lng = longitude
+ *
+ * MongoDB needs:
+ * coordinates: [longitude, latitude]
+ */
+export const getNearbyVenuesService = async (query = {}) => {
+ const { lat, lng, maxDistance = 45000 } = query;
+
+ if (!lat || !lng) {
+ const error = new Error("lat and lng are required");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const latitude = Number(lat);
+ const longitude = Number(lng);
+ const distance = Number(maxDistance);
+
+ if (
+ Number.isNaN(latitude) ||
+ Number.isNaN(longitude) ||
+ Number.isNaN(distance)
+ ) {
+ const error = new Error("lat, lng, and maxDistance must be numbers");
+ error.statusCode = 400;
+ throw error;
+ }
+
+ const filter = await buildVenueFilter(query);
+
+ const venues = await Venue.aggregate([
+ {
+ $geoNear: {
+ near: {
+ type: "Point",
+ // MongoDB expects [longitude, latitude], not [latitude, longitude]
+ coordinates: [longitude, latitude],
+ },
+ distanceField: "distanceMeters",
+ maxDistance: distance,
+ spherical: true,
+ query: filter,
+ },
+ },
+ {
+ $addFields: {
+ // Convert meters to km and round to 1 decimal place
+ distanceKm: {
+ $round: [{ $divide: ["$distanceMeters", 1000] }, 1],
+ },
+ },
+ },
+ {
+ $sort: {
+ distanceMeters: 1,
+ },
+ },
+ ]);
+
+ return addAvailabilityToVenues(venues, query.eventDate);
+};
+
+export const getTownSuggestionsService = async (query = {}) => {
+ const { district, keyword } = query;
+
+ const filter = {
+ status: "approved",
+ isActive: true,
+ };
+
+ if (district) {
+ filter.district = {
+ $regex: `^${escapeRegex(district)}$`,
+ $options: "i",
+ };
+ }
+
+ if (keyword) {
+ filter.town = {
+ $regex: escapeRegex(keyword),
+ $options: "i",
+ };
+ }
+
+ const towns = await Venue.distinct("town", filter);
+
+ return towns
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b))
+ .slice(0, 10);
+};
\ No newline at end of file
diff --git a/server/src/utils/.gitkeep b/server/src/utils/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/src/utils/assignSpecificVenuesToOwners.js b/server/src/utils/assignSpecificVenuesToOwners.js
new file mode 100644
index 000000000..9992bc63f
--- /dev/null
+++ b/server/src/utils/assignSpecificVenuesToOwners.js
@@ -0,0 +1,84 @@
+import "dotenv/config";
+
+import connectDB from "../config/db.js";
+import Venue from "../models/Venue.js";
+import User from "../models/User.js";
+import BookingInquiry from "../models/BookingInquiry.js";
+
+const venueOwnerMap = [
+ {
+ ownerEmail: "owner1@example.com",
+ venueNames: ["Kochi Grand Convention Hall","Kozhikode Marina Banquet Hall", "Kottayam Lakeview Resort Venue", "Kollam Sapphire Community Hall", "Kannur Coastal Rooftop Venue", "Kasaragod Pearl Conference Room"],
+ },
+ {
+ ownerEmail: "owner2@example.com",
+ venueNames: ["Trivandrum Royal Auditorium","Thrissur Heritage Wedding Hall", "Alappuzha Backwater Outdoor Venue", "Palakkad Greenfield Party Hall", "Malappuram Crescent Wedding Hall", "Wayanad Misty Outdoor Venue"],
+ },
+];
+
+const assignSpecificVenuesToOwners = async () => {
+ try {
+ await connectDB();
+
+ for (const mapping of venueOwnerMap) {
+ const owner = await User.findOne({
+ email: mapping.ownerEmail.toLowerCase(),
+ role: "owner",
+ });
+
+ if (!owner) {
+ console.log(`Owner not found: ${mapping.ownerEmail}`);
+ continue;
+ }
+
+ const venues = await Venue.find({
+ name: { $in: mapping.venueNames },
+ });
+
+ if (venues.length === 0) {
+ console.log(`No venues found for owner: ${mapping.ownerEmail}`);
+ continue;
+ }
+
+ const venueIds = venues.map((venue) => venue._id);
+
+ const venueResult = await Venue.updateMany(
+ {
+ _id: { $in: venueIds },
+ },
+ {
+ $set: {
+ owner: owner._id,
+ },
+ }
+ );
+
+ const inquiryResult = await BookingInquiry.updateMany(
+ {
+ venue: { $in: venueIds },
+ $or: [
+ { owner: { $exists: false } },
+ { owner: null },
+ ],
+ },
+ {
+ $set: {
+ owner: owner._id,
+ },
+ }
+ );
+
+ console.log(`Assigned venues to ${mapping.ownerEmail}`);
+ console.log(`Modified venues: ${venueResult.modifiedCount}`);
+ console.log(`Modified inquiries: ${inquiryResult.modifiedCount}`);
+ }
+
+ console.log("Specific venue-owner assignment completed");
+ process.exit(0);
+ } catch (error) {
+ console.error("Migration failed:", error);
+ process.exit(1);
+ }
+};
+
+assignSpecificVenuesToOwners();
\ No newline at end of file
diff --git a/server/src/utils/capacityMigrate.js b/server/src/utils/capacityMigrate.js
new file mode 100644
index 000000000..bf3c80ef4
--- /dev/null
+++ b/server/src/utils/capacityMigrate.js
@@ -0,0 +1,35 @@
+import "dotenv/config";
+
+import connectDB from "../config/db.js";
+import Venue from "../models/Venue.js";
+
+const migrateCapacityToNumber = async () => {
+ try {
+ await connectDB();
+
+ const result = await Venue.collection.updateMany(
+ {
+ capacity: { $type: "object" },
+ "capacity.max": { $exists: true },
+ },
+ [
+ {
+ $set: {
+ capacity: "$capacity.max",
+ },
+ },
+ ]
+ );
+
+ console.log("Capacity migration completed");
+ console.log(`Matched documents: ${result.matchedCount}`);
+ console.log(`Modified documents: ${result.modifiedCount}`);
+
+ process.exit(0);
+ } catch (error) {
+ console.error("Capacity migration failed:", error);
+ process.exit(1);
+ }
+};
+
+migrateCapacityToNumber();
\ No newline at end of file
diff --git a/server/src/utils/migrateBookingInquiryTrackingCode.js b/server/src/utils/migrateBookingInquiryTrackingCode.js
new file mode 100644
index 000000000..74588ac9c
--- /dev/null
+++ b/server/src/utils/migrateBookingInquiryTrackingCode.js
@@ -0,0 +1,63 @@
+import "dotenv/config";
+
+import connectDB from "../config/db.js";
+import BookingInquiry from "../models/BookingInquiry.js";
+
+const generateTrackingCode = () => {
+ const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase();
+ return `BMV-${randomPart}`;
+};
+
+const generateUniqueTrackingCode = async () => {
+ let trackingCode;
+ let exists = true;
+
+ while (exists) {
+ trackingCode = generateTrackingCode();
+
+ const existingInquiry = await BookingInquiry.findOne({
+ trackingCode,
+ });
+
+ if (!existingInquiry) {
+ exists = false;
+ }
+ }
+
+ return trackingCode;
+};
+
+const migrateBookingInquiryTrackingCode = async () => {
+ try {
+ await connectDB();
+
+ const inquiriesWithoutTrackingCode = await BookingInquiry.find({
+ $or: [
+ { trackingCode: { $exists: false } },
+ { trackingCode: null },
+ { trackingCode: "" },
+ ],
+ });
+
+ console.log(
+ `Found ${inquiriesWithoutTrackingCode.length} inquiries without tracking code`
+ );
+
+ for (const inquiry of inquiriesWithoutTrackingCode) {
+ inquiry.trackingCode = await generateUniqueTrackingCode();
+ await inquiry.save();
+
+ console.log(
+ `Updated inquiry ${inquiry._id} with ${inquiry.trackingCode}`
+ );
+ }
+
+ console.log("Booking inquiry tracking code migration completed");
+ process.exit(0);
+ } catch (error) {
+ console.error("Migration failed:", error);
+ process.exit(1);
+ }
+};
+
+migrateBookingInquiryTrackingCode();
\ No newline at end of file
diff --git a/server/src/utils/seedVenues.js b/server/src/utils/seedVenues.js
new file mode 100644
index 000000000..a6814cd8e
--- /dev/null
+++ b/server/src/utils/seedVenues.js
@@ -0,0 +1,26 @@
+import dotenv from "dotenv";
+import connectDB from "../config/db.js";
+import Venue from "../models/Venue.js";
+import venues from "../data/venues.js";
+
+dotenv.config();
+
+const seedVenues = async () => {
+ try {
+ await connectDB();
+
+ // Delete only old seed venues, not real owner-submitted venues
+ await Venue.deleteMany({ isSeed: true });
+
+ const insertedVenues = await Venue.insertMany(venues);
+
+ console.log(`${insertedVenues.length} seed venues inserted successfully`);
+
+ process.exit(0);
+ } catch (error) {
+ console.error("Venue seeding failed:", error.message);
+ process.exit(1);
+ }
+};
+
+seedVenues();
\ No newline at end of file