diff --git a/server/config/multer.ts b/server/config/multer.ts new file mode 100644 index 0000000..08c0bcc --- /dev/null +++ b/server/config/multer.ts @@ -0,0 +1,11 @@ +import multer from "multer"; + +const storage = multer.memoryStorage(); + +const upload= multer({ + storage, + limits:{fileSize: 5*1024*1024}// 5MB limit + +}) + +export default upload \ No newline at end of file diff --git a/server/controllers/ownerController.ts b/server/controllers/ownerController.ts new file mode 100644 index 0000000..151f0d2 --- /dev/null +++ b/server/controllers/ownerController.ts @@ -0,0 +1,348 @@ +import { Response } from "express"; +import { AuthRequest } from "../middlewares/auth.js"; +import Restaurant from "../models/Restaurants.js"; +import {v2 as cloudinary} from 'cloudinary' +import Booking from "../models/Booking.js"; + +// Helper Function to upload buffer to cloudinary +const uploadToCloudinary = (fileBuffer: Buffer ): Promise<{secure_url: string}>=> { + return new Promise((resolve, reject)=>{ + const stream= cloudinary.uploader.upload_stream({folder: "Quickdine"}, (error,result)=>{ + if(error ) return reject(error ) + if(!result) return reject(new Error("Upload failed")); + resolve({secure_url: result.secure_url}) + }) + stream.end(fileBuffer) + }) + +} + +// ====================================================== +// Get owner's restaurant +// GET /api/owner/restaurant +// ====================================================== +export const getOwnerRestaurant = async ( + req: AuthRequest, + res: Response +): Promise => { + try { + const ownerId = req.user?.id; + + if (!ownerId) { + res.status(401).json({ + success: false, + message: "Unauthorized", + }); + return; + } + + const restaurant = await Restaurant.findOne({ + owner: ownerId, + }); + + if (!restaurant) { + res.status(404).json({ + success: false, + message: "Restaurant not found", + }); + return; + } + + res.status(200).json({ + success: true, + restaurant, + }); + } catch (error: any) { + console.error(error); + + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// ====================================================== +// Create owner's restaurant +// POST /api/owner/restaurant +// ====================================================== +export const createRestaurant = async ( + req: AuthRequest, + res: Response +): Promise => { + try { + const ownerId = req.user?.id; + + if (!ownerId) { + res.status(401).json({ + success: false, + message: "Unauthorized", + }); + return; + } + + // Check if owner already has a restaurant + const existingRestaurant = await Restaurant.findOne({ + owner: ownerId, + }); + + if (existingRestaurant) { + res.status(400).json({ + success: false, + message: "You already own a restaurant.", + }); + return; + } + + const { + name, + description, + cuisine, + priceRange, + location, + address, + image, + chef, + tags, + availableSlots, + totalSeats, + } = req.body; + + const slug = name + .toLowerCase() + .trim() + .replace(/\s+/g, "-"); + // handle image + let imageUrl =""; + if(req.file){ + const result = await uploadToCloudinary(req.file.buffer); + imageUrl = result.secure_url; + + } + + // setup parsed tags and slots + const parsedTags = typeof tags === "string" ? tags.split(",").map((t)=> t.trim()) : tags || []; + const parsedSlots = typeof availableSlots === "string" ? availableSlots.split(",").map((s)=> s.trim()) : availableSlots || ["17:00", "18:00","19:00", "20:00", "21:00"]; + + const restaurant = await Restaurant.create({ + name, + slug, + description, + cuisine, + priceRange, + location, + address, + image: imageUrl, + chef, + tags: parsedTags, + availableSlots: parsedSlots, + totalSeats, + owner: ownerId, + featured: false, + exclusive: false, + rating: 0, + reviewCount: 0, + status: "pending", + }); + + res.status(201).json({ + success: true, + message: "Restaurant submitted successfully. Waiting for admin approval.", + restaurant, + }); + } catch (error: any) { + console.error(error); + + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// ====================================================== +// Update owner's restaurant +// PUT /api/owner/restaurant +// ====================================================== +export const updateOwnerRestaurant = async ( + req: AuthRequest, + res: Response +): Promise => { + try { + const restaurant = await Restaurant.findOne({ + owner: req.user?.id, + }); + + if (!restaurant) { + res.status(404).json({ + success: false, + message: "Restaurant profile not found", + }); + return; + } + + const { + name, + description, + cuisine, + priceRange, + location, + address, + chef, + tags, + availableSlots, + totalSeats, + } = req.body; + + // Upload image if provided + let imageUrl = restaurant.image; + + if (req.file) { + const result = await uploadToCloudinary(req.file.buffer); + imageUrl = result.secure_url; + } + + // Parse tags and slots + const parsedTags = + typeof tags === "string" + ? tags.split(",").map((t: string) => t.trim()) + : tags; + + const parsedSlots = + typeof availableSlots === "string" + ? availableSlots.split(",").map((s: string) => s.trim()) + : availableSlots; + + // Update fields + if (name) { + restaurant.name = name; + restaurant.slug = name + .toLowerCase() + .trim() + .replace(/\s+/g, "-"); + } + + if (description) restaurant.description = description; + if (cuisine) restaurant.cuisine = cuisine; + if (priceRange) restaurant.priceRange = priceRange; + if (location) restaurant.location = location; + if (address) restaurant.address = address; + if (chef) restaurant.chef = chef; + if (totalSeats) restaurant.totalSeats = totalSeats; + + if (parsedTags) restaurant.tags = parsedTags; + if (parsedSlots) restaurant.availableSlots = parsedSlots; + if(tags){ + restaurant.tags = typeof tags === "string" ? tags.split(",").map((t)=> t.trim()): tags; + } + if(availableSlots){ + restaurant.availableSlots = typeof availableSlots === "string" ? availableSlots.split(",").map((s)=> s.trim()): availableSlots; + } + + + // Handle new image upload if any + + // Handle new image upload if any + + + if (req.file) { + const result = await uploadToCloudinary(req.file.buffer); + imageUrl = result.secure_url; + } + + const updated = await restaurant.save() + res.json(updated); + + restaurant.image = imageUrl; + + await restaurant.save(); + + res.status(200).json({ + success: true, + message: "Restaurant updated successfully", + restaurant, + }); + } catch (error: any) { + console.error(error); + + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; +// ====================================================== +// Get bookings for owner's restaurants +// GET /api/owner/bookings +// ====================================================== +export const getOwnerBookings = async ( + req: AuthRequest, + res: Response +): Promise => { + try { + const restaurant = await Restaurant.findOne({ + owner: req.user?.id, + }); + + if (!restaurant) { + res.status(404).json({ + message: "Restaurant profile not found", + }); + return; + } + + const bookings = await Booking.find({ + restaurant: restaurant.id, + }) + .populate("user", "name email phone") + .sort({ + bookingDate: -1, + timeSlot: -1, + }); + + res.status(200).json({ + success: true, + bookings, + }); +} catch (error: any) { + console.error(error); + + res.status(500).json({ + success: false, + message: error.message, + }); +} +} + +// Update status of a booking +// PUT / api / owner/booking/:id?status +export const updateBookingStatus = async (req: AuthRequest, res: Response ): Promise => { + try { + const { status }= req.body; + if(!status|| !["confirmed", "cancelled", "completed"].includes(status)){ + res.status(400).json({message: "Please enter a valid booking status "}); + return + } + const booking = await Booking.findById(req.params.id) + if(!booking){ + res.status(404).json({message: "Booking not found"}); + return + } + // Verify booking bleongs to the owner's restaurants + const restaurant = await Restaurant.findById(booking.restaurant) + if(!restaurant|| restaurant.owner.toString() !== req.user?._id.toString()){ + res.status(404).json({message: "Not authorized to manage this booking "}); + return + } + booking.status = status ; + await booking.save(); + res.json(booking); + + } + catch (error: any) { + console.error(error); + res.status(400).json({message: error.message}); + + + } +} \ No newline at end of file diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index c382069..0573665 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts @@ -1,74 +1,89 @@ import { Request, Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; +import { HydratedDocument } from "mongoose"; import { User, IUser } from "../models/User.js"; + + + + export interface AuthRequest extends Request { - user?: IUser; + user?: HydratedDocument; + file?: any; } export const protect = async ( - req: AuthRequest, - res: Response, - next: NextFunction + req: AuthRequest, + res: Response, + next: NextFunction, ): Promise => { + try { + const authHeader = req.headers.authorization; - let token; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + res.status(401).json({ + success: false, + message: "Not authorized, no token", + }); + return; + } - if ( - req.headers.authorization && - req.headers.authorization.startsWith("Bearer") - ) { - try { + const token = authHeader.split(" ")[1]; - token = req.headers.authorization.split(" ")[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { + id: string; + }; - const decoded = jwt.verify( - token, - process.env.JWT_SECRET! - ) as { id: string }; + const user = await User.findById(decoded.id).select("-password"); - const user = await User.findById(decoded.id).select("-password"); + if (!user) { + res.status(401).json({ + success: false, + message: "User not found", + }); + return; + } - if (!user) { - res.status(401).json({ - message: "Not authorized, user not found" - }); - return; - } + req.user = user; + next(); + } catch (error) { + console.error(error); - req.user = user; + res.status(401).json({ + success: false, + message: "Invalid token", + }); + } +}; - next(); +export const adminOnly = ( + req: AuthRequest, + res: Response, + next: NextFunction, +): void => { + if (req.user?.role === "admin") { + next(); + return; + } - } catch (error) { - console.error("Auth Middleware Error:", error); + res.status(403).json({ + success: false, + message: "Access denied. Admin only.", + }); +}; - res.status(401).json({ - message: "Not authorized, token failed" - }); - return; - } - } +export const ownerOnly = ( + req: AuthRequest, + res: Response, + next: NextFunction, +): void => { + if (req.user && (req.user.role === "owner" || req.user.role === "admin")) { + next(); + return; + } - if (!token) { - res.status(401).json({ - message: "Not authorized, no token" - }); - return; - } -} -export const adminOnly = (req: AuthRequest, res: Response, next: NextFunction ): void=> { - if(req.user &&req.user.role==="admin"){ - next() - }else{ - res.status(403).json({message:" Access denied, admin role required"}); - } -} - -export const ownerOnly = (req: AuthRequest, res: Response, next: NextFunction ): void=> { - if(req.user &&(req.user.role==="owner"|| req.user.role==="admin")){ - next() - }else{ - res.status(403).json({message:" Access denied, admin role required"}); - } -} \ No newline at end of file + res.status(403).json({ + success: false, + message: "Access denied. Owner only.", + }); +}; diff --git a/server/models/User.ts b/server/models/User.ts index 7b4e9f3..a249fab 100644 --- a/server/models/User.ts +++ b/server/models/User.ts @@ -1,33 +1,58 @@ -import {Document, model, Schema } from "mongoose" - -export interface IUser extends Document{ - name: string; - email: string; - password?: string; - phone?: string; - role: "user" | "admin" | "owner"; - createdAt: Date; - updatedAt: Date; +import { Document, Schema, model } from "mongoose"; + +export interface IUser { + name: string; + email: string; + password: string; + phone?: string; + role: "user" | "owner" | "admin"; + createdAt?: Date; + updatedAt?: Date; } const UserSchema = new Schema( - { - name: {type: String, required: true, trim: true}, - email: {type: String, required: true, unique: true, trim: true, lowercase: true}, - password: {type: String, required: true, minlength: 6}, - phone: {type: String, trim: true, minlength: 6 }, - role: {type: String, enum: ["user" , "admin" , "owner"], default: "user"}, - + { + 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, }, - {timestamps: true} - -) -// Remove password when converting to JSON + phone: { + type: String, + trim: true, + }, + + role: { + type: String, + enum: ["user", "owner", "admin"], + default: "user", + }, + }, + { + timestamps: true, + } +); + UserSchema.set("toJSON", { - transform: (doc, ret)=>{ - delete ret.password; - return ret; - } -}) -export const User = model("User", UserSchema) \ No newline at end of file + transform: (_doc, ret: any) => { + delete ret.password; + return ret; + }, +}); + +export const User = model("User", UserSchema); \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 1c11442..66e4a8c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,17 +10,20 @@ "license": "ISC", "dependencies": { "bcrypt": "^6.0.0", + "cloudinary": "^2.10.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", - "mongoose": "^9.7.4" + "mongoose": "^8.19.1", + "multer": "^2.2.0" }, "devDependencies": { "@types/bcrypt": "^6.0.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.2.0", "@types/node": "^26.1.1", "nodemon": "^3.1.14", "ts-node": "^10.9.2", @@ -489,11 +492,6 @@ "sparse-bitfield": "^3.0.3" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" - }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -600,6 +598,15 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true }, + "node_modules/@types/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "26.1.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", @@ -646,9 +653,9 @@ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, "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==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", "dependencies": { "@types/webidl-conversions": "*" } @@ -1022,6 +1029,11 @@ "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==" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1122,11 +1134,11 @@ } }, "node_modules/bson": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", - "integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", "engines": { - "node": ">=20.19.0" + "node": ">=16.20.1" } }, "node_modules/buffer-equal-constant-time": { @@ -1134,6 +1146,22 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "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==" + }, + "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", @@ -1193,6 +1221,31 @@ "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==", + "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" + ], + "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", @@ -1752,13 +1805,18 @@ } }, "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==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", "engines": { - "node": ">=18.0.0" + "node": ">=12.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==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1871,25 +1929,25 @@ } }, "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==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "dependencies": { "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.2.0", - "mongodb-connection-string-url": "^7.0.0" + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" }, "engines": { - "node": ">=20.19.0" + "node": ">=16.20.1" }, "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", + "@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.8.6" + "socks": "^2.7.1" }, "peerDependenciesMeta": { "@aws-sdk/credential-providers": { @@ -1916,32 +1974,29 @@ } }, "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==", + "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==", "dependencies": { - "@types/whatwg-url": "^13.0.0", - "whatwg-url": "^14.1.0" - }, - "engines": { - "node": ">=20.19.0" + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.7.4.tgz", - "integrity": "sha512-nuSYGUWWzNd4EAbGYxE469wPTL+kmxb5+91YvCvMkJ08rvNRht/usZUU3LuFuk7rDutF2QWBZHPHuzM8TxXApA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", + "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "kareem": "3.3.0", - "mongodb": "~7.2", + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", "mpath": "0.9.0", - "mquery": "6.0.0", + "mquery": "5.0.0", "ms": "2.1.3", "sift": "17.1.3" }, "engines": { - "node": ">=20.19.0" + "node": ">=16.20.1" }, "funding": { "type": "opencollective", @@ -1957,11 +2012,14 @@ } }, "node_modules/mquery": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", - "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, "engines": { - "node": ">=20.19.0" + "node": ">=14.0.0" } }, "node_modules/ms": { @@ -1969,6 +2027,63 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "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==", + "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==", + "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==", + "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==", + "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", @@ -2166,6 +2281,19 @@ "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==", + "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", @@ -2377,6 +2505,22 @@ "node": ">= 0.8" } }, + "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==", + "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", @@ -2519,6 +2663,11 @@ "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==" + }, "node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", @@ -2573,6 +2722,11 @@ "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==" + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/server/package.json b/server/package.json index e91ced6..671d303 100644 --- a/server/package.json +++ b/server/package.json @@ -15,17 +15,20 @@ "type": "module", "dependencies": { "bcrypt": "^6.0.0", + "cloudinary": "^2.10.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", - "mongoose": "^9.7.4" + "mongoose": "^8.19.1", + "multer": "^2.2.0" }, "devDependencies": { "@types/bcrypt": "^6.0.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.2.0", "@types/node": "^26.1.1", "nodemon": "^3.1.14", "ts-node": "^10.9.2", diff --git a/server/routes/booking.routes.ts b/server/routes/bookingRoutes.ts similarity index 100% rename from server/routes/booking.routes.ts rename to server/routes/bookingRoutes.ts diff --git a/server/routes/ownerRoutes.ts b/server/routes/ownerRoutes.ts new file mode 100644 index 0000000..c1eed8c --- /dev/null +++ b/server/routes/ownerRoutes.ts @@ -0,0 +1,44 @@ +import { Router } from "express"; +import { + getOwnerRestaurant, + createRestaurant, + updateOwnerRestaurant, + getOwnerBookings, + updateBookingStatus, +} from "../controllers/ownerController.js"; + +import { ownerOnly, protect } from "../middlewares/auth.js"; +import upload from "../config/multer.js"; + +const ownerRouter = Router(); + + +ownerRouter.use(protect) +ownerRouter.use(ownerOnly) +// Get owner's restaurant +ownerRouter.get("/restaurant", getOwnerRestaurant); + +// Create restaurant +ownerRouter.post( + "/restaurant", + protect, + upload.single("image"), + createRestaurant +); + +// Update restaurant +ownerRouter.put( + "/restaurant", + protect, + upload.single("image"), + updateOwnerRestaurant +); + +// Get owner's bookings +ownerRouter.get("/bookings", protect, getOwnerBookings); + +ownerRouter.put("/bookings/:id/status", updateBookingStatus); + + + +export default ownerRouter; \ No newline at end of file diff --git a/server/routes/restaurant.routes.ts b/server/routes/restaurantRoutes.ts similarity index 100% rename from server/routes/restaurant.routes.ts rename to server/routes/restaurantRoutes.ts diff --git a/server/server.ts b/server/server.ts index 8289d2e..712b822 100644 --- a/server/server.ts +++ b/server/server.ts @@ -7,8 +7,9 @@ import express, { Request, Response, NextFunction } from "express"; import cors from "cors"; import connectDB from "./config/db.js"; import authRouter from "./routes/authRoutes.js"; -import restaurantRouter from "./routes/restaurant.routes.js"; -import bookingRouter from "./routes/booking.routes.js"; +import restaurantRouter from "./routes/restaurantRoutes.js"; +import bookingRouter from "./routes/bookingRoutes.js"; +import ownerRouter from "./routes/ownerRoutes.js"; const app = express(); @@ -28,6 +29,8 @@ app.get("/", (req: Request, res: Response) => { app.use("/api/auth", authRouter); app.use("/api/restaurants", restaurantRouter); app.use("/api/bookings", bookingRouter); +app.use("/api/owner", ownerRouter); + // Global Error Handler app.use(( err: Error,