diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..406ca0508a --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Dependencies + +node_modules/ +backend/node_modules/ +frontend/node_modules/ + +# Environment variables + +.env +*.env +backend/.env +frontend/.env + +# Build files + +dist/ +build/ +.vite/ + +# Logs + +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS files + +.DS_Store +Thumbs.db + +# IDE + +.vscode/ +.idea/ diff --git a/backend/docs/admin.docs.js b/backend/docs/admin.docs.js new file mode 100644 index 0000000000..d7dd7aedb5 --- /dev/null +++ b/backend/docs/admin.docs.js @@ -0,0 +1,1453 @@ +/** + * Admin API documentation (OpenAPI). + * Keep route/controller files free of Swagger comments. + * + * All /api/admin routes use router-level: + * - userAuthMiddleware (cookieAuth) + * - authorizeRoles("admin") + * + * Sections: Dashboard, Users, Venues, Bookings, Payments + */ + +/** + * @openapi + * /api/admin/dashboard/stats: + * get: + * tags: + * - Admin + * summary: Dashboard stats + * description: > + * Returns marketplace overview counts and total revenue from confirmed paid bookings. + * Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * responses: + * "200": + * description: Dashboard stats fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Dashboard stats fetched successfully + * data: + * type: object + * properties: + * totalUsers: + * type: integer + * totalProviders: + * type: integer + * totalVenues: + * type: integer + * activeVenues: + * type: integer + * confirmedPaidBookings: + * type: integer + * totalRevenue: + * type: number + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied. You do not have the permission to perform this action. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/dashboard/recent-activity: + * get: + * tags: + * - Admin + * summary: Recent activity + * description: > + * Returns the 5 most recent marketplace users, venues, and bookings. + * Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * responses: + * "200": + * description: Recent activity fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Recent activity fetched successfully + * data: + * type: object + * properties: + * users: + * type: array + * items: + * type: object + * venues: + * type: array + * items: + * type: object + * bookings: + * type: array + * items: + * type: object + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/users: + * get: + * tags: + * - Admin + * summary: List users + * description: > + * Paginated marketplace user list. Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * maximum: 100 + * - in: query + * name: role + * schema: + * type: string + * description: Filter by role value in the user's roles array + * - in: query + * name: isActive + * schema: + * type: string + * enum: ["true", "false"] + * - in: query + * name: search + * schema: + * type: string + * description: Search name, email, or phone + * responses: + * "200": + * description: Users fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Users fetched successfully + * count: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * data: + * type: array + * items: + * type: object + * description: Sanitized user (password and OTP fields excluded) + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/users/{id}: + * get: + * tags: + * - Admin + * summary: Get user by ID + * description: > + * Returns a marketplace user plus booking and venue counts. + * Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * "200": + * description: User fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: User fetched successfully + * data: + * type: object + * properties: + * user: + * type: object + * bookingCount: + * type: integer + * venueCount: + * type: integer + * "400": + * description: Invalid user ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid user ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "404": + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/users/{id}/activate: + * patch: + * tags: + * - Admin + * summary: Activate user + * description: > + * Sets `isActive` to true. Platform operator accounts cannot be managed here. + * Requires a valid `token` cookie and the `admin` role. No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * "200": + * description: User activated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: User activated successfully + * data: + * type: object + * description: Sanitized user + * "400": + * description: Invalid user ID, or platform operator account + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Platform operator accounts cannot be managed here + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "404": + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/users/{id}/deactivate: + * patch: + * tags: + * - Admin + * summary: Deactivate user + * description: > + * Sets `isActive` to false. Cannot deactivate your own account or platform operator accounts. + * Requires a valid `token` cookie and the `admin` role. No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * "200": + * description: User deactivated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: User deactivated successfully + * data: + * type: object + * description: Sanitized user + * "400": + * description: Invalid user ID, self-deactivation, or platform operator account + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: You cannot deactivate your own account + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "404": + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/venues: + * get: + * tags: + * - Admin + * summary: List venues + * description: > + * Paginated venue list with owner populated. Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * maximum: 100 + * - in: query + * name: isActive + * schema: + * type: string + * enum: ["true", "false"] + * - in: query + * name: city + * schema: + * type: string + * - in: query + * name: ownerId + * schema: + * type: string + * - in: query + * name: search + * schema: + * type: string + * description: Search title, city, or address + * responses: + * "200": + * description: Venues fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venues fetched successfully + * count: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * data: + * type: array + * items: + * type: object + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/venues/{id}: + * get: + * tags: + * - Admin + * summary: Get venue by ID + * description: > + * Returns a venue with owner details. Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * "200": + * description: Venue fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venue fetched successfully + * data: + * type: object + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/venues/{id}/activate: + * patch: + * tags: + * - Admin + * summary: Activate venue + * description: > + * Sets venue `isActive` to true. Requires a valid `token` cookie and the `admin` role. No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * "200": + * description: Venue activated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venue activated successfully + * data: + * type: object + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/venues/{id}/deactivate: + * patch: + * tags: + * - Admin + * summary: Deactivate venue + * description: > + * Sets venue `isActive` to false. Requires a valid `token` cookie and the `admin` role. No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * "200": + * description: Venue deactivated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venue deactivated successfully + * data: + * type: object + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/bookings: + * get: + * tags: + * - Admin + * summary: List bookings + * description: > + * Paginated booking list with user, venue, and availability populated. + * Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * maximum: 100 + * - in: query + * name: bookingStatus + * schema: + * type: string + * - in: query + * name: paymentStatus + * schema: + * type: string + * - in: query + * name: venueId + * schema: + * type: string + * - in: query + * name: userId + * schema: + * type: string + * responses: + * "200": + * description: Bookings fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Bookings fetched successfully + * count: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * data: + * type: array + * items: + * type: object + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/bookings/{id}: + * get: + * tags: + * - Admin + * summary: Get booking by ID + * description: > + * Returns a booking with user, venue, and availability populated. + * Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * "200": + * description: Booking fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Booking fetched successfully + * data: + * type: object + * "400": + * description: Invalid booking ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid booking ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "404": + * description: Booking not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Booking not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/payments/orders: + * get: + * tags: + * - Admin + * summary: List payment orders + * description: > + * Paginated Razorpay payment orders. Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * maximum: 100 + * - in: query + * name: status + * schema: + * type: string + * description: Filter by payment order status (e.g. created, completed) + * responses: + * "200": + * description: Payment orders fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Payment orders fetched successfully + * count: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * data: + * type: array + * items: + * type: object + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/payments/history: + * get: + * tags: + * - Admin + * summary: Payment history + * description: > + * Paginated list of bookings with `paymentStatus` equal to `paid`. + * Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * maximum: 100 + * responses: + * "200": + * description: Payment history fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Payment history fetched successfully + * count: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * data: + * type: array + * items: + * type: object + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/admin/payments/abandoned: + * get: + * tags: + * - Admin + * summary: Abandoned payment orders + * description: > + * Paginated payment orders with status `created` older than the given hours cutoff (default 24). + * Requires a valid `token` cookie and the `admin` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * maximum: 100 + * - in: query + * name: hours + * schema: + * type: integer + * default: 24 + * description: Minimum age in hours for an order to be considered abandoned + * responses: + * "200": + * description: Abandoned payment orders fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Abandoned payment orders fetched successfully + * count: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * hours: + * type: integer + * data: + * type: array + * items: + * type: object + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "403": + * description: Authenticated user is not an admin + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ diff --git a/backend/docs/auth.docs.js b/backend/docs/auth.docs.js new file mode 100644 index 0000000000..378d83c23f --- /dev/null +++ b/backend/docs/auth.docs.js @@ -0,0 +1,889 @@ +/** + * Auth API documentation (OpenAPI). + * Keep route/controller files free of Swagger comments. + */ + +/** + * @openapi + * /api/auth/login: + * post: + * tags: + * - Auth + * summary: Login + * description: > + * Authenticates a verified, active user with email and password. + * On success, sets an HTTP-only JWT cookie named `token` (7-day max age). + * This endpoint is public and does not require an existing cookie. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * example: rahul@example.com + * password: + * type: string + * format: password + * example: Password1 + * responses: + * "200": + * description: Login successful; `token` cookie is set + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Login successful + * user: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * email: + * type: string + * roles: + * type: array + * items: + * type: string + * profileImage: + * type: string + * "400": + * description: > + * Missing fields, invalid email format, email not verified, + * or invalid password + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid password + * "403": + * description: Account has been disabled + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Your account has been disabled. Please contact support. + * "404": + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/auth/register: + * post: + * tags: + * - Auth + * summary: Register + * description: > + * Creates a new customer account with name, email, phone, and password. + * Sends a 6-digit email OTP for verification. Does not return a JWT or set a cookie. + * This endpoint is public. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - email + * - phone + * - password + * properties: + * name: + * type: string + * example: Rahul Sharma + * email: + * type: string + * format: email + * example: rahul@example.com + * phone: + * type: string + * description: Indian 10-digit mobile number starting with 6-9 + * example: "9876543210" + * password: + * type: string + * format: password + * description: At least 8 characters with uppercase, lowercase, and a number + * example: Password1 + * responses: + * "201": + * description: Registration successful; verification OTP sent by email + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Registration successful. Please check your email for the verification OTP. + * "400": + * description: > + * Missing fields, invalid email or phone, weak password, + * or email/phone already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: All fields are required + * "500": + * description: Registration failed due to a server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Registration failed. Please try again. + */ + +/** + * @openapi + * /api/auth/verify-email: + * post: + * tags: + * - Auth + * summary: Verify email + * description: > + * Verifies a registered user's email using the OTP sent during registration or resend. + * This endpoint is public and does not set a cookie. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - otp + * properties: + * email: + * type: string + * format: email + * example: rahul@example.com + * otp: + * type: string + * example: "123456" + * responses: + * "200": + * description: Email verified successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Email verified successfully + * "400": + * description: > + * Missing email or OTP, email already verified, invalid OTP, or expired OTP + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid OTP + * "401": + * description: User not found for the given email (returned as invalid email or OTP) + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid email or OTP + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/auth/resend-otp: + * post: + * tags: + * - Auth + * summary: Resend OTP + * description: > + * Generates and emails a new verification OTP for an unverified user. + * This endpoint is public and does not set a cookie. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * format: email + * example: rahul@example.com + * responses: + * "200": + * description: OTP resent successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: OTP resent successfully. Please check your email. + * "400": + * description: Email is missing or already verified + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Email is already verified + * "404": + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/auth/forgot-password: + * post: + * tags: + * - Auth + * summary: Forgot password + * description: > + * Sends a password-reset OTP to the user's email. + * This endpoint is public and does not set a cookie. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * format: email + * example: rahul@example.com + * responses: + * "200": + * description: Password-reset OTP sent to email + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: OTP sent to your email. Please check your inbox. + * "400": + * description: Email is required + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Email is required + * "404": + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/auth/reset-password: + * post: + * tags: + * - Auth + * summary: Reset password + * description: > + * Resets the user password after validating the email OTP from forgot-password. + * This endpoint is public and does not set a cookie. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - otp + * - newPassword + * properties: + * email: + * type: string + * format: email + * example: rahul@example.com + * otp: + * type: string + * example: "123456" + * newPassword: + * type: string + * format: password + * description: At least 8 characters with uppercase, lowercase, and a number + * example: NewPassword1 + * responses: + * "200": + * description: Password reset successful + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Password reset successful + * "400": + * description: > + * Missing fields, weak password, invalid OTP, or expired OTP + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid OTP + * "404": + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/auth/logout: + * post: + * tags: + * - Auth + * summary: Logout + * description: > + * Clears the HTTP-only `token` cookie. This route does not use auth middleware, + * so it is public and does not require a valid cookie to succeed. + * responses: + * "200": + * description: Logout successful; `token` cookie cleared + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Logout successful + * "500": + * description: Logout failed + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Logout failed. Please try again. + */ + +/** + * @openapi + * /api/auth/me: + * get: + * tags: + * - Auth + * summary: Get current user + * description: > + * Returns the authenticated user's profile. + * Requires a valid HTTP-only `token` cookie (auth middleware). + * security: + * - cookieAuth: [] + * responses: + * "200": + * description: Current user profile + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * user: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * email: + * type: string + * phone: + * type: string + * roles: + * type: array + * items: + * type: string + * profileImage: + * type: string + * bio: + * type: string + * address: + * type: string + * dob: + * type: string + * format: date-time + * nullable: true + * gender: + * type: string + * enum: [male, female, other] + * city: + * type: string + * state: + * type: string + * isEmailVerified: + * type: boolean + * isActive: + * type: boolean + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Account deactivated + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Your account has been deactivated. Please contact support. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/auth/me: + * patch: + * tags: + * - Auth + * summary: Update current user profile + * description: > + * Partially updates the authenticated user's profile fields via JSON. + * Profile image upload is a separate endpoint (`PATCH /api/auth/me/avatar`) + * and is not part of this route. Requires a valid HTTP-only `token` cookie. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * phone: + * type: string + * example: "9876543210" + * bio: + * type: string + * address: + * type: string + * city: + * type: string + * state: + * type: string + * dob: + * type: string + * format: date + * nullable: true + * gender: + * type: string + * enum: [male, female, other] + * responses: + * "200": + * description: Profile updated + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Profile updated + * user: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * email: + * type: string + * phone: + * type: string + * roles: + * type: array + * items: + * type: string + * profileImage: + * type: string + * bio: + * type: string + * address: + * type: string + * dob: + * type: string + * format: date-time + * nullable: true + * gender: + * type: string + * city: + * type: string + * state: + * type: string + * isEmailVerified: + * type: boolean + * isActive: + * type: boolean + * "400": + * description: > + * No valid fields, validation error, or duplicate phone number + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: No valid profile fields to update + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Account disabled or deactivated + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Your account has been disabled. Please contact support. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/auth/become-provider: + * patch: + * tags: + * - Auth + * summary: Become provider + * description: > + * Adds the `provider` role to the authenticated customer. + * Requires a valid HTTP-only `token` cookie. No request body. + * security: + * - cookieAuth: [] + * responses: + * "200": + * description: Provider role added + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Provider account created successfully + * roles: + * type: array + * items: + * type: string + * example: [customer, provider] + * "400": + * description: User is already a provider + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: You are already a provider + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Account deactivated + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Your account has been deactivated. Please contact support. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ diff --git a/backend/docs/availability.docs.js b/backend/docs/availability.docs.js new file mode 100644 index 0000000000..65bd2667aa --- /dev/null +++ b/backend/docs/availability.docs.js @@ -0,0 +1,429 @@ +/** + * Availability API documentation (OpenAPI). + * Keep route/controller files free of Swagger comments. + */ + +/** + * @openapi + * /api/availability/create: + * post: + * tags: + * - Availability + * summary: Create availability slot + * description: > + * Creates an availability slot for a venue owned by the authenticated provider. + * Requires a valid `token` cookie and the `provider` role. + * Times use 12-hour format such as `09:00 AM`. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - venueId + * - date + * - slotLabel + * - startTime + * - endTime + * properties: + * venueId: + * type: string + * description: MongoDB ObjectId of the venue + * date: + * type: string + * format: date + * slotLabel: + * type: string + * enum: [morning, evening, night, fullday] + * startTime: + * type: string + * example: "09:00 AM" + * endTime: + * type: string + * example: "12:00 PM" + * responses: + * "201": + * description: Availability created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Availability created successfully + * data: + * type: object + * description: Created availability slot document + * "400": + * description: > + * Missing fields, invalid venue ID/date/time, past date, + * end time not after start time, or slot already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Please fill all required fields + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Not a provider, or not the venue owner + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: You can only manage your own venue + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/availability/{venueId}: + * get: + * tags: + * - Availability + * summary: List venue availability + * description: > + * Returns all availability slots for a venue, sorted by date and start time. + * Response may be served from Redis cache when available. + * This endpoint is public and does not require authentication. + * parameters: + * - in: path + * name: venueId + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the venue + * responses: + * "200": + * description: Availability slots retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * count: + * type: integer + * data: + * type: array + * items: + * type: object + * properties: + * _id: + * type: string + * venueId: + * type: string + * date: + * type: string + * format: date-time + * slotLabel: + * type: string + * enum: [morning, evening, night, fullday] + * startTime: + * type: string + * endTime: + * type: string + * isBooked: + * type: boolean + * bookingId: + * type: string + * nullable: true + * isActive: + * type: boolean + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/availability/deactivate/{slotId}: + * patch: + * tags: + * - Availability + * summary: Deactivate availability slot + * description: > + * Sets `isActive` to false for a slot on a venue owned by the authenticated provider. + * Booked slots cannot be deactivated. Requires a valid `token` cookie and the `provider` role. + * No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: slotId + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the availability slot + * responses: + * "200": + * description: Slot deactivated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Slot deactivated successfully + * data: + * type: object + * description: Updated availability slot document + * "400": + * description: Invalid slot ID, or slot is booked + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Booked slots cannot be deactivated + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Not a provider, or not the venue owner + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized + * "404": + * description: Slot or venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Slot not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/availability/activate/{slotId}: + * patch: + * tags: + * - Availability + * summary: Activate availability slot + * description: > + * Sets `isActive` to true for a slot on a venue owned by the authenticated provider. + * Requires a valid `token` cookie and the `provider` role. No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: slotId + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the availability slot + * responses: + * "200": + * description: Slot activated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Slot activated successfully + * data: + * type: object + * description: Updated availability slot document + * "400": + * description: Invalid slot ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid slot ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Not a provider, or not the venue owner + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized + * "404": + * description: Slot or venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Slot not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ diff --git a/backend/docs/bookings.docs.js b/backend/docs/bookings.docs.js new file mode 100644 index 0000000000..ca39c1d84c --- /dev/null +++ b/backend/docs/bookings.docs.js @@ -0,0 +1,252 @@ +/** + * Bookings API documentation (OpenAPI). + * Keep route/controller files free of Swagger comments. + * + * Note: Booking creation is not handled under /api/bookings. + * Bookings are created via the Payments module (payment verification). + */ + +/** + * @openapi + * /api/bookings/my-bookings: + * get: + * tags: + * - Bookings + * summary: List my bookings + * description: > + * Returns all bookings for the authenticated user, sorted by newest first. + * Each booking populates `venueId` and `availabilityId`. + * Requires a valid `token` cookie. No request body. + * There is no `POST /api/bookings` endpoint; bookings are created through Payments. + * security: + * - cookieAuth: [] + * responses: + * "200": + * description: User bookings retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Fetched all bookings + * count: + * type: integer + * data: + * type: array + * items: + * type: object + * properties: + * _id: + * type: string + * bookingReference: + * type: string + * userId: + * type: string + * venueId: + * type: object + * description: Populated venue document + * availabilityId: + * type: object + * description: Populated availability slot document + * amount: + * type: number + * bookingStatus: + * type: string + * enum: [confirmed, cancelled] + * paymentMethod: + * type: string + * nullable: true + * razorpayOrderId: + * type: string + * nullable: true + * paymentId: + * type: string + * nullable: true + * paymentStatus: + * type: string + * enum: [pending, paid, failed, refunded] + * contactPhone: + * type: string + * bookedAt: + * type: string + * format: date-time + * cancelledAt: + * type: string + * format: date-time + * nullable: true + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Account deactivated + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Your account has been deactivated. Please contact support. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/bookings/provider-bookings: + * get: + * tags: + * - Bookings + * summary: List provider venue bookings + * description: > + * Returns all bookings for venues owned by the authenticated provider, + * sorted by newest first. Populates `userId` (name, email, phone), + * `venueId`, and `availabilityId`. + * Requires a valid `token` cookie and the `provider` role. No request body. + * security: + * - cookieAuth: [] + * responses: + * "200": + * description: Provider bookings retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * count: + * type: integer + * data: + * type: array + * items: + * type: object + * properties: + * _id: + * type: string + * bookingReference: + * type: string + * userId: + * type: object + * description: Populated user subset (name, email, phone) + * properties: + * name: + * type: string + * email: + * type: string + * phone: + * type: string + * venueId: + * type: object + * description: Populated venue document + * availabilityId: + * type: object + * description: Populated availability slot document + * amount: + * type: number + * bookingStatus: + * type: string + * enum: [confirmed, cancelled] + * paymentMethod: + * type: string + * nullable: true + * razorpayOrderId: + * type: string + * nullable: true + * paymentId: + * type: string + * nullable: true + * paymentStatus: + * type: string + * enum: [pending, paid, failed, refunded] + * contactPhone: + * type: string + * bookedAt: + * type: string + * format: date-time + * cancelledAt: + * type: string + * format: date-time + * nullable: true + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Account deactivated, or authenticated user is not a provider + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied. You do not have the permission to perform this action. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ diff --git a/backend/docs/venues.docs.js b/backend/docs/venues.docs.js new file mode 100644 index 0000000000..5ea7ea377e --- /dev/null +++ b/backend/docs/venues.docs.js @@ -0,0 +1,969 @@ +/** + * Venues API documentation (OpenAPI). + * Keep route/controller files free of Swagger comments. + */ + +/** + * @openapi + * /api/venues: + * get: + * tags: + * - Venues + * summary: List active venues + * description: > + * Returns all active venues, sorted by newest first. + * Response may be served from Redis cache when available. + * This endpoint is public and does not require authentication. + * The controller does not accept query parameters. + * responses: + * "200": + * description: Active venues retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * count: + * type: integer + * example: 3 + * data: + * type: array + * items: + * type: object + * properties: + * _id: + * type: string + * ownerId: + * type: string + * title: + * type: string + * description: + * type: string + * category: + * type: string + * enum: + * - wedding + * - corporate + * - birthday + * - party + * - function + * - photoshoot + * - other + * images: + * type: array + * items: + * type: object + * properties: + * url: + * type: string + * public_id: + * type: string + * coverImage: + * type: object + * properties: + * url: + * type: string + * public_id: + * type: string + * venueType: + * type: string + * enum: [offline, online, hybrid] + * indoorOutdoor: + * type: string + * enum: [indoor, outdoor, both] + * price: + * type: number + * pricingUnit: + * type: string + * enum: [perhour, perday] + * capacity: + * type: number + * amenities: + * type: array + * items: + * type: string + * rules: + * type: array + * items: + * type: string + * address: + * type: string + * city: + * type: string + * state: + * type: string + * pincode: + * type: string + * location: + * type: object + * properties: + * latitude: + * type: number + * nullable: true + * longitude: + * type: number + * nullable: true + * averageRating: + * type: number + * totalReviews: + * type: number + * isActive: + * type: boolean + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/venues/{venueId}: + * get: + * tags: + * - Venues + * summary: Get venue by ID + * description: > + * Returns a single active venue by ID. + * Response may be served from Redis cache when available. + * This endpoint is public and does not require authentication. + * parameters: + * - in: path + * name: venueId + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the venue + * responses: + * "200": + * description: Venue retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * _id: + * type: string + * ownerId: + * type: string + * title: + * type: string + * description: + * type: string + * category: + * type: string + * enum: + * - wedding + * - corporate + * - birthday + * - party + * - function + * - photoshoot + * - other + * images: + * type: array + * items: + * type: object + * properties: + * url: + * type: string + * public_id: + * type: string + * coverImage: + * type: object + * properties: + * url: + * type: string + * public_id: + * type: string + * venueType: + * type: string + * enum: [offline, online, hybrid] + * indoorOutdoor: + * type: string + * enum: [indoor, outdoor, both] + * price: + * type: number + * pricingUnit: + * type: string + * enum: [perhour, perday] + * capacity: + * type: number + * amenities: + * type: array + * items: + * type: string + * rules: + * type: array + * items: + * type: string + * address: + * type: string + * city: + * type: string + * state: + * type: string + * pincode: + * type: string + * location: + * type: object + * properties: + * latitude: + * type: number + * nullable: true + * longitude: + * type: number + * nullable: true + * averageRating: + * type: number + * totalReviews: + * type: number + * isActive: + * type: boolean + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "404": + * description: Venue not found or not active + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/venues/create: + * post: + * tags: + * - Venues + * summary: Create venue + * description: > + * Creates a new venue for the authenticated provider. + * Requires a valid `token` cookie and the `provider` role. + * Accepts multipart/form-data with venue fields and up to 5 image files + * under the field name `images`. At least one image is required. + * `amenities` and `rules` must be JSON array strings when provided. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - title + * - description + * - category + * - price + * - capacity + * - address + * - images + * properties: + * title: + * type: string + * description: + * type: string + * category: + * type: string + * description: Venue category (validated/normalized by the controller) + * venueType: + * type: string + * indoorOutdoor: + * type: string + * price: + * type: number + * pricingUnit: + * type: string + * capacity: + * type: integer + * amenities: + * type: string + * description: JSON string of a string array, e.g. '["wifi","parking"]' + * rules: + * type: string + * description: JSON string of a string array + * address: + * type: string + * city: + * type: string + * state: + * type: string + * pincode: + * type: string + * latitude: + * type: number + * longitude: + * type: number + * images: + * type: array + * items: + * type: string + * format: binary + * description: One to five image files (multer field name `images`, max 5) + * responses: + * "201": + * description: Venue created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venue created successfully. + * data: + * type: object + * description: Created venue document + * "400": + * description: > + * Missing required fields, invalid price/capacity/coordinates/category, + * invalid amenities/rules JSON, or no images uploaded + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Please fill all required fields. + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Authenticated user is not a provider + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied. You do not have the permission to perform this action. + * "500": + * description: Failed to create venue + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Failed to create venue. Please try again. + */ + +/** + * @openapi + * /api/venues/update/{id}: + * put: + * tags: + * - Venues + * summary: Update venue + * description: > + * Updates an existing venue owned by the authenticated provider. + * Requires a valid `token` cookie and the `provider` role. + * Accepts multipart/form-data. All form fields are optional; only provided + * fields are updated. Optional new images use multer field name `images` + * (up to 5). If new images are uploaded, they replace existing venue images. + * `amenities` and `rules` must be JSON array strings when provided. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the venue to update + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * title: + * type: string + * description: + * type: string + * category: + * type: string + * venueType: + * type: string + * indoorOutdoor: + * type: string + * price: + * type: number + * capacity: + * type: integer + * amenities: + * type: string + * description: JSON string of a string array + * rules: + * type: string + * description: JSON string of a string array + * address: + * type: string + * city: + * type: string + * state: + * type: string + * pincode: + * type: string + * latitude: + * type: number + * longitude: + * type: number + * images: + * type: array + * items: + * type: string + * format: binary + * description: Optional replacement images (multer field name `images`, max 5) + * responses: + * "200": + * description: Venue updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venue updated successfully + * data: + * type: object + * description: Updated venue document + * "400": + * description: > + * Invalid venue ID, category, price, capacity, coordinates, + * or amenities/rules JSON + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Not a provider, or not the venue owner + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/venues/deactivate/{id}: + * patch: + * tags: + * - Venues + * summary: Deactivate venue + * description: > + * Soft-deletes a venue by setting `isActive` to false. + * There is no hard-delete venue endpoint in this API. + * Requires a valid `token` cookie and the `provider` role. + * Only the venue owner can deactivate it. No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the venue to deactivate + * responses: + * "200": + * description: Venue deactivated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venue deactivated successfully + * data: + * type: object + * description: Updated venue document with isActive set to false + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Not a provider, or not the venue owner + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/venues/my-venues: + * get: + * tags: + * - Venues + * summary: List my venues + * description: > + * Returns all venues owned by the authenticated provider, including inactive ones, + * sorted by newest first. Requires a valid `token` cookie and the `provider` role. + * security: + * - cookieAuth: [] + * responses: + * "200": + * description: Provider venues retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * description: Venue document + * count: + * type: integer + * example: 2 + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Authenticated user is not a provider + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied. You do not have the permission to perform this action. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + */ + +/** + * @openapi + * /api/venues/provider/{id}: + * get: + * tags: + * - Venues + * summary: Get owned venue by ID + * description: > + * Returns a single venue by ID for the authenticated provider who owns it. + * Unlike the public detail route, this can return inactive venues owned by the provider. + * Requires a valid `token` cookie and the `provider` role. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the venue + * responses: + * "200": + * description: Venue retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * description: Venue document + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Not a provider, or not the venue owner + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied. + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found. + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ + +/** + * @openapi + * /api/venues/activate/{id}: + * patch: + * tags: + * - Venues + * summary: Activate venue + * description: > + * Sets `isActive` to true for a venue owned by the authenticated provider. + * Requires a valid `token` cookie and the `provider` role. No request body. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: MongoDB ObjectId of the venue to activate + * responses: + * "200": + * description: Venue activated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Venue activated successfully + * data: + * type: object + * description: Updated venue document with isActive set to true + * "400": + * description: Invalid venue ID + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid venue ID + * "401": + * description: Missing, invalid, or expired token cookie + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access. Please login. + * "403": + * description: Not a provider, or not the venue owner + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Access denied + * "404": + * description: Venue not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Venue not found + * "500": + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Something went wrong. Please try again later. + */ diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000000..e8c6a94eac --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2705 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcrypt": "^6.0.0", + "cloudinary": "^2.10.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "express-validator": "^7.3.2", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.7.0", + "multer": "^2.2.0", + "nodemailer": "^9.0.0", + "razorpay": "^2.9.6", + "redis": "^6.1.0", + "sib-api-v3-sdk": "^8.5.0", + "swagger-jsdoc": "^6.3.0", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/@redis/bloom": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-6.1.0.tgz", + "integrity": "sha512-Rzascjd9J9bJsM45T/Z9CTg1QY/B63B6YO8QorLVMeXnbBDsKiSCVR/+GQ061hYPk8FpTzWmPY8tAv2sT+JEtQ==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.1.0" + } + }, + "node_modules/@redis/client": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-6.1.0.tgz", + "integrity": "sha512-7u1LefkezJF0HESlhO7ZFLEPfyY+NejP3SGv+Z4pGaT3oM5GVVLa0u3f4rDLUrcw+SRo8IlX9Y8JAONeDdg1Ag==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-6.1.0.tgz", + "integrity": "sha512-/GFjQA6bu5pG9ClCJAI5Xx4bNXe7UTpxBBlIupBNTrn1+nY860apGnYJuaSCDV2BmEbTidpa7O2qa28oxKx+rg==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.1.0" + } + }, + "node_modules/@redis/search": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-6.1.0.tgz", + "integrity": "sha512-kS5agg+3yZbrdrt8omrew7FLCD8eOm7tarG1CROekPBRe+QGDR9aOpnHIQaYsYi6wPRTH70nQiF06AIjgURefQ==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.1.0" + } + }, + "node_modules/@redis/time-series": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-6.1.0.tgz", + "integrity": "sha512-uIDBtV8MmG/xpJsRqbGSO4iX6ryj37MLMP82lRpFvI7ykAVe5GyqgxigEbU+uZNv9kDPNMKw3dvI/S/J1BNBzA==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.1.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "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/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axios/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/axios/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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "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/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==", + "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/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "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/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/concat-stream/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/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-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/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.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/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/express-validator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", + "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.18.1", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express/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/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/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/form-data/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/formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "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": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "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/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/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.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==", + "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/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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.7.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.7.0.tgz", + "integrity": "sha512-pkrLZ6U41pD4Ai0ju/FYL7o5I5k+rV3RZINQTG937hbhnLGKRuqqYm1Dlt/kTQ+M4FHijzV6JawzsdHKRGt7QA==", + "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/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/node-addon-api": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", + "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemailer": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.0.tgz", + "integrity": "sha512-tbPTid7d/p9jAA8CRZ3iomvrMaST0o6NYuY7v6JQZHpPRZ61mLFSPKYd7342NtOFuej9/+L48SOIxwfu2uDvtw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "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/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=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/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "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/razorpay": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/razorpay/-/razorpay-2.9.6.tgz", + "integrity": "sha512-zsHAQzd6e1Cc6BNoCNZQaf65ElL6O6yw0wulxmoG5VQDr363fZC90Mp1V5EktVzG45yPyNomNXWlf4cQ3622gQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/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/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/redis": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-6.1.0.tgz", + "integrity": "sha512-0kvUPM8RHP/ZMa0xYaDTcG5e8tIGW6kz6MToVT0V8iOnk6bkXp2jncGRGe2bZEk41lZwiDspUqjZCSk5ohjcKw==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "6.1.0", + "@redis/client": "6.1.0", + "@redis/json": "6.1.0", + "@redis/search": "6.1.0", + "@redis/time-series": "6.1.0" + }, + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.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.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sib-api-v3-sdk": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/sib-api-v3-sdk/-/sib-api-v3-sdk-8.5.0.tgz", + "integrity": "sha512-6Ratp5kLN/rEEvk4XVIQ4L8IrCIrcfE9m1HjvHz/WepC+CVXPsjOlgRcK/jQjpN5kC+dmhDAqrTo1OtnF6i1wA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "querystring": "0.2.0", + "superagent": "3.7.0" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/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/superagent": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.7.0.tgz", + "integrity": "sha512-/8trxO6NbLx4YXb7IeeFTSmsQ35pQBiTBsLNvobZx7qBzBeHYvKCyIIhW2gNcWbLzYxPAjdgFbiepd8ypwC0Gw==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "license": "MIT", + "dependencies": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "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/swagger-jsdoc": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz", + "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^12.1.0", + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "11.1.0", + "lodash.mergewith": "^4.6.2", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.10", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.10.tgz", + "integrity": "sha512-RTrAPrp/J5H/H8JvRBOpR3qCsIhntOweFwaasR0TD2Y6a/7VUYYFnjyB3MFe1niAg3VkEQGT9pVZ0r225jvJJA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "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/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000000..e9ba7f6df0 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,37 @@ +{ + "name": "backend", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "seed:admin": "node scripts/seedAdmin.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "bcrypt": "^6.0.0", + "cloudinary": "^2.10.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "express-validator": "^7.3.2", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.7.0", + "multer": "^2.2.0", + "nodemailer": "^9.0.0", + "razorpay": "^2.9.6", + "redis": "^6.1.0", + "sib-api-v3-sdk": "^8.5.0", + "swagger-jsdoc": "^6.3.0", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } +} diff --git a/backend/scripts/seedAdmin.js b/backend/scripts/seedAdmin.js new file mode 100644 index 0000000000..fb1a27691e --- /dev/null +++ b/backend/scripts/seedAdmin.js @@ -0,0 +1,93 @@ +import dotenv from "dotenv"; +import mongoose from "mongoose"; +import bcrypt from "bcrypt"; +import userModel from "../src/models/userModel.js"; + +dotenv.config(); + +const seedAdmin = async () => { + const { ADMIN_NAME, ADMIN_EMAIL, ADMIN_PHONE, ADMIN_PASSWORD, MONGO_URI } = + process.env; + + if (!ADMIN_NAME || !ADMIN_EMAIL || !ADMIN_PHONE || !ADMIN_PASSWORD) { + console.error( + "Missing required env vars: ADMIN_NAME, ADMIN_EMAIL, ADMIN_PHONE, ADMIN_PASSWORD" + ); + process.exit(1); + } + + if (!MONGO_URI) { + console.error("Missing required env var: MONGO_URI"); + process.exit(1); + } + + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; + if (!passwordRegex.test(ADMIN_PASSWORD)) { + console.error( + "ADMIN_PASSWORD must be at least 8 characters and include uppercase, lowercase, and a number" + ); + process.exit(1); + } + + const phoneRegex = /^[6-9]\d{9}$/; + if (!phoneRegex.test(ADMIN_PHONE)) { + console.error("ADMIN_PHONE must be a valid 10-digit Indian mobile number"); + process.exit(1); + } + + try { + await mongoose.connect(MONGO_URI); + console.log("Database connected"); + + const existingAdmin = await userModel.findOne({ + email: ADMIN_EMAIL.toLowerCase().trim(), + roles: "admin", + }); + + if (existingAdmin) { + console.log(`Admin already exists: ${existingAdmin.email}`); + await mongoose.disconnect(); + process.exit(0); + } + + const existingEmail = await userModel.findOne({ + email: ADMIN_EMAIL.toLowerCase().trim(), + }); + + if (existingEmail) { + if (!existingEmail.roles.includes("admin")) { + existingEmail.roles.push("admin"); + existingEmail.isEmailVerified = true; + existingEmail.isActive = true; + await existingEmail.save(); + console.log(`Admin role added to existing user: ${existingEmail.email}`); + } else { + console.log(`Admin already exists: ${existingEmail.email}`); + } + await mongoose.disconnect(); + process.exit(0); + } + + const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10); + + const admin = await userModel.create({ + name: ADMIN_NAME.trim(), + email: ADMIN_EMAIL.toLowerCase().trim(), + phone: ADMIN_PHONE.trim(), + password: hashedPassword, + roles: ["admin"], + isEmailVerified: true, + isActive: true, + }); + + console.log(`Admin created successfully: ${admin.email}`); + await mongoose.disconnect(); + process.exit(0); + } catch (error) { + console.error("Seed admin error:", error.message); + await mongoose.disconnect(); + process.exit(1); + } +}; + +seedAdmin(); diff --git a/backend/src/config/cloudinary.js b/backend/src/config/cloudinary.js new file mode 100644 index 0000000000..6ea3fc0199 --- /dev/null +++ b/backend/src/config/cloudinary.js @@ -0,0 +1,12 @@ +import {v2 as cloudinary} from 'cloudinary'; +import dotenv from 'dotenv'; + +dotenv.config(); + +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/backend/src/config/db.js b/backend/src/config/db.js new file mode 100644 index 0000000000..de0c4dd081 --- /dev/null +++ b/backend/src/config/db.js @@ -0,0 +1,13 @@ +import mongoose from "mongoose"; + +const connection = async () => { + try { + await mongoose.connect(process.env.MONGO_URI); + console.log("Database connected successfully"); + } catch (error) { + console.error("Database connection failed:", error); + process.exit(1); + }; +} + +export default connection; \ No newline at end of file diff --git a/backend/src/config/redis.js b/backend/src/config/redis.js new file mode 100644 index 0000000000..edf2dfe823 --- /dev/null +++ b/backend/src/config/redis.js @@ -0,0 +1,33 @@ +import { createClient } from "redis"; + +let redisClient = null; + +const connectRedis = async () => { + try { + if (!redisClient) { + redisClient = createClient({ + username: process.env.REDIS_USERNAME, + password: process.env.REDIS_PASSWORD, + socket: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + }); + + redisClient.on("error", (err) => { + console.error("Redis Client Error:", err); + }); + } + + if (!redisClient.isOpen) { + await redisClient.connect(); + } + + console.log("Redis connected successfully"); + } catch (error) { + console.error("Redis connection failed:", error); + } +}; + +export { connectRedis, redisClient }; +export { redisClient as default }; diff --git a/backend/src/config/swagger.js b/backend/src/config/swagger.js new file mode 100644 index 0000000000..c36344fe11 --- /dev/null +++ b/backend/src/config/swagger.js @@ -0,0 +1,50 @@ +import path from "path"; +import { fileURLToPath } from "url"; +import swaggerJsdoc from "swagger-jsdoc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const options = { + definition: { + openapi: "3.0.0", + info: { + title: "BookMyVenue API", + version: "1.0.0", + description: + "REST API for BookMyVenue, a MERN-based venue booking platform. Supports customer, provider, and admin workflows with JWT cookie authentication, venue management, availability scheduling, bookings, and Razorpay payment integration.", + }, + servers: [ + { + url: "http://localhost:5000", + description: "Local development server", + }, + ], + tags: [ + { name: "Auth", description: "Authentication and user profile" }, + { name: "Venues", description: "Venue listing and management" }, + { name: "Availability", description: "Venue availability slots" }, + { name: "Bookings", description: "Booking listings" }, + { name: "Payments", description: "Razorpay payment and order flow" }, + { name: "Admin", description: "Admin dashboard and management" }, + ], + components: { + securitySchemes: { + cookieAuth: { + type: "apiKey", + in: "cookie", + name: "token", + description: + "JWT access token stored in an HTTP-only cookie named `token`. Log in via POST /api/auth/login first, then authenticated requests send this cookie automatically.", + }, + }, + }, + }, + // Scan documentation-only modules (not routes/controllers). + // Forward slashes required so glob matching works on Windows. + apis: [path.join(__dirname, "../../docs/**/*.docs.js").replace(/\\/g, "/")], +}; + +const swaggerSpec = swaggerJsdoc(options); + +export default swaggerSpec; diff --git a/backend/src/controllers/adminBookingController.js b/backend/src/controllers/adminBookingController.js new file mode 100644 index 0000000000..555de03dab --- /dev/null +++ b/backend/src/controllers/adminBookingController.js @@ -0,0 +1,101 @@ +import bookingModel from "../models/bookingModel.js"; +import parsePagination from "../utils/parsePagination.js"; +import mongoose from "mongoose"; + +const buildBookingFilter = (query) => { + const filter = {}; + + if (query.bookingStatus) { + filter.bookingStatus = query.bookingStatus; + } + + if (query.paymentStatus) { + filter.paymentStatus = query.paymentStatus; + } + + if (query.venueId) { + filter.venueId = query.venueId; + } + + if (query.userId) { + filter.userId = query.userId; + } + + return filter; +}; + +const getBookings = async (req, res) => { + try { + const { page, limit, skip } = parsePagination(req.query); + const filter = buildBookingFilter(req.query); + + const [bookings, count] = await Promise.all([ + bookingModel + .find(filter) + .populate("userId", "name email phone") + .populate("venueId", "title city coverImage") + .populate("availabilityId") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + bookingModel.countDocuments(filter), + ]); + + return res.status(200).json({ + success: true, + message: "Bookings fetched successfully", + count, + page, + limit, + data: bookings, + }); + } catch (error) { + console.error("Admin get bookings error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const getBookingById = async (req, res) => { + try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid booking ID", + }); + } + + const booking = await bookingModel + .findById(id) + .populate("userId", "name email phone profileImage") + .populate("venueId") + .populate("availabilityId"); + + if (!booking) { + return res.status(404).json({ + success: false, + message: "Booking not found", + }); + } + + return res.status(200).json({ + success: true, + message: "Booking fetched successfully", + data: booking, + }); + } catch (error) { + console.error("Admin get booking by id error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +export { getBookings, getBookingById }; diff --git a/backend/src/controllers/adminDashboardController.js b/backend/src/controllers/adminDashboardController.js new file mode 100644 index 0000000000..06be7bf970 --- /dev/null +++ b/backend/src/controllers/adminDashboardController.js @@ -0,0 +1,107 @@ +import userModel from "../models/userModel.js"; +import venueModel from "../models/venueModel.js"; +import bookingModel from "../models/bookingModel.js"; +import sanitizeUser from "../utils/sanitizeUser.js"; +import { MARKETPLACE_USER_FILTER, withMarketplaceUserFilter } from "../utils/marketplaceUserFilter.js"; + +const RECENT_LIMIT = 5; + +const getDashboardStats = async (req, res) => { + try { + const [ + totalUsers, + totalProviders, + totalVenues, + activeVenues, + confirmedPaidBookings, + revenueResult, + ] = await Promise.all([ + userModel.countDocuments(MARKETPLACE_USER_FILTER), + userModel.countDocuments(withMarketplaceUserFilter({ roles: "provider" })), + venueModel.countDocuments(), + venueModel.countDocuments({ isActive: true }), + bookingModel.countDocuments({ + bookingStatus: "confirmed", + paymentStatus: "paid", + }), + bookingModel.aggregate([ + { + $match: { + bookingStatus: "confirmed", + paymentStatus: "paid", + }, + }, + { + $group: { + _id: null, + totalRevenue: { $sum: "$amount" }, + }, + }, + ]), + ]); + + const totalRevenue = revenueResult[0]?.totalRevenue ?? 0; + + return res.status(200).json({ + success: true, + message: "Dashboard stats fetched successfully", + data: { + totalUsers, + totalProviders, + totalVenues, + activeVenues, + confirmedPaidBookings, + totalRevenue, + }, + }); + } catch (error) { + console.error("Admin dashboard stats error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const getRecentActivity = async (req, res) => { + try { + const [users, venues, bookings] = await Promise.all([ + userModel + .find(MARKETPLACE_USER_FILTER) + .select("-password -otp -otpExpiresAt") + .sort({ createdAt: -1 }) + .limit(RECENT_LIMIT), + venueModel + .find() + .populate("ownerId", "name email profileImage") + .sort({ createdAt: -1 }) + .limit(RECENT_LIMIT), + bookingModel + .find() + .populate("userId", "name email") + .populate("venueId", "title city") + .sort({ createdAt: -1 }) + .limit(RECENT_LIMIT), + ]); + + return res.status(200).json({ + success: true, + message: "Recent activity fetched successfully", + data: { + users: users.map(sanitizeUser), + venues, + bookings, + }, + }); + } catch (error) { + console.error("Admin recent activity error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +export { getDashboardStats, getRecentActivity }; diff --git a/backend/src/controllers/adminPaymentController.js b/backend/src/controllers/adminPaymentController.js new file mode 100644 index 0000000000..dd3745be81 --- /dev/null +++ b/backend/src/controllers/adminPaymentController.js @@ -0,0 +1,130 @@ +import paymentOrderModel from "../models/paymentOrderModel.js"; +import bookingModel from "../models/bookingModel.js"; +import parsePagination from "../utils/parsePagination.js"; + +const getPaymentOrders = async (req, res) => { + try { + const { page, limit, skip } = parsePagination(req.query); + const filter = {}; + + if (req.query.status) { + filter.status = req.query.status; + } + + const [orders, count] = await Promise.all([ + paymentOrderModel + .find(filter) + .populate("userId", "name email") + .populate("venueId", "title city") + .populate("availabilityId", "date slotLabel startTime endTime") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + paymentOrderModel.countDocuments(filter), + ]); + + return res.status(200).json({ + success: true, + message: "Payment orders fetched successfully", + count, + page, + limit, + data: orders, + }); + } catch (error) { + console.error("Admin get payment orders error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const getPaymentHistory = async (req, res) => { + try { + const { page, limit, skip } = parsePagination(req.query); + + const filter = { paymentStatus: "paid" }; + + const [bookings, count] = await Promise.all([ + bookingModel + .find(filter) + .select( + "paymentId razorpayOrderId amount paymentStatus bookingReference bookingStatus bookedAt createdAt userId venueId availabilityId" + ) + .populate("userId", "name email phone") + .populate("venueId", "title city") + .populate("availabilityId", "date slotLabel startTime endTime") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + bookingModel.countDocuments(filter), + ]); + + return res.status(200).json({ + success: true, + message: "Payment history fetched successfully", + count, + page, + limit, + data: bookings, + }); + } catch (error) { + console.error("Admin get payment history error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const getAbandonedPayments = async (req, res) => { + try { + const { page, limit, skip } = parsePagination(req.query); + + let hours = parseInt(req.query.hours, 10); + if (Number.isNaN(hours) || hours < 1) { + hours = 24; + } + + const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000); + + const filter = { + status: "created", + createdAt: { $lt: cutoff }, + }; + + const [orders, count] = await Promise.all([ + paymentOrderModel + .find(filter) + .populate("userId", "name email") + .populate("venueId", "title city") + .populate("availabilityId", "date slotLabel startTime endTime") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + paymentOrderModel.countDocuments(filter), + ]); + + return res.status(200).json({ + success: true, + message: "Abandoned payment orders fetched successfully", + count, + page, + limit, + hours, + data: orders, + }); + } catch (error) { + console.error("Admin get abandoned payments error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +export { getPaymentOrders, getPaymentHistory, getAbandonedPayments }; diff --git a/backend/src/controllers/adminUserController.js b/backend/src/controllers/adminUserController.js new file mode 100644 index 0000000000..b1ae913535 --- /dev/null +++ b/backend/src/controllers/adminUserController.js @@ -0,0 +1,215 @@ +import mongoose from "mongoose"; +import userModel from "../models/userModel.js"; +import venueModel from "../models/venueModel.js"; +import bookingModel from "../models/bookingModel.js"; +import parsePagination from "../utils/parsePagination.js"; +import sanitizeUser from "../utils/sanitizeUser.js"; +import { + withMarketplaceUserFilter, + isPlatformOperator, + buildSearchRegex, +} from "../utils/marketplaceUserFilter.js"; + +const OPERATOR_ACCOUNT_MESSAGE = + "Platform operator accounts cannot be managed here"; + +const buildUserFilter = (query) => { + const filter = {}; + + if (query.role) { + filter.roles = query.role; + } + + if (query.isActive !== undefined && query.isActive !== "") { + if (query.isActive === "true") { + filter.isActive = true; + } else if (query.isActive === "false") { + filter.isActive = false; + } + } + + if (query.search?.trim()) { + const searchRegex = buildSearchRegex(query.search); + filter.$or = [ + { name: searchRegex }, + { email: searchRegex }, + { phone: searchRegex }, + ]; + } + + return withMarketplaceUserFilter(filter); +}; + +const getUsers = async (req, res) => { + try { + const { page, limit, skip } = parsePagination(req.query); + const filter = buildUserFilter(req.query); + + const [users, count] = await Promise.all([ + userModel + .find(filter) + .select("-password -otp -otpExpiresAt") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + userModel.countDocuments(filter), + ]); + + return res.status(200).json({ + success: true, + message: "Users fetched successfully", + count, + page, + limit, + data: users.map(sanitizeUser), + }); + } catch (error) { + console.error("Admin get users error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const getUserById = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid user ID", + }); + } + + const user = await userModel + .findOne(withMarketplaceUserFilter({ _id: id })) + .select("-password -otp -otpExpiresAt"); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found", + }); + } + + const [bookingCount, venueCount] = await Promise.all([ + bookingModel.countDocuments({ userId: user._id }), + venueModel.countDocuments({ ownerId: user._id }), + ]); + + return res.status(200).json({ + success: true, + message: "User fetched successfully", + data: { + user: sanitizeUser(user), + bookingCount, + venueCount, + }, + }); + } catch (error) { + console.error("Admin get user by id error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const activateUser = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid user ID", + }); + } + const user = await userModel.findById(id); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found", + }); + } + + if (isPlatformOperator(user)) { + return res.status(400).json({ + success: false, + message: OPERATOR_ACCOUNT_MESSAGE, + }); + } + + user.isActive = true; + await user.save(); + + return res.status(200).json({ + success: true, + message: "User activated successfully", + data: sanitizeUser(user), + }); + } catch (error) { + console.error("Admin activate user error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const deactivateUser = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid user ID", + }); + } + + if (req.user._id.toString() === id) { + return res.status(400).json({ + success: false, + message: "You cannot deactivate your own account", + }); + } + + const user = await userModel.findById(id); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found", + }); + } + + if (isPlatformOperator(user)) { + return res.status(400).json({ + success: false, + message: OPERATOR_ACCOUNT_MESSAGE, + }); + } + + user.isActive = false; + await user.save(); + + return res.status(200).json({ + success: true, + message: "User deactivated successfully", + data: sanitizeUser(user), + }); + } catch (error) { + console.error("Admin deactivate user error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +export { getUsers, getUserById, activateUser, deactivateUser }; diff --git a/backend/src/controllers/adminVenueController.js b/backend/src/controllers/adminVenueController.js new file mode 100644 index 0000000000..5df748ec13 --- /dev/null +++ b/backend/src/controllers/adminVenueController.js @@ -0,0 +1,187 @@ +import venueModel from "../models/venueModel.js"; +import parsePagination from "../utils/parsePagination.js"; +import { buildSearchRegex } from "../utils/marketplaceUserFilter.js"; +import { invalidateActiveVenuesCache, invalidateActiveVenueCache, } from "../utils/cache.js"; +import mongoose from "mongoose"; + +const buildVenueFilter = (query) => { + const filter = {}; + + if (query.isActive !== undefined && query.isActive !== "") { + if (query.isActive === "true") { + filter.isActive = true; + } else if (query.isActive === "false") { + filter.isActive = false; + } + } + + if (query.city?.trim()) { + filter.city = buildSearchRegex(query.city); + } + + if (query.ownerId) { + filter.ownerId = query.ownerId; + } + + if (query.search?.trim()) { + const searchRegex = buildSearchRegex(query.search); + filter.$or = [ + { title: searchRegex }, + { city: searchRegex }, + { address: searchRegex }, + ]; + } + + return filter; +}; + +const getVenues = async (req, res) => { + try { + const { page, limit, skip } = parsePagination(req.query); + const filter = buildVenueFilter(req.query); + + const [venues, count] = await Promise.all([ + venueModel + .find(filter) + .populate("ownerId", "name email profileImage") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit), + venueModel.countDocuments(filter), + ]); + + return res.status(200).json({ + success: true, + message: "Venues fetched successfully", + count, + page, + limit, + data: venues, + }); + } catch (error) { + console.error("Admin get venues error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const getVenueById = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel + .findById(id) + .populate("ownerId", "name email profileImage phone"); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + return res.status(200).json({ + success: true, + message: "Venue fetched successfully", + data: venue, + }); + } catch (error) { + console.error("Admin get venue by id error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const activateVenue = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel.findById(id); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + venue.isActive = true; + await venue.save(); + + await invalidateActiveVenuesCache(); + await invalidateActiveVenueCache(id); + + return res.status(200).json({ + success: true, + message: "Venue activated successfully", + data: venue, + }); + } catch (error) { + console.error("Admin activate venue error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +const deactivateVenue = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel.findById(id); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + venue.isActive = false; + await venue.save(); + + await invalidateActiveVenuesCache(); + await invalidateActiveVenueCache(id); + + return res.status(200).json({ + success: true, + message: "Venue deactivated successfully", + data: venue, + }); + } catch (error) { + console.error("Admin deactivate venue error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +export { getVenues, getVenueById, activateVenue, deactivateVenue }; diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js new file mode 100644 index 0000000000..8cd8c00131 --- /dev/null +++ b/backend/src/controllers/authController.js @@ -0,0 +1,770 @@ +import bcrypt from "bcrypt"; +import userModel from "../models/userModel.js"; +import sendEmail from "../utils/sendEmail.js"; +import generateOtp from "../utils/generateOtp.js"; +import uploadToCloudinary from "../utils/uploadToCloudinary.js"; +import deleteFromCloudinary from "../utils/deleteFromCloudinary.js"; +import jwt from "jsonwebtoken"; + +const register = async (req, res) => { + try { + const { name, email, phone, password } = req.body; + + if (!name || !email || !phone || !password) { + return res.status(400).json({ + success: false, + message: "All fields are required" + }); + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ + success: false, + message: "Invalid email format" + }); + } + + const phoneRegex = /^[6-9]\d{9}$/; + + if (!phoneRegex.test(phone)) { + return res.status(400).json({ + success: false, + message: "Invalid phone number" + }); + } + + const passwordRegex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; + + if (!passwordRegex.test(password)) { + return res.status(400).json({ + success: false, + message: + "Password must contain uppercase, lowercase and number", + }); + } + if (password.length < 8) { + return res.status(400).json({ + success: false, + message: "Password must be at least 8 characters long" + }); + } + + const existingEmail = await userModel.findOne({ email }); + if (existingEmail) { + return res.status(400).json({ + success: false, + message: "Email already exists" + }); + } + + const existingPhone = await userModel.findOne({ phone }); + if (existingPhone) { + return res.status(400).json({ + success: false, + message: "Phone number already exists" + }); + } + + + const hashedPassword = await bcrypt.hash(password, 10); + + const { otp, expiresAt } = generateOtp(); + + + await sendEmail(email, otp,); + await userModel.create({ + name, + email, + phone, + password: hashedPassword, + otp, + otpExpiresAt: expiresAt, + }); + + + return res.status(201).json({ + success: true, + message: "Registration successful. Please check your email for the verification OTP.", + }); + } catch (error) { + console.error("Registration error:", error); + return res.status(500).json({ + success: false, + message: "Registration failed. Please try again.", + }); + } + +} + +//verify the regsterd users through otp + +const verifyEmail = async (req, res) => { + try { + const { email, otp } = req.body; + + if (!email || !otp) { + return res.status(400).json({ + success: false, + message: "Email and OTP are required" + }); + } + + const user = await userModel.findOne({ email }); + + if (!user) { + return res.status(401).json({ + success: false, + message: "Invalid email or OTP" + }); + } + + if (user.isEmailVerified) { + return res.status(400).json({ + success: false, + message: "Email is already verified" + }); + } + + if (user.otp !== otp) { + return res.status(400).json({ + success: false, + message: "Invalid OTP" + }); + } + + if (user.otpExpiresAt < new Date()) { + return res.status(400).json({ + success: false, + message: "OTP has expired" + }); + } + + user.isEmailVerified = true; + user.otp = null; + user.otpExpiresAt = null; + await user.save(); + + return res.status(200).json({ + success: true, + message: "Email verified successfully", + }); + } catch (error) { + console.error("Email verification error:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } + +} + + +//resend otp in verify email page + +const resendOtp = async (req, res) => { + try { + const { email } = req.body; + if (!email) { + return res.status(400).json({ + success: false, + message: "Email is required" + }); + } + + const user = await userModel.findOne({ email }); + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found" + }); + } + + if (user.isEmailVerified) { + return res.status(400).json({ + success: false, + message: "Email is already verified" + }); + } + + const { otp, expiresAt } = generateOtp(); + user.otp = otp; + user.otpExpiresAt = expiresAt; + await user.save(); + + await sendEmail(email, otp); + + return res.status(200).json({ + success: true, + message: "OTP resent successfully. Please check your email.", + }); + } catch (error) { + console.error("Resend OTP error:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } +} + +//login of the user +const login = async (req, res) => { + + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + success: false, + message: "Email and password are required" + }); + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!emailRegex.test(email)) { + return res.status(400).json({ + success: false, + message: "Invalid email format" + }); + } + + const user = await userModel.findOne({ email }); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found" + }); + } + + if (!user.isEmailVerified) { + return res.status(400).json({ + success: false, + message: "Please verify your email first" + }); + } + + if (!user.isActive) { + return res.status(403).json({ + success: false, + message: "Your account has been disabled. Please contact support." + }); + } + + const isMatch = await bcrypt.compare(password, user.password); + + if (!isMatch) { + return res.status(400).json({ + success: false, + message: "Invalid password" + }); + } + + user.lastLogin = new Date(); + await user.save(); + + + const token = jwt.sign( + { + id: user._id, + roles: user.roles, + }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: + process.env.NODE_ENV === "production" + ? "none" + : "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + + return res.status(200).json({ + success: true, + message: "Login successful", + user: { + id: user._id, + name: user.name, + email: user.email, + roles: user.roles, + profileImage: user.profileImage, + }, + }); + } catch (error) { + console.error("Login error:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } + + +} + +//change password +const forgotPassword = async (req, res) => { + + try { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ + success: false, + message: "Email is required" + }); + } + + const user = await userModel.findOne({ email }); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found" + }); + } + + const { otp, expiresAt } = generateOtp(); + user.otp = otp; + user.otpExpiresAt = expiresAt; + await user.save(); + + await sendEmail(user.email, otp, "reset"); + + return res.status(200).json({ + success: true, + message: "OTP sent to your email. Please check your inbox.", + }); + } + catch (error) { + console.error("Forgot password error:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } + +} + +const resetPassword = async (req, res) => { + try { + const { email, otp, newPassword } = req.body; + + if (!email || !otp || !newPassword) { + return res.status(400).json({ + success: false, + message: "Email, OTP and new password are required" + }); + } + + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; + if (!passwordRegex.test(newPassword)) { + return res.status(400).json({ + success: false, + message: "Password must contain 8 characters, uppercase, lowercase and number" + }); + } + + const user = await userModel.findOne({ email }); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found" + }); + } + + if (user.otp !== otp) { + return res.status(400).json({ + success: false, + message: "Invalid OTP" + }); + } + + if (user.otpExpiresAt < new Date()) { + return res.status(400).json({ + success: false, + message: "OTP has expired" + }); + } + + const hashedPassword = await bcrypt.hash(newPassword, 10); + user.password = hashedPassword; + user.otp = null; + user.otpExpiresAt = null; + await user.save(); + + return res.status(200).json({ + success: true, + message: "Password reset successful", + }); + } catch (error) { + console.error("Reset password error:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + + + + + + +const PHONE_REGEX = /^[6-9]\d{9}$/; +const GENDER_VALUES = ["male", "female", "other"]; + +//logged in user + +const getMe = async (req, res) => { + try { + const user = req.user; + + return res.status(200).json({ + success: true, + user: { + id: user._id, + name: user.name, + email: user.email, + phone: user.phone, + roles: user.roles, + profileImage: user.profileImage, + bio: user.bio, + address: user.address, + dob: user.dob, + gender: user.gender, + city: user.city, + state: user.state, + isEmailVerified: user.isEmailVerified, + isActive: user.isActive, + }, + }); + } catch (error) { + console.error("Error in getMe:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +const updateMe = async (req, res) => { + try { + const user = req.user; + + if (!user.isActive) { + return res.status(403).json({ + success: false, + message: + "Your account has been disabled. Please contact support.", + }); + } + + const { name, phone, bio, address, city, state, dob, gender } = req.body; + + const hasProfileUpdate = + name !== undefined || + phone !== undefined || + bio !== undefined || + address !== undefined || + city !== undefined || + state !== undefined || + dob !== undefined || + gender !== undefined; + + if (!hasProfileUpdate) { + return res.status(400).json({ + success: false, + message: "No valid profile fields to update", + }); + } + + if (name !== undefined) { + const trimmedName = String(name).trim(); + if (!trimmedName) { + return res.status(400).json({ + success: false, + message: "Name cannot be empty", + }); + } + if (trimmedName.length > 100) { + return res.status(400).json({ + success: false, + message: "Name must be 100 characters or fewer", + }); + } + user.name = trimmedName; + } + + if (phone !== undefined) { + const trimmedPhone = String(phone).trim(); + if (!PHONE_REGEX.test(trimmedPhone)) { + return res.status(400).json({ + success: false, + message: "Invalid phone number", + }); + } + if (trimmedPhone !== user.phone) { + const existingPhone = await userModel.findOne({ phone: trimmedPhone }); + if ( + existingPhone && + existingPhone._id.toString() !== user._id.toString() + ) { + return res.status(400).json({ + success: false, + message: "Phone number already exists", + }); + } + } + user.phone = trimmedPhone; + } + + if (bio !== undefined) { + const trimmedBio = String(bio).trim(); + if (trimmedBio.length > 500) { + return res.status(400).json({ + success: false, + message: "Bio must be 500 characters or fewer", + }); + } + user.bio = trimmedBio; + } + + if (address !== undefined) { + const trimmedAddress = String(address).trim(); + if (trimmedAddress.length > 300) { + return res.status(400).json({ + success: false, + message: "Address must be 300 characters or fewer", + }); + } + user.address = trimmedAddress; + } + + if (city !== undefined) { + const trimmedCity = String(city).trim(); + if (trimmedCity.length > 100) { + return res.status(400).json({ + success: false, + message: "City must be 100 characters or fewer", + }); + } + user.city = trimmedCity; + } + + if (state !== undefined) { + const trimmedState = String(state).trim(); + if (trimmedState.length > 100) { + return res.status(400).json({ + success: false, + message: "State must be 100 characters or fewer", + }); + } + user.state = trimmedState; + } + + if (gender !== undefined) { + const trimmedGender = String(gender).trim().toLowerCase(); + if (!GENDER_VALUES.includes(trimmedGender)) { + return res.status(400).json({ + success: false, + message: "Gender must be male, female, or other", + }); + } + user.gender = trimmedGender; + } + + if (dob !== undefined) { + if (dob === null || dob === "") { + user.dob = null; + } else { + const parsedDob = new Date(dob); + if (Number.isNaN(parsedDob.getTime())) { + return res.status(400).json({ + success: false, + message: "Invalid date of birth", + }); + } + if (parsedDob > new Date()) { + return res.status(400).json({ + success: false, + message: "Date of birth cannot be in the future", + }); + } + user.dob = parsedDob; + } + } + + await user.save(); + + return res.status(200).json({ + success: true, + message: "Profile updated", + user: { + id: user._id, + name: user.name, + email: user.email, + phone: user.phone, + roles: user.roles, + profileImage: user.profileImage, + bio: user.bio, + address: user.address, + dob: user.dob, + gender: user.gender, + city: user.city, + state: user.state, + isEmailVerified: user.isEmailVerified, + isActive: user.isActive, + }, + }); + } catch (error) { + if (error.code === 11000) { + return res.status(400).json({ + success: false, + message: "Phone number already exists", + }); + } + + console.error("Error in updateMe:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +const updateProfileImage = async (req, res) => { + try { + const user = req.user; + + if (!user.isActive) { + return res.status(403).json({ + success: false, + message: + "Your account has been disabled. Please contact support.", + }); + } + + if (!req.file) { + return res.status(400).json({ + success: false, + message: "Please upload a profile image", + }); + } + + if (user.profileImagePublicId) { + await deleteFromCloudinary(user.profileImagePublicId); + } + + const result = await uploadToCloudinary(req.file.buffer); + + user.profileImage = result.secure_url; + user.profileImagePublicId = result.public_id; + + await user.save(); + + return res.status(200).json({ + success: true, + message: "Profile image updated", + user: { + id: user._id, + name: user.name, + email: user.email, + phone: user.phone, + roles: user.roles, + profileImage: user.profileImage, + bio: user.bio, + address: user.address, + dob: user.dob, + gender: user.gender, + city: user.city, + state: user.state, + isEmailVerified: user.isEmailVerified, + isActive: user.isActive, + }, + }); + } catch (error) { + console.error("Error in updateProfileImage:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +const logout = (req, res) => { + try { + res.clearCookie("token", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + }); + + return res.status(200).json({ + success: true, + message: "Logout successful", + }); + } catch (error) { + console.error("Logout error:", error); + return res.status(500).json({ + success: false, + message: "Logout failed. Please try again.", + }); + } +} + + +//become provider , initially custumer then provider in one click + +const becomeProvider = async (req, res) => { + try { + + const user = req.user; + + if (user.roles.includes("provider")) { + return res.status(400).json({ + success: false, + message: "You are already a provider", + }); + } + + user.roles.push("provider"); + + await user.save(); + + return res.status(200).json({ + success: true, + message: "Provider account created successfully", + roles: user.roles, + }); + + } catch (error) { + console.error("Become provider error:", error); + + return res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + + +export { + register, + verifyEmail, + resendOtp, + login, + forgotPassword, + resetPassword, + getMe, + updateMe, + updateProfileImage, + logout, + becomeProvider, +}; \ No newline at end of file diff --git a/backend/src/controllers/bookingController.js b/backend/src/controllers/bookingController.js new file mode 100644 index 0000000000..c084a920c0 --- /dev/null +++ b/backend/src/controllers/bookingController.js @@ -0,0 +1,68 @@ +import venueAvailabilityModel from "../models/venueAvailabilityModel.js"; +import venueModel from "../models/venueModel.js"; +import bookingModel from "../models/bookingModel.js"; + + + +//get users all bookings for normal users profile + +const getMyBookings = async (req, res) => { + try { + + const bookings = await bookingModel.find({ + userId: req.user._id + }) + .populate("venueId") + .populate("availabilityId") + .sort({ createdAt: -1 }) + + return res.status(200).json({ + success: true, + message:"Fetched all bookings", + count: bookings.length, + data: bookings, + }); + + } catch (error) { + console.error("Error fetching bookings:",error); + return res.status(500).json({ + success:false, + message: "Something went wrong. Please try again later.", + }); + } +}; + + +//bookings for provider venues(fetch for provider) + +const getProviderBookings = async (req,res)=>{ + try { + const venues = await venueModel.find({ + ownerId: req.user._id, + }); + + const venueIds = venues.map((venue)=> venue._id); + + const bookings = await bookingModel.find({ + venueId:{$in: venueIds}, + }) + .populate("userId", "name email phone") + .populate("venueId") + .populate("availabilityId") + .sort({ createdAt: -1 }); + + return res.status(200).json({ + success: true, + count: bookings.length, + data: bookings, + }); + } catch (error) { + console.error("Error fetching provider bookings:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +} +export { getMyBookings, getProviderBookings } \ No newline at end of file diff --git a/backend/src/controllers/paymentController.js b/backend/src/controllers/paymentController.js new file mode 100644 index 0000000000..c2a40147d8 --- /dev/null +++ b/backend/src/controllers/paymentController.js @@ -0,0 +1,399 @@ +import venueModel from '../models/venueModel.js'; +import venueAvailabilityModel from '../models/venueAvailabilityModel.js'; +import bookingModel from '../models/bookingModel.js'; +import paymentOrderModel from '../models/paymentOrderModel.js'; +import Razorpay from 'razorpay'; +import crypto from 'crypto'; +import { invalidateVenueAvailabilityCache } from '../utils/cache.js'; + +const razorpayInstance = new Razorpay({ + key_id: process.env.RAZORPAY_KEY_ID, + key_secret: process.env.RAZORPAY_KEY_SECRET, +}); + +const isSlotExpired = (slot) => { + const now = new Date(); + + const slotDate = new Date(slot.date); + + const [time, period] = slot.endTime.split(" "); + + let [hours, minutes] = time.split(":").map(Number); + + if (period === "PM" && hours !== 12) { + hours += 12; + } + + if (period === "AM" && hours === 12) { + hours = 0; + } + + slotDate.setHours(hours, minutes, 0, 0); + + return slotDate < now; +}; + +const createOrder = async (req, res) => { + try { + const { venueId, availabilityId } = req.body; + if (!venueId || !availabilityId) { + return res.status(400).json({ + success: false, + message: "Venue ID and Availability ID are required", + }); + } + + const venue = await venueModel.findById(venueId); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + if (!venue.isActive) { + return res.status(400).json({ + success: false, + message: "Venue is inactive", + }); + } + if (venue.ownerId.toString() === req.user._id.toString()) { + return res.status(400).json({ + success: false, + message: "you cannot book your own venue", + }); + } + + const slot = await venueAvailabilityModel.findById(availabilityId); + + if (!slot) { + return res.status(404).json({ + success: false, + message: "Availability slot not found", + }); + } + + if (!slot.isActive) { + return res.status(400).json({ + success: false, + message: "This slot is inactive", + }); + } + + if (slot.isBooked) { + return res.status(400).json({ + success: false, + message: "This slot is already booked", + }); + } + if (isSlotExpired(slot)) { + return res.status(400).json({ + success: false, + message: "This slot has already expired", + }); + } + + if (slot.venueId.toString() !== venue._id.toString()) { + return res.status(400).json({ + success: false, + message: "Selected slot does not belong to this venue", + }); + } + + const amountInPaise = venue.price * 100; + + const options = { + amount: amountInPaise, + currency: "INR", + receipt: `BMV_${Date.now()}` + }; + + const order = await razorpayInstance.orders.create(options); + + await paymentOrderModel.create({ + userId: req.user._id, + venueId: venue._id, + availabilityId: slot._id, + razorpayOrderId: order.id, + amountInPaise, + }); + + return res.status(200).json({ + success: true, + message: "Order created successfully", + order, + }); + } catch (error) { + console.error("Create order error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + + +const verifyPayment = async (req, res) => { + try { + + const { + razorpay_order_id, + razorpay_payment_id, + razorpay_signature, + venueId, + availabilityId, + } = req.body; + + if ( + !razorpay_order_id || + !razorpay_payment_id || + !razorpay_signature || + !venueId || + !availabilityId + ) { + return res.status(400).json({ + success: false, + message: "Missing required payment details", + }); + } + + const paymentOrder = await paymentOrderModel.findOne({ + razorpayOrderId: razorpay_order_id, + }); + + if (!paymentOrder) { + return res.status(400).json({ + success: false, + message: "Invalid or unknown payment order", + }); + } + + const existingByPaymentId = await bookingModel.findOne({ + paymentId: razorpay_payment_id, + paymentStatus: "paid", + }); + + if (existingByPaymentId) { + if (existingByPaymentId.razorpayOrderId === razorpay_order_id) { + return res.status(200).json({ + success: true, + message: "Payment already verified", + data: existingByPaymentId, + }); + } + + return res.status(400).json({ + success: false, + message: "This payment has already been used for another booking", + }); + } + + if (paymentOrder.status === "completed") { + const existingBooking = await bookingModel.findOne({ + razorpayOrderId: razorpay_order_id, + paymentStatus: "paid", + }); + + if (existingBooking) { + return res.status(200).json({ + success: true, + message: "Payment already verified", + data: existingBooking, + }); + } + } + + if (paymentOrder.userId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "This payment order does not belong to you", + }); + } + + if (paymentOrder.venueId.toString() !== venueId.toString()) { + return res.status(400).json({ + success: false, + message: "Payment order does not match the selected venue", + }); + } + + if (paymentOrder.availabilityId.toString() !== availabilityId.toString()) { + return res.status(400).json({ + success: false, + message: "Payment order does not match the selected slot", + }); + } + + const generatedSignature = crypto + .createHmac("sha256", process.env.RAZORPAY_KEY_SECRET) + .update(`${razorpay_order_id}|${razorpay_payment_id}`) + .digest("hex"); + + if (generatedSignature !== razorpay_signature) { + return res.status(400).json({ + success: false, + message: "Payment verification failed", + }); + } + + let razorpayOrder; + try { + razorpayOrder = await razorpayInstance.orders.fetch(razorpay_order_id); + } catch { + return res.status(400).json({ + success: false, + message: "Unable to verify payment order with Razorpay", + }); + } + + if (razorpayOrder.amount !== paymentOrder.amountInPaise) { + return res.status(400).json({ + success: false, + message: "Payment amount does not match the expected order amount", + }); + } + + const venue = await venueModel.findById(venueId); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + if (!venue.isActive) { + return res.status(400).json({ + success: false, + message: "Venue is inactive", + }); + } + + if (venue.ownerId.toString() === req.user._id.toString()) { + return res.status(400).json({ + success: false, + message: "You cannot book your own venue", + }); + } + + const slot = await venueAvailabilityModel.findById(availabilityId); + + if (!slot) { + return res.status(404).json({ + success: false, + message: "Availability slot not found", + }); + } + + if (!slot.isActive) { + return res.status(400).json({ + success: false, + message: "This slot is inactive", + }); + } + + if (slot.isBooked) { + return res.status(400).json({ + success: false, + message: "This slot is already booked", + }); + } + + if (slot.venueId.toString() !== venue._id.toString()) { + return res.status(400).json({ + success: false, + message: "Selected slot does not belong to this venue", + }); + } + + const bookingAmount = paymentOrder.amountInPaise / 100; + + const bookingReference = + "BMV-" + Date.now().toString().slice(-6); + + const booking = await bookingModel.create({ + bookingReference, + userId: req.user._id, + venueId, + availabilityId, + amount: bookingAmount, + contactPhone: req.user.phone, + bookingStatus: "confirmed", + paymentStatus: "paid", + paymentMethod: "razorpay", + razorpayOrderId: razorpay_order_id, + paymentId: razorpay_payment_id, + }); + + const updatedSlot = + await venueAvailabilityModel.findOneAndUpdate( + { + _id: availabilityId, + isBooked: false, + isActive: true, + }, + { + $set: { + isBooked: true, + bookingId: booking._id, + }, + }, + { + new: true, + } + ); + + if (!updatedSlot) { + await bookingModel.findByIdAndDelete(booking._id); + + return res.status(400).json({ + success: false, + message: "This slot was already booked by another user", + }); + } + + paymentOrder.status = "completed"; + await paymentOrder.save(); + + await invalidateVenueAvailabilityCache(venueId); + + return res.status(201).json({ + success: true, + message: "Payment verified and venue booked successfully", + data: booking, + }); + + } catch (error) { + console.error("Verify payment error:", error); + + if (error.code === 11000 && error.keyPattern?.paymentId) { + const existingBooking = await bookingModel.findOne({ + paymentId: req.body.razorpay_payment_id, + paymentStatus: "paid", + }); + + if (existingBooking) { + return res.status(200).json({ + success: true, + message: "Payment already verified", + data: existingBooking, + }); + } + + return res.status(400).json({ + success: false, + message: "This payment has already been used for another booking", + }); + } + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + + +export { createOrder, verifyPayment } diff --git a/backend/src/controllers/venueAvailabilityController.js b/backend/src/controllers/venueAvailabilityController.js new file mode 100644 index 0000000000..72f875177c --- /dev/null +++ b/backend/src/controllers/venueAvailabilityController.js @@ -0,0 +1,285 @@ +import mongoose from "mongoose"; +import venueAvailabilityModel from "../models/venueAvailabilityModel.js"; +import venueModel from "../models/venueModel.js"; +import { isValidSlotTime, parseSlotTimeToMinutes, } from "../utils/parseSlotTime.js"; +import { getCache, setCache, getVenueAvailabilityCacheKey, invalidateVenueAvailabilityCache, VENUE_AVAILABILITY_CACHE_TTL, } from "../utils/cache.js"; + +const createAvailability = async (req, res) => { + try { + const { + venueId, date, slotLabel, startTime, endTime, } = req.body; + + if (!venueId || !date || !slotLabel || !startTime || !endTime) { + return res.status(400).json({ + success: false, + message: "Please fill all required fields", + }); + } + + if (!mongoose.Types.ObjectId.isValid(venueId)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel.findById(venueId); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + // Ownership check + if (venue.ownerId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "You can only manage your own venue", + }); + } + const selectedDate = new Date(date); + + if (Number.isNaN(selectedDate.getTime())) { + return res.status(400).json({ + success: false, + message: "Invalid date", + }); + } + + const today = new Date(); + + today.setHours(0, 0, 0, 0); + selectedDate.setHours(0, 0, 0, 0); + + if (selectedDate < today) { + return res.status(400).json({ + success: false, + message: "Cannot create availability for past dates", + }); + } + const startMinutes = parseSlotTimeToMinutes(startTime); + const endMinutes = parseSlotTimeToMinutes(endTime); + + if (!isValidSlotTime(startTime) || !isValidSlotTime(endTime)) { + return res.status(400).json({ + success: false, + message: "Invalid time format", + }); + } + + if (startMinutes >= endMinutes) { + return res.status(400).json({ + success: false, + message: "End time must be after start time", + }); + } + + const existingSlot = await venueAvailabilityModel.findOne({ + venueId, date, slotLabel, + }); + + if (existingSlot) { + return res.status(400).json({ + success: false, + message: "Slot already exists for this date", + }); + } + + + + const availability = + await venueAvailabilityModel.create({ + venueId, date, slotLabel, startTime, endTime, + }); + + await invalidateVenueAvailabilityCache(venueId); + + return res.status(201).json({ + success: true, + message: "Availability created successfully", + data: availability, + }); + + } catch (error) { + console.error("Create availability error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later." + }); + } +}; + + +const getVenueAvailability = async (req, res) => { + try { + + const { venueId } = req.params; + + if (!mongoose.Types.ObjectId.isValid(venueId)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const cacheKey = getVenueAvailabilityCacheKey(venueId); + const cached = await getCache(cacheKey); + if (cached) { + return res.status(200).json(cached); + } + + const availability = await venueAvailabilityModel.find({ venueId, }).sort({ + date: 1, + startTime: 1, + }); + + const payload = { + success: true, + count: availability.length, + data: availability, + }; + + await setCache(cacheKey, payload, VENUE_AVAILABILITY_CACHE_TTL); + + return res.status(200).json(payload); + + } catch (error) { + console.error("Get availability error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later." + }); + } +}; + +//deactivate venue slotes of if not available by provider + +const deactivateAvailability = async (req, res) => { + try { + + + const { slotId } = req.params; + if (!mongoose.Types.ObjectId.isValid(slotId)) { + return res.status(400).json({ + success: false, + message: "Invalid slot ID", + }); + } + const slot = await venueAvailabilityModel.findById(slotId); + + if (!slot) { + return res.status(404).json({ + success: false, + message: "Slot not found", + }); + } + + const venue = await venueModel.findById(slot.venueId); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + if (venue.ownerId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "Unauthorized", + + }) + } + + + if (slot.isBooked) { + return res.status(400).json({ + success: false, + message: "Booked slots cannot be deactivated", + }); + } + slot.isActive = false; + await slot.save(); + + await invalidateVenueAvailabilityCache(slot.venueId); + + return res.status(200).json({ + success: true, + message: "Slot deactivated successfully", + data: slot, + }); + + } catch (error) { + console.error("Deactivated slot error", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later." + }); + } +}; + +//acivate that + +const activateAvailability = async (req, res) => { + try { + const { slotId } = req.params; + if (!mongoose.Types.ObjectId.isValid(slotId)) { + return res.status(400).json({ + success: false, + message: "Invalid slot ID", + }); + } + + const slot = await venueAvailabilityModel.findById(slotId); + + if (!slot) { + return res.status(404).json({ + success: false, + message: "Slot not found", + }); + } + + const venue = await venueModel.findById(slot.venueId); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + if (venue.ownerId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "Unauthorized", + }); + } + + slot.isActive = true; + await slot.save(); + + await invalidateVenueAvailabilityCache(slot.venueId); + + return res.status(200).json({ + success: true, + message: "Slot activated successfully", + data: slot, + }); + } catch (error) { + console.error("Activate slot error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later." + }); + } +}; + + + +export { createAvailability, getVenueAvailability, deactivateAvailability, activateAvailability }; \ No newline at end of file diff --git a/backend/src/controllers/venueController.js b/backend/src/controllers/venueController.js new file mode 100644 index 0000000000..1c056347f5 --- /dev/null +++ b/backend/src/controllers/venueController.js @@ -0,0 +1,706 @@ +import venueModel from "../models/venueModel.js"; +import uploadToCloudinary from "../utils/uploadToCloudinary.js"; +import deleteFromCloudinary from "../utils/deleteFromCloudinary.js"; +import { resolveVenueCategory } from "../utils/venueCategory.js"; +import { getCache, setCache, getActiveVenueCacheKey, invalidateActiveVenuesCache, invalidateActiveVenueCache, ACTIVE_VENUES_CACHE_KEY, ACTIVE_VENUES_CACHE_TTL, } from "../utils/cache.js"; +import mongoose from "mongoose"; + + +const parseJsonArrayField = (value, fieldName) => { + if (!value) { + return { data: [] }; + } + + try { + const parsed = JSON.parse(value); + + if (!Array.isArray(parsed)) { + return { + error: `Invalid ${fieldName}. Expected a JSON array.`, + }; + } + + return { data: parsed }; + } catch { + return { + error: `Invalid ${fieldName}. Please provide valid JSON.`, + }; + } +}; + +//provider creates venue +const createVenue = async (req, res) => { + const uploadedImages = []; + + try { + const { + title, + description, + category, + venueType, + indoorOutdoor, + price, + pricingUnit, + capacity, + amenities, + rules, + address, + city, + state, + pincode, + latitude, + longitude, + } = req.body; + + if ( + !title || + !description || + !category || + !price || + !capacity || + !address + ) { + return res.status(400).json({ + success: false, + message: "Please fill all required fields.", + }); + } + + // Validate numeric fields + const parsedPrice = Number(price); + const parsedCapacity = Number(capacity); + + if (Number.isNaN(parsedPrice) || parsedPrice <= 0) { + return res.status(400).json({ + success: false, + message: "Invalid price.", + }); + } + + if (Number.isNaN(parsedCapacity) || parsedCapacity <= 0 || !Number.isInteger(parsedCapacity)) { + return res.status(400).json({ + success: false, + message: "Invalid capacity.", + }); + } + + let parsedLatitude; + let parsedLongitude; + + if (latitude !== undefined && latitude !== "") { + parsedLatitude = Number(latitude); + + if ( + Number.isNaN(parsedLatitude) || + parsedLatitude < -90 || + parsedLatitude > 90 + ) { + return res.status(400).json({ + success: false, + message: "Invalid latitude.", + }); + } + } + + if (longitude !== undefined && longitude !== "") { + parsedLongitude = Number(longitude); + + if ( + Number.isNaN(parsedLongitude) || + parsedLongitude < -180 || + parsedLongitude > 180 + ) { + return res.status(400).json({ + success: false, + message: "Invalid longitude.", + }); + } + } + + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + success: false, + message: "Please upload at least one image.", + }); + } + + // Upload images + for (const file of req.files) { + const result = await uploadToCloudinary(file.buffer); + + uploadedImages.push({ + url: result.secure_url, + public_id: result.public_id, + }); + } + + const parsedAmenities = parseJsonArrayField( + amenities, + "amenities" + ); + + if (parsedAmenities.error) { + return res.status(400).json({ + success: false, + message: parsedAmenities.error, + }); + } + + const parsedRules = parseJsonArrayField( + rules, + "rules" + ); + + if (parsedRules.error) { + return res.status(400).json({ + success: false, + message: parsedRules.error, + }); + } + + const normalizedCategory = resolveVenueCategory(category); + + if (!normalizedCategory) { + return res.status(400).json({ + success: false, + message: "Invalid venue category.", + }); + } + + const venuePayload = { + ownerId: req.user._id, + title, + description, + category: normalizedCategory, + venueType, + indoorOutdoor, + price: parsedPrice, + capacity: parsedCapacity, + amenities: parsedAmenities.data, + rules: parsedRules.data, + address, + city, + state, + pincode, + location: { + latitude: parsedLatitude, + longitude: parsedLongitude, + }, + images: uploadedImages, + coverImage: uploadedImages[0], + }; + + if (pricingUnit !== undefined && pricingUnit !== "") { + venuePayload.pricingUnit = pricingUnit; + } + + const venue = await venueModel.create(venuePayload); + + await invalidateActiveVenuesCache(); + + return res.status(201).json({ + success: true, + message: "Venue created successfully.", + data: venue, + }); + } catch (error) { + console.error("Create venue error:", error); + + // delete uploaded images from cloudinary if error occurs + for (const image of uploadedImages) { + try { + await deleteFromCloudinary(image.public_id); + } catch (cleanupError) { + console.error( + "Cloudinary cleanup failed:", + cleanupError + ); + } + } + + return res.status(500).json({ + success: false, + message: "Failed to create venue. Please try again.", + }); + } +}; + +//fetch owners all venues + +const getMyVenues = async (req, res) => { + try { + const venues = await venueModel.find({ ownerId: req.user._id }) + .sort({ createdAt: -1 }); + + return res.status(200).json({ + success: true, + data: venues, + count: venues.length, + }); + } catch (error) { + console.error("Error fetching my venues:", error); + return res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +//show details of a venue for provider + +const getVenueById = async (req, res) => { + try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel.findById(id); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found.", + }); + } + + if (venue.ownerId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "Access denied.", + }); + } + + return res.status(200).json({ + success: true, + data: venue, + }); + } catch (error) { + console.error("Error fetching venue details:", error); + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +//update my-venue + +const updateVenue = async (req, res) => { + const newlyUploadedImages = []; + + try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel.findById(id); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + if (venue.ownerId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "Access denied", + }); + } + + const { + title, + description, + category, + venueType, + indoorOutdoor, + price, + pricingUnit, + capacity, + amenities, + rules, + address, + city, + state, + pincode, + latitude, + longitude, + } = req.body; + + if (title !== undefined) venue.title = title; + if (description !== undefined) venue.description = description; + + if (category !== undefined) { + const normalizedCategory = resolveVenueCategory(category); + + if (!normalizedCategory) { + return res.status(400).json({ + success: false, + message: "Invalid venue category.", + }); + } + + venue.category = normalizedCategory; + } + if (venueType !== undefined) venue.venueType = venueType; + if (indoorOutdoor !== undefined) venue.indoorOutdoor = indoorOutdoor; + if (address !== undefined) venue.address = address; + if (city !== undefined) venue.city = city; + if (state !== undefined) venue.state = state; + if (pincode !== undefined) venue.pincode = pincode; + + // Price validation + if (price !== undefined) { + const parsedPrice = Number(price); + + if (Number.isNaN(parsedPrice) || parsedPrice <= 0) { + return res.status(400).json({ + success: false, + message: "Invalid price.", + }); + } + + venue.price = parsedPrice; + } + + // Capacity validation + if (capacity !== undefined) { + const parsedCapacity = Number(capacity); + + if ( + Number.isNaN(parsedCapacity) || + parsedCapacity <= 0 || + !Number.isInteger(parsedCapacity) + ) { + return res.status(400).json({ + success: false, + message: "Invalid capacity.", + }); + } + + venue.capacity = parsedCapacity; + } + + // Amenities + if (amenities !== undefined) { + const parsedAmenities = parseJsonArrayField( + amenities, + "amenities" + ); + + if (parsedAmenities.error) { + return res.status(400).json({ + success: false, + message: parsedAmenities.error, + }); + } + + venue.amenities = parsedAmenities.data; + } + + // Rules + if (rules !== undefined) { + const parsedRules = parseJsonArrayField( + rules, + "rules" + ); + + if (parsedRules.error) { + return res.status(400).json({ + success: false, + message: parsedRules.error, + }); + } + + venue.rules = parsedRules.data; + } + + // Location validation + if (latitude !== undefined && latitude !== "") { + const parsedLatitude = Number(latitude); + + if ( + Number.isNaN(parsedLatitude) || + parsedLatitude < -90 || + parsedLatitude > 90 + ) { + return res.status(400).json({ + success: false, + message: "Invalid latitude.", + }); + } + + venue.location.latitude = parsedLatitude; + } + + if (longitude !== undefined && longitude !== "") { + const parsedLongitude = Number(longitude); + + if ( + Number.isNaN(parsedLongitude) || + parsedLongitude < -180 || + parsedLongitude > 180 + ) { + return res.status(400).json({ + success: false, + message: "Invalid longitude.", + }); + } + + venue.location.longitude = parsedLongitude; + } + + // Upload new images FIRST + if (req.files && req.files.length > 0) { + + for (const file of req.files) { + const result = await uploadToCloudinary(file.buffer); + + newlyUploadedImages.push({ + url: result.secure_url, + public_id: result.public_id, + }); + } + + // Delete old images only after successful upload + for (const image of venue.images) { + await deleteFromCloudinary(image.public_id); + } + + venue.images = newlyUploadedImages; + venue.coverImage = newlyUploadedImages[0]; + } + + await venue.save(); + + await invalidateActiveVenuesCache(); + await invalidateActiveVenueCache(id); + + return res.status(200).json({ + success: true, + message: "Venue updated successfully", + data: venue, + }); + + } catch (error) { + + // Cleanup newly uploaded images if save fails + for (const image of newlyUploadedImages) { + try { + await deleteFromCloudinary(image.public_id); + } catch (cleanupError) { + console.error( + "Cloudinary cleanup failed:", + cleanupError + ); + } + } + + console.error("Update venue error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +//deactivate venue already listed + +const deactivateVenue = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel.findById(id); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found" + }); + } + + if (venue.ownerId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "Access denied", + }); + } + + venue.isActive = false; + + await venue.save(); + + await invalidateActiveVenuesCache(); + await invalidateActiveVenueCache(id); + + return res.status(200).json({ + success: true, + message: "Venue deactivated successfully", + data: venue, + }); + + + } catch (error) { + console.error("Deactivate venue error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +//acivate owner venue which listed + +const activateVenue = async (req, res) => { + try { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const venue = await venueModel.findById(id); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + if (venue.ownerId.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + message: "Access denied", + }); + } + + venue.isActive = true; + + await venue.save(); + + await invalidateActiveVenuesCache(); + await invalidateActiveVenueCache(id); + + return res.status(200).json({ + success: true, + message: "Venue activated successfully", + data: venue, + }); + + } catch (error) { + console.error("Activate venue error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +}; + +//get venues for users/public + +const getAllVenues = async (req, res) => { + try { + const cached = await getCache(ACTIVE_VENUES_CACHE_KEY); + if (cached) { + return res.status(200).json(cached); + } + console.log("❌ Redis MISS"); + + const venues = await venueModel.find({ + isActive: true, + }).select("-__v").sort({ createdAt: -1 }); + + const payload = { + success: true, + count: venues.length, + data: venues, + }; + + await setCache(ACTIVE_VENUES_CACHE_KEY, payload, ACTIVE_VENUES_CACHE_TTL); + console.log("Saved data to Redis"); + return res.status(200).json(payload); + + } catch (error) { + console.error("Get venues error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +} + +//get venue details for public/users + +const getPublicVenueById = async (req, res) => { + try { + const { venueId } = req.params; + if (!mongoose.Types.ObjectId.isValid(venueId)) { + return res.status(400).json({ + success: false, + message: "Invalid venue ID", + }); + } + + const cacheKey = getActiveVenueCacheKey(venueId); + const cached = await getCache(cacheKey); + if (cached) { + return res.status(200).json(cached); + } + + const venue = await venueModel.findOne({ + _id: venueId, + isActive: true, + }); + + if (!venue) { + return res.status(404).json({ + success: false, + message: "Venue not found", + }); + } + + const payload = { + success: true, + data: venue, + }; + + await setCache(cacheKey, payload, ACTIVE_VENUES_CACHE_TTL); + + return res.status(200).json(payload); + } catch (error) { + console.error("Get venue error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } +} +export { + createVenue, getMyVenues, getVenueById, updateVenue, deactivateVenue, activateVenue, + getAllVenues, getPublicVenueById +}; \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000000..584e3abbd7 --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,65 @@ +import express from "express"; +import cors from "cors"; +import connection from "./config/db.js"; +import { connectRedis } from "./config/redis.js"; +import dotenv from "dotenv"; +import authRouter from "./routes/authRoutes.js"; +import venueRouter from "./routes/venueRoutes.js"; +import cookieParser from "cookie-parser"; +import venueAvailabilityRoutes from './routes/venueAvailabilityRoutes.js' +import bookingRoutes from './routes/bookingRoutes.js' +import paymentRoutes from './routes/paymentRoutes.js' +import adminRouter from "./routes/adminRoutes.js" +import swaggerUi from "swagger-ui-express"; +import swaggerSpec from "./config/swagger.js"; + + +dotenv.config(); +const app = express(); + +//middleware +const allowedOrigins = [ + "http://localhost:5173", + process.env.CLIENT_URL, +]; + +app.use( + cors({ + origin: allowedOrigins, + credentials: true, + }) +); + +app.use(express.json()); +app.use(cookieParser()); + +//db +connection(); + +//redis +connectRedis(); + +app.use("/api/auth", authRouter); +app.use("/api/venues", venueRouter); +app.use('/api/availability', venueAvailabilityRoutes); +app.use("/api/bookings", bookingRoutes); +app.use("/api/payments", paymentRoutes); +app.use("/api/admin", adminRouter); + +if (process.env.ENABLE_SWAGGER === "true") { + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); +} + +const PORT = process.env.PORT || 5000; + +app.get("/", (req, res) => { + res.status(200).json({ + success: true, + message: "BookMyVenue API is running", + environment: process.env.NODE_ENV + }); +}); + +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); \ No newline at end of file diff --git a/backend/src/middleware/authorizeRoles.js b/backend/src/middleware/authorizeRoles.js new file mode 100644 index 0000000000..41773ed283 --- /dev/null +++ b/backend/src/middleware/authorizeRoles.js @@ -0,0 +1,34 @@ +const authorizeRoles = (...allowedRoles) => { + return (req, res, next) => { + try { + const userRoles = req.user.roles; + + if (!userRoles || userRoles.length === 0) { + return res.status(403).json({ + success: false, + message: "Access denied. No roles assigned.", + }); + } + + const hasAccess = userRoles.some(role => allowedRoles.includes(role)); + + if (!hasAccess) { + return res.status(403).json({ + success: false, + message: "Access denied. You do not have the permission to perform this action.", + }); + } + + next(); + } catch (error) { + console.error("Role authorization error:", error); + + return res.status(500).json({ + success: false, + message: "Something went wrong. Please try again later.", + }); + } + }; +}; + +export default authorizeRoles; \ No newline at end of file diff --git a/backend/src/middleware/upload.js b/backend/src/middleware/upload.js new file mode 100644 index 0000000000..74dad18844 --- /dev/null +++ b/backend/src/middleware/upload.js @@ -0,0 +1,9 @@ +import multer from "multer"; + +const storage = multer.memoryStorage(); + +const upload = multer({ storage: storage, + limits: { fileSize: 5 * 1024 * 1024 }, // mx 5MB +}); + +export default upload; diff --git a/backend/src/middleware/userAuthMiddleware.js b/backend/src/middleware/userAuthMiddleware.js new file mode 100644 index 0000000000..9e6d06dd35 --- /dev/null +++ b/backend/src/middleware/userAuthMiddleware.js @@ -0,0 +1,46 @@ +import jwt from "jsonwebtoken"; +import userModel from "../models/userModel.js"; + +const authMiddleware = async (req, res, next) => { + try { + const token = req.cookies.token; + + if (!token) { + return res.status(401).json({ + success: false, + message: "Unauthorized access. Please login.", + }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + const user = await userModel.findById(decoded.id) + .select("-password -otp -otpExpiresAt"); + + if (!user) { + return res.status(401).json({ + success: false, + message: "Unauthorized access. User not found.", + }); + } + + if (!user.isActive) { + return res.status(403).json({ + success: false, + message: "Your account has been deactivated. Please contact support.", + }); + } + + req.user = user; + next(); + } catch (error) { + console.error("Error in authMiddleware:", error); + return res.status(401).json({ + success: false, + message: "Unauthorized access. Invalid token.", + }); + } +}; + + +export default authMiddleware; \ No newline at end of file diff --git a/backend/src/models/bookingModel.js b/backend/src/models/bookingModel.js new file mode 100644 index 0000000000..e30882864d --- /dev/null +++ b/backend/src/models/bookingModel.js @@ -0,0 +1,77 @@ +import mongoose from "mongoose"; + + +const bookingSchema = new mongoose.Schema( + { + bookingReference: { + type: String, + required: true, + unique: true, + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + venueId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Venue", + required: true, + }, + availabilityId: { + type: mongoose.Schema.Types.ObjectId, + ref: "VenueAvailability", + required: true, + }, + amount: { + type: Number, + required: true, + }, + bookingStatus: { + type: String, + enum: ["confirmed", "cancelled"], + default: "confirmed", + }, + paymentMethod: { + type: String, + default: null, + }, + razorpayOrderId: { + type: String, + default: null, + }, + paymentId: { + type: String, + default: null, + unique: true, + sparse: true, + }, + + paymentStatus: { + type: String, + enum: ["pending", "paid", "failed", "refunded"], + default: "pending", + }, + + contactPhone: { + type: String, + required: true, + }, + + bookedAt: { + type: Date, + default: Date.now, + }, + + cancelledAt: { + type: Date, + default: null, + }, + }, + { + timestamps: true, + } +) + +const bookingModel = mongoose.model("Bookings", bookingSchema); +export default bookingModel; \ No newline at end of file diff --git a/backend/src/models/paymentOrderModel.js b/backend/src/models/paymentOrderModel.js new file mode 100644 index 0000000000..de7ac6b2ee --- /dev/null +++ b/backend/src/models/paymentOrderModel.js @@ -0,0 +1,42 @@ +import mongoose from "mongoose"; + +const paymentOrderSchema = new mongoose.Schema( + { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + venueId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Venue", + required: true, + }, + availabilityId: { + type: mongoose.Schema.Types.ObjectId, + ref: "VenueAvailability", + required: true, + }, + razorpayOrderId: { + type: String, + required: true, + unique: true, + }, + amountInPaise: { + type: Number, + required: true, + }, + status: { + type: String, + enum: ["created", "completed"], + default: "created", + }, + }, + { + timestamps: true, + } +); + +const paymentOrderModel = mongoose.model("PaymentOrder", paymentOrderSchema); + +export default paymentOrderModel; diff --git a/backend/src/models/userModel.js b/backend/src/models/userModel.js new file mode 100644 index 0000000000..5884499a43 --- /dev/null +++ b/backend/src/models/userModel.js @@ -0,0 +1,115 @@ +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, + }, + + phone: { + type: String, + required: true, + unique: true, + trim: true, + }, + + password: { + type: String, + required: true, + }, + + isEmailVerified: { + type: Boolean, + default: false, + }, + + otp: { + type: String, + default: null, + }, + + otpExpiresAt: { + type: Date, + default: null, + }, + + roles: { + type: [String], + enum: ["customer", "provider", "admin"], + default: ["customer"], + }, + + profileImage: { + type: String, + default: "", + }, + + profileImagePublicId: { + type: String, + default: "", + }, + + bio: { + type: String, + default: "", + }, + + address: { + type: String, + default: "", + }, + + dob: { + type: Date, + default: null, + }, + + gender: { + type: String, + enum: ["male", "female", "other"], + default: "other", + }, + + city: { + type: String, + default: "", + }, + + state: { + type: String, + default: "", + }, + + providerProfileCompleted: { + type: Boolean, + default: false, + }, + + isActive: { + type: Boolean, + default: true, + }, + + lastLogin: { + type: Date, + default: null, + }, + }, + { + timestamps: true, + } +); + +const userModel = mongoose.model("User", userSchema); + +export default userModel; \ No newline at end of file diff --git a/backend/src/models/venueAvailabilityModel.js b/backend/src/models/venueAvailabilityModel.js new file mode 100644 index 0000000000..081df07143 --- /dev/null +++ b/backend/src/models/venueAvailabilityModel.js @@ -0,0 +1,56 @@ +import mongoose from "mongoose"; + +const venueAvailabilitySchema = new mongoose.Schema( + { + venueId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Venue", + required: true, + }, + + date: { + type: Date, + required: true, + }, + + slotLabel: { + type: String, + enum: ["morning", "evening", "night", "fullday"], + required: true, + }, + + startTime: { + type: String, + required: true, + }, + + endTime: { + type: String, + required: true, + }, + + isBooked: { + type: Boolean, + default: false, + }, + + bookingId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Booking", + default: null, + }, + + isActive: { + type: Boolean, + default: true, + }, + }, + { + timestamps: true, + } +); + +const venueAvailabilityModel = mongoose.model("VenueAvailability",venueAvailabilitySchema +); + +export default venueAvailabilityModel; \ No newline at end of file diff --git a/backend/src/models/venueModel.js b/backend/src/models/venueModel.js new file mode 100644 index 0000000000..ca740e9a8e --- /dev/null +++ b/backend/src/models/venueModel.js @@ -0,0 +1,141 @@ +import mongoose from "mongoose"; + +const venueSchema = new mongoose.Schema( + { + ownerId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + + title: { + type: String, + required: true, + trim: true, + }, + + description: { + type: String, + required: true, + }, + + category:{ + type:String, + enum:[ + "wedding", + "corporate", + "birthday", + "party", + "function", + "photoshoot", + "other" + ], + required:true + }, + + images: [ + { + url: { + type: String, + required: true, + }, + + public_id: { + type: String, + required: true, + }, + }, + ], + coverImage: { + url: { + type: String, + default: "", + }, + + public_id: { + type: String, + default: "", + }, + }, + venueType: { + type: String, + enum: ["offline", "online", "hybrid"], + default: "offline", + }, + + indoorOutdoor: { + type: String, + enum: ["indoor", "outdoor", "both"], + default: "indoor", + }, + + price: { + type: Number, + required: true, + }, + + pricingUnit: { + type: String, + enum: ["perhour", "perday"], + default: "perhour", + }, + + capacity: { + type: Number, + required: true, + }, + + amenities: [ + { + type: String, + }, + ], + rules: [ + { + type: String, + }, + ], + + address: { + type: String, + required: true, + }, + + city: String, + state: String, + pincode: String, + + location: { + latitude: { + type: Number, + default: null, + }, + + longitude: { + type: Number, + default: null, + }, + }, + + averageRating: { + type: Number, + default: 0, + }, + + totalReviews: { + type: Number, + default: 0, + }, + + isActive: { + type: Boolean, + default: true, + }, + }, + { + timestamps: true, + } +); + +const venueModel = mongoose.model("Venue", venueSchema); +export default venueModel; \ No newline at end of file diff --git a/backend/src/routes/adminRoutes.js b/backend/src/routes/adminRoutes.js new file mode 100644 index 0000000000..1de6e5a9af --- /dev/null +++ b/backend/src/routes/adminRoutes.js @@ -0,0 +1,36 @@ +import express from "express"; +import userAuthMiddleware from "../middleware/userAuthMiddleware.js"; +import authorizeRoles from "../middleware/authorizeRoles.js"; +import { getDashboardStats, getRecentActivity} from "../controllers/adminDashboardController.js"; +import { getUsers,getUserById,activateUser,deactivateUser} from "../controllers/adminUserController.js"; +import { getVenues,getVenueById,activateVenue,deactivateVenue} from "../controllers/adminVenueController.js"; +import { getBookings,getBookingById} from "../controllers/adminBookingController.js"; +import { getPaymentOrders,getPaymentHistory,getAbandonedPayments} from "../controllers/adminPaymentController.js"; + + +const adminRouter = express.Router(); + +adminRouter.use(userAuthMiddleware); +adminRouter.use(authorizeRoles("admin")); + +adminRouter.get("/dashboard/stats", getDashboardStats); +adminRouter.get("/dashboard/recent-activity", getRecentActivity); + +adminRouter.get("/users", getUsers); +adminRouter.get("/users/:id", getUserById); +adminRouter.patch("/users/:id/activate", activateUser); +adminRouter.patch("/users/:id/deactivate", deactivateUser); + +adminRouter.get("/venues", getVenues); +adminRouter.get("/venues/:id", getVenueById); +adminRouter.patch("/venues/:id/activate", activateVenue); +adminRouter.patch("/venues/:id/deactivate", deactivateVenue); + +adminRouter.get("/bookings", getBookings); +adminRouter.get("/bookings/:id", getBookingById); + +adminRouter.get("/payments/orders", getPaymentOrders); +adminRouter.get("/payments/history", getPaymentHistory); +adminRouter.get("/payments/abandoned", getAbandonedPayments); + +export default adminRouter; diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js new file mode 100644 index 0000000000..ff6465b29c --- /dev/null +++ b/backend/src/routes/authRoutes.js @@ -0,0 +1,24 @@ +import express from "express"; +import {becomeProvider,forgotPassword,getMe,login,logout,register,resendOtp,resetPassword,updateMe,updateProfileImage,verifyEmail} from "../controllers/authController.js"; +import authMiddleware from "../middleware/userAuthMiddleware.js"; +import upload from "../middleware/upload.js"; + + + +const authRouter = express.Router(); + +authRouter.post("/register", register); +authRouter.post("/verify-email", verifyEmail); +authRouter.post("/resend-otp", resendOtp); +authRouter.post("/login", login); +authRouter.post("/forgot-password", forgotPassword); +authRouter.post("/reset-password", resetPassword); +authRouter.get("/me", authMiddleware, getMe); +authRouter.patch("/me", authMiddleware, updateMe); +authRouter.patch("/me/avatar",authMiddleware,upload.single("profileImage"), updateProfileImage); +authRouter.post("/logout", logout); + +authRouter.patch("/become-provider", authMiddleware, becomeProvider); + + +export default authRouter; \ No newline at end of file diff --git a/backend/src/routes/bookingRoutes.js b/backend/src/routes/bookingRoutes.js new file mode 100644 index 0000000000..b53b4fbf84 --- /dev/null +++ b/backend/src/routes/bookingRoutes.js @@ -0,0 +1,12 @@ +import express from 'express'; +import { getMyBookings,getProviderBookings } from '../controllers/bookingController.js'; +import userAuthMiddleware from '../middleware/userAuthMiddleware.js'; +import authorizeRoles from '../middleware/authorizeRoles.js'; + +const router = express.Router(); + +router.get("/my-bookings", userAuthMiddleware, getMyBookings); +router.get("/provider-bookings",userAuthMiddleware,authorizeRoles("provider"),getProviderBookings) + + +export default router; diff --git a/backend/src/routes/paymentRoutes.js b/backend/src/routes/paymentRoutes.js new file mode 100644 index 0000000000..4e106c49fb --- /dev/null +++ b/backend/src/routes/paymentRoutes.js @@ -0,0 +1,12 @@ +import express from 'express'; +import userAuthMiddleware from '../middleware/userAuthMiddleware.js'; +import { createOrder, verifyPayment } from '../controllers/paymentController.js'; + + + +const router = express.Router() + +router.post("/create-order", userAuthMiddleware, createOrder); +router.post("/verify-payment", userAuthMiddleware, verifyPayment); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/venueAvailabilityRoutes.js b/backend/src/routes/venueAvailabilityRoutes.js new file mode 100644 index 0000000000..43b1a41906 --- /dev/null +++ b/backend/src/routes/venueAvailabilityRoutes.js @@ -0,0 +1,12 @@ +import express from 'express' +import { activateAvailability, createAvailability, deactivateAvailability, getVenueAvailability } from '../controllers/venueAvailabilityController.js' +import userAuthMiddleware from "../middleware/userAuthMiddleware.js"; +import authorizeRoles from '../middleware/authorizeRoles.js'; + +const router = express.Router(); + +router.post("/create", userAuthMiddleware, authorizeRoles("provider"), createAvailability) +router.get("/:venueId",getVenueAvailability) +router.patch("/deactivate/:slotId",userAuthMiddleware,authorizeRoles("provider"), deactivateAvailability); +router.patch("/activate/:slotId",userAuthMiddleware,authorizeRoles("provider"),activateAvailability); +export default router; \ No newline at end of file diff --git a/backend/src/routes/venueRoutes.js b/backend/src/routes/venueRoutes.js new file mode 100644 index 0000000000..de749402ee --- /dev/null +++ b/backend/src/routes/venueRoutes.js @@ -0,0 +1,28 @@ +import express from "express"; +import { activateVenue, createVenue, deactivateVenue, getAllVenues, getMyVenues, getPublicVenueById, + getVenueById, updateVenue} from "../controllers/venueController.js"; +import userAuthMiddleware from "../middleware/userAuthMiddleware.js"; +import authorizeRoles from "../middleware/authorizeRoles.js"; +import upload from "../middleware/upload.js"; + +const router = express.Router(); + + +router.post("/create", userAuthMiddleware,authorizeRoles("provider"),upload.array("images", 5), createVenue); + +router.get("/my-venues",userAuthMiddleware,authorizeRoles("provider"),getMyVenues); + +router.get("/provider/:id",userAuthMiddleware,authorizeRoles("provider"),getVenueById); + +router.put("/update/:id",userAuthMiddleware,authorizeRoles("provider"),upload.array("images",5),updateVenue); + +router.patch("/deactivate/:id",userAuthMiddleware,authorizeRoles("provider"),deactivateVenue); + +router.patch("/activate/:id",userAuthMiddleware,authorizeRoles("provider"),activateVenue); + +//public apis for veune and details +router.get("/",getAllVenues) +router.get('/:venueId', getPublicVenueById) + + +export default router; \ No newline at end of file diff --git a/backend/src/utils/cache.js b/backend/src/utils/cache.js new file mode 100644 index 0000000000..0b66f6409f --- /dev/null +++ b/backend/src/utils/cache.js @@ -0,0 +1,73 @@ +import { redisClient } from "../config/redis.js"; + +const ACTIVE_VENUES_CACHE_KEY = "venues:active:all"; +const ACTIVE_VENUES_CACHE_TTL = 300; // 5 minutes +const VENUE_AVAILABILITY_CACHE_TTL = 60; // 1 minute + +const getActiveVenueCacheKey = (venueId) => `venues:active:${venueId}`; +const getVenueAvailabilityCacheKey = (venueId) => + `availability:venue:${venueId}`; + +const isRedisReady = () => Boolean(redisClient?.isOpen); + +const getCache = async (key) => { + try { + if (!isRedisReady()) { + return null; + } + + const cached = await redisClient.get(key); + if (!cached) { + return null; + } + + return JSON.parse(cached); + } catch (error) { + console.error("Redis getCache error:", error); + return null; + } +}; + +const setCache = async (key, value, ttlSeconds) => { + try { + if (!isRedisReady()) { + return; + } + + await redisClient.set(key, JSON.stringify(value), { + EX: ttlSeconds, + }); + } catch (error) { + console.error("Redis setCache error:", error); + } +}; + +const delCache = async (key) => { + try { + if (!isRedisReady()) { + return; + } + + await redisClient.del(key); + } catch (error) { + console.error("Redis delCache error:", error); + } +}; + +const invalidateActiveVenuesCache = async () => { + await delCache(ACTIVE_VENUES_CACHE_KEY); +}; + +const invalidateActiveVenueCache = async (venueId) => { + await delCache(getActiveVenueCacheKey(venueId)); +}; + +const invalidateVenueAvailabilityCache = async (venueId) => { + await delCache(getVenueAvailabilityCacheKey(venueId)); +}; + +export { + getCache, setCache, delCache, getActiveVenueCacheKey, getVenueAvailabilityCacheKey, + invalidateActiveVenuesCache, invalidateActiveVenueCache, invalidateVenueAvailabilityCache, + ACTIVE_VENUES_CACHE_KEY, ACTIVE_VENUES_CACHE_TTL, VENUE_AVAILABILITY_CACHE_TTL, +}; diff --git a/backend/src/utils/deleteFromCloudinary.js b/backend/src/utils/deleteFromCloudinary.js new file mode 100644 index 0000000000..41316a0a3a --- /dev/null +++ b/backend/src/utils/deleteFromCloudinary.js @@ -0,0 +1,12 @@ +import cloudinary from "../config/cloudinary.js"; + +const deleteFromCloudinary = async (publicId)=>{ + try { + await cloudinary.uploader.destroy(publicId); + } catch (error) { + console.error("Error deleting from Cloudinary:", error); + throw error; + } +} + +export default deleteFromCloudinary; \ No newline at end of file diff --git a/backend/src/utils/generateOtp.js b/backend/src/utils/generateOtp.js new file mode 100644 index 0000000000..09e42f4c9f --- /dev/null +++ b/backend/src/utils/generateOtp.js @@ -0,0 +1,7 @@ +const generateOtp = () => { + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const expiresAt = new Date(Date.now() + 10 * 60 * 1000) + return { otp, expiresAt } +} + +export default generateOtp; diff --git a/backend/src/utils/marketplaceUserFilter.js b/backend/src/utils/marketplaceUserFilter.js new file mode 100644 index 0000000000..894e2fa900 --- /dev/null +++ b/backend/src/utils/marketplaceUserFilter.js @@ -0,0 +1,24 @@ +export const MARKETPLACE_USER_FILTER = { + roles: { $nin: ["admin"] }, +}; + +export const withMarketplaceUserFilter = (filter = {}) => { + const hasConditions = Object.keys(filter).length > 0; + + if (!hasConditions) { + return { ...MARKETPLACE_USER_FILTER }; + } + + return { + $and: [MARKETPLACE_USER_FILTER, filter], + }; +}; + +export const isPlatformOperator = (user) => + Array.isArray(user?.roles) && user.roles.includes("admin"); + +export const escapeRegex = (value = "") => + String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +export const buildSearchRegex = (value = "") => + new RegExp(escapeRegex(value.trim()), "i"); diff --git a/backend/src/utils/parsePagination.js b/backend/src/utils/parsePagination.js new file mode 100644 index 0000000000..6d01a4b2e9 --- /dev/null +++ b/backend/src/utils/parsePagination.js @@ -0,0 +1,24 @@ +const parsePagination = (query) => { + const maxLimit = 100; + + let page = parseInt(query.page, 10); + let limit = parseInt(query.limit, 10); + + if (Number.isNaN(page) || page < 1) { + page = 1; + } + + if (Number.isNaN(limit) || limit < 1) { + limit = 10; + } + + if (limit > maxLimit) { + limit = maxLimit; + } + + const skip = (page - 1) * limit; + + return { page, limit, skip }; +}; + +export default parsePagination; diff --git a/backend/src/utils/parseSlotTime.js b/backend/src/utils/parseSlotTime.js new file mode 100644 index 0000000000..80bbade25e --- /dev/null +++ b/backend/src/utils/parseSlotTime.js @@ -0,0 +1,47 @@ +const SLOT_TIME_REGEX = + /^(\d{1,2}):(\d{2})\s*(AM|PM)$/i; + +/** + * Parses a 12-hour slot time string (e.g. "09:00 AM") to minutes from midnight. + * Returns null when the value is invalid. + */ +export const parseSlotTimeToMinutes = (value) => { + if (value == null || typeof value !== "string") { + return null; + } + + const trimmed = value.trim(); + const match = trimmed.match(SLOT_TIME_REGEX); + + if (!match) { + return null; + } + + let hours = Number(match[1]); + const minutes = Number(match[2]); + const meridiem = match[3].toUpperCase(); + + if ( + !Number.isInteger(hours) || + !Number.isInteger(minutes) || + hours < 1 || + hours > 12 || + minutes < 0 || + minutes > 59 + ) { + return null; + } + + if (meridiem === "PM" && hours !== 12) { + hours += 12; + } + + if (meridiem === "AM" && hours === 12) { + hours = 0; + } + + return hours * 60 + minutes; +}; + +export const isValidSlotTime = (value) => + parseSlotTimeToMinutes(value) !== null; diff --git a/backend/src/utils/sanitizeUser.js b/backend/src/utils/sanitizeUser.js new file mode 100644 index 0000000000..459b9822c2 --- /dev/null +++ b/backend/src/utils/sanitizeUser.js @@ -0,0 +1,19 @@ +const sanitizeUser = (user) => { + if (!user) { + return null; + } + + const doc = user.toObject ? user.toObject() : user; + + const { + password, + otp, + otpExpiresAt, + __v, + ...safeUser + } = doc; + + return safeUser; +}; + +export default sanitizeUser; diff --git a/backend/src/utils/sendEmail.js b/backend/src/utils/sendEmail.js new file mode 100644 index 0000000000..34d1494930 --- /dev/null +++ b/backend/src/utils/sendEmail.js @@ -0,0 +1,68 @@ +import SibApiV3Sdk from "sib-api-v3-sdk"; +import dotenv from "dotenv"; + +dotenv.config(); + +const client = SibApiV3Sdk.ApiClient.instance; + +const apiKey = client.authentications["api-key"]; +apiKey.apiKey = process.env.BREVO_API_KEY; + +const emailApi = new SibApiV3Sdk.TransactionalEmailsApi(); + +const sendEmail = async ( + to, + otp, + purpose = "verification" +) => { + try { + const subject = + purpose === "reset" + ? "Password Reset OTP" + : "Email Verification OTP"; + + const htmlContent = ` +
+

${subject}

+ +

Your OTP code is:

+ +

+ ${otp} +

+ +

+ This OTP will expire in + 10 minutes. +

+ +

+ Ignore this email if you did not request it. +

+
+ `; + + await emailApi.sendTransacEmail({ + sender: { + email: process.env.SENDER_EMAIL, + name: process.env.SENDER_NAME, + }, + + to: [{ email: to }], + + subject, + htmlContent, + }); + + return true; + } catch (error) { + console.log( + "Email error:", + error.response?.body || error.message + ); + + throw new Error("Failed to send email"); + } +}; + +export default sendEmail; \ No newline at end of file diff --git a/backend/src/utils/uploadToCloudinary.js b/backend/src/utils/uploadToCloudinary.js new file mode 100644 index 0000000000..65749dbafc --- /dev/null +++ b/backend/src/utils/uploadToCloudinary.js @@ -0,0 +1,15 @@ +import cloudinary from "../config/cloudinary.js"; + +const uploadToCloudinary = async (fileBuffer)=>{ + return new Promise((resolve, reject)=>{ + cloudinary.uploader.upload_stream({folder: 'bookmyvenue'}, (error, result)=>{ + if(error){ + reject(error); + }else{ + resolve(result); + } + }).end(fileBuffer); + }); +} + +export default uploadToCloudinary; \ No newline at end of file diff --git a/backend/src/utils/venueCategory.js b/backend/src/utils/venueCategory.js new file mode 100644 index 0000000000..bb13bed66d --- /dev/null +++ b/backend/src/utils/venueCategory.js @@ -0,0 +1,36 @@ +export const VENUE_CATEGORY_SLUGS = [ + "wedding", + "corporate", + "birthday", + "party", + "function", + "photoshoot", + "other", +]; + +const LEGACY_CATEGORY_MAP = { + meetings: "corporate", + meeting: "corporate", +}; + +export const resolveVenueCategory = (value) => { + if (value == null || typeof value !== "string") { + return null; + } + + const normalized = value.trim().toLowerCase(); + + if (!normalized) { + return null; + } + + if (VENUE_CATEGORY_SLUGS.includes(normalized)) { + return normalized; + } + + if (LEGACY_CATEGORY_MAP[normalized]) { + return LEGACY_CATEGORY_MAP[normalized]; + } + + return null; +}; diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000000..cf8df74928 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,7 @@ +# Vite only exposes variables prefixed with VITE_ +# Restart the dev server after changing this file: npm run dev + +VITE_API_URL=http://localhost:5000/api + +# Public Razorpay key (same as backend RAZORPAY_KEY_ID — never use KEY_SECRET here) +VITE_RAZORPAY_KEY_ID=rzp_test_your_key_here diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000..a36934d874 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000000..ea36dd3dc4 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000..ea03b1ce13 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + BookMyVenue + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000..79290e5aa1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3144 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.3.1", + "axios": "^1.17.0", + "framer-motion": "^12.40.0", + "lucide-react": "^1.18.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-hot-toast": "^2.6.0", + "react-icons": "^5.6.0", + "react-router-dom": "^7.17.0", + "tailwindcss": "^4.3.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "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/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/goober": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz", + "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", + "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/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/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "license": "MIT", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000..02fc1234bd --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.1", + "axios": "^1.17.0", + "framer-motion": "^12.40.0", + "lucide-react": "^1.18.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-hot-toast": "^2.6.0", + "react-icons": "^5.6.0", + "react-router-dom": "^7.17.0", + "tailwindcss": "^4.3.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000000..6893eb1323 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000000..e9522193d9 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000000..149f4a9392 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,124 @@ +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { Toaster } from "react-hot-toast"; + +import { AuthProvider } from "./context/AuthContext"; + +import PublicLayout from "./layouts/PublicLayout"; + +import Home from "./pages/guest/Home"; +import Venues from "./pages/guest/Venues"; +import VenueDetails from "./pages/guest/VenueDetails"; +import NotFound from "./pages/guest/NotFound"; + +import Login from "./pages/auth/Login"; +import Register from "./pages/auth/Register"; +import VerifyEmail from "./pages/auth/VerifyEmail"; +import ForgotPassword from "./pages/auth/ForgotPassword"; +import ResetPassword from "./pages/auth/ResetPassword"; + +import Profile from "./pages/user/Profile"; +import MyBookings from "./pages/user/MyBookings"; +import ProviderDashboard from "./pages/provider/Dashboard"; +import MyVenues from "./pages/provider/MyVenues"; +import CreateVenue from "./pages/provider/CreateVenue"; +import EditVenue from "./pages/provider/EditVenue"; +import ManageAvailability from "./pages/provider/ManageAvailability"; +import ProviderBookings from "./pages/provider/ProviderBookings"; + +import AdminDashboard from "./pages/admin/Dashboard"; +import AdminUsers from "./pages/admin/Users"; +import AdminUserDetail from "./pages/admin/UserDetail"; +import AdminVenues from "./pages/admin/Venues"; +import AdminVenueDetail from "./pages/admin/VenueDetail"; +import AdminBookings from "./pages/admin/Bookings"; +import AdminBookingDetail from "./pages/admin/BookingDetail"; +import AdminPayments from "./pages/admin/Payments"; + +import ProtectedRoute from "./components/protected/ProtectedRoutes"; +import ProviderRoute from "./components/protected/ProviderRoute"; +import MarketplaceGuard from "./components/protected/MarketplaceGuard"; +import AdminRoute from "./components/protected/AdminRoute"; +import ProviderLayout from "./layouts/ProviderLayout"; +import AdminLayout from "./layouts/AdminLayout"; +import ScrollToTop from "./components/common/ScrollToTop"; + +function App() { + return ( + + + + + + + } /> + } /> + } /> + } /> + } /> + + }> + }> + } /> + } /> + } /> + + + + + } + /> + + + + + } + /> + + + + + + } + > + } /> + } /> + } /> + } /> + } + /> + } /> + + + + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } /> + + + + ); +} + +export default App; diff --git a/frontend/src/assets/bmv-logo.jpeg b/frontend/src/assets/bmv-logo.jpeg new file mode 100644 index 0000000000..341ae00dbd Binary files /dev/null and b/frontend/src/assets/bmv-logo.jpeg differ diff --git a/frontend/src/assets/event1.jpg b/frontend/src/assets/event1.jpg new file mode 100644 index 0000000000..92a80d5ff3 Binary files /dev/null and b/frontend/src/assets/event1.jpg differ diff --git a/frontend/src/assets/event2.webp b/frontend/src/assets/event2.webp new file mode 100644 index 0000000000..883ad1a068 Binary files /dev/null and b/frontend/src/assets/event2.webp differ diff --git a/frontend/src/assets/event3.webp b/frontend/src/assets/event3.webp new file mode 100644 index 0000000000..440d9ea803 Binary files /dev/null and b/frontend/src/assets/event3.webp differ diff --git a/frontend/src/assets/event4.webp b/frontend/src/assets/event4.webp new file mode 100644 index 0000000000..b878579ddf Binary files /dev/null and b/frontend/src/assets/event4.webp differ diff --git a/frontend/src/assets/event5.webp b/frontend/src/assets/event5.webp new file mode 100644 index 0000000000..7f44600c29 Binary files /dev/null and b/frontend/src/assets/event5.webp differ diff --git a/frontend/src/assets/event6.webp b/frontend/src/assets/event6.webp new file mode 100644 index 0000000000..0c09cd28c1 Binary files /dev/null and b/frontend/src/assets/event6.webp differ diff --git a/frontend/src/assets/event7.jfif b/frontend/src/assets/event7.jfif new file mode 100644 index 0000000000..b400bb0aa7 Binary files /dev/null and b/frontend/src/assets/event7.jfif differ diff --git a/frontend/src/assets/hero/hero-main.jpg b/frontend/src/assets/hero/hero-main.jpg new file mode 100644 index 0000000000..065230afca Binary files /dev/null and b/frontend/src/assets/hero/hero-main.jpg differ diff --git a/frontend/src/assets/hero/hero-venue-1.jpg b/frontend/src/assets/hero/hero-venue-1.jpg new file mode 100644 index 0000000000..065230afca Binary files /dev/null and b/frontend/src/assets/hero/hero-venue-1.jpg differ diff --git a/frontend/src/assets/hero/hero-venue-2.jpg b/frontend/src/assets/hero/hero-venue-2.jpg new file mode 100644 index 0000000000..065230afca Binary files /dev/null and b/frontend/src/assets/hero/hero-venue-2.jpg differ diff --git a/frontend/src/assets/log-theme.webp b/frontend/src/assets/log-theme.webp new file mode 100644 index 0000000000..befb9d00ae Binary files /dev/null and b/frontend/src/assets/log-theme.webp differ diff --git a/frontend/src/assets/logo.jpeg b/frontend/src/assets/logo.jpeg new file mode 100644 index 0000000000..065230afca Binary files /dev/null and b/frontend/src/assets/logo.jpeg differ diff --git a/frontend/src/assets/reg-theme.jpg b/frontend/src/assets/reg-theme.jpg new file mode 100644 index 0000000000..6d1a6a851b Binary files /dev/null and b/frontend/src/assets/reg-theme.jpg differ diff --git a/frontend/src/components/admin/AdminBrand.jsx b/frontend/src/components/admin/AdminBrand.jsx new file mode 100644 index 0000000000..5b96b81b64 --- /dev/null +++ b/frontend/src/components/admin/AdminBrand.jsx @@ -0,0 +1,38 @@ +import { Link } from "react-router-dom"; +import { Shield } from "lucide-react"; +import Logo from "../../assets/logo.jpeg"; + +const AdminBrand = ({ compact = false, onClick }) => { + return ( + + + + + + + + Book My Venue + + + + + ); +}; + +export default AdminBrand; diff --git a/frontend/src/components/admin/AdminDetailRow.jsx b/frontend/src/components/admin/AdminDetailRow.jsx new file mode 100644 index 0000000000..91472cefe8 --- /dev/null +++ b/frontend/src/components/admin/AdminDetailRow.jsx @@ -0,0 +1,12 @@ +const AdminDetailRow = ({ label, value, align = "center" }) => ( +
+
{label}
+
{value}
+
+); + +export default AdminDetailRow; diff --git a/frontend/src/components/admin/AdminFilterSelect.jsx b/frontend/src/components/admin/AdminFilterSelect.jsx new file mode 100644 index 0000000000..541bf8e386 --- /dev/null +++ b/frontend/src/components/admin/AdminFilterSelect.jsx @@ -0,0 +1,28 @@ +const AdminFilterSelect = ({ + label, + value, + onChange, + options, + className = "", +}) => ( + +); + +export default AdminFilterSelect; diff --git a/frontend/src/components/admin/AdminPageHeader.jsx b/frontend/src/components/admin/AdminPageHeader.jsx new file mode 100644 index 0000000000..90fe956f90 --- /dev/null +++ b/frontend/src/components/admin/AdminPageHeader.jsx @@ -0,0 +1,15 @@ +const AdminPageHeader = ({ title, description, children }) => ( +
+
+

+ {title} +

+ {description && ( +

{description}

+ )} +
+ {children &&
{children}
} +
+); + +export default AdminPageHeader; diff --git a/frontend/src/components/admin/AdminPagination.jsx b/frontend/src/components/admin/AdminPagination.jsx new file mode 100644 index 0000000000..6f8c36154b --- /dev/null +++ b/frontend/src/components/admin/AdminPagination.jsx @@ -0,0 +1,54 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; + +const AdminPagination = ({ + page = 1, + limit = 20, + count = 0, + onPageChange, +}) => { + const totalPages = Math.max(1, Math.ceil(count / limit)); + const canGoPrev = page > 1; + const canGoNext = page < totalPages; + const start = count === 0 ? 0 : (page - 1) * limit + 1; + const end = Math.min(page * limit, count); + + if (count === 0) { + return null; + } + + return ( +
+

+ Showing {start}–{end} of {count} +

+ +
+ + + + Page {page} of {totalPages} + + + +
+
+ ); +}; + +export default AdminPagination; diff --git a/frontend/src/components/admin/AdminSearchInput.jsx b/frontend/src/components/admin/AdminSearchInput.jsx new file mode 100644 index 0000000000..8c32308477 --- /dev/null +++ b/frontend/src/components/admin/AdminSearchInput.jsx @@ -0,0 +1,24 @@ +import { Search } from "lucide-react"; + +const AdminSearchInput = ({ + value, + onChange, + placeholder = "Search...", + className = "", +}) => ( + +); + +export default AdminSearchInput; diff --git a/frontend/src/components/admin/AdminStatCard.jsx b/frontend/src/components/admin/AdminStatCard.jsx new file mode 100644 index 0000000000..d8f92323e3 --- /dev/null +++ b/frontend/src/components/admin/AdminStatCard.jsx @@ -0,0 +1,55 @@ +import { Eye, EyeOff } from "lucide-react"; + +const AdminStatCard = ({ + label, + value, + icon: Icon, + iconClass, + maskedValue, + isValueVisible, + onToggleVisibility, +}) => { + const isHideable = Boolean(maskedValue && onToggleVisibility); + const displayValue = + isHideable && !isValueVisible ? maskedValue : value; + + return ( +
+
+ {Icon && ( + + + )} +
+
+

+ {displayValue} +

+ {isHideable && ( + + )} +
+

+ {label} +

+
+
+
+ ); +}; + +export default AdminStatCard; diff --git a/frontend/src/components/admin/AdminTable.jsx b/frontend/src/components/admin/AdminTable.jsx new file mode 100644 index 0000000000..73c0d66173 --- /dev/null +++ b/frontend/src/components/admin/AdminTable.jsx @@ -0,0 +1,9 @@ +const AdminTable = ({ children, className = "" }) => ( +
+ {children} +
+); + +export default AdminTable; diff --git a/frontend/src/components/admin/AdminTextFilter.jsx b/frontend/src/components/admin/AdminTextFilter.jsx new file mode 100644 index 0000000000..5a30682d1b --- /dev/null +++ b/frontend/src/components/admin/AdminTextFilter.jsx @@ -0,0 +1,23 @@ +const AdminTextFilter = ({ + label, + value, + onChange, + placeholder = "", + type = "text", + min, + className = "", +}) => ( + +); + +export default AdminTextFilter; diff --git a/frontend/src/components/auth/AuthLayout.jsx b/frontend/src/components/auth/AuthLayout.jsx new file mode 100644 index 0000000000..a080da7cd7 --- /dev/null +++ b/frontend/src/components/auth/AuthLayout.jsx @@ -0,0 +1,85 @@ +import { Link } from "react-router-dom"; +import { ArrowLeft, Check } from "lucide-react"; +import bmvLogo from "../../assets/bmv-logo.jpeg"; + +const AuthLayout = ({ + brandingTitle, + brandingSubtitle, + brandingPoints = [], + brandingAlign = "end", + brandingImage = bmvLogo, + brandingImagePosition = "center", + children, +}) => { + const brandingPositionClass = + brandingAlign === "center" ? "justify-center" : "justify-end"; + + return ( +
+ + +
+
+ +
+ +
+
{children}
+
+
+
+ ); +}; + +export default AuthLayout; diff --git a/frontend/src/components/auth/OtpInput.jsx b/frontend/src/components/auth/OtpInput.jsx new file mode 100644 index 0000000000..5bb7eb8949 --- /dev/null +++ b/frontend/src/components/auth/OtpInput.jsx @@ -0,0 +1,110 @@ +import { useRef, useEffect } from "react"; + +const OTP_LENGTH = 6; + +const OtpInput = ({ value, onChange, disabled = false, error }) => { + const inputsRef = useRef([]); + const digits = value.padEnd(OTP_LENGTH, " ").slice(0, OTP_LENGTH).split(""); + const errorId = error ? "otp-error" : undefined; + + useEffect(() => { + inputsRef.current[0]?.focus(); + }, []); + + const updateValue = (nextDigits) => { + onChange(nextDigits.join("").replace(/\s/g, "")); + }; + + const handleChange = (index, char) => { + const sanitized = char.replace(/\D/g, ""); + const next = [...digits]; + + if (!sanitized) { + next[index] = " "; + updateValue(next); + return; + } + + next[index] = sanitized.slice(-1); + updateValue(next); + + if (index < OTP_LENGTH - 1) { + inputsRef.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = (index, event) => { + if (event.key === "Backspace") { + event.preventDefault(); + const next = [...digits]; + + if (digits[index] && digits[index] !== " ") { + next[index] = " "; + updateValue(next); + return; + } + + if (index > 0) { + next[index - 1] = " "; + updateValue(next); + inputsRef.current[index - 1]?.focus(); + } + } + }; + + const handlePaste = (event) => { + event.preventDefault(); + const pasted = event.clipboardData + .getData("text") + .replace(/\D/g, "") + .slice(0, OTP_LENGTH); + + if (!pasted) return; + + updateValue(pasted.padEnd(OTP_LENGTH, " ").split("")); + inputsRef.current[Math.min(pasted.length, OTP_LENGTH - 1)]?.focus(); + }; + + return ( +
+
+ {digits.map((digit, index) => ( + { + inputsRef.current[index] = el; + }} + type="text" + inputMode="numeric" + autoComplete={index === 0 ? "one-time-code" : "off"} + maxLength={1} + value={digit.trim()} + disabled={disabled} + aria-invalid={Boolean(error)} + onChange={(event) => handleChange(index, event.target.value)} + onKeyDown={(event) => handleKeyDown(index, event)} + onPaste={handlePaste} + onFocus={(event) => event.target.select()} + className={`h-12 w-full min-w-0 flex-1 rounded-lg border bg-white text-center text-lg font-semibold text-gray-900 shadow-sm focus:outline-none focus:ring-2 disabled:opacity-70 ${ + error + ? "border-red-400 focus:border-red-500 focus:ring-red-100" + : "border-gray-300 focus:border-red-500 focus:ring-red-100" + }`} + /> + ))} +
+ {error && ( + + )} +
+ ); +}; + +export default OtpInput; diff --git a/frontend/src/components/auth/PasswordInput.jsx b/frontend/src/components/auth/PasswordInput.jsx new file mode 100644 index 0000000000..6b81ddfe02 --- /dev/null +++ b/frontend/src/components/auth/PasswordInput.jsx @@ -0,0 +1,61 @@ +import { useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; + +const inputClass = + "w-full rounded-lg border border-gray-300 bg-white px-3.5 py-3 text-sm text-gray-900 placeholder:text-gray-400 shadow-sm transition-colors focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-100 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-70"; + +const PasswordInput = ({ + id, + name, + value, + onChange, + placeholder = "Enter your password", + autoComplete = "current-password", + disabled = false, + error, +}) => { + const [visible, setVisible] = useState(false); + const errorId = error ? `${id}-error` : undefined; + + return ( +
+
+ + +
+ {error && ( + + )} +
+ ); +}; + +export default PasswordInput; diff --git a/frontend/src/components/bookings/BookingFiltersBar.jsx b/frontend/src/components/bookings/BookingFiltersBar.jsx new file mode 100644 index 0000000000..2e063cad55 --- /dev/null +++ b/frontend/src/components/bookings/BookingFiltersBar.jsx @@ -0,0 +1,52 @@ +import { Search } from "lucide-react"; +import { BOOKING_FILTERS } from "../../utils/bookingFilters"; + +const BookingFiltersBar = ({ + activeFilter, + onFilterChange, + searchQuery, + onSearchChange, + searchPlaceholder = "Search by venue name", + filters = BOOKING_FILTERS, +}) => ( +
+
+ {filters.map((filter) => { + const isActive = activeFilter === filter.id; + + return ( + + ); + })} +
+ + +
+); + +export default BookingFiltersBar; diff --git a/frontend/src/components/bookings/BookingSummary.jsx b/frontend/src/components/bookings/BookingSummary.jsx new file mode 100644 index 0000000000..5aa4b5273c --- /dev/null +++ b/frontend/src/components/bookings/BookingSummary.jsx @@ -0,0 +1,63 @@ +import { CalendarCheck, CalendarClock, CalendarX, LayoutList } from "lucide-react"; + +const statConfig = [ + { + key: "total", + label: "Total Bookings", + icon: LayoutList, + iconClass: "bg-red-50 text-red-600", + }, + { + key: "upcoming", + label: "Upcoming", + icon: CalendarClock, + iconClass: "bg-emerald-50 text-emerald-600", + }, + { + key: "completed", + label: "Completed", + icon: CalendarCheck, + iconClass: "bg-sky-50 text-sky-600", + }, + { + key: "cancelled", + label: "Cancelled", + icon: CalendarX, + iconClass: "bg-gray-100 text-gray-600", + }, +]; + +const BookingSummary = ({ stats, hideCancelled = false }) => { + const visibleStats = hideCancelled + ? statConfig.filter((item) => item.key !== "cancelled") + : statConfig; + + return ( +
+ {visibleStats.map(({ key, label, icon: Icon, iconClass }) => ( +
+
+ + +
+

+ {stats[key] ?? 0} +

+

+ {label} +

+
+
+
+ ))} +
+ ); +}; + +export default BookingSummary; diff --git a/frontend/src/components/bookings/BookingsEmptyState.jsx b/frontend/src/components/bookings/BookingsEmptyState.jsx new file mode 100644 index 0000000000..42b47b710c --- /dev/null +++ b/frontend/src/components/bookings/BookingsEmptyState.jsx @@ -0,0 +1,25 @@ +import { Link } from "react-router-dom"; +import { CalendarDays } from "lucide-react"; + +const BookingsEmptyState = () => ( +
+ + + +

No bookings yet

+

+ Your reservations will appear here once you book a venue. Browse venues to + find your next event space. +

+ + + Browse Venues + +
+); + +export default BookingsEmptyState; diff --git a/frontend/src/components/common/BookingCard.jsx b/frontend/src/components/common/BookingCard.jsx new file mode 100644 index 0000000000..53e4e0fdfa --- /dev/null +++ b/frontend/src/components/common/BookingCard.jsx @@ -0,0 +1,115 @@ +import { MapPin } from "lucide-react"; +import { formatSlotLabel,formatTimeRange,} from "../../utils/formatDate"; +import { formatPrice } from "../../utils/formatPrice"; +import { resolvePopulatedRef } from "../../utils/booking"; +import { getBookingDisplayStatus } from "../../utils/bookingFilters"; +import { getVenueCoverUrl } from "../../utils/venue"; + +const statusToneStyles = { + upcoming: "bg-emerald-50 text-emerald-700 ring-emerald-100", + completed: "bg-sky-50 text-sky-700 ring-sky-100", + cancelled: "bg-gray-100 text-gray-600 ring-gray-200", +}; + +const formatCompactDate = (date) => { + if (!date) return "—"; + + return new Date(date).toLocaleDateString("en-IN", { + weekday: "short", + day: "numeric", + month: "short", + year: "numeric", + }); +}; + +const BookingCard = ({ booking }) => { + const venue = resolvePopulatedRef(booking?.venueId); + const slot = resolvePopulatedRef(booking?.availabilityId); + const { amount } = formatPrice(booking?.amount); + const displayStatus = getBookingDisplayStatus(booking); + + const slotDate = slot?.date ? formatCompactDate(slot.date) : "—"; + const slotTime = slot + ? formatTimeRange(slot.startTime, slot.endTime) + : "—"; + const slotLabel = slot?.slotLabel ? formatSlotLabel(slot.slotLabel) : null; + const coverUrl = getVenueCoverUrl(venue); + const location = + venue?.city || venue?.state + ? [venue.city, venue.state].filter(Boolean).join(", ") + : venue?.address || "Location unavailable"; + + return ( +
+
+
+
+ {coverUrl ? ( + + ) : ( +
+ No image +
+ )} +
+ +
+

+ {venue?.title || "Venue unavailable"} +

+ +

+

+ +

+ {slotDate} + {slotLabel ? ` · ${slotLabel}` : ""} + {slotTime !== "—" ? ` · ${slotTime}` : ""} +

+ +

+ Ref: {booking.bookingReference || "—"} +

+
+
+ +
+

{slotDate}

+ {slotLabel && ( +

{slotLabel}

+ )} +

{slotTime}

+
+ +
+

+ Reference +

+

+ {booking.bookingReference || "—"} +

+
+ +
+ + {displayStatus.label} + + +

{amount}

+
+
+
+ ); +}; + +export default BookingCard; diff --git a/frontend/src/components/common/BrandName.jsx b/frontend/src/components/common/BrandName.jsx new file mode 100644 index 0000000000..79a1e30547 --- /dev/null +++ b/frontend/src/components/common/BrandName.jsx @@ -0,0 +1,46 @@ +import Logo from "../../assets/logo.jpeg"; + +const variantStyles = { + navbar: { + wrapper: "gap-2.5 sm:gap-3", + logo: "h-11 w-auto object-contain sm:h-12", + text: "font-brand text-lg text-red-600 sm:text-xl lg:text-2xl", + }, + inline: { + wrapper: "gap-1.5", + logo: "h-5 w-auto object-contain sm:h-6", + text: "font-brand text-base text-red-400 sm:text-lg", + }, + inlineLight: { + wrapper: "", + text: "font-brand text-base text-red-600 sm:text-lg", + }, + heading: { + wrapper: "", + logo: "h-6 w-auto object-contain sm:h-7", + text: "font-brand text-2xl text-red-600 sm:text-3xl", + }, +}; + +const BrandName = ({ variant = "navbar", showLogo, className = "" }) => { + const styles = variantStyles[variant] ?? variantStyles.navbar; + const displayLogo = showLogo ?? true; + + return ( + + {displayLogo && ( + + )} + Book My Venue + + ); +}; + +export default BrandName; diff --git a/frontend/src/components/common/ConfirmModal.jsx b/frontend/src/components/common/ConfirmModal.jsx new file mode 100644 index 0000000000..39ce20e2d0 --- /dev/null +++ b/frontend/src/components/common/ConfirmModal.jsx @@ -0,0 +1,57 @@ +const ConfirmModal = ({ + open, + title, + message, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + isLoading = false, + onConfirm, + onCancel, +}) => { + if (!open) return null; + + return ( +
+ + +
+ + + ); +}; + +export default ConfirmModal; diff --git a/frontend/src/components/common/EmptyState.jsx b/frontend/src/components/common/EmptyState.jsx new file mode 100644 index 0000000000..e59dc29831 --- /dev/null +++ b/frontend/src/components/common/EmptyState.jsx @@ -0,0 +1,16 @@ +const EmptyState = ({ title, description }) => { + return ( +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ ); +}; + +export default EmptyState; \ No newline at end of file diff --git a/frontend/src/components/common/ErrorState.jsx b/frontend/src/components/common/ErrorState.jsx new file mode 100644 index 0000000000..80694851c5 --- /dev/null +++ b/frontend/src/components/common/ErrorState.jsx @@ -0,0 +1,23 @@ +const ErrorState = ({ message, onRetry }) => { + return ( +
+

+ Something went wrong +

+ +

{message}

+ + {onRetry && ( + + )} +
+ ); +}; + +export default ErrorState; diff --git a/frontend/src/components/common/Footer.jsx b/frontend/src/components/common/Footer.jsx new file mode 100644 index 0000000000..671801ceab --- /dev/null +++ b/frontend/src/components/common/Footer.jsx @@ -0,0 +1,148 @@ +import { Link } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import BrandName from "./BrandName"; + +const footerLinkClass = + "text-sm text-gray-600 transition-colors duration-200 hover:text-red-600"; + +const footerHeadingClass = + "text-xs font-semibold uppercase tracking-[0.14em] text-gray-900"; + +const scrollToProviderCta = (event) => { + if (window.location.pathname !== "/") return; + + event.preventDefault(); + + document.getElementById("provider-cta")?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + + window.history.replaceState(null, "", "/#provider-cta"); +}; + +const Footer = () => { + const { loading, authReady, isAuthenticated, isProvider } = useAuth(); + const authSettled = !loading && authReady; + + return ( + + ); +}; + +export default Footer; diff --git a/frontend/src/components/common/Loader.jsx b/frontend/src/components/common/Loader.jsx new file mode 100644 index 0000000000..550e3cbed1 --- /dev/null +++ b/frontend/src/components/common/Loader.jsx @@ -0,0 +1,14 @@ +const Loader = ({ label = "Loading..." }) => { + return ( +
+
+

{label}

+
+ ); +}; + +export default Loader; diff --git a/frontend/src/components/common/Navbar.jsx b/frontend/src/components/common/Navbar.jsx new file mode 100644 index 0000000000..0cc2ef47e3 --- /dev/null +++ b/frontend/src/components/common/Navbar.jsx @@ -0,0 +1,253 @@ +import { useState } from "react"; +import { Link, NavLink } from "react-router-dom"; +import { Building2, CalendarCheck, Home, LayoutDashboard, Menu, X, User, } from "lucide-react"; +import { useAuth } from "../../context/AuthContext"; +import BrandName from "./BrandName"; + +const desktopNavLinkClass = ({ isActive }) => + [ + "px-4 py-2 text-sm font-medium transition-colors duration-300", + isActive + ? "text-red-600" + : "text-gray-600 hover:text-red-600", + ].join(" "); + +const mobileDrawerNavClass = ({ isActive }) => + [ + "flex items-center gap-3 px-4 py-3 text-sm font-medium transition-colors duration-300", + isActive + ? "text-red-600" + : "text-gray-600 hover:text-red-600", + ].join(" "); + +const dashboardLinkClass = + "hidden items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 lg:inline-flex"; + +const Navbar = () => { + const { user, loading, authReady, userRoles, isAuthenticated } = useAuth(); + const [menuOpen, setMenuOpen] = useState(false); + const isProvider = userRoles.includes("provider"); + const userInitial = user?.name?.charAt(0)?.toUpperCase(); + + const closeMenu = () => setMenuOpen(false); + const toggleMenu = () => setMenuOpen((open) => !open); + + const profileAvatarClass = + "flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full border border-gray-200 bg-gradient-to-br from-red-50 to-white text-sm font-semibold text-red-600 shadow-sm transition-all duration-200 hover:border-red-200 hover:shadow-md"; + + const menuButtonClass = [ + "flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 lg:hidden", + ].join(" "); + + const renderMobileNavLinks = () => ( + <> + + + + + + + {isAuthenticated && ( + + + )} + + {isAuthenticated && isProvider && ( + + + )} + + {!isAuthenticated && ( + + Get started + + )} + + ); + + const renderDesktopPillNav = () => ( + + ); + + return ( +
+
+ + + + +
+ {renderDesktopPillNav()} +
+ +
+ {loading || !authReady ? ( + <> + +
+ + {menuOpen && ( + +
+ +
+ {isAuthenticated && user && ( + +
+ {userInitial ?? ( + + )} +
+ +
+

+ {user?.name || "My Profile"} +

+

+ {user?.email || "View your account"} +

+
+ + )} + + +
+ +
+ ); +}; + +export default Navbar; diff --git a/frontend/src/components/common/ScrollToTop.jsx b/frontend/src/components/common/ScrollToTop.jsx new file mode 100644 index 0000000000..26d6789f2e --- /dev/null +++ b/frontend/src/components/common/ScrollToTop.jsx @@ -0,0 +1,34 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; + +const ScrollToTop = () => { + const { pathname, hash } = useLocation(); + + useEffect(() => { + if (hash) { + const id = hash.replace("#", ""); + + const scrollToTarget = () => { + const element = document.getElementById(id); + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "start" }); + return true; + } + return false; + }; + + if (!scrollToTarget()) { + const timer = window.setTimeout(scrollToTarget, 100); + return () => window.clearTimeout(timer); + } + + return; + } + + window.scrollTo({ top: 0, left: 0, behavior: "auto" }); + }, [pathname, hash]); + + return null; +}; + +export default ScrollToTop; diff --git a/frontend/src/components/home/FeaturedVenueCard.jsx b/frontend/src/components/home/FeaturedVenueCard.jsx new file mode 100644 index 0000000000..e0a5933c47 --- /dev/null +++ b/frontend/src/components/home/FeaturedVenueCard.jsx @@ -0,0 +1,79 @@ +import { Link } from "react-router-dom"; +import { MapPin, Users } from "lucide-react"; +import { getVenueCoverUrl } from "../../utils/venue"; +import { getCategoryLabel } from "../../utils/venueFilters"; +import { formatBookingPriceDisplay } from "../../utils/formatPrice"; + +const formatVenueLocation = (venue) => { + const parts = [venue.city, venue.state].filter(Boolean); + + if (parts.length > 0) { + return `${parts.join(", ")}, India`; + } + + return venue.address || "Location not specified"; +}; + +const FeaturedVenueCard = ({ venue }) => { + const coverUrl = getVenueCoverUrl(venue); + const priceLabel = formatBookingPriceDisplay(venue?.price); + + return ( + + {coverUrl ? ( + {venue.title} + ) : ( +
+ No image +
+ )} + +