From 20d5559832490c44c06be9ee634932a04a5bb371 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:39:37 +0000 Subject: [PATCH] other --- .env.backend | 29 + .env.tor | 5 + COMBINED-README.md | 83 + Dockerfile | 4 +- TOR-README.md | 61 + app/dashboard/page.tsx | 47 +- backend/config/db.js | 19 + backend/controllers/chat.controller.js | 521 +++++ backend/controllers/cryptoController.js | 69 + backend/controllers/promotion.controller.js | 271 +++ backend/controllers/stock.controller.js | 151 ++ backend/controllers/vendor.controller.js | 39 + backend/index.js | 112 + backend/middleware/apiAuthMiddleware.js | 10 + backend/middleware/authMiddleware.js | 32 + backend/middleware/staffAuthMiddleware.js | 49 + backend/middleware/telegramAuthMiddleware.js | 70 + backend/middleware/vendorAuthMiddleware.js | 32 + backend/models/BlockedUser.model.js | 14 + backend/models/Buyer.model.js | 10 + backend/models/Chat.model.js | 68 + backend/models/Escrow.model.js | 43 + backend/models/Invitation.model.js | 40 + backend/models/Order.model.js | 100 + backend/models/Product.model.js | 69 + backend/models/Promotion.model.js | 126 + backend/models/PromotionUse.model.js | 54 + backend/models/Staff.model.js | 11 + backend/models/Store.model.js | 132 ++ backend/models/TelegramUser.model.js | 22 + backend/models/Vendor.model.js | 13 + backend/models/Wallet.model.js | 56 + backend/routes/auth.routes.js | 158 ++ backend/routes/blockedUsers.routes.js | 75 + backend/routes/categories.routes.js | 154 ++ backend/routes/chat.routes.js | 41 + backend/routes/crypto.routes.js | 9 + backend/routes/invites.routes.js | 21 + backend/routes/orders.routes.js | 598 +++++ backend/routes/products.routes.js | 456 ++++ backend/routes/promotion.routes.js | 24 + backend/routes/shipping.routes.js | 151 ++ backend/routes/staffAuth.routes.js | 84 + backend/routes/stock.routes.js | 13 + backend/routes/storefront.routes.js | 265 +++ backend/scripts/disableAllStockTracking.js | 17 + backend/test.js | 8 + backend/utils/createAdmin.js | 44 + backend/utils/createFakeOrder.js | 56 + backend/utils/createInvitation.js | 67 + backend/utils/createKeys.js | 7 + backend/utils/disableStockTracking.js | 55 + backend/utils/encryptPgp.js | 54 + backend/utils/litecoin/index.js | 96 + backend/utils/logger.js | 77 + backend/utils/telegramUtils.js | 171 ++ backend/utils/walletUtils.js | 0 docker-compose.yml | 2 +- lib/.server-service.ts.swp | Bin 0 -> 12288 bytes lib/auth-helpers.ts | 33 + lib/client-service.ts | 117 +- lib/server-service.ts | 46 +- lib/server-utils.ts | 32 + middleware.ts | 15 +- next.config.mjs | 40 + package-lock.json | 2185 +++++++++++++++++- package.json | 19 + server.js | 127 + setup-backend.js | 75 + 69 files changed, 7676 insertions(+), 78 deletions(-) create mode 100644 .env.backend create mode 100644 .env.tor create mode 100644 COMBINED-README.md create mode 100644 TOR-README.md create mode 100644 backend/config/db.js create mode 100644 backend/controllers/chat.controller.js create mode 100644 backend/controllers/cryptoController.js create mode 100644 backend/controllers/promotion.controller.js create mode 100644 backend/controllers/stock.controller.js create mode 100644 backend/controllers/vendor.controller.js create mode 100644 backend/index.js create mode 100644 backend/middleware/apiAuthMiddleware.js create mode 100644 backend/middleware/authMiddleware.js create mode 100644 backend/middleware/staffAuthMiddleware.js create mode 100644 backend/middleware/telegramAuthMiddleware.js create mode 100644 backend/middleware/vendorAuthMiddleware.js create mode 100644 backend/models/BlockedUser.model.js create mode 100644 backend/models/Buyer.model.js create mode 100644 backend/models/Chat.model.js create mode 100644 backend/models/Escrow.model.js create mode 100644 backend/models/Invitation.model.js create mode 100644 backend/models/Order.model.js create mode 100644 backend/models/Product.model.js create mode 100644 backend/models/Promotion.model.js create mode 100644 backend/models/PromotionUse.model.js create mode 100644 backend/models/Staff.model.js create mode 100644 backend/models/Store.model.js create mode 100644 backend/models/TelegramUser.model.js create mode 100644 backend/models/Vendor.model.js create mode 100644 backend/models/Wallet.model.js create mode 100644 backend/routes/auth.routes.js create mode 100644 backend/routes/blockedUsers.routes.js create mode 100644 backend/routes/categories.routes.js create mode 100644 backend/routes/chat.routes.js create mode 100644 backend/routes/crypto.routes.js create mode 100644 backend/routes/invites.routes.js create mode 100644 backend/routes/orders.routes.js create mode 100644 backend/routes/products.routes.js create mode 100644 backend/routes/promotion.routes.js create mode 100644 backend/routes/shipping.routes.js create mode 100644 backend/routes/staffAuth.routes.js create mode 100644 backend/routes/stock.routes.js create mode 100644 backend/routes/storefront.routes.js create mode 100644 backend/scripts/disableAllStockTracking.js create mode 100644 backend/test.js create mode 100644 backend/utils/createAdmin.js create mode 100644 backend/utils/createFakeOrder.js create mode 100644 backend/utils/createInvitation.js create mode 100644 backend/utils/createKeys.js create mode 100644 backend/utils/disableStockTracking.js create mode 100644 backend/utils/encryptPgp.js create mode 100644 backend/utils/litecoin/index.js create mode 100644 backend/utils/logger.js create mode 100644 backend/utils/telegramUtils.js create mode 100644 backend/utils/walletUtils.js create mode 100644 lib/.server-service.ts.swp create mode 100644 lib/auth-helpers.ts create mode 100644 lib/server-utils.ts create mode 100644 server.js create mode 100644 setup-backend.js diff --git a/.env.backend b/.env.backend new file mode 100644 index 0000000..334af48 --- /dev/null +++ b/.env.backend @@ -0,0 +1,29 @@ +MONGO_URI=mongodb://root:OkZl9TEPteFk75@captain.inboxi.ng/mydatabase?authSource=admin +PORT=3001 + +JWT_SECRET=450417bc552e573f95c6d115b45dee27b8c544e5015ec8dd34147d59e1b0c360e3ab705e08511285bd0f5900e11cc4b6841f6391b24dc2c0e86ac2de473f4cb60b18fdc995304a27225db7ea13cece60f4afa50296a7b640b2042fe9072e1cd675cbd134ffeef9ee5a4b234830c53b98b21dcf8224acc6aec3745f5b7e46106eb2db946263cbf3d41add1a269d96de4034533cc9dcf1b74a155eccd0eae55cd958150c31ef0518346ac0471b7b5c83a6e4b26f9526b1c6702c65c38166db4bde57da1ba65a425e4ffba14b86d0279b907bd2eb29a86fd5b57ed78f5319dd5d78dc258cd801595978e042c4a2ca355825d894f8c4fcd39b57a3f78b1a9fd563758f0688d8f3b439c1cdfd161eb92e5ffea647b3a69c2889acd79e33c3a7dd64d32ddd14919be47cbfc46cea68df355f041530a3b0cddfb1042f7f96af7316a66a09fde4e18391dbe5d001a75ae8f8d44edc764b418a7525f55d5d593ca219336f467090b3d329528fd6535ad2c706eda9ed481da824931167dc4fa44a51cdbd13202ffea1375380c8dbdfec85665612c66d2df4bb44a322c54915fb46da50db6ff8e02d4b602069f0267fd29ec4bf865ac03597488d4ac59837fc1d51704f8dcf176681713ee9a24c3abaf58907f01581f7b2ac44a98cf177d6403ef1cf2b61e92cfa2939654ac9ef805d717c846520a02e6e054bcd2fee5394e1a05eb3d85f0d +INTERNAL_API_KEY=5cffe0dedbbcc8761f6ddc7dc7cc2d299d599 + +ENCRYPTION_KEY=48c66ee5a54e596e2029ea832a512401099533ece34cb0fbbb8c4023ca68ba8e +ENCRYPTION_IV=539e26d426cd4bac9844a8e446d63ab1 + +BTC_RPC_USER=your_rpc_user +BTC_RPC_PASSWORD=your_rpc_password + +BTC_RPC_HOST=127.0.0.1 +BTC_RPC_PORT=8332 + +LTC_RPC_USER=notiiwasntherexdddd +LTC_RPC_PASSWORD=NYwsxePgMrThiapHnfCzUfaEfVlNKZECwvlqhHcWjerlZfcaTp + +LTC_RPC_HOST=127.0.0.1 +LTC_RPC_PORT=9332 + +XMR_RPC_USER=your_rpc_user +XMR_RPC_PASSWORD=your_rpc_password + +XMR_RPC_HOST=127.0.0.1 +XMR_RPC_PORT=18081 + +OkZl9TEPteFk75=OkZl9TEPteFk75 +WL4E8H72nvSF=WL4E8H72nvSF \ No newline at end of file diff --git a/.env.tor b/.env.tor new file mode 100644 index 0000000..0667fff --- /dev/null +++ b/.env.tor @@ -0,0 +1,5 @@ +# Tor-specific environment variables +# Using relative URLs for maximum compatibility +NEXT_PUBLIC_API_URL=/api +# Flag to indicate we're running in Tor mode +NEXT_PUBLIC_TOR_MODE=true \ No newline at end of file diff --git a/COMBINED-README.md b/COMBINED-README.md new file mode 100644 index 0000000..aa32843 --- /dev/null +++ b/COMBINED-README.md @@ -0,0 +1,83 @@ +# Ember Market Integrated Application + +This project combines both the Ember Market frontend (Next.js) and backend (Express) into a single application. This approach eliminates the need to run two separate servers and avoids routing conflicts between Next.js and Express. + +## Setup Instructions + +1. **Clone both repositories** if you haven't already: +```bash +git clone https://github.com/your-username/ember-market-frontend.git +git clone https://github.com/your-username/ember-market-backend.git +``` + +2. **Install dependencies**: +```bash +cd ember-market-frontend +npm install +``` + +3. **Run the setup script** to copy backend files to the frontend project: +```bash +npm run setup-backend +``` + +4. **Update environment variables**: + - The setup script copies the backend `.env` file to `.env.backend` in the frontend project + - Make sure to review and adjust environment variables as needed + - Ensure MongoDB connection string and other critical variables are set correctly + +## Running the Application + +### Development Mode + +```bash +npm run dev:custom +``` + +This starts the combined server in development mode on port 3000 (or the port specified in your environment variables). + +### Production Mode + +```bash +npm run build +npm run start:custom +``` + +## How It Works + +This integration uses a custom Express server that: + +1. Handles API routes at `/api/*` using the existing Express backend code +2. Delegates all other routes to Next.js to handle frontend rendering + +### Key Files + +- `server.js`: Custom server that combines Express and Next.js +- `setup-backend.js`: Script to copy backend files into the frontend project +- `next.config.mjs`: Configured to work with the custom Express server + +## API Routes + +All backend API routes are available at the same paths as before, but now they're served from the same server as your frontend: + +- Authentication: `/api/auth/*` +- Products: `/api/products/*` +- Orders: `/api/orders/*` +- etc. + +## Development Workflow + +When making changes: + +1. **Frontend changes**: Modify files in the frontend project as usual +2. **Backend changes**: You have two options: + - Make changes in the original backend repo and run `npm run setup-backend` to sync + - Make changes directly in the frontend project's `backend/` directory + +Remember that changes made directly in the frontend project's `backend/` directory won't automatically sync back to the original backend repository. + +## Troubleshooting + +- **Port conflicts**: Make sure no other service is using port 3000 (or your configured port) +- **MongoDB connection issues**: Verify your connection string in the environment variables +- **Path issues**: Ensure paths in imports are correct for the new project structure \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 530d365..4f93337 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN npm install --force COPY . . -ENV NEXT_PUBLIC_API_URL=https://internal-api.inboxi.ng/api +ENV NEXT_PUBLIC_API_URL=/api # Build the Next.js application RUN npm run build @@ -28,7 +28,7 @@ COPY --from=builder /app/node_modules ./node_modules EXPOSE 3000 ENV NODE_ENV=production -ENV NEXT_PUBLIC_API_URL=https://internal-api.inboxi.ng/api +ENV NEXT_PUBLIC_API_URL=/api # Start Next.js server diff --git a/TOR-README.md b/TOR-README.md new file mode 100644 index 0000000..11deabf --- /dev/null +++ b/TOR-README.md @@ -0,0 +1,61 @@ +# Ember Market on Tor + +This document explains how to run Ember Market in a Tor-friendly way, ensuring compatibility with .onion domains and the Tor network. + +## Tor-Optimized Configuration + +The application is designed to work seamlessly with Tor by: + +1. Using relative URLs throughout the codebase +2. Avoiding hardcoded domain names +3. Ensuring all API requests work regardless of the domain + +## Running in Tor Mode + +To start the application in Tor mode: + +```bash +# Development +npm run dev:tor + +# Production +npm run start:tor +``` + +These commands use the `.env.tor` configuration file, which sets appropriate environment variables for Tor usage. + +## Accessing via .onion Domain + +When deployed to a Tor hidden service: + +1. The application automatically detects the .onion domain +2. All API requests are made relative to the current domain +3. No changes are needed to the codebase or configuration + +## Security Considerations for Tor + +- All API requests use relative URLs to prevent domain leakage +- Authentication is performed on the same domain to maintain anonymity +- No external resources are loaded that could compromise Tor anonymity + +## Deployment to a Tor Hidden Service + +To deploy as a Tor hidden service: + +1. Set up a Tor hidden service pointing to the application port (default: 3000) +2. Use the `start:tor` script to ensure proper environment variables +3. The application will automatically work with the .onion domain + +## Checking Tor Compatibility + +To verify the application is working correctly with Tor: + +1. Check network requests in the browser's developer tools +2. All API requests should go to relative paths (e.g., `/api/auth/me`) +3. No absolute URLs should be used in requests + +## Troubleshooting Tor Issues + +- If authentication fails, check that cookies are enabled in your Tor browser +- If API requests fail, verify that the application is accessible via the .onion domain +- For security issues, ensure JavaScript is enabled for the .onion domain in Tor browser settings \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 840fc25..90a03a0 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,6 +1,9 @@ +'use client'; + +import { useEffect, useState } from 'react'; import Dashboard from "@/components/dashboard/dashboard"; import Content from "@/components/dashboard/content"; -import { fetchServer } from '@/lib/server-service'; +import { fetchClient } from '@/lib/client-service'; // ✅ Corrected Vendor Type interface Vendor { @@ -22,13 +25,43 @@ interface OrderStats { cancelledOrders: number; } -export default async function DashboardPage() { - const [userResponse, orderStats] = await Promise.all([ - fetchServer("/auth/me"), - fetchServer("/orders/stats"), - ]); +export default function DashboardPage() { + const [vendor, setVendor] = useState(null); + const [orderStats, setOrderStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const vendor = userResponse.vendor; + useEffect(() => { + async function fetchData() { + try { + setLoading(true); + + // Fetch data from API using relative URLs and client-side fetch + const [userResponse, stats] = await Promise.all([ + fetchClient("auth/me"), + fetchClient("orders/stats"), + ]); + + setVendor(userResponse.vendor); + setOrderStats(stats); + } catch (err) { + console.error('Error fetching dashboard data:', err); + setError('Failed to load dashboard data'); + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); + + if (loading) { + return
Loading dashboard...
; + } + + if (error || !vendor || !orderStats) { + return
Error: {error || 'Failed to load data'}
; + } return ( diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 0000000..74e80f6 --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,19 @@ +import mongoose from "mongoose"; +import dotenv from "dotenv"; + +dotenv.config(); + +const connectDB = async () => { + try { + await mongoose.connect(process.env.MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + console.log("✅ MongoDB Connected"); + } catch (error) { + console.error("❌ MongoDB Connection Error:", error); + process.exit(1); // Exit on failure + } +}; + +export default connectDB; diff --git a/backend/controllers/chat.controller.js b/backend/controllers/chat.controller.js new file mode 100644 index 0000000..e2c19ed --- /dev/null +++ b/backend/controllers/chat.controller.js @@ -0,0 +1,521 @@ +import Chat from "../models/Chat.model.js"; +import Store from "../models/Store.model.js"; +import Vendor from "../models/Vendor.model.js"; +import { encryptWithPGP } from "../utils/encryptPgp.js"; +import logger from "../utils/logger.js"; +import { sendTelegramMessage } from "../utils/telegramUtils.js"; +import axios from "axios"; + +// Get all chats for a vendor +export const getVendorChats = async (req, res) => { + try { + const { vendorId } = req.params; + + // Check if vendor exists and requester has access + if (req.user._id.toString() !== vendorId) { + return res.status(403).json({ error: "Not authorized to access these chats" }); + } + + const chats = await Chat.find({ vendorId }) + .sort({ lastUpdated: -1 }) + .select("-messages") // Don't include messages in the list view + .lean(); + + return res.status(200).json(chats); + } catch (error) { + logger.error("Error getting vendor chats", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error getting chats" }); + } +}; + +// Get all messages in a specific chat +export const getChatMessages = async (req, res) => { + try { + const { chatId } = req.params; + const { markAsRead = "true" } = req.query; // Default to true for backward compatibility + const shouldMarkAsRead = markAsRead === "true"; + + const chat = await Chat.findById(chatId).lean(); + + if (!chat) { + return res.status(404).json({ error: "Chat not found" }); + } + + // Check if user has access to this chat + if (req.user && chat.vendorId.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "Not authorized to access this chat" }); + } + + // Only mark messages as read if markAsRead parameter is true + if (shouldMarkAsRead) { + // Mark all vendor messages as read if the request is from the buyer + if (req.telegramUser && chat.buyerId === req.telegramUser.telegramId) { + await Chat.updateMany( + { _id: chatId, "messages.sender": "vendor", "messages.read": false }, + { $set: { "messages.$[elem].read": true } }, + { arrayFilters: [{ "elem.sender": "vendor", "elem.read": false }] } + ); + } + + // Mark all buyer messages as read if the request is from the vendor + if (req.user && chat.vendorId.toString() === req.user._id.toString()) { + await Chat.updateMany( + { _id: chatId, "messages.sender": "buyer", "messages.read": false }, + { $set: { "messages.$[elem].read": true } }, + { arrayFilters: [{ "elem.sender": "buyer", "elem.read": false }] } + ); + } + } + + return res.status(200).json(chat); + } catch (error) { + logger.error("Error getting chat messages", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error getting chat messages" }); + } +}; + +// Explicitly mark messages as read +export const markMessagesAsRead = async (req, res) => { + try { + const { chatId } = req.params; + + const chat = await Chat.findById(chatId); + + if (!chat) { + return res.status(404).json({ error: "Chat not found" }); + } + + // Check if user has access to this chat + if (req.user && chat.vendorId.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "Not authorized to access this chat" }); + } + + // Mark all buyer messages as read if the request is from the vendor + if (req.user && chat.vendorId.toString() === req.user._id.toString()) { + await Chat.updateMany( + { _id: chatId, "messages.sender": "buyer", "messages.read": false }, + { $set: { "messages.$[elem].read": true } }, + { arrayFilters: [{ "elem.sender": "buyer", "elem.read": false }] } + ); + + logger.info("Marked all buyer messages as read", { chatId }); + } + + return res.status(200).json({ success: true }); + } catch (error) { + logger.error("Error marking messages as read", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error marking messages as read" }); + } +}; + +// Send a message from vendor to buyer +export const sendVendorMessage = async (req, res) => { + try { + const { chatId } = req.params; + const { content, attachments } = req.body; + + if (!content && (!attachments || attachments.length === 0)) { + return res.status(400).json({ error: "Message content or attachments required" }); + } + + const chat = await Chat.findById(chatId); + + if (!chat) { + return res.status(404).json({ error: "Chat not found" }); + } + + // Check vendor authorization + if (chat.vendorId.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "Not authorized to send messages in this chat" }); + } + + // Create the new message + const newMessage = { + sender: "vendor", + buyerId: chat.buyerId, + vendorId: chat.vendorId, + content: content || "", + attachments: attachments || [], + read: false, + createdAt: new Date() + }; + + // Add message to chat + chat.messages.push(newMessage); + chat.lastUpdated = new Date(); + await chat.save(); + + // Send notification to Telegram client if configured + try { + const store = await Store.findById(chat.storeId); + if (!store) { + logger.warn("Store not found for chat notification", { chatId, storeId: chat.storeId }); + } else { + // Get store name for the notification + const storeName = store.storeName || "Vendor"; + + // Check if store has telegram token + if (store.telegramToken) { + logger.info("Sending Telegram notification", { buyerId: chat.buyerId, storeId: store._id }); + + // Format the message with emoji and preview + const notificationMessage = `📬 New message from ${storeName}: ${content.substring(0, 50)}${content.length > 50 ? '...' : ''}`; + + // Use the sendTelegramMessage utility instead of axios + const success = await sendTelegramMessage( + store.telegramToken, + chat.buyerId, + notificationMessage + ); + + if (success) { + logger.info("Telegram notification sent successfully", { chatId, buyerId: chat.buyerId }); + } else { + logger.error("Failed to send Telegram notification", { + chatId, + buyerId: chat.buyerId + }); + } + } else { + logger.warn("Store missing telegramToken", { storeId: store._id }); + } + } + } catch (notifyError) { + // Log but don't fail if notification fails + logger.error("Notification system error", { + error: notifyError.message, + stack: notifyError.stack, + chatId + }); + } + + return res.status(201).json(newMessage); + } catch (error) { + logger.error("Error sending vendor message", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error sending message" }); + } +}; + +// Process an incoming message from the Telegram client +export const processTelegramMessage = async (req, res) => { + try { + const { buyerId, storeId, content, attachments } = req.body; + + if (!buyerId || !storeId) { + return res.status(400).json({ error: "Buyer ID and Store ID are required" }); + } + + if (!content && (!attachments || attachments.length === 0)) { + return res.status(400).json({ error: "Message content or attachments required" }); + } + + // Find the store + const store = await Store.findById(storeId); + + if (!store) { + logger.error("Store not found for Telegram message", { storeId }); + return res.status(404).json({ error: "Store not found" }); + } + + // Check if vendorId field exists in store + if (!store.vendorId) { + logger.error("Store missing vendorId field", { storeId, store }); + return res.status(500).json({ error: "Store data is invalid (missing vendorId)" }); + } + + // Find or create a chat + let chat = await Chat.findOne({ buyerId, storeId }); + + if (!chat) { + // Create a new chat + chat = new Chat({ + buyerId, + vendorId: store.vendorId, + storeId, + messages: [], + }); + } + + // Create the new message + const newMessage = { + sender: "buyer", + buyerId, + vendorId: store.vendorId, + content: content || "", + attachments: attachments || [], + read: false, + createdAt: new Date() + }; + + // Add message to chat + chat.messages.push(newMessage); + chat.lastUpdated = new Date(); + await chat.save(); + + return res.status(201).json({ + success: true, + chatId: chat._id, + message: newMessage + }); + } catch (error) { + logger.error("Error processing Telegram message", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error processing message" }); + } +}; + +// Get unread message counts for a vendor +export const getVendorUnreadCounts = async (req, res) => { + try { + const { vendorId } = req.params; + + // Check if vendor is authorized + if (req.user._id.toString() !== vendorId) { + return res.status(403).json({ error: "Not authorized" }); + } + + // Find all chats for this vendor + const chats = await Chat.find({ vendorId }); + + // Calculate unread counts + const result = { + totalUnread: 0, + chatCounts: {} + }; + + chats.forEach(chat => { + const unreadCount = chat.messages.filter( + msg => msg.sender === "buyer" && !msg.read + ).length; + + if (unreadCount > 0) { + result.totalUnread += unreadCount; + result.chatCounts[chat._id] = unreadCount; + } + }); + + return res.status(200).json(result); + } catch (error) { + logger.error("Error getting unread counts", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error getting unread counts" }); + } +}; + +// Create a new chat directly (for customer service purposes) +export const createChat = async (req, res) => { + try { + const { buyerId, storeId, initialMessage } = req.body; + + if (!buyerId || !storeId) { + return res.status(400).json({ error: "Buyer ID and Store ID are required" }); + } + + // Find the store and vendor + const store = await Store.findById(storeId); + console.log("Store data:", JSON.stringify(store, null, 2)); + console.log("Authenticated user:", JSON.stringify(req.user, null, 2)); + + if (!store) { + return res.status(404).json({ error: "Store not found" }); + } + + // Check if vendorId field exists in store + if (!store.vendorId) { + logger.error("Store missing vendorId field", { storeId, store }); + return res.status(500).json({ error: "Store data is invalid (missing vendorId)" }); + } + + // Check if requester is authorized for this store + if (req.user._id.toString() !== store.vendorId.toString()) { + return res.status(403).json({ error: "Not authorized to create chats for this store" }); + } + + // Check if chat already exists + const existingChat = await Chat.findOne({ buyerId, storeId }); + if (existingChat) { + return res.status(409).json({ + error: "Chat already exists", + chatId: existingChat._id + }); + } + + // Create a new chat + const chat = new Chat({ + buyerId, + vendorId: store.vendorId, + storeId, + messages: [], + }); + + // Add initial message if provided + if (initialMessage) { + chat.messages.push({ + sender: "vendor", + buyerId, + vendorId: store.vendorId, + content: initialMessage, + read: false, + createdAt: new Date() + }); + } + + chat.lastUpdated = new Date(); + await chat.save(); + + // Send notification to Telegram if configured + try { + if (initialMessage) { + // Get store name for the notification + const storeName = store.storeName || "Vendor"; + + // Check if store has telegram token + if (store.telegramToken) { + logger.info("Sending Telegram notification for new chat", { buyerId, storeId: store._id }); + + // Format the message with emoji and preview + const notificationMessage = `📬 New chat created by ${storeName}: ${initialMessage.substring(0, 50)}${initialMessage.length > 50 ? '...' : ''}`; + + // Use the sendTelegramMessage utility instead of axios + const success = await sendTelegramMessage( + store.telegramToken, + buyerId, + notificationMessage + ); + + if (success) { + logger.info("Telegram notification for new chat sent successfully", { chatId: chat._id, buyerId }); + } else { + logger.error("Failed to send Telegram notification for new chat", { + chatId: chat._id, + buyerId + }); + } + } else { + logger.warn("Store missing telegramToken for new chat notification", { storeId: store._id }); + } + } + } catch (notifyError) { + logger.error("Notification system error for new chat", { + error: notifyError.message, + stack: notifyError.stack, + chatId: chat._id, + buyerId, + storeId + }); + } + + return res.status(201).json({ + success: true, + chatId: chat._id + }); + } catch (error) { + logger.error("Error creating chat", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error creating chat" }); + } +}; + +// Create a chat from Telegram client +export const createTelegramChat = async (req, res) => { + try { + const { buyerId, storeId, initialMessage } = req.body; + + if (!buyerId || !storeId) { + return res.status(400).json({ error: "Buyer ID and Store ID are required" }); + } + + // Find the store + const store = await Store.findById(storeId); + + if (!store) { + logger.error("Store not found for Telegram chat creation", { storeId }); + return res.status(404).json({ error: "Store not found" }); + } + + // Check if vendorId field exists in store + if (!store.vendorId) { + logger.error("Store missing vendorId field", { storeId, store }); + return res.status(500).json({ error: "Store data is invalid (missing vendorId)" }); + } + + // Check if chat already exists + const existingChat = await Chat.findOne({ buyerId, storeId }); + + // If chat exists, just return it + if (existingChat) { + logger.info("Chat already exists, returning existing chat", { + chatId: existingChat._id, + buyerId, + storeId + }); + + // Add initial message to existing chat if provided + if (initialMessage) { + existingChat.messages.push({ + sender: "buyer", + buyerId, + vendorId: store.vendorId, + content: initialMessage, + read: false, + createdAt: new Date() + }); + + existingChat.lastUpdated = new Date(); + await existingChat.save(); + + // Notify vendor about the new message + // This would typically be done through a notification system + logger.info("Added message to existing chat", { + chatId: existingChat._id, + buyerId, + message: initialMessage.substring(0, 50) + }); + } + + return res.status(200).json({ + message: "Using existing chat", + chatId: existingChat._id + }); + } + + // Create a new chat + const chat = new Chat({ + buyerId, + vendorId: store.vendorId, + storeId, + messages: [], + }); + + // Add initial message if provided + if (initialMessage) { + chat.messages.push({ + sender: "buyer", + buyerId, + vendorId: store.vendorId, + content: initialMessage, + read: false, + createdAt: new Date() + }); + } + + chat.lastUpdated = new Date(); + await chat.save(); + + logger.info("New chat created from Telegram", { + chatId: chat._id, + buyerId, + storeId + }); + + return res.status(201).json({ + message: "Chat created successfully", + chatId: chat._id + }); + } catch (error) { + logger.error("Error creating chat from Telegram", { + error: error.message, + stack: error.stack, + buyerId: req.body.buyerId, + storeId: req.body.storeId + }); + return res.status(500).json({ error: "Server error creating chat" }); + } +}; \ No newline at end of file diff --git a/backend/controllers/cryptoController.js b/backend/controllers/cryptoController.js new file mode 100644 index 0000000..61b9cd0 --- /dev/null +++ b/backend/controllers/cryptoController.js @@ -0,0 +1,69 @@ +import ky from "ky"; +import logger from "../utils/logger.js" + +// Global object to store the latest crypto prices +const cryptoPrices = { + btc: null, + ltc: null, + xmr: null, + lastUpdated: null, +}; + +/** + * Fetch crypto prices from the CoinGecko API and update the global `cryptoPrices` object. + */ +const fetchCryptoPrices = async () => { + try { + const url = + "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,litecoin,monero&vs_currencies=gbp"; + + // Fetch using Ky with automatic JSON parsing + const data = await ky.get(url).json(); + + // Update the stored crypto prices + cryptoPrices.btc = data.bitcoin?.gbp ?? null; + cryptoPrices.ltc = data.litecoin?.gbp ?? null; + cryptoPrices.xmr = data.monero?.gbp ?? null; + cryptoPrices.lastUpdated = new Date().toISOString(); + + logger.info("✅ Crypto prices updated", { cryptoPrices }); + } catch (error) { + logger.error("❌ Error fetching crypto prices", { + message: error.message || "Unknown error", + }); + } +}; + +/** + * Starts the automatic crypto price updater. + * @param {number} interval - Update interval in seconds. + */ +const startCryptoPriceUpdater = (interval) => { + logger.info(`🚀 Starting crypto price updater (every ${interval} seconds)`); + fetchCryptoPrices(); // Fetch immediately + setInterval(fetchCryptoPrices, interval * 1000); // Fetch periodically +}; + +/** + * API Route: Get the latest crypto prices. + * @route GET /api/crypto + */ +const getCryptoPrices = async (req, res) => { + try { + res.json({ + success: true, + prices: cryptoPrices, + }); + } catch (error) { + logger.error("❌ Error getting crypto prices", { + message: error.message || "Unknown error", + }); + res.status(500).json({ success: false, error: "Internal Server Error" }); + } +}; + +const returnCryptoPrices = () => { + return cryptoPrices; +}; + +export { startCryptoPriceUpdater, getCryptoPrices, returnCryptoPrices }; \ No newline at end of file diff --git a/backend/controllers/promotion.controller.js b/backend/controllers/promotion.controller.js new file mode 100644 index 0000000..af2ed0c --- /dev/null +++ b/backend/controllers/promotion.controller.js @@ -0,0 +1,271 @@ +import Promotion from "../models/Promotion.model.js"; +import logger from "../utils/logger.js"; + +/** + * Get all promotions for a store + */ +export const getPromotions = async (req, res) => { + try { + const { query } = req; + const filter = { storeId: req.user.storeId }; + + // Apply filters if provided + if (query.isActive) { + filter.isActive = query.isActive === 'true'; + } + + if (query.search) { + filter.code = { $regex: query.search, $options: 'i' }; + } + + // Handle date filters + if (query.active === 'true') { + const now = new Date(); + filter.startDate = { $lte: now }; + filter.$or = [ + { endDate: null }, + { endDate: { $gte: now } } + ]; + filter.isActive = true; + } + + const promotions = await Promotion.find(filter).sort({ createdAt: -1 }); + + return res.status(200).json(promotions); + } catch (error) { + logger.error("Error fetching promotions", { error }); + return res.status(500).json({ message: "Failed to fetch promotions", error: error.message }); + } +}; + +/** + * Get a single promotion by ID + */ +export const getPromotionById = async (req, res) => { + try { + const { id } = req.params; + + const promotion = await Promotion.findOne({ + _id: id, + storeId: req.user.storeId + }); + + if (!promotion) { + return res.status(404).json({ message: "Promotion not found" }); + } + + return res.status(200).json(promotion); + } catch (error) { + logger.error("Error fetching promotion", { error }); + return res.status(500).json({ message: "Failed to fetch promotion", error: error.message }); + } +}; + +/** + * Create a new promotion + */ +export const createPromotion = async (req, res) => { + try { + const { + code, + discountType, + discountValue, + minOrderAmount = 0, + maxUsage = null, + isActive = true, + startDate = new Date(), + endDate = null, + description = "" + } = req.body; + + // Validate required fields + if (!code || !discountType || discountValue === undefined) { + return res.status(400).json({ message: "Code, discount type, and discount value are required" }); + } + + // Check if code already exists for this store + const existingPromo = await Promotion.findOne({ + storeId: req.user.storeId, + code: code.toUpperCase() + }); + + if (existingPromo) { + return res.status(400).json({ message: "A promotion with this code already exists" }); + } + + // Create new promotion + const newPromotion = new Promotion({ + storeId: req.user.storeId, + code: code.toUpperCase(), + discountType, + discountValue, + minOrderAmount, + maxUsage, + isActive, + startDate, + endDate, + description + }); + + await newPromotion.save(); + + return res.status(201).json(newPromotion); + } catch (error) { + logger.error("Error creating promotion", { error }); + + if (error.name === 'ValidationError') { + return res.status(400).json({ + message: "Invalid promotion data", + details: Object.values(error.errors).map(err => err.message) + }); + } + + return res.status(500).json({ message: "Failed to create promotion", error: error.message }); + } +}; + +/** + * Update an existing promotion + */ +export const updatePromotion = async (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + + // Ensure the promotion exists and belongs to the vendor's store + const promotion = await Promotion.findOne({ + _id: id, + storeId: req.user.storeId + }); + + if (!promotion) { + return res.status(404).json({ message: "Promotion not found" }); + } + + // Remove storeId from updates if present (can't change store) + if (updates.storeId) { + delete updates.storeId; + } + + // Ensure code is uppercase if present + if (updates.code) { + updates.code = updates.code.toUpperCase(); + + // Check if updated code conflicts with existing promo + const codeExists = await Promotion.findOne({ + storeId: req.user.storeId, + code: updates.code, + _id: { $ne: id } // Exclude current promotion + }); + + if (codeExists) { + return res.status(400).json({ message: "A promotion with this code already exists" }); + } + } + + // Update the promotion + const updatedPromotion = await Promotion.findByIdAndUpdate( + id, + { ...updates, updatedAt: new Date() }, + { new: true, runValidators: true } + ); + + return res.status(200).json(updatedPromotion); + } catch (error) { + logger.error("Error updating promotion", { error }); + + if (error.name === 'ValidationError') { + return res.status(400).json({ + message: "Invalid promotion data", + details: Object.values(error.errors).map(err => err.message) + }); + } + + return res.status(500).json({ message: "Failed to update promotion", error: error.message }); + } +}; + +/** + * Delete a promotion + */ +export const deletePromotion = async (req, res) => { + try { + const { id } = req.params; + + // Ensure the promotion exists and belongs to the vendor's store + const promotion = await Promotion.findOne({ + _id: id, + storeId: req.user.storeId + }); + + if (!promotion) { + return res.status(404).json({ message: "Promotion not found" }); + } + + await Promotion.findByIdAndDelete(id); + + return res.status(200).json({ message: "Promotion deleted successfully" }); + } catch (error) { + logger.error("Error deleting promotion", { error }); + return res.status(500).json({ message: "Failed to delete promotion", error: error.message }); + } +}; + +/** + * Validate a promotion code for a store + */ +export const validatePromotion = async (req, res) => { + try { + const { code, orderTotal } = req.body; + const { storeId } = req.params; + + if (!code || !storeId) { + return res.status(400).json({ message: "Promotion code and store ID are required" }); + } + + // Find the promotion + const promotion = await Promotion.findOne({ + storeId, + code: code.toUpperCase(), + isActive: true, + }); + + if (!promotion) { + return res.status(404).json({ message: "Promotion not found or inactive" }); + } + + // Check if the promotion is valid + if (!promotion.isValid()) { + return res.status(400).json({ message: "Promotion is no longer valid" }); + } + + // Check minimum order amount + if (orderTotal && orderTotal < promotion.minOrderAmount) { + return res.status(400).json({ + message: `Order total must be at least £${promotion.minOrderAmount} to use this promotion`, + minOrderAmount: promotion.minOrderAmount + }); + } + + // Calculate discount if order total provided + let discountAmount = null; + if (orderTotal) { + discountAmount = promotion.calculateDiscount(orderTotal); + } + + return res.status(200).json({ + promotion: { + _id: promotion._id, + code: promotion.code, + discountType: promotion.discountType, + discountValue: promotion.discountValue, + minOrderAmount: promotion.minOrderAmount, + }, + discountAmount, + message: "Promotion is valid" + }); + } catch (error) { + logger.error("Error validating promotion", { error }); + return res.status(500).json({ message: "Failed to validate promotion", error: error.message }); + } +}; \ No newline at end of file diff --git a/backend/controllers/stock.controller.js b/backend/controllers/stock.controller.js new file mode 100644 index 0000000..12f1f86 --- /dev/null +++ b/backend/controllers/stock.controller.js @@ -0,0 +1,151 @@ +import Product from "../models/Product.model.js"; +import Order from "../models/Order.model.js"; +import Store from "../models/Store.model.js"; +import mongoose from "mongoose"; +import logger from "../utils/logger.js"; + +/** + * Updates a product's stock quantity + */ +export const updateStock = async (req, res) => { + try { + const { productId } = req.params; + const { currentStock, stockTracking, lowStockThreshold } = req.body; + + if (currentStock === undefined) { + return res.status(400).json({ message: "Stock quantity is required" }); + } + + // Validate product exists and belongs to the vendor's store + const product = await Product.findOne({ + _id: productId, + storeId: req.user.storeId + }); + + if (!product) { + return res.status(404).json({ message: "Product not found" }); + } + + // Update stock values + let stockStatus = "in_stock"; + if (currentStock <= 0) { + stockStatus = "out_of_stock"; + } else if (currentStock <= (lowStockThreshold || product.lowStockThreshold)) { + stockStatus = "low_stock"; + } + + const updatedProduct = await Product.findByIdAndUpdate( + productId, + { + currentStock: currentStock, + stockTracking: stockTracking !== undefined ? stockTracking : product.stockTracking, + lowStockThreshold: lowStockThreshold !== undefined ? lowStockThreshold : product.lowStockThreshold, + stockStatus: stockStatus + }, + { new: true } + ); + + return res.status(200).json(updatedProduct); + + } catch (error) { + logger.error("Error updating stock", { error }); + return res.status(500).json({ message: "Failed to update stock", error: error.message }); + } +}; + +/** + * Gets stock information for all products in a store + */ +export const getStoreStock = async (req, res) => { + try { + const products = await Product.find({ storeId: req.user.storeId }) + .select('_id name currentStock stockTracking stockStatus lowStockThreshold') + .sort({ stockStatus: 1, name: 1 }); + + return res.status(200).json(products); + } catch (error) { + logger.error("Error fetching store stock", { error }); + return res.status(500).json({ message: "Failed to fetch stock information", error: error.message }); + } +}; + +/** + * Updates stock levels when an order is placed + * This should be called from the order creation logic + */ +export const decreaseStockOnOrder = async (order) => { + try { + // Process each ordered product + for (const item of order.products) { + const product = await Product.findById(item.productId); + + // Skip if product doesn't exist or stock tracking is disabled + if (!product || !product.stockTracking) continue; + + // Calculate new stock level + let newStock = Math.max(0, product.currentStock - item.quantity); + let stockStatus = "in_stock"; + + if (newStock <= 0) { + stockStatus = "out_of_stock"; + } else if (newStock <= product.lowStockThreshold) { + stockStatus = "low_stock"; + } + + // Update the product stock + await Product.findByIdAndUpdate( + item.productId, + { + currentStock: newStock, + stockStatus: stockStatus + }, + { new: true } + ); + } + + return true; + } catch (error) { + logger.error("Error decreasing stock on order", { orderId: order._id, error }); + return false; + } +}; + +/** + * Restores stock when an order is cancelled + */ +export const restoreStockOnCancel = async (order) => { + try { + // Process each ordered product + for (const item of order.products) { + const product = await Product.findById(item.productId); + + // Skip if product doesn't exist or stock tracking is disabled + if (!product || !product.stockTracking) continue; + + // Calculate new stock level + let newStock = product.currentStock + item.quantity; + let stockStatus = "in_stock"; + + if (newStock <= 0) { + stockStatus = "out_of_stock"; + } else if (newStock <= product.lowStockThreshold) { + stockStatus = "low_stock"; + } + + // Update the product stock + await Product.findByIdAndUpdate( + item.productId, + { + currentStock: newStock, + stockStatus: stockStatus + }, + { new: true } + ); + } + + return true; + } catch (error) { + logger.error("Error restoring stock on cancel", { orderId: order._id, error }); + return false; + } +}; \ No newline at end of file diff --git a/backend/controllers/vendor.controller.js b/backend/controllers/vendor.controller.js new file mode 100644 index 0000000..d3e8c21 --- /dev/null +++ b/backend/controllers/vendor.controller.js @@ -0,0 +1,39 @@ +// Handle notifications from Telegram to vendors +export const notifyVendor = async (req, res) => { + try { + const { buyerId, storeId, message, source } = req.body; + + if (!buyerId || !storeId || !message) { + return res.status(400).json({ error: "Missing required parameters" }); + } + + // Find the store and check if it exists + const store = await Store.findById(storeId); + if (!store) { + logger.warn("Store not found for vendor notification", { storeId }); + return res.status(404).json({ error: "Store not found" }); + } + + // Get the vendor + const vendor = await Vendor.findById(store.vendorId); + if (!vendor) { + logger.warn("Vendor not found for notification", { storeId, vendorId: store.vendorId }); + return res.status(404).json({ error: "Vendor not found" }); + } + + // Future enhancement: could implement WebSocket/Server-Sent Events for real-time notifications + // For now, we'll just track that the notification was processed + + logger.info("Processed vendor notification", { + buyerId, + storeId, + vendorId: vendor._id, + source: source || "unknown" + }); + + return res.status(200).json({ success: true }); + } catch (error) { + logger.error("Error processing vendor notification", { error: error.message, stack: error.stack }); + return res.status(500).json({ error: "Server error processing notification" }); + } +}; \ No newline at end of file diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..8f46f19 --- /dev/null +++ b/backend/index.js @@ -0,0 +1,112 @@ +import express from "express"; +import dotenv from "dotenv"; +import cors from "cors"; +import connectDB from "./config/db.js"; +import logger from "./utils/logger.js"; + +// Routes +import authRoutes from "./routes/auth.routes.js"; +import inviteRoutes from "./routes/invites.routes.js"; +import staffAuthRoutes from "./routes/staffAuth.routes.js"; +import orderRoutes from "./routes/orders.routes.js"; +import productRoutes from "./routes/products.routes.js"; +import categoryRoutes from "./routes/categories.routes.js"; +import shippingRoutes from "./routes/shipping.routes.js"; +import storeRoutes from "./routes/storefront.routes.js"; +import cryptoRoutes from "./routes/crypto.routes.js"; +import blockedUsersRoutes from "./routes/blockedUsers.routes.js"; +import chatRoutes from "./routes/chat.routes.js"; +import stockRoutes from "./routes/stock.routes.js"; +import promotionRoutes from "./routes/promotion.routes.js"; + +import { startCryptoPriceUpdater } from "./controllers/cryptoController.js"; + +// Direct routes for Telegram API to bypass JWT middleware +import { protectTelegramApi } from "./middleware/telegramAuthMiddleware.js"; +import { processTelegramMessage, createTelegramChat } from "./controllers/chat.controller.js"; + +dotenv.config(); + +const app = express(); + +connectDB(); + +// Add security headers and handle CORS +app.use((req, res, next) => { + // Basic security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + + // Handle CORS + const origin = req.headers.origin; + const host = req.headers.host; + + // For Tor (null origin), use the .onion address if we're on the API domain + if (!origin || origin === 'null') { + if (host.includes('internal-api')) { + res.setHeader('Access-Control-Allow-Origin', 'http://6n6f6krmcudhzalzuqckms5bhc4afxc7xgjngumkafvgzmjmd2tmzeid.onion'); + } else { + res.setHeader('Access-Control-Allow-Origin', `https://${host}`); + } + } else { + res.setHeader('Access-Control-Allow-Origin', origin); + } + + // Always enable credentials since we're using specific origins + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin'); + res.setHeader('Access-Control-Expose-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Max-Age', '86400'); + + // Log the request for debugging + logger.info(`Request from ${req.ip} - Origin: ${origin || 'null'} - Host: ${host}`); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(204).end(); + return; + } + + next(); +}); + +// Parse JSON for all routes +app.use(express.json({ limit: "15mb" })); + +// Direct routes for Telegram API to bypass JWT middleware +app.post("/telegram/message", protectTelegramApi, processTelegramMessage); +app.post("/telegram/create", protectTelegramApi, createTelegramChat); +app.get("/telegram/test-auth", protectTelegramApi, (req, res) => { + res.status(200).json({ + success: true, + message: "Authentication successful", + headers: { + authHeader: req.headers.authorization ? req.headers.authorization.substring(0, 10) + "..." : "undefined", + xApiKey: req.headers['x-api-key'] ? req.headers['x-api-key'].substring(0, 10) + "..." : "undefined" + } + }); +}); + +// Register API routes +app.use("/api/products", productRoutes); +app.use("/api/chats", chatRoutes); +app.use("/api/auth", authRoutes); +app.use("/api/staff/auth", staffAuthRoutes); +app.use("/api/invite", inviteRoutes); +app.use("/api/orders", orderRoutes); +app.use("/api/categories", categoryRoutes); +app.use("/api/shipping-options", shippingRoutes); +app.use("/api/storefront", storeRoutes); +app.use("/api/crypto", cryptoRoutes); +app.use("/api/blocked-users", blockedUsersRoutes); +app.use("/api/stock", stockRoutes); +app.use("/api/promotions", promotionRoutes); + +startCryptoPriceUpdater(60); + +const PORT = process.env.PORT || 3001; +app.listen(PORT, () => { + logger.info(`Server running on port ${PORT}`); +}); \ No newline at end of file diff --git a/backend/middleware/apiAuthMiddleware.js b/backend/middleware/apiAuthMiddleware.js new file mode 100644 index 0000000..65b09ff --- /dev/null +++ b/backend/middleware/apiAuthMiddleware.js @@ -0,0 +1,10 @@ +export const protectCrypto = async (req, res, next) => { + if ( + req.headers.authorization && + req.headers.authorization === process.env.INTERNAL_API_KEY + ) { + return next(); + } + + return res.status(403).json({ error: "Forbidden: Invalid API key" }); +}; diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 0000000..5e9d283 --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,32 @@ +import jwt from "jsonwebtoken"; +import Vendor from "../models/Vendor.model.js"; + +export const protectVendor = async (req, res, next) => { + if (req.method === "OPTIONS") { + return res.status(200).end(); + } + + let token; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer") + ) { + try { + token = req.headers.authorization.split(" ")[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + const vendor = await Vendor.findById(decoded.id); + if (!vendor) return res.status(401).json({ message: "Unauthorized" }); + + req.user = vendor; + req.user.storeId = vendor.storeId; + + next(); + } catch (error) { + return res.status(401).json({ message: "Token failed" }); + } + } else { + return res.status(401).json({ message: "Not authorized, no token" }); + } +}; diff --git a/backend/middleware/staffAuthMiddleware.js b/backend/middleware/staffAuthMiddleware.js new file mode 100644 index 0000000..beb1433 --- /dev/null +++ b/backend/middleware/staffAuthMiddleware.js @@ -0,0 +1,49 @@ +import jwt from "jsonwebtoken"; +import Staff from "../models/Staff.model.js"; + +/** + * Middleware to protect staff-only routes - Verify JWT from DB + */ +export const protectStaff = async (req, res, next) => { + let token = req.headers.authorization; + + if (!token || !token.startsWith("Bearer ")) { + return res.status(401).json({ error: "Not authorized, no token" }); + } + + try { + token = token.split(" ")[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // Verify staff user exists and token matches stored value + const staff = await Staff.findById(decoded.id); + if (!staff || staff.currentToken !== token) { + return res.status(401).json({ error: "Invalid or expired session" }); + } + + req.user = staff; // Attach staff user data to request + next(); + } catch (error) { + res.status(401).json({ error: "Token is invalid or expired" }); + } +}; + +/** + * 📌 Staff Logout - Remove JWT from Database + */ +export const logoutStaff = async (req, res) => { + try { + const staff = await Staff.findById(req.user.id); + if (!staff) { + return res.status(401).json({ error: "User not found" }); + } + + // Clear stored token + staff.currentToken = null; + await staff.save(); + + res.json({ message: "Logged out successfully" }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; diff --git a/backend/middleware/telegramAuthMiddleware.js b/backend/middleware/telegramAuthMiddleware.js new file mode 100644 index 0000000..0c50f5a --- /dev/null +++ b/backend/middleware/telegramAuthMiddleware.js @@ -0,0 +1,70 @@ +import logger from "../utils/logger.js"; + +// Middleware for protecting Telegram API routes +export const protectTelegramApi = async (req, res, next) => { + // Log the headers for debugging + logger.info("Telegram API request headers:", { + authorization: req.headers.authorization ? req.headers.authorization.substring(0, 10) + "..." : "undefined", + "x-api-key": req.headers['x-api-key'] ? req.headers['x-api-key'].substring(0, 10) + "..." : "undefined", + method: req.method, + path: req.path, + allHeaders: JSON.stringify(req.headers) + }); + + // Full debug for non-production environments + logger.info("FULL HEADER DEBUG (KEYS ONLY):", Object.keys(req.headers)); + logger.info("AUTH HEADER TYPE:", typeof req.headers.authorization); + + const expectedKey = process.env.INTERNAL_API_KEY; + logger.info("Expected API Key (first 10 chars):", expectedKey ? expectedKey.substring(0, 10) + "..." : "undefined"); + + // Check if the environment variable is actually defined + if (!expectedKey) { + logger.error("INTERNAL_API_KEY environment variable is not defined"); + return res.status(500).json({ error: "Server configuration error" }); + } + + // Check if API key is in the expected header + if (req.headers.authorization === expectedKey) { + logger.info("Telegram API auth successful via Authorization header"); + return next(); + } + + // Also try x-api-key as a fallback + if (req.headers['x-api-key'] === expectedKey) { + logger.info("Telegram API auth successful via x-api-key header"); + return next(); + } + + // Try trimming whitespace + if (req.headers.authorization && req.headers.authorization.trim() === expectedKey) { + logger.info("Telegram API auth successful via Authorization header (after trimming)"); + return next(); + } + + // Also try x-api-key with trimming + if (req.headers['x-api-key'] && req.headers['x-api-key'].trim() === expectedKey) { + logger.info("Telegram API auth successful via x-api-key header (after trimming)"); + return next(); + } + + // Check for Bearer prefix and try to extract the token + if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { + const token = req.headers.authorization.substring(7).trim(); + if (token === expectedKey) { + logger.info("Telegram API auth successful via Bearer token in Authorization header"); + return next(); + } + } + + logger.warn("Telegram API auth failed:", { + expectedKeyPrefix: expectedKey ? expectedKey.substring(0, 5) + "..." : "undefined", + expectedKeyLength: expectedKey ? expectedKey.length : 0, + authHeaderPrefix: req.headers.authorization ? req.headers.authorization.substring(0, 5) + "..." : "undefined", + authHeaderLength: req.headers.authorization ? req.headers.authorization.length : 0, + xApiKeyPrefix: req.headers['x-api-key'] ? req.headers['x-api-key'].substring(0, 5) + "..." : "undefined", + xApiKeyLength: req.headers['x-api-key'] ? req.headers['x-api-key'].length : 0 + }); + + return res.status(401).json({ error: "Unauthorized: Invalid API key" }); +}; \ No newline at end of file diff --git a/backend/middleware/vendorAuthMiddleware.js b/backend/middleware/vendorAuthMiddleware.js new file mode 100644 index 0000000..5e9d283 --- /dev/null +++ b/backend/middleware/vendorAuthMiddleware.js @@ -0,0 +1,32 @@ +import jwt from "jsonwebtoken"; +import Vendor from "../models/Vendor.model.js"; + +export const protectVendor = async (req, res, next) => { + if (req.method === "OPTIONS") { + return res.status(200).end(); + } + + let token; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer") + ) { + try { + token = req.headers.authorization.split(" ")[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + const vendor = await Vendor.findById(decoded.id); + if (!vendor) return res.status(401).json({ message: "Unauthorized" }); + + req.user = vendor; + req.user.storeId = vendor.storeId; + + next(); + } catch (error) { + return res.status(401).json({ message: "Token failed" }); + } + } else { + return res.status(401).json({ message: "Not authorized, no token" }); + } +}; diff --git a/backend/models/BlockedUser.model.js b/backend/models/BlockedUser.model.js new file mode 100644 index 0000000..b309b96 --- /dev/null +++ b/backend/models/BlockedUser.model.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; + +const BlockedUserSchema = new mongoose.Schema({ + telegramUserId: { type: Number, required: true, unique: true }, + reason: { type: String }, + blockedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "Staff", + required: true + }, + blockedAt: { type: Date, default: Date.now } +}); + +export default mongoose.model("BlockedUser", BlockedUserSchema); \ No newline at end of file diff --git a/backend/models/Buyer.model.js b/backend/models/Buyer.model.js new file mode 100644 index 0000000..7633e8d --- /dev/null +++ b/backend/models/Buyer.model.js @@ -0,0 +1,10 @@ +import mongoose from 'mongoose'; + +const BuyerSchema = new mongoose.Schema({ + telegramId: { type: Number, required: true, unique: true }, + username: { type: String, required: true }, + createdAt: { type: Date, default: Date.now }, + banned: { type: Boolean, default: false } +}); + +export default mongoose.model('Buyer', BuyerSchema); diff --git a/backend/models/Chat.model.js b/backend/models/Chat.model.js new file mode 100644 index 0000000..2428e9c --- /dev/null +++ b/backend/models/Chat.model.js @@ -0,0 +1,68 @@ +import mongoose from "mongoose"; + +const MessageSchema = new mongoose.Schema({ + sender: { + type: String, + enum: ["buyer", "vendor"], + required: true + }, + buyerId: { + type: String, + required: true + }, + vendorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Vendor", + required: true + }, + content: { + type: String, + required: true + }, + attachments: [{ + type: String, + required: false + }], + read: { + type: Boolean, + default: false + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +const ChatSchema = new mongoose.Schema({ + buyerId: { + type: String, + required: true + }, + vendorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Vendor", + required: true + }, + storeId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Store", + required: true + }, + messages: [MessageSchema], + orderId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Order", + required: false + }, + lastUpdated: { + type: Date, + default: Date.now + } +}); + +// Create indexes for faster queries +ChatSchema.index({ buyerId: 1, vendorId: 1 }); +ChatSchema.index({ vendorId: 1, lastUpdated: -1 }); +ChatSchema.index({ buyerId: 1, lastUpdated: -1 }); + +export default mongoose.model("Chat", ChatSchema); \ No newline at end of file diff --git a/backend/models/Escrow.model.js b/backend/models/Escrow.model.js new file mode 100644 index 0000000..e8f54ae --- /dev/null +++ b/backend/models/Escrow.model.js @@ -0,0 +1,43 @@ +import mongoose from "mongoose"; + +const EscrowSchema = new mongoose.Schema({ + orderId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Order", + required: true, + }, + buyerId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Buyer", + required: true, + }, + vendorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Vendor", + required: true, + }, + amount: { + type: Number, + required: true, + min: 0.01, + }, + currency: { + type: String, + enum: ["ltc", "btc", "xmr"], + required: true, + }, + status: { + type: String, + enum: ["held", "released", "disputed"], + default: "held", + }, + releaseDate: { + type: Date, + required: true, + default: function () { + return new Date(Date.now() + 8 * 24 * 60 * 60 * 1000); // Auto set to 8 days from now + }, + }, +}); + +export default mongoose.model("Escrow", EscrowSchema); diff --git a/backend/models/Invitation.model.js b/backend/models/Invitation.model.js new file mode 100644 index 0000000..d3a12ca --- /dev/null +++ b/backend/models/Invitation.model.js @@ -0,0 +1,40 @@ +import mongoose from "mongoose"; + +const InvitationSchema = new mongoose.Schema({ + code: { + type: String, + required: true, + unique: true, + }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Staffs', + required: true, + }, + isUsed: { + type: Boolean, + default: false, + }, + usedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Vendor', + default: null, + }, + expiresAt: { + type: Date, + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + }, + usedAt: { + type: Date, + default: null, + } +}); + +InvitationSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); +InvitationSchema.index({ code: 1 }); + +export default mongoose.model("Invitation", InvitationSchema); diff --git a/backend/models/Order.model.js b/backend/models/Order.model.js new file mode 100644 index 0000000..b19115b --- /dev/null +++ b/backend/models/Order.model.js @@ -0,0 +1,100 @@ +import mongoose from "mongoose"; +import AutoIncrement from "mongoose-sequence"; + +const connection = mongoose.connection; + +const OrderSchema = new mongoose.Schema({ + orderId: { type: Number, unique: true }, + + vendorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Vendor", + required: true, + }, + storeId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Store", + required: true, + }, + + pgpAddress: { type: String, required: true }, + orderDate: { type: Date, default: Date.now, }, + txid: { type: Array, default: [] }, + + products: [ + { + productId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Product", + required: true, + }, + quantity: { type: Number, required: true, min: 0.1 }, + pricePerUnit: { type: Number, required: true, min: 0.01 }, + totalItemPrice: { type: Number, required: true, min: 0.01 }, + }, + ], + + shippingMethod: { type: Object }, + + totalPrice: { type: Number, required: true, min: 0.01 }, + + // Promotion fields + promotion: { + type: mongoose.Schema.Types.ObjectId, + ref: "Promotion", + default: null + }, + promotionCode: { + type: String, + default: null + }, + discountAmount: { + type: Number, + default: 0, + min: 0 + }, + subtotalBeforeDiscount: { + type: Number, + default: 0, + min: 0 + }, + + status: { + type: String, + enum: [ + "unpaid", + "cancelled", + "confirming", + "paid", + "shipped", + "disputed", + "completed", + "acknowledged" + ], + default: "unpaid", + }, + + paymentAddress: { type: String, required: true }, + + wallet: { + type: mongoose.Schema.Types.ObjectId, + ref: "Wallet", + }, + + cryptoTotal: { type: Number, required: true, default: 0 }, + //txid: { type: String, default: null }, + + telegramChatId: { type: String, default: null }, + telegramBuyerId: { type: String, default: null }, + telegramUsername: { type: String, default: null }, + trackingNumber: { type: String, default: null }, + + escrowExpiresAt: { + type: Date, + required: true, + default: () => new Date(Date.now() + 8 * 24 * 60 * 60 * 1000), + }, +}); + +OrderSchema.plugin(AutoIncrement(connection), { inc_field: "orderId" }); +export default mongoose.model("Order", OrderSchema); diff --git a/backend/models/Product.model.js b/backend/models/Product.model.js new file mode 100644 index 0000000..869d68b --- /dev/null +++ b/backend/models/Product.model.js @@ -0,0 +1,69 @@ +import mongoose from "mongoose"; + +const ProductSchema = new mongoose.Schema({ + storeId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Store", + required: true, + }, + category: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "Store.categories" + }, + + name: { type: String, required: true }, + description: { type: String }, + + unitType: { + type: String, + enum: ["pcs", "gr", "kg"], + required: true, + }, + + // Add inventory tracking fields + stockTracking: { type: Boolean, default: true }, + currentStock: { + type: Number, + default: 0, + validate: { + validator: function(value) { + return !this.stockTracking || value >= 0; + }, + message: "Stock cannot be negative" + } + }, + lowStockThreshold: { type: Number, default: 10 }, + stockStatus: { + type: String, + enum: ["in_stock", "low_stock", "out_of_stock"], + default: "out_of_stock" + }, + + pricing: [ + { + minQuantity: { + type: Number, + required: true, + validate: { + validator: function(value) { + if (this.parent().unitType === "gr") { + return value >= 0.1; + } + return value >= 1; + }, + message: "Invalid minQuantity for unitType" + } + }, + pricePerUnit: { + type: Number, + required: true, + min: 0.01 + }, + }, + ], + + image: { type: String }, +}); + +export default mongoose.model("Product", ProductSchema); diff --git a/backend/models/Promotion.model.js b/backend/models/Promotion.model.js new file mode 100644 index 0000000..1e39292 --- /dev/null +++ b/backend/models/Promotion.model.js @@ -0,0 +1,126 @@ +import mongoose from "mongoose"; + +const PromotionSchema = new mongoose.Schema( + { + storeId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Store", + required: [true, "Store ID is required"] + }, + code: { + type: String, + required: [true, "Promotion code is required"], + trim: true, + uppercase: true, + minlength: [3, "Promotion code must be at least 3 characters"], + maxlength: [20, "Promotion code cannot exceed 20 characters"] + }, + discountType: { + type: String, + required: [true, "Discount type is required"], + enum: { + values: ["percentage", "fixed"], + message: "Discount type must be either percentage or fixed" + } + }, + discountValue: { + type: Number, + required: [true, "Discount value is required"], + validate: { + validator: function(value) { + if (this.discountType === "percentage") { + return value > 0 && value <= 100; + } + return value > 0; + }, + message: props => + props.value <= 0 + ? "Discount value must be greater than 0" + : "Percentage discount cannot exceed 100%" + } + }, + minOrderAmount: { + type: Number, + default: 0, + min: [0, "Minimum order amount cannot be negative"] + }, + maxUsage: { + type: Number, + default: null + }, + usageCount: { + type: Number, + default: 0 + }, + isActive: { + type: Boolean, + default: true + }, + startDate: { + type: Date, + default: Date.now + }, + endDate: { + type: Date, + default: null + }, + description: { + type: String, + trim: true, + maxlength: [200, "Description cannot exceed 200 characters"] + } + }, + { + timestamps: true + } +); + +// Compound index to ensure unique promo codes per store +PromotionSchema.index({ storeId: 1, code: 1 }, { unique: true }); + +// Check if a promotion is valid and can be applied +PromotionSchema.methods.isValid = function() { + const now = new Date(); + + // Check if promotion is active + if (!this.isActive) return false; + + // Check if promotion has expired + if (this.endDate && now > this.endDate) return false; + + // Check if promotion has reached max usage (if set) + if (this.maxUsage !== null && this.usageCount >= this.maxUsage) return false; + + return true; +}; + +// Calculate discount amount for a given order total +PromotionSchema.methods.calculateDiscount = function(orderTotal) { + if (!this.isValid()) { + return 0; + } + + if (orderTotal < this.minOrderAmount) { + return 0; + } + + let discountAmount = 0; + + if (this.discountType === "percentage") { + discountAmount = (orderTotal * this.discountValue) / 100; + } else { + // Fixed amount discount + discountAmount = this.discountValue; + + // Ensure discount doesn't exceed order total + if (discountAmount > orderTotal) { + discountAmount = orderTotal; + } + } + + return parseFloat(discountAmount.toFixed(2)); +}; + +const Promotion = mongoose.model("Promotion", PromotionSchema); + +export default Promotion; \ No newline at end of file diff --git a/backend/models/PromotionUse.model.js b/backend/models/PromotionUse.model.js new file mode 100644 index 0000000..3396e5c --- /dev/null +++ b/backend/models/PromotionUse.model.js @@ -0,0 +1,54 @@ +import mongoose from 'mongoose'; + +const PromotionUseSchema = new mongoose.Schema( + { + promotionId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Promotion', + required: [true, 'Promotion ID is required'] + }, + orderId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Order', + required: [true, 'Order ID is required'] + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: [true, 'User ID is required'] + }, + storeId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Store', + required: [true, 'Store ID is required'] + }, + code: { + type: String, + required: [true, 'Promotion code is required'] + }, + discountType: { + type: String, + enum: ['percentage', 'fixed'], + required: [true, 'Discount type is required'] + }, + discountValue: { + type: Number, + required: [true, 'Discount value is required'] + }, + discountAmount: { + type: Number, + required: [true, 'Discount amount is required'] + }, + orderTotal: { + type: Number, + required: [true, 'Order total is required'] + } + }, + { + timestamps: true + } +); + +const PromotionUse = mongoose.model('PromotionUse', PromotionUseSchema); + +export default PromotionUse; \ No newline at end of file diff --git a/backend/models/Staff.model.js b/backend/models/Staff.model.js new file mode 100644 index 0000000..ca66789 --- /dev/null +++ b/backend/models/Staff.model.js @@ -0,0 +1,11 @@ +import mongoose from "mongoose"; + +const StaffSchema = new mongoose.Schema({ + username: { type: String, required: true, unique: true }, + passwordHash: { type: String, required: true }, + role: { type: String, enum: ["admin", "moderator"], default: "moderator" }, + currentToken: { type: String, default: null }, + createdAt: { type: Date, default: Date.now }, +}); + +export default mongoose.model("Staff", StaffSchema); \ No newline at end of file diff --git a/backend/models/Store.model.js b/backend/models/Store.model.js new file mode 100644 index 0000000..89d6bcb --- /dev/null +++ b/backend/models/Store.model.js @@ -0,0 +1,132 @@ +import mongoose from "mongoose"; +import { type } from "os"; + +const CategorySchema = new mongoose.Schema({ + _id: { + type: mongoose.Schema.Types.ObjectId, + auto: true + }, + name: { + type: String, + required: true + }, + parentId: { + type: mongoose.Schema.Types.ObjectId, + default: null + } +}, { _id: true }); + +const StoreSchema = new mongoose.Schema({ + vendorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Vendor", + required: true + }, + storeName: { + type: String, + required: true + }, + welcomeMessage: { + type: String, + default: "Welcome to my store!" + }, + telegramToken: { + type: String, + default: "" + }, + pgpKey: { + type: String, + default: "" + }, + createdAt: { + type: Date, + default: Date.now + }, + shipsFrom: { + type: String, + default: "UK", + }, + shipsTo: { + type: String, + default: "UK", + }, + categories: [CategorySchema], + + shippingOptions: [ + { + name: { + type: String, + required: true + }, + price: { + type: Number, + required: true, + min: 0 + } + } + ], + + wallets:{ + type: Object, + default: { + "litecoin": "", + "bitcoin": "", + "monero":"" + } + }, + + feeRate:{ + type: Number, + default: 2 + } +}); + +// Add a method to get category hierarchy +StoreSchema.methods.getCategoryHierarchy = function() { + const categories = this.categories.toObject(); + + // Helper function to build tree structure + const buildTree = (parentId = null) => { + return categories + .filter(cat => + (!parentId && !cat.parentId) || + (cat.parentId?.toString() === parentId?.toString()) + ) + .map(cat => ({ + ...cat, + children: buildTree(cat._id) + })); + }; + + return buildTree(); +}; + +// Add validation to prevent circular references +CategorySchema.pre('save', function(next) { + if (!this.parentId) { + return next(); + } + + const checkCircular = (categoryId, parentId) => { + if (!parentId) return false; + if (categoryId.toString() === parentId.toString()) return true; + + const parent = this.parent().categories.id(parentId); + if (!parent) return false; + + return checkCircular(categoryId, parent.parentId); + }; + + if (checkCircular(this._id, this.parentId)) { + next(new Error('Circular reference detected in category hierarchy')); + } else { + next(); + } +}); + +// Add index for better query performance +StoreSchema.index({ 'categories.name': 1, 'categories.parentId': 1 }); + +const Store = mongoose.model("Store", StoreSchema); + +export default Store; \ No newline at end of file diff --git a/backend/models/TelegramUser.model.js b/backend/models/TelegramUser.model.js new file mode 100644 index 0000000..8fa6a0a --- /dev/null +++ b/backend/models/TelegramUser.model.js @@ -0,0 +1,22 @@ +import mongoose from "mongoose"; + +const { Schema, model, Types } = mongoose; + +/** + * Defines the schema for Telegram users. + * - `telegramUserId`: Unique Telegram user ID. + * - `stores`: Array of objects storing store references and chat IDs. + * - `createdAt`: Timestamp for when the user was added. + */ +const TelegramUserSchema = new Schema({ + telegramUserId: { type: Number, required: true, unique: true }, + stores: [ + { + store: { type: Types.ObjectId, ref: "Store", required: true }, + chatId: { type: Number, required: true } + } + ], + createdAt: { type: Date, default: Date.now } +}); + +export default model("TelegramUser", TelegramUserSchema); \ No newline at end of file diff --git a/backend/models/Vendor.model.js b/backend/models/Vendor.model.js new file mode 100644 index 0000000..4b016eb --- /dev/null +++ b/backend/models/Vendor.model.js @@ -0,0 +1,13 @@ +import mongoose from "mongoose"; + +const VendorSchema = new mongoose.Schema({ + username: { type: String, required: true, unique: true }, + passwordHash: { type: String, required: true }, + currentToken: { type: String, default: null }, + storeId: { type: mongoose.Schema.Types.ObjectId, ref: "Store", default: null }, + pgpKey: { type: String, default: ""}, + lastOnline: { type: Date, default: Date.now }, + createdAt: { type: Date, default: Date.now }, +}); + +export default mongoose.model("Vendor", VendorSchema); diff --git a/backend/models/Wallet.model.js b/backend/models/Wallet.model.js new file mode 100644 index 0000000..c2ee49d --- /dev/null +++ b/backend/models/Wallet.model.js @@ -0,0 +1,56 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +// Set default values if environment variables are not available +const encryptionKeyHex = process.env.ENCRYPTION_KEY || '48c66ee5a54e596e2029ea832a512401099533ece34cb0fbbb8c4023ca68ba8e'; +const encryptionIvHex = process.env.ENCRYPTION_IV || '539e26d426cd4bac9844a8e446d63ab1'; + +const algorithm = "aes-256-cbc"; +const encryptionKey = Buffer.from(encryptionKeyHex, "hex"); +const iv = Buffer.from(encryptionIvHex, "hex"); + +function encrypt(text) { + const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv); + let encrypted = cipher.update(text, "utf8", "hex"); + encrypted += cipher.final("hex"); + return encrypted; +} + +function decrypt(text) { + const decipher = crypto.createDecipheriv(algorithm, encryptionKey, iv); + let decrypted = decipher.update(text, "hex", "utf8"); + decrypted += decipher.final("utf8"); + return decrypted; +} + +const WalletSchema = new mongoose.Schema({ + walletName: { + type: String, + }, + orderId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Order", + required: true, + unique: true, + }, + address: { + type: String, + required: true, + }, + encryptedPrivateKey: { + type: String, + required: true, + }, +}); + +WalletSchema.pre("save", function (next) { + if (!this.isModified("encryptedPrivateKey")) return next(); + this.encryptedPrivateKey = encrypt(this.encryptedPrivateKey); + next(); +}); + +WalletSchema.methods.getDecryptedPrivateKey = function () { + return decrypt(this.encryptedPrivateKey); +}; + +export default mongoose.model("Wallet", WalletSchema); \ No newline at end of file diff --git a/backend/routes/auth.routes.js b/backend/routes/auth.routes.js new file mode 100644 index 0000000..f99bac3 --- /dev/null +++ b/backend/routes/auth.routes.js @@ -0,0 +1,158 @@ +import express from "express"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import Vendor from "../models/Vendor.model.js"; +import Store from "../models/Store.model.js"; +import Invitation from "../models/Invitation.model.js"; +import { protectVendor } from "../middleware/vendorAuthMiddleware.js"; + +const router = express.Router(); + +/** + * Register a New Vendor (and create a corresponding Store) + * @route POST /api/auth/register + */ +router.post("/register", async (req, res) => { + const { username, password, invitationCode } = req.body; + + try { + if (!username || !password || !invitationCode) { + return res.status(400).json({ error: "All fields are required." }); + } + + // Verify invitation code + const invitation = await Invitation.findOne({ + code: invitationCode, + isUsed: false, + }); + + console.log(`Invitation: ${invitation}`); + + if (!invitation) { + return res.status(400).json({ error: "Invalid or used invitation code." }); + } + + // Check if vendor already exists + const existingVendor = await Vendor.findOne({ username }); + if (existingVendor) { + return res.status(400).json({ error: "Vendor already exists." }); + } + + // Hash the password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create the vendor + const vendor = new Vendor({ + username, + passwordHash: hashedPassword, + }); + await vendor.save(); + + // Create a store for this vendor + const store = new Store({ + vendorId: vendor._id, + storeName: `${username}'s Store`, + welcomeMessage: "Welcome to my store!", + categories: [], + }); + await store.save(); + + // Attach `storeId` to vendor + vendor.storeId = store._id; + await vendor.save(); + + // Mark invitation as used + invitation.isUsed = true; + invitation.usedBy = vendor._id; + invitation.usedAt = new Date(); + await invitation.save(); + + return res + .status(201) + .json({ message: "Vendor registered successfully", store }); + } catch (error) { + console.error("Error registering vendor:", error); + return res.status(500).json({ error: error.message }); + } +}); + +/** + * Vendor Login + * @route POST /api/auth/login + */ +router.post("/login", async (req, res) => { + const { username, password } = req.body; + + try { + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required." }); + } + + const vendor = await Vendor.findOne({ username }); + if (!vendor) { + return res.status(401).json({ error: "Invalid credentials." }); + } + + const isMatch = await bcrypt.compare(password, vendor.passwordHash); + if (!isMatch) { + return res.status(401).json({ error: "Invalid credentials." }); + } + + // Generate a JWT + const token = jwt.sign( + { id: vendor._id, role: "vendor" }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ); + + // Store the token in the DB to identify the current session + vendor.currentToken = token; + await vendor.save(); + + return res.json({ token, role: "vendor" }); + } catch (error) { + console.error("Error logging in vendor:", error); + return res.status(500).json({ error: error.message }); + } +}); + +/** + * Vendor Logout + * @route POST /api/auth/logout + * @access Private (Vendors only) + */ +router.post("/logout", protectVendor, async (req, res) => { + try { + await Vendor.findByIdAndUpdate(req.user._id, { currentToken: null }); + return res.json({ message: "Successfully logged out." }); + } catch (error) { + console.error("Error logging out vendor:", error); + return res.status(500).json({ error: "Failed to log out." }); + } +}); + +/** + * Get Vendor Info + * @route GET /api/auth/me + * @access Private (Vendors only) + */ +router.get("/me", protectVendor, async (req, res) => { + try { + const vendor = await Vendor.findById(req.user._id).select("-passwordHash -currentToken"); + + if (!vendor) { + return res.status(404).json({ error: "Vendor not found." }); + } + + vendor.lastOnline = new Date(); + await vendor.save(); + + const store = await Store.findOne({ vendorId: vendor._id }); + return res.json({ vendor, store }); + } catch (error) { + console.error("Error fetching vendor info:", error); + return res.status(500).json({ error: "Failed to fetch vendor data." }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/routes/blockedUsers.routes.js b/backend/routes/blockedUsers.routes.js new file mode 100644 index 0000000..ef9decd --- /dev/null +++ b/backend/routes/blockedUsers.routes.js @@ -0,0 +1,75 @@ +import express from "express"; +import { protectStaff } from "../middleware/staffAuthMiddleware.js"; +import BlockedUser from "../models/BlockedUser.model.js"; + +const router = express.Router(); + +/** + * Get all blocked users + * @route GET /api/blocked-users + * @access Private (Staff only) + */ +router.get("/", protectStaff, async (req, res) => { + try { + const blockedUsers = await BlockedUser.find() + .sort({ blockedAt: -1 }); + res.json(blockedUsers); + } catch (error) { + console.error("Error fetching blocked users:", error); + res.status(500).json({ error: "Failed to fetch blocked users" }); + } +}); + +/** + * Block a user + * @route POST /api/blocked-users + * @access Private (Staff only) + */ +router.post("/", protectStaff, async (req, res) => { + try { + const { telegramUserId, reason } = req.body; + + if (!telegramUserId) { + return res.status(400).json({ error: "Telegram user ID is required" }); + } + + const existingBlock = await BlockedUser.findOne({ telegramUserId }); + if (existingBlock) { + return res.status(400).json({ error: "User is already blocked" }); + } + + const blockedUser = await BlockedUser.create({ + telegramUserId, + reason, + blockedBy: req.user._id + }); + + res.status(201).json(blockedUser); + } catch (error) { + console.error("Error blocking user:", error); + res.status(500).json({ error: "Failed to block user" }); + } +}); + +/** + * Unblock a user + * @route DELETE /api/blocked-users/:telegramUserId + * @access Private (Staff only) + */ +router.delete("/:telegramUserId", protectStaff, async (req, res) => { + try { + const { telegramUserId } = req.params; + + const result = await BlockedUser.findOneAndDelete({ telegramUserId }); + if (!result) { + return res.status(404).json({ error: "User is not blocked" }); + } + + res.json({ message: "User unblocked successfully" }); + } catch (error) { + console.error("Error unblocking user:", error); + res.status(500).json({ error: "Failed to unblock user" }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/routes/categories.routes.js b/backend/routes/categories.routes.js new file mode 100644 index 0000000..631db93 --- /dev/null +++ b/backend/routes/categories.routes.js @@ -0,0 +1,154 @@ +import express from "express"; +import Store from "../models/Store.model.js"; +import { protectVendor } from "../middleware/vendorAuthMiddleware.js"; +const router = express.Router(); + +/** + * 📌 Fetch all categories for the vendor's store + */ +router.get("/", protectVendor, async (req, res) => { + try { + const store = await Store.findById(req.user.storeId); + if (!store) return res.status(404).json({ message: "Store not found" }); + + res.json(store.categories); + } catch (error) { + res.status(500).json({ message: "Failed to fetch categories", error }); + } +}); + +/** + * 📌 Add a new category + */ +router.post("/", protectVendor, async (req, res) => { + try { + const { name, parentId } = req.body; + const store = await Store.findById(req.user.storeId); + + if (!store) return res.status(404).json({ message: "Store not found" }); + + // Check if the category name already exists at the same level + const categoryExists = store.categories.some(category => + category.name === name && + (!parentId ? !category.parentId : category.parentId?.toString() === parentId) + ); + + if (categoryExists) { + return res.status(400).json({ message: "Category already exists at this level" }); + } + + // If parentId is provided, verify it exists + if (parentId) { + const parentExists = store.categories.some(cat => cat._id.toString() === parentId); + if (!parentExists) { + return res.status(400).json({ message: "Parent category not found" }); + } + } + + // Add the new category with a unique _id, name, and optional parentId + const newCategory = { + name, + ...(parentId && { parentId }) // Only add parentId if it exists + }; + store.categories.push(newCategory); + await store.save(); + + // Get the newly created category with its _id + const createdCategory = store.categories[store.categories.length - 1]; + + res.status(201).json({ + _id: createdCategory._id, + name: createdCategory.name, + parentId: createdCategory.parentId + }); + } catch (error) { + console.error("Error adding category:", error); + res.status(400).json({ message: "Failed to add category", error }); + } +}); + +/** + * 📌 Update a category + */ +router.put("/:categoryId", protectVendor, async (req, res) => { + try { + const { categoryId } = req.params; + const { name } = req.body; + const store = await Store.findById(req.user.storeId); + + if (!store) return res.status(404).json({ message: "Store not found" }); + + // Find the category + const category = store.categories.id(categoryId); + if (!category) { + return res.status(404).json({ message: "Category not found" }); + } + + // Check if the new name already exists at the same level (excluding this category) + const categoryExists = store.categories.some(cat => + cat.name === name && + cat._id.toString() !== categoryId && + (!category.parentId ? !cat.parentId : cat.parentId?.toString() === category.parentId.toString()) + ); + + if (categoryExists) { + return res.status(400).json({ message: "Category name already exists at this level" }); + } + + // Update the category + category.name = name; + await store.save(); + + res.json({ + _id: category._id, + name: category.name, + parentId: category.parentId + }); + } catch (error) { + console.error("Error updating category:", error); + res.status(400).json({ message: "Failed to update category", error }); + } +}); + +/** + * 📌 Delete a category and its subcategories + */ +router.delete("/:categoryId", protectVendor, async (req, res) => { + const { categoryId } = req.params; + + try { + const store = await Store.findById(req.user.storeId); + if (!store) { + return res.status(404).json({ message: "Store not found" }); + } + + // Find all subcategories recursively + const getAllSubcategoryIds = (categoryId) => { + const subcategories = store.categories.filter(cat => + cat.parentId?.toString() === categoryId + ); + + return [ + categoryId, + ...subcategories.flatMap(subcat => getAllSubcategoryIds(subcat._id.toString())) + ]; + }; + + const categoryIdsToDelete = getAllSubcategoryIds(categoryId); + + // Remove all categories and their subcategories + store.categories = store.categories.filter( + cat => !categoryIdsToDelete.includes(cat._id.toString()) + ); + + // Save the updated store document + await store.save(); + + res.status(200).json({ message: "Category and subcategories deleted successfully" }); + } catch (error) { + console.error("Error deleting category:", error); + res.status(500).json({ message: "Failed to delete category", error }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/routes/chat.routes.js b/backend/routes/chat.routes.js new file mode 100644 index 0000000..5f07832 --- /dev/null +++ b/backend/routes/chat.routes.js @@ -0,0 +1,41 @@ +import express from "express"; +import { + getVendorChats, + getChatMessages, + sendVendorMessage, + processTelegramMessage, + getVendorUnreadCounts, + createChat, + createTelegramChat, + markMessagesAsRead +} from "../controllers/chat.controller.js"; +import { protectVendor as vendorAuth } from "../middleware/vendorAuthMiddleware.js"; +import { protectTelegramApi } from "../middleware/telegramAuthMiddleware.js"; + +const router = express.Router(); + +// Routes that require vendor authentication +router.get("/vendor/:vendorId", vendorAuth, getVendorChats); +router.get("/vendor/:vendorId/unread", vendorAuth, getVendorUnreadCounts); +router.get("/:chatId", vendorAuth, getChatMessages); +router.post("/:chatId/message", vendorAuth, sendVendorMessage); +router.post("/:chatId/mark-read", vendorAuth, markMessagesAsRead); +router.post("/create", vendorAuth, createChat); + +// Routes for Telegram client (secured with API key) +router.post("/telegram/message", protectTelegramApi, processTelegramMessage); +router.post("/telegram/create", protectTelegramApi, createTelegramChat); + +// Test route for Telegram API auth +router.get("/telegram/test-auth", protectTelegramApi, (req, res) => { + res.status(200).json({ + success: true, + message: "Authentication successful", + headers: { + authHeader: req.headers.authorization ? req.headers.authorization.substring(0, 10) + "..." : "undefined", + xApiKey: req.headers['x-api-key'] ? req.headers['x-api-key'].substring(0, 10) + "..." : "undefined" + } + }); +}); + +export default router; \ No newline at end of file diff --git a/backend/routes/crypto.routes.js b/backend/routes/crypto.routes.js new file mode 100644 index 0000000..da83392 --- /dev/null +++ b/backend/routes/crypto.routes.js @@ -0,0 +1,9 @@ +import express from "express"; +import ky from "ky" +import { getCryptoPrices } from "../controllers/cryptoController.js"; + +const router = express.Router(); + +router.get("/", getCryptoPrices); + +export default router; \ No newline at end of file diff --git a/backend/routes/invites.routes.js b/backend/routes/invites.routes.js new file mode 100644 index 0000000..5236afe --- /dev/null +++ b/backend/routes/invites.routes.js @@ -0,0 +1,21 @@ +import express from "express"; +import crypto from "crypto"; +import { protectStaff } from "../middleware/staffAuthMiddleware.js"; +import Invitation from "../models/Invitation.model.js"; + +const router = express.Router(); + +router.post("/generate", protectStaff, async (req, res) => { + try { + const invitationCode = crypto.randomBytes(6).toString("hex"); + + const invitation = new Invitation({ code: invitationCode, createdBy: req.user._id }); + await invitation.save(); + + res.status(201).json({ invitationCode }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +export default router; diff --git a/backend/routes/orders.routes.js b/backend/routes/orders.routes.js new file mode 100644 index 0000000..beeb289 --- /dev/null +++ b/backend/routes/orders.routes.js @@ -0,0 +1,598 @@ +import express from "express"; +import { protectVendor } from "../middleware/vendorAuthMiddleware.js"; +import { protectCrypto } from "../middleware/apiAuthMiddleware.js"; +import Order from "../models/Order.model.js"; +import Wallet from "../models/Wallet.model.js"; + +import crypto from "crypto"; +import { setupWallet } from "../utils/litecoin/index.js"; +import { returnCryptoPrices } from "../controllers/cryptoController.js"; +import Store from "../models/Store.model.js"; +import { sendTelegramMessage } from "../utils/telegramUtils.js" +import Product from "../models/Product.model.js"; +import mongoose from "mongoose"; +import { decreaseStockOnOrder, restoreStockOnCancel } from "../controllers/stock.controller.js"; + +const router = express.Router(); + +/** + * 📌 Get Orders for Vendor + * @route GET /api/orders + * @access Private (Vendors only) + */ +router.get("/", protectVendor, async (req, res) => { + try { + // Extract query params for filtering and navigation + const { status, page = 1, limit = 25, before, after, storeId } = req.query; + + // Initialize query object + let query = {}; + + // Use storeId from query parameter if provided, otherwise use the one from user object + query.storeId = storeId || req.user.storeId; + + // If for some reason there's no storeId in user object and none provided in query + if (!query.storeId) { + return res.status(400).json({ error: "No store ID found. Please specify a storeId parameter." }); + } + // Filter by order status if provided + if (status) { + query.status = status; + } + + // Add orderId filters for navigation + if (before) { + query.orderId = { $lt: before }; + } else if (after) { + query.orderId = { $gt: after }; + } + + // Count total orders for pagination + const totalOrders = await Order.countDocuments(query); + + // Fetch orders with pagination and sorting + const orders = await Order.find(query) + .sort({ orderId: -1 }) // Always sort by newest first + .limit(parseInt(limit)) + .skip((parseInt(page) - 1) * parseInt(limit)); + + res.json({ + orders, + page: parseInt(page), + totalPages: Math.ceil(totalOrders / limit), + totalOrders, + }); + } catch (error) { + console.error("Error fetching orders:", error); + res.status(500).json({ error: "Failed to retrieve orders" }); + } +}); + +router.get("/stats", protectVendor, async (req, res) => { + try { + const vendorId = req.user._id; + + const totalOrders = await Order.countDocuments({ vendorId }); + const pendingOrders = await Order.countDocuments({ + vendorId, + status: { $in: ["unpaid", "confirming"] }, + }); + const ongoingOrders = await Order.countDocuments({ + vendorId, + status: { $in: ["paid", "shipped"] }, + }); + const cancelledOrders = await Order.countDocuments({ + vendorId, + status: "cancelled", + }); + const completedOrders = await Order.countDocuments({ + vendorId, + status: "completed", + }); + + res.json({ totalOrders, pendingOrders, ongoingOrders, cancelledOrders, completedOrders }); + } catch (error) { + console.error("Error fetching order stats:", error); + res.status(500).json({ error: "Failed to retrieve order statistics" }); + } +}); + +/** + * 📌 Get Vendor's Best Selling Products + * @route GET /api/orders/top-products + * @access Private (Vendors only) + * @returns Most sold products for the logged-in vendor + */ +router.get("/top-products", protectVendor, async (req, res) => { + try { + const vendorId = req.user._id; + + // Find the vendor's store + const store = await Store.findOne({ vendorId }); + if (!store) { + return res.status(404).json({ message: "Store not found for this vendor" }); + } + + // Aggregate orders to find the vendor's best-selling products + const topProducts = await Order.aggregate([ + // Match only orders for this vendor + { $match: { vendorId: new mongoose.Types.ObjectId(vendorId) } }, + // Unwind the products array + { $unwind: "$products" }, + // Group by product ID and count occurrences + { + $group: { + _id: "$products.productId", + count: { $sum: 1 }, + revenue: { $sum: "$products.price" } + } + }, + // Sort by count in descending order + { $sort: { count: -1 } }, + // Limit to top 10 products + { $limit: 10 } + ]); + + // Get the actual product details + const productIds = topProducts.map(item => item._id); + const products = await Product.find({ + _id: { $in: productIds }, + storeId: store._id + }).select('name price image'); + + // Combine the count with product details + const result = topProducts.map(item => { + const productDetails = products.find(p => p._id.toString() === item._id.toString()); + return { + id: item._id, + name: productDetails?.name || 'Unknown Product', + price: productDetails?.price || 0, + image: productDetails?.image || '', + count: item.count, + revenue: item.revenue + }; + }); + + res.json(result); + } catch (error) { + console.error("Error fetching top products:", error); + res.status(500).json({ error: "Failed to fetch top products data" }); + } +}); + +router.post("/create", protectCrypto, async (req, res) => { + try { + const { vendorId, storeId, products, totalPrice, shippingMethod, telegramChatId, telegramBuyerId, telegramUsername, cryptoCurrency, pgpAddress } = req.body; + + console.log("Create order request:", req.body); + + if (!vendorId || !storeId || !products || !totalPrice || !shippingMethod || !cryptoCurrency || !pgpAddress) { + console.log("Missing required fields"); + return res.status(400).json({ error: "Missing required fields" }); + } + + /* + // TEMPORARILY DISABLED: Stock check disabled to give vendors time to add stock + // Check stock levels before creating order + const outOfStockProducts = []; + for (const item of products) { + const product = await Product.findOne({ _id: item.productId, storeId }); + + if (product && product.stockTracking && product.currentStock < item.quantity) { + outOfStockProducts.push({ + productId: item.productId, + name: product.name, + requested: item.quantity, + available: product.currentStock + }); + } + } + + // If any products are out of stock, return an error + if (outOfStockProducts.length > 0) { + return res.status(400).json({ + error: "Some products are out of stock or have insufficient quantity", + outOfStockProducts + }); + } + */ + + const cryptoPrices = returnCryptoPrices(); + const cryptoRate = cryptoPrices[cryptoCurrency.toLowerCase()]; + + if (!cryptoRate) { + console.log("Invalid or unsupported cryptocurrency"); + return res.status(400).json({ error: "Invalid or unsupported cryptocurrency" }); + } + + const cryptoTotal = (totalPrice / cryptoRate).toFixed(8); + + const walletName = "order_" + crypto.randomBytes(8).toString("hex"); + const walletData = await setupWallet(walletName); + + if (!walletData || !walletData.address || !walletData.privKey) { + return res.status(500).json({ error: "Failed to generate payment address" }); + } + + // ✅ Create Order with `cryptoTotal` + const newOrder = await Order.create({ + vendorId, + storeId, + products, + pgpAddress, + totalPrice, + cryptoTotal, + shippingMethod, + telegramChatId, + telegramUsername, + telegramBuyerId, + status: "unpaid", + paymentAddress: walletData.address, + }); + + // ✅ Link the Wallet to the Order + const newWallet = await Wallet.create({ + walletName, + orderId: newOrder._id, + address: walletData.address, + encryptedPrivateKey: walletData.privKey, + }); + + // ✅ Update Order with Wallet ID + newOrder.wallet = newWallet._id; + await newOrder.save(); + + // TEMPORARILY DISABLED: Stock decrease disabled to give vendors time to add stock + // Decrease stock for ordered products + // await decreaseStockOnOrder(newOrder); + + res.status(201).json({ + message: "Order created successfully", + order: newOrder, + paymentAddress: walletData.address, + walletName: walletName, + cryptoTotal, + }); + + } catch (error) { + console.error("Error creating order:", error); + res.status(500).json({ error: "Failed to create order", details: error.message }); + } +}); + +router.put("/:id/status", protectVendor, async (req, res) => { + try { + const { id } = req.params; + const { status } = req.body; + + if (!status || !["acknowledged", "paid", "shipped", "completed", "cancelled"].includes(status)) { + return res.status(400).json({ message: "Invalid status" }); + } + + // Get storeId from req.user + const storeId = req.user.storeId; + if (!storeId) { + return res.status(400).json({ message: "No store associated with this user" }); + } + + // Find order by ID and storeId + const order = await Order.findOne({ _id: id, storeId }); + if (!order) { + return res.status(404).json({ message: "Order not found" }); + } + + const previousStatus = order.status; + + // TEMPORARILY DISABLED: Stock restoration disabled to give vendors time to add stock + // Handle stock changes based on status transitions + /* + if (status === "cancelled" && previousStatus !== "cancelled") { + // Restore stock quantities if order is cancelled + await restoreStockOnCancel(order); + } + */ + + const store = await Store.findById(order.storeId); + if (!store) return res.status(404).json({ message: "Store not found" }); + + order.status = status; + await order.save(); + + if (store.telegramToken && order.telegramChatId) { + let message = ''; + + switch (status) { + case 'acknowledged': + message = `✅ Your order ${order.orderId} has been acknowledged.`; + break; + case 'paid': + message = `💰 Your order ${order.orderId} has been marked as paid.`; + break; + case 'shipped': + message = `🚚 Your order ${order.orderId} has been shipped!`; + break; + case 'completed': + message = `🎉 Your order ${order.orderId} has been completed.`; + break; + case 'cancelled': + message = `❌ Your order ${order.orderId} has been cancelled.`; + break; + } + + if (message) { + const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message); + if (!sent) { + console.error(`Failed to notify user ${order.telegramChatId} about status update to ${status}.`); + } + } + } + + return res.status(200).json({ message: "Order status updated successfully" }); + } catch (error) { + console.error("❌ Error updating order status:", error); + return res.status(500).json({ error: "Failed to update order status", details: error.message }); + } +}); + +router.get("/:id", protectVendor, async (req, res) => { + if (!req.params.id) return res.status(400).json({ message: "Missing order ID" }); + + try { + // Get storeId from req.user + const storeId = req.user.storeId; + if (!storeId) { + return res.status(400).json({ message: "No store associated with this user" }); + } + + // Find order by ID and storeId + const order = await Order.findOne({ _id: req.params.id, storeId: storeId }); + + if(!order) return res.status(404).json({ message: "Order not found for this store" }); + + return res.status(200).json(order); + } catch (error) { + console.error("Error fetching order:", error); + res.status(500).json({ error: "Failed to retrieve order" }); + } +}) + +/** + * Mark Multiple Orders as Shipped + * @route POST /api/orders/mark-shipped + * @access Private (Vendors only) + */ +router.post("/mark-shipped", protectVendor, async (req, res) => { + try { + const { orderIds } = req.body; + + if (!Array.isArray(orderIds) || orderIds.length === 0) { + return res.status(400).json({ error: "Order IDs array is required" }); + } + + // Get storeId from req.user + const storeId = req.user.storeId; + if (!storeId) { + return res.status(400).json({ error: "No store associated with this user" }); + } + + const updatedOrders = []; + const failedOrders = []; + + // Get store for notifications + const store = await Store.findById(storeId); + if (!store) { + return res.status(404).json({ error: "Store not found" }); + } + + for (const orderId of orderIds) { + try { + // Find and update order using storeId instead of vendorId + const order = await Order.findOne({ _id: orderId, storeId: storeId }); + + if (!order) { + failedOrders.push({ orderId, reason: "Order not found or not associated with this store" }); + continue; + } + + if (order.status !== "paid") { + failedOrders.push({ orderId, reason: "Order must be in paid status to be marked as shipped" }); + continue; + } + + order.status = "shipped"; + await order.save(); + updatedOrders.push(order); + + // Send notification if possible + if (store.telegramToken && order.telegramChatId) { + const message = `🚚 Your order ${order.orderId} has been marked as shipped!`; + const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message); + + if (!sent) { + console.error(`Failed to notify user ${order.telegramChatId} about shipping update.`); + } + } + } catch (error) { + console.error(`Error updating order ${orderId}:`, error); + failedOrders.push({ orderId, reason: "Internal server error" }); + } + } + + return res.json({ + message: "Orders processed", + success: { + count: updatedOrders.length, + orders: updatedOrders.map(o => ({ + id: o._id, + orderId: o.orderId, + status: o.status + })) + }, + failed: { + count: failedOrders.length, + orders: failedOrders + } + }); + + } catch (error) { + console.error("Error marking orders as shipped:", error); + return res.status(500).json({ + error: "Failed to mark orders as shipped", + details: error.message + }); + } +}); + +/** + * Update Order Tracking Number + * @route PUT /api/orders/:id/tracking + * @access Private (Vendors only) + */ +router.put("/:id/tracking", protectVendor, async (req, res) => { + const { id } = req.params; + const { trackingNumber } = req.body; + + if (!id) return res.status(400).json({ error: "Missing order ID" }); + if (!trackingNumber) return res.status(400).json({ error: "Missing tracking number" }); + + try { + // Get storeId from req.user + const storeId = req.user.storeId; + if (!storeId) { + return res.status(400).json({ error: "No store associated with this user" }); + } + + // Use storeId for lookup instead of vendorId + const order = await Order.findOne({ _id: id, storeId: storeId }); + if (!order) return res.status(404).json({ error: "Order not found for this store" }); + + // Only allow tracking updates for paid or shipped orders + if (order.status !== "paid" && order.status !== "shipped") { + return res.status(400).json({ error: "Can only add tracking to paid or shipped orders" }); + } + + // Get store details for notification + const store = await Store.findById(order.storeId); + if (!store) return res.status(404).json({ error: "Store not found" }); + + // Update the order with tracking number + order.trackingNumber = trackingNumber; + await order.save(); + + // Send tracking notification + if (store.telegramToken && order.telegramChatId) { + const message = `📦 Tracking added for order ${order.orderId}\!`; + const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message); + + if (!sent) { + console.error(`Failed to notify user ${order.telegramChatId} about tracking update.`); + } + } + + return res.json({ + message: "Tracking number updated successfully", + order: { + id: order._id, + orderId: order.orderId, + status: order.status, + trackingNumber: order.trackingNumber + } + }); + } catch (error) { + console.error("Error updating tracking number:", error); + return res.status(500).json({ + error: "Failed to update tracking number", + details: error.message + }); + } +}); + +/** + * Delete Order + * @route DELETE /api/orders/:id + * @access Private (Vendors only) + */ +router.delete("/:id", protectVendor, async (req, res) => { + try { + const { id } = req.params; + + // Get storeId from req.user + const storeId = req.user.storeId; + if (!storeId) { + return res.status(400).json({ message: "No store associated with this user" }); + } + + // Find order by ID and storeId + const order = await Order.findOne({ _id: id, storeId: storeId }); + + if (!order) { + return res.status(404).json({ message: "Order not found for this store" }); + } + + // Delete associated wallet if exists + if (order.wallet) { + await Wallet.findByIdAndDelete(order.wallet); + } + + // Delete the order + await Order.findByIdAndDelete(id); + + res.status(200).json({ message: "Order deleted successfully" }); + } catch (error) { + console.error("Error deleting order:", error); + res.status(500).json({ error: "Failed to delete order" }); + } +}); + +/** + * 📌 Get Adjacent Orders (for navigation) + * @route GET /api/orders/adjacent/:orderId + * @access Private (Vendors only) + * @description Returns the immediately previous and next orders by orderId + */ +router.get("/adjacent/:orderId", protectVendor, async (req, res) => { + try { + const { orderId } = req.params; + const numericOrderId = parseInt(orderId); + + if (isNaN(numericOrderId)) { + return res.status(400).json({ error: "Invalid order ID format" }); + } + + // Get the store ID from user or query parameter + const storeId = req.query.storeId || req.user.storeId; + if (!storeId) { + return res.status(400).json({ error: "No store ID found. Please specify a storeId parameter." }); + } + + // Find the next newer order (higher orderId) + const newerOrder = await Order.findOne({ + storeId, + orderId: { $gt: numericOrderId } + }) + .sort({ orderId: 1 }) // Ascending to get the immediately next one + .select('_id orderId') + .lean(); + + // Find the next older order (lower orderId) + const olderOrder = await Order.findOne({ + storeId, + orderId: { $lt: numericOrderId } + }) + .sort({ orderId: -1 }) // Descending to get the immediately previous one + .select('_id orderId') + .lean(); + + res.json({ + current: { orderId: numericOrderId }, + newer: newerOrder || null, + older: olderOrder || null + }); + + } catch (error) { + console.error("Error fetching adjacent orders:", error); + res.status(500).json({ error: "Failed to retrieve adjacent orders" }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/routes/products.routes.js b/backend/routes/products.routes.js new file mode 100644 index 0000000..88086cf --- /dev/null +++ b/backend/routes/products.routes.js @@ -0,0 +1,456 @@ +import express from "express"; +import Product from "../models/Product.model.js"; +import multer from "multer"; +import sharp from "sharp"; +import { protectVendor } from "../middleware/authMiddleware.js"; +import fs from "fs"; +import path, { dirname } from "path"; +import Store from "../models/Store.model.js"; +import mongoose from "mongoose"; +import { fileURLToPath } from "url"; + +const router = express.Router(); + +// Get the current directory and set up a relative uploads path +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const uploadsDir = path.join(process.cwd(), 'uploads'); + +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 15 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/")) { + cb(null, true); + } else { + cb(new Error("Only image files are allowed!"), false); + } + }, +}); + +const handleMulterError = (err, req, res, next) => { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(400).json({ message: "File size too large. Maximum allowed size is 15 MB." }); + } + return res.status(400).json({ message: "Multer error occurred.", error: err.message }); + } + next(err); +}; + +router.use(handleMulterError) + +// 📌 Upload Image for Product +router.put("/:id/image", protectVendor, upload.single("file"), handleMulterError, async (req, res) => { + try { + const productId = req.params.id; + if (!productId) return res.status(400).json({ message: "Missing product ID." }); + + const product = await Product.findOne({ _id: productId, storeId: req.user.storeId }); + if (!product) { + return res.status(404).json({ message: "Product not found." }); + } + + const file = req.file; + if (!file) return res.status(400).json({ message: "No file uploaded." }); + + // Generate a new filename for the uploaded image + const outputFileName = `${Date.now()}-${file.originalname.split('.').slice(0, -1).join('.')}.jpg`; + const outputFilePath = path.join(uploadsDir, outputFileName); + + await sharp(file.buffer) + .jpeg({ quality: 80 }) + .toFile(outputFilePath); + + // Check the final file size + const stats = fs.statSync(outputFilePath); + if (stats.size > 10 * 1024 * 1024) { + fs.unlinkSync(outputFilePath); + return res.status(400).json({ message: "File size too large after processing." }); + } + + // Update the product in the database + const updatedProduct = await Product.findOneAndUpdate( + { _id: productId, storeId: req.user.storeId }, + { image: outputFileName }, + { new: true } + ); + + if (!updatedProduct) { + fs.unlinkSync(outputFilePath); // Clean up uploaded file if product not found + return res.status(404).json({ message: "Product not found." }); + } + + res.json(updatedProduct); + } catch (error) { + if(req.file){ + const outputFileName = `${Date.now()}-${req.file.originalname.split('.').slice(0, -1).join('.')}.jpg`; + const outputFilePath = path.join(uploadsDir, outputFileName); + + if (fs.existsSync(outputFilePath)) { + fs.unlinkSync(outputFilePath); + } + } + res.status(500).json({ message: "Failed to upload image.", error }); + } +}); + +router.use(express.json({ limit: "50mb" })); + +router.get("/", protectVendor, async (req, res) => { + try { + if (!req.user.storeId) { + return res.status(400).json({ message: "Store not found for this vendor" }); + } + + const products = await Product.find({ storeId: req.user.storeId }).select("-base64Image"); + res.json(products); + } catch (error) { + res.status(500).json({ message: "Failed to fetch products", error }); + } +}); + +// 📌 Add New Product +router.post("/", protectVendor, async (req, res) => { + try { + if (!req.user.storeId) { + return res.status(400).json({ message: "Store not found for this vendor" }); + } + + const { name, description, category, unitType, pricing } = req.body; + + if (!name || !category || !unitType || !pricing || !pricing.length) { + return res.status(400).json({ message: "Missing required fields" }); + } + + const formattedPricing = pricing.map((tier) => ({ + minQuantity: tier.minQuantity, + pricePerUnit: tier.pricePerUnit, + })); + + const newProduct = new Product({ + storeId: req.user.storeId, + name, + description, + category, + unitType, + pricing: formattedPricing, + }); + + const savedProduct = await newProduct.save(); + res.status(201).json(savedProduct); + } catch (error) { + res.status(400).json({ message: "Failed to add product", error }); + } +}); + +// 📌 Edit a Product by ID +router.put("/:id", protectVendor, async (req, res) => { + try { + const productId = req.params.id; + + const updatableFields = ["name", "description", "category", "unitType"]; + + const updateFields = {}; + + for (const field of updatableFields) { + if (req.body[field] !== undefined) { + updateFields[field] = req.body[field]; + } + } + + if (req.body.pricing !== undefined) { + if (!Array.isArray(req.body.pricing)) { + return res.status(400).json({ message: "Pricing must be an array" }); + } + + updateFields.pricing = req.body.pricing.map((tier) => ({ + minQuantity: tier.minQuantity, + pricePerUnit: tier.pricePerUnit, + })); + } + + const updatedProduct = await Product.findOneAndUpdate( + { _id: productId, storeId: req.user.storeId }, + updateFields, + { new: true } + ); + + if (!updatedProduct) { + return res + .status(404) + .json({ message: "Product not found or not owned by this store" }); + } + + return res.json(updatedProduct); + } catch (error) { + res.status(400).json({ message: "Failed to update product", error }); + } +}); + +router.get("/:id/image", async (req, res) => { + try { + const productId = req.params.id; + const product = await Product.findById(productId); + + if (!product || !product.image) { + return res.status(404).json({ message: "Image not found" }); + } + + const imagePath = path.join(uploadsDir, product.image); + console.log("Image path:", imagePath); + if (fs.existsSync(imagePath)) { + res.sendFile(imagePath); + } else { + res.status(404).json({ message: "Image file does not exist" }); + } + } catch (error) { + console.error("Error fetching image:", error); + res.status(400).json({ message: "Failed to fetch image", error }); + } +}); + +router.delete("/:id", protectVendor, async (req, res) => { + try { + const productId = req.params.id; + + const deletedProduct = await Product.findByIdAndDelete({ _id: productId, storeId: req.user.storeId }); + + if (!deletedProduct) { + return res.status(404).json({ message: "Product not found" }); + } + + res.json({ message: "Product deleted successfully" }); + } catch (error) { + res.status(400).json({ message: "Failed to delete product", error }); + } +}); + +router.get("/:id", protectVendor, async (req, res) => { + try { + const productId = req.params.id; + const product = await Product.findById(productId); + + if (!product) { + return res.status(404).json({ message: "Product not found" }); + } + + res.status(200).json(product); + } catch (error) { + res.status(400).json({ message: "Failed to fetch product", error }); + } +}); + +// Helper function to escape special regex characters +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Helper function to standardize unit types +function standardizeUnitType(unit) { + const unitMap = { + 'gram': 'gr', + 'grams': 'gr', + 'g': 'gr', + 'gr': 'gr', + 'piece': 'pcs', + 'pieces': 'pcs', + 'pcs': 'pcs', + 'kilo': 'kg', + 'kilos': 'kg', + 'kilogram': 'kg', + 'kilograms': 'kg', + 'kg': 'kg' + }; + + return unitMap[unit.toLowerCase()] || unit.toLowerCase(); +} + +// 📌 Batch Add Products +router.post("/batch", protectVendor, async (req, res) => { + try { + if (!req.user.storeId) { + return res.status(400).json({ message: "Store not found for this vendor" }); + } + + const { products } = req.body; + + if (!Array.isArray(products) || products.length === 0) { + return res.status(400).json({ message: "Products array is required and cannot be empty" }); + } + + // Get store to validate categories + const store = await Store.findById(req.user.storeId); + if (!store) { + return res.status(404).json({ message: "Store not found" }); + } + + const processedProducts = []; + const skippedProducts = []; + let completedCount = 0; + + console.log(`Starting batch processing of ${products.length} products...`); + + for (const product of products) { + const { name, category, subcategory, prices } = product; + completedCount++; + + // Validate required fields + if (!name || !category || !prices || !Array.isArray(prices) || prices.length === 0) { + console.log(`Skipping product ${completedCount}/${products.length}: Missing required fields`); + skippedProducts.push({ name: name || 'Unknown', reason: 'Missing required fields' }); + continue; + } + + // Check if product already exists - with escaped special characters + const escapedName = escapeRegExp(name); + const existingProduct = await Product.findOne({ + storeId: req.user.storeId, + name: { $regex: new RegExp(`^${escapedName}$`, 'i') } + }); + + if (existingProduct) { + console.log(`Skipping product ${completedCount}/${products.length}: "${name}" already exists`); + skippedProducts.push({ name, reason: 'Product already exists' }); + continue; + } + + // Find or create main category + let targetCategory; + const existingMainCategory = store.categories.find(cat => + cat.name.toLowerCase() === category.toLowerCase() && !cat.parentId + ); + + if (!existingMainCategory) { + const newCategory = { + _id: new mongoose.Types.ObjectId(), + name: category, + parentId: null + }; + store.categories.push(newCategory); + targetCategory = newCategory; + console.log(`Created new main category: ${category}`); + } else { + targetCategory = existingMainCategory; + } + + // If subcategory is provided, find or create it + if (subcategory) { + const existingSubcategory = store.categories.find(cat => + cat.name.toLowerCase() === subcategory.toLowerCase() && + cat.parentId?.toString() === targetCategory._id.toString() + ); + + if (existingSubcategory) { + targetCategory = existingSubcategory; + } else { + const newSubcategory = { + _id: new mongoose.Types.ObjectId(), + name: subcategory, + parentId: targetCategory._id + }; + store.categories.push(newSubcategory); + targetCategory = newSubcategory; + console.log(`Created new subcategory: ${subcategory} under ${category}`); + } + } + + console.log(targetCategory._id) + + // Convert prices array to pricing format + const pricing = prices.map(price => ({ + minQuantity: price.quantity, + pricePerUnit: price.pricePerUnit + })); + + const standardizedUnitType = standardizeUnitType(prices[0].unit); + + // Validate unit type + if (!["pcs", "gr", "kg"].includes(standardizedUnitType)) { + console.log(`Skipping product ${completedCount}/${products.length}: Invalid unit type "${prices[0].unit}"`); + skippedProducts.push({ name, reason: `Invalid unit type: ${prices[0].unit}` }); + continue; + } + + const newProduct = new Product({ + storeId: req.user.storeId, + name, + category: targetCategory._id, + unitType: standardizedUnitType, + pricing + }); + + try { + await store.save(); + const savedProduct = await newProduct.save(); + processedProducts.push(savedProduct); + console.log(`Processed ${completedCount}/${products.length}: Successfully added "${name}"`); + } catch (error) { + console.error(`Failed to save product ${completedCount}/${products.length}: "${name}"`, error); + skippedProducts.push({ name, reason: `Save error: ${error.message}` }); + } + } + + console.log(`Batch processing completed. Processed: ${processedProducts.length}, Skipped: ${skippedProducts.length}`); + + if (processedProducts.length === 0) { + return res.status(400).json({ + message: "No valid products were processed", + totalSubmitted: products.length, + totalProcessed: 0, + skippedProducts + }); + } + + res.status(201).json({ + message: "Products batch processed successfully", + totalSubmitted: products.length, + totalProcessed: processedProducts.length, + totalSkipped: skippedProducts.length, + products: processedProducts, + skippedProducts, + categories: store.categories + }); + + } catch (error) { + console.error("Batch processing error:", error); + res.status(500).json({ + message: "Failed to process product batch", + error: error.message + }); + } +}); + +// 📌 Disable stock tracking for all products (one-time setup route) +router.post("/disable-stock-tracking", protectVendor, async (req, res) => { + try { + // Ensure the user has admin privileges or apply appropriate restrictions here + if (!req.user.storeId) { + return res.status(400).json({ message: "Store not found for this vendor" }); + } + + // Update all products for this store to disable stock tracking + const result = await Product.updateMany( + { storeId: req.user.storeId }, + { + stockTracking: false, + stockStatus: "in_stock" // Set a default status to avoid UI issues + } + ); + + res.status(200).json({ + message: "Stock tracking disabled for all products", + modifiedCount: result.modifiedCount, + matchedCount: result.matchedCount + }); + } catch (error) { + console.error("Error disabling stock tracking:", error); + res.status(500).json({ message: "Failed to disable stock tracking", error: error.message }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/routes/promotion.routes.js b/backend/routes/promotion.routes.js new file mode 100644 index 0000000..0daa955 --- /dev/null +++ b/backend/routes/promotion.routes.js @@ -0,0 +1,24 @@ +import express from "express"; +import { + getPromotions, + getPromotionById, + createPromotion, + updatePromotion, + deletePromotion, + validatePromotion +} from "../controllers/promotion.controller.js"; +import { protectVendor } from "../middleware/vendorAuthMiddleware.js"; + +const router = express.Router(); + +// Vendor routes for managing their own promotions (protected) +router.get("/", protectVendor, getPromotions); +router.get("/:id", protectVendor, getPromotionById); +router.post("/", protectVendor, createPromotion); +router.put("/:id", protectVendor, updatePromotion); +router.delete("/:id", protectVendor, deletePromotion); + +// Public route for validating a promotion code +router.post("/validate/:storeId", validatePromotion); + +export default router; \ No newline at end of file diff --git a/backend/routes/shipping.routes.js b/backend/routes/shipping.routes.js new file mode 100644 index 0000000..cac664f --- /dev/null +++ b/backend/routes/shipping.routes.js @@ -0,0 +1,151 @@ +import express from "express"; +import Store from "../models/Store.model.js"; +import { protectVendor } from "../middleware/authMiddleware.js"; // Protect the vendor +import mongoose from "mongoose"; // Import mongoose to use ObjectId + +const router = express.Router(); + +/** + * 📌 Get Shipping Options for Vendor's Store + * @route GET /api/shipping-options + * @access Private (Vendors only) + */ +router.get("/", protectVendor, async (req, res) => { + try { + if (!req.user.storeId) { + return res + .status(400) + .json({ message: "Store not found for this vendor" }); + } + + const store = await Store.findOne({ vendorId: req.user._id }).select( + "shippingOptions" + ); + + if (!store) { + return res.status(404).json({ message: "Store not found" }); + } + + res.json(store.shippingOptions); + } catch (error) { + res + .status(500) + .json({ message: "Failed to fetch shipping options", error }); + } +}); + +/** + * 📌 Add New Shipping Option for Vendor's Store + * @route POST /api/shipping-options + * @access Private (Vendors only) + */ +router.post("/", protectVendor, async (req, res) => { + const { name, price } = req.body; + + if (!name || !price) { + return res.status(400).json({ message: "Missing required fields" }); + } + + try { + // Find the store by vendorId (user) + const store = await Store.findOne({ vendorId: req.user._id }); + + if (!store) { + return res.status(404).json({ message: "Store not found" }); + } + + // Add the new shipping option to the store's shippingOptions array + store.shippingOptions.push({ name, price }); + + // Save the store with the new shipping option + await store.save(); + + res.status(201).json(store.shippingOptions); + } catch (error) { + res.status(500).json({ message: "Failed to add shipping option", error }); + } +}); + +/** + * 📌 Delete Shipping Option for Vendor's Store + * @route DELETE /api/shipping-options/:id + * @access Private (Vendors only) + */ +router.delete("/:id", protectVendor, async (req, res) => { + const { id } = req.params; + + try { + const store = await Store.findOne({ vendorId: req.user._id }); + + if (!store) { + return res.status(404).json({ message: "Store not found" }); + } + + // Use find to find the shipping option by its 'id' field + const shippingOption = store.shippingOptions.find( + (option) => option.id.toString() === id + ); + + if (!shippingOption) { + return res.status(404).json({ message: "Shipping option not found" }); + } + + // Remove the shipping option + const index = store.shippingOptions.indexOf(shippingOption); + store.shippingOptions.splice(index, 1); // Remove the shipping option from the array + + await store.save(); // Save the updated store + + res.status(204).json({ message: "Shipping option deleted successfully" }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ message: "Failed to delete shipping option", error }); + } +}); + +/** + * 📌 Edit Shipping Option for Vendor's Store + * @route PUT /api/shipping-options/:id + * @access Private (Vendors only) + */ +router.put("/:id", protectVendor, async (req, res) => { + const { name, price } = req.body; + const { id } = req.params; + + if (!name || !price) { + return res.status(400).json({ message: "Missing required fields" }); + } + + try { + const store = await Store.findOne({ vendorId: req.user._id }); + + if (!store) { + return res.status(404).json({ message: "Store not found" }); + } + + // Use find to find the shipping option by its 'id' field + const shippingOption = store.shippingOptions.find( + (option) => option.id.toString() === id + ); + + if (!shippingOption) { + return res.status(404).json({ message: "Shipping option not found" }); + } + + shippingOption.name = name; // Update the name + shippingOption.price = price; // Update the price + + await store.save(); // Save the updated store + + res.json(shippingOption); // Return the updated shipping option + } catch (error) { + console.error(error); + res + .status(500) + .json({ message: "Failed to update shipping option", error }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/routes/staffAuth.routes.js b/backend/routes/staffAuth.routes.js new file mode 100644 index 0000000..6bc9f94 --- /dev/null +++ b/backend/routes/staffAuth.routes.js @@ -0,0 +1,84 @@ +import express from "express"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import Staff from "../models/Staff.model.js"; +import { protectStaff, logoutStaff } from "../middleware/staffAuthMiddleware.js"; + +const router = express.Router(); + +/** + * 📌 Staff Login - Store JWT in Database + */ +router.post("/login", async (req, res) => { + const { username, password } = req.body; + + try { + const staff = await Staff.findOne({ username }); + if (!staff) { + return res.status(401).json({ error: "Staff user not found" }); + } + + const isMatch = await bcrypt.compare(password, staff.passwordHash); + if (!isMatch) { + return res.status(401).json({ error: "Invalid credentials" }); + } + + // Generate JWT for Staff/Admin + const token = jwt.sign( + { id: staff._id, role: staff.role }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ); + + // Store token in database + staff.currentToken = token; + await staff.save(); + + res.json({ token, role: staff.role }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +/** + * 📌 Staff Logout - Remove JWT from Database + */ +router.post("/logout", protectStaff, logoutStaff); + +/** + * 📌 Force Logout All Staff Users (Admin Only) + */ +router.post("/logout/all", protectStaff, async (req, res) => { + try { + if (req.user.role !== "admin") { + return res.status(403).json({ error: "Access restricted to admins only" }); + } + + await Staff.updateMany({}, { currentToken: null }); + + res.json({ message: "All staff users have been logged out" }); + } catch (error) { + res.status(500).json({ error: "Failed to log out all staff users" }); + } +}); + +/** + * 📌 Check Staff Sessions (Admin Only) + */ +router.get("/sessions", protectStaff, async (req, res) => { + try { + if (req.user.role !== "admin") { + return res.status(403).json({ error: "Access restricted to admins only" }); + } + + const activeSessions = await Staff.find({ currentToken: { $ne: null } }) + .select("username role currentToken createdAt"); + + res.json({ activeSessions }); + } catch (error) { + res.status(500).json({ error: "Failed to fetch active sessions" }); + } +}); + + +export default router; \ No newline at end of file diff --git a/backend/routes/stock.routes.js b/backend/routes/stock.routes.js new file mode 100644 index 0000000..166a3bf --- /dev/null +++ b/backend/routes/stock.routes.js @@ -0,0 +1,13 @@ +import express from "express"; +import { protectVendor } from "../middleware/authMiddleware.js"; +import { updateStock, getStoreStock } from "../controllers/stock.controller.js"; + +const router = express.Router(); + +// Get all product stock information for a store +router.get("/", protectVendor, getStoreStock); + +// Update stock for a specific product +router.put("/:productId", protectVendor, updateStock); + +export default router; \ No newline at end of file diff --git a/backend/routes/storefront.routes.js b/backend/routes/storefront.routes.js new file mode 100644 index 0000000..f0f0a66 --- /dev/null +++ b/backend/routes/storefront.routes.js @@ -0,0 +1,265 @@ +import express from "express"; +import { protectVendor } from "../middleware/vendorAuthMiddleware.js"; +import Store from "../models/Store.model.js"; +import TelegramUser from "../models/TelegramUser.model.js"; +import { sendBulkTelegramMessages } from "../utils/telegramUtils.js"; +import multer from "multer"; +import sharp from "sharp"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const router = express.Router(); + +// Get the current directory and set up a relative path for uploads +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const uploadsBaseDir = path.join(process.cwd(), 'uploads'); +const broadcastsDir = path.join(uploadsBaseDir, 'broadcasts'); + +console.log('Upload directory:', { + path: broadcastsDir, + exists: fs.existsSync(broadcastsDir), + absolutePath: path.resolve(broadcastsDir) +}); + +// Create both the base uploads directory and the broadcasts subdirectory +if (!fs.existsSync(uploadsBaseDir)) { + fs.mkdirSync(uploadsBaseDir, { recursive: true }); +} + +if (!fs.existsSync(broadcastsDir)) { + fs.mkdirSync(broadcastsDir, { recursive: true }); +} + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit for Telegram + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/")) { + cb(null, true); + } else { + cb(new Error("Only image files are allowed!"), false); + } + }, +}); + +const handleMulterError = (err, req, res, next) => { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(400).json({ error: "File size too large. Maximum allowed size is 10MB." }); + } + return res.status(400).json({ error: "Upload error occurred.", details: err.message }); + } + next(err); +}; + +/** + * Get Storefront Details + * @route GET /api/storefront + * @access Private (Vendors only) + */ +router.get("/", protectVendor, async (req, res) => { + try { + const storeId = req.user.storeId; + const store = await Store.findById(storeId); + + if (!store) { + return res.status(404).json({ error: "Storefront not found" }); + } + + return res.json(store); + } catch (error) { + console.error("Error fetching storefront:", error); + return res.status(500).json({ error: "Failed to retrieve storefront" }); + } +}); + +/** + * Update Storefront Settings + * @route PUT /api/storefront + * @access Private (Vendors only) + */ +router.put("/", protectVendor, async (req, res) => { + try { + const storeId = req.user.storeId; + const { pgpKey, welcomeMessage, telegramToken, shipsFrom, shipsTo, wallets } = req.body; + + const updatedStore = await Store.findByIdAndUpdate( + storeId, + { + ...(pgpKey !== undefined && { pgpKey }), + ...(welcomeMessage !== undefined && { welcomeMessage }), + ...(telegramToken !== undefined && { telegramToken }), + ...(shipsFrom !== undefined && { shipsFrom }), + ...(shipsTo !== undefined && { shipsTo }), + ...(wallets !== undefined && { wallets }) + }, + { new: true } + ); + + if (!updatedStore) { + return res.status(404).json({ error: "Storefront not found" }); + } + + return res.json({ + message: "Storefront updated successfully", + store: updatedStore, + }); + } catch (error) { + console.error("Error updating storefront:", error); + return res.status(500).json({ error: "Failed to update storefront" }); + } +}); + +/** + * Broadcast a Message + * @route POST /api/storefront/broadcast + * @access Private (Vendors only) + */ +router.post("/broadcast", protectVendor, upload.single("file"), handleMulterError, async (req, res) => { + let photoPath = null; + try { + const storeId = req.user.storeId; + const { message } = req.body; + + // Handle image upload if present + if (req.file) { + const outputFileName = `${Date.now()}-${req.file.originalname.split('.').slice(0, -1).join('.')}.jpg`; + const outputFilePath = path.join(broadcastsDir, outputFileName); + + // Ensure upload directory exists + if (!fs.existsSync(broadcastsDir)) { + fs.mkdirSync(broadcastsDir, { recursive: true }); + } + + console.log('Processing upload:', { + fileName: outputFileName, + filePath: outputFilePath, + absolutePath: path.resolve(outputFilePath), + uploadDirExists: fs.existsSync(broadcastsDir), + uploadDirContents: fs.readdirSync(broadcastsDir) + }); + + try { + // Process and save the image + await sharp(req.file.buffer) + .resize(1280, 1280, { fit: 'inside', withoutEnlargement: true }) + .jpeg({ quality: 80 }) + .toFile(outputFilePath); + + // Verify file was saved and get stats + await new Promise((resolve, reject) => { + fs.stat(outputFilePath, (err, stats) => { + if (err) { + console.error('Error verifying file:', err); + reject(err); + } else { + console.log('File processed and verified:', { + path: outputFilePath, + size: stats.size, + exists: fs.existsSync(outputFilePath), + stats: stats + }); + resolve(stats); + } + }); + }); + + // Additional verification + if (!fs.existsSync(outputFilePath)) { + throw new Error('File was not saved successfully'); + } + + const stats = fs.statSync(outputFilePath); + if (stats.size > 10 * 1024 * 1024) { + fs.unlinkSync(outputFilePath); + return res.status(400).json({ error: "File size too large after processing." }); + } + + // Set the photo path for sending + photoPath = outputFilePath; + } catch (err) { + console.error("Error processing image:", { + error: err, + outputFilePath, + exists: fs.existsSync(outputFilePath) + }); + + // Cleanup if file exists + if (outputFilePath && fs.existsSync(outputFilePath)) { + fs.unlinkSync(outputFilePath); + } + + return res.status(500).json({ error: "Failed to process image file." }); + } + } else if (!message || !message.trim()) { + return res.status(400).json({ error: "Either message or image must be provided" }); + } + + const users = await TelegramUser.find({ "stores.store": storeId }); + + if (!users.length) { + return res.status(404).json({ error: "No users found to broadcast to." }); + } + + const store = await Store.findById(storeId); + if (!store || !store.telegramToken) { + return res.status(400).json({ error: "Store has no Telegram bot token configured." }); + } + + // Flatten out each store entry to gather all chat IDs belonging to this store + const chatIds = users.flatMap((user) => + user.stores + .filter((s) => s.store.toString() === storeId.toString()) + .map((s) => s.chatId) + ); + + console.log("Broadcasting to chat IDs:", chatIds); + + try { + if (photoPath) { + console.log('Sending photo broadcast:', { + photoPath, + exists: fs.existsSync(photoPath), + stats: fs.existsSync(photoPath) ? fs.statSync(photoPath) : null + }); + + await sendBulkTelegramMessages(store.telegramToken, chatIds, message, photoPath); + } else { + await sendBulkTelegramMessages(store.telegramToken, chatIds, message); + } + + return res.json({ + message: "Broadcast sent successfully", + totalUsers: chatIds.length, + }); + } catch (error) { + console.error("Error sending broadcast:", { + error: error.message, + photoPath, + messageLength: message?.length, + fileExists: photoPath ? fs.existsSync(photoPath) : null + }); + + throw error; + } + } catch (error) { + console.error("Error broadcasting message:", error); + return res.status(500).json({ + error: "Failed to broadcast message", + details: error.message, + }); + } finally { + // Clean up the file after everything is done + if (photoPath && fs.existsSync(photoPath)) { + try { + fs.unlinkSync(photoPath); + } catch (err) { + console.error("Error cleaning up file:", err); + } + } + } +}); + +export default router; \ No newline at end of file diff --git a/backend/scripts/disableAllStockTracking.js b/backend/scripts/disableAllStockTracking.js new file mode 100644 index 0000000..cdbe6e5 --- /dev/null +++ b/backend/scripts/disableAllStockTracking.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +import disableStockTrackingForAllProducts from '../utils/disableStockTracking.js'; +import logger from '../utils/logger.js'; + +logger.info('Starting script to disable stock tracking for all products'); + +disableStockTrackingForAllProducts() + .then((result) => { + logger.info('Operation completed successfully!'); + logger.info(`Modified ${result.modifiedCount} products out of ${result.matchedCount} total products`); + process.exit(0); + }) + .catch((error) => { + logger.error('Operation failed:', error); + process.exit(1); + }); \ No newline at end of file diff --git a/backend/test.js b/backend/test.js new file mode 100644 index 0000000..ebbb247 --- /dev/null +++ b/backend/test.js @@ -0,0 +1,8 @@ +import logger from './utils/logger.js'; + +logger.info('Backend integration test successful!'); +console.log('Backend integration test successful!'); + +export default function testBackendIntegration() { + return { success: true, message: 'Backend integration test successful!' }; +} \ No newline at end of file diff --git a/backend/utils/createAdmin.js b/backend/utils/createAdmin.js new file mode 100644 index 0000000..5d266d4 --- /dev/null +++ b/backend/utils/createAdmin.js @@ -0,0 +1,44 @@ +import mongoose from "mongoose"; +import bcrypt from "bcryptjs"; +import dotenv from "dotenv"; +import Staff from "../models/Staff.model.js"; + +dotenv.config(); + +mongoose + .connect(process.env.MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }) + .then(() => console.log("✅ MongoDB Connected")) + .catch((err) => console.error("❌ MongoDB Connection Error:", err)); + +const createAdmin = async () => { + const username = "admin"; + const password = "88sO)£2igu-:"; + + try { + const existingAdmin = await Staff.findOne({ username }); + if (existingAdmin) { + console.log("⚠️ Admin user already exists."); + process.exit(1); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const admin = new Staff({ + username, + passwordHash: hashedPassword, + role: "admin", + }); + + await admin.save(); + console.log(`✅ Admin user '${username}' created successfully!`); + process.exit(); + } catch (error) { + console.error("❌ Error creating admin user:", error); + process.exit(1); + } +}; + +createAdmin(); diff --git a/backend/utils/createFakeOrder.js b/backend/utils/createFakeOrder.js new file mode 100644 index 0000000..49c34ba --- /dev/null +++ b/backend/utils/createFakeOrder.js @@ -0,0 +1,56 @@ +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import Order from "../models/Order.model.js"; // Adjust path if needed +import Vendor from "../models/Vendor.model.js"; // Import Vendor model + +dotenv.config(); + +// ✅ Connect to MongoDB +const mongoUri = process.env.MONGO_URI || "mongodb://localhost:27017/yourDatabaseName"; +mongoose + .connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }) + .then(() => console.log("✅ Connected to MongoDB")) + .catch((err) => console.error("❌ MongoDB Connection Error:", err)); + +// ✅ Insert Fake Order for an Existing Vendor +async function insertFakeOrder() { + try { + // ✅ Find an existing vendor + const existingVendor = await Vendor.findOne(); + if (!existingVendor) { + console.log("❌ No vendors found. Create a vendor first."); + return; + } + + console.log(`✅ Using Vendor: ${existingVendor.username} (${existingVendor._id})`); + + const fakeOrder = new Order({ + buyerId: new mongoose.Types.ObjectId(), // Fake buyer + vendorId: existingVendor._id, // Assign to existing vendor + storeId: new mongoose.Types.ObjectId(), + products: [ + { + productId: new mongoose.Types.ObjectId(), + quantity: 2, + pricePerUnit: 25.99, + totalItemPrice: 51.98, + }, + ], + totalPrice: 51.98, + status: "paid", + paymentAddress: "ltc1qxyzfakeaddress123456", + txid: "faketxid1234567890abcdef", + escrowExpiresAt: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000), // 8 days from now + }); + + const savedOrder = await fakeOrder.save(); + console.log("✅ Fake Order Inserted:", savedOrder); + } catch (error) { + console.error("❌ Error inserting fake order:", error); + } finally { + mongoose.connection.close(); // Close DB connection + } +} + +// ✅ Run Script +insertFakeOrder(); diff --git a/backend/utils/createInvitation.js b/backend/utils/createInvitation.js new file mode 100644 index 0000000..2566df4 --- /dev/null +++ b/backend/utils/createInvitation.js @@ -0,0 +1,67 @@ +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import Staff from "../models/Staff.model.js" +import Invitation from "../models/Invitation.model.js" +import crypto from "crypto"; + + +dotenv.config(); + +// ✅ Connect to MongoDB +const mongoUri = process.env.MONGO_URI; +mongoose + .connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }) + .then(() => console.log("✅ Connected to MongoDB")) + .catch((err) => console.error("❌ MongoDB Connection Error:", err)); + +const generateInviteCode = () => { + return crypto.randomBytes(16).toString('hex'); +}; + +const createInvitation = async (staffEmail) => { + try { + // Find staff member + const staff = await Staff.findOne({ username: "admin" }); + if (!staff) { + throw new Error("Staff member not found"); + } + + // Check if staff has permission to create invitations + if (!['admin', 'support'].includes(staff.role)) { + throw new Error("Insufficient permissions to create vendor invitations"); + } + + // Generate unique invite code + const inviteCode = generateInviteCode(); + + // Create invitation + const invitation = await Invitation.create({ + code: inviteCode, + createdBy: staff._id, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now + isUsed: false + }); + + console.log(`✅ Vendor invitation created successfully! +Code: ${invitation.code} +Created by: ${staff.email} +Expires: ${invitation.expiresAt} + `); + + // Exit process after creating invitation + process.exit(0); + } catch (error) { + console.error("❌ Error creating invitation:", error.message); + process.exit(1); + } +}; + +// Get staff email from command line argument +const staffEmail = process.argv[2]; + +if (!staffEmail) { + console.error("❌ Please provide staff email: node createInvitation.js "); + process.exit(1); +} + +createInvitation(staffEmail); \ No newline at end of file diff --git a/backend/utils/createKeys.js b/backend/utils/createKeys.js new file mode 100644 index 0000000..cb5c357 --- /dev/null +++ b/backend/utils/createKeys.js @@ -0,0 +1,7 @@ +import crypto from "crypto"; + +const encryptionKey = crypto.randomBytes(32).toString("hex"); // 32 bytes (256-bit) +const iv = crypto.randomBytes(16).toString("hex"); // 16 bytes (128-bit) + +console.log("Encryption Key:", encryptionKey); +console.log("IV:", iv); \ No newline at end of file diff --git a/backend/utils/disableStockTracking.js b/backend/utils/disableStockTracking.js new file mode 100644 index 0000000..055ad0a --- /dev/null +++ b/backend/utils/disableStockTracking.js @@ -0,0 +1,55 @@ +import mongoose from 'mongoose'; +import dotenv from 'dotenv'; +import Product from '../models/Product.model.js'; +import logger from './logger.js'; + +// Load environment variables +dotenv.config(); + +// Connect to MongoDB +mongoose.connect(process.env.MONGO_URI) + .then(() => logger.info('MongoDB connected')) + .catch(err => { + logger.error('MongoDB connection error:', err); + process.exit(1); + }); + +async function disableStockTrackingForAllProducts() { + try { + logger.info('Disabling stock tracking for all products...'); + + const result = await Product.updateMany( + {}, // Empty filter matches all documents + { + stockTracking: false, + stockStatus: "in_stock" // Set a default status + } + ); + + logger.info(`Stock tracking disabled for ${result.modifiedCount} products out of ${result.matchedCount} total products`); + + return result; + } catch (error) { + logger.error('Error disabling stock tracking:', error); + throw error; + } finally { + // Close database connection + mongoose.connection.close(); + logger.info('Database connection closed'); + } +} + +// Execute the function if this script is run directly +if (process.argv[1].includes('disableStockTracking.js')) { + disableStockTrackingForAllProducts() + .then(() => { + logger.info('Script completed successfully'); + process.exit(0); + }) + .catch(err => { + logger.error('Script failed:', err); + process.exit(1); + }); +} + +export default disableStockTrackingForAllProducts; \ No newline at end of file diff --git a/backend/utils/encryptPgp.js b/backend/utils/encryptPgp.js new file mode 100644 index 0000000..b4ede13 --- /dev/null +++ b/backend/utils/encryptPgp.js @@ -0,0 +1,54 @@ +import * as openpgp from 'openpgp'; +import logger from './logger.js'; + +/** + * Encrypts a message using PGP + * @param {string} message - The message to encrypt + * @param {string} publicKey - PGP public key for encryption + * @returns {Promise} - The encrypted message + */ +export const encryptWithPGP = async (message, publicKey) => { + try { + // Parse the public key + const decodedPublicKey = await openpgp.readKey({ armoredKey: publicKey }); + + // Encrypt the message + const encrypted = await openpgp.encrypt({ + message: await openpgp.createMessage({ text: message }), + encryptionKeys: decodedPublicKey + }); + + return encrypted; + } catch (error) { + logger.error('Error during PGP encryption', { error: error.message }); + throw new Error('Failed to encrypt message: ' + error.message); + } +}; + +/** + * Decrypts a message using PGP + * @param {string} encryptedMessage - The encrypted message + * @param {string} privateKey - PGP private key for decryption + * @param {string} passphrase - Passphrase for the private key + * @returns {Promise} - The decrypted message + */ +export const decryptWithPGP = async (encryptedMessage, privateKey, passphrase) => { + try { + // Parse the private key + const decodedPrivateKey = await openpgp.readPrivateKey({ + armoredKey: privateKey + }); + + // Decrypt the message + const decrypted = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: encryptedMessage }), + decryptionKeys: decodedPrivateKey, + config: { allowInsecureDecryptionWithSignature: true } + }); + + return decrypted.data; + } catch (error) { + logger.error('Error during PGP decryption', { error: error.message }); + throw new Error('Failed to decrypt message: ' + error.message); + } +}; \ No newline at end of file diff --git a/backend/utils/litecoin/index.js b/backend/utils/litecoin/index.js new file mode 100644 index 0000000..829e8a4 --- /dev/null +++ b/backend/utils/litecoin/index.js @@ -0,0 +1,96 @@ +import ky from "ky"; + +const rpcUrl = "http://152.53.124.126:9332/"; +const rpcUser = "notiiwasntherexdddd"; +const rpcPassword = "NYwsxePgMrThiapHnfCzUfaEfVlNKZECwvlqhHcWjerlZfcaTp"; + +const headers = { + "Content-Type": "application/json", + Authorization: `Basic ${Buffer.from(`${rpcUser}:${rpcPassword}`).toString( + "base64" + )}`, +}; + +async function callRpc(method, params = [], walletName = null) { + console.log(`Calling RPC method: ${method} with params: ${JSON.stringify(params)}`); + const url = walletName ? `${rpcUrl}wallet/${walletName}` : rpcUrl; + + try { + const response = await ky + .post(url, { + json: { + jsonrpc: "1.0", + id: "curltest", + method, + params, + }, + headers, + }) + .json(); + + console.log( + `RPC Response for method ${method}:`, + JSON.stringify(response, null, 2) + ); + + if (response.error) { + throw new Error(`RPC Error: ${JSON.stringify(response.error, null, 2)}`); + } + + return response.result; + } catch (error) { + console.error(`Error calling RPC method ${method}:`, error); + throw error; + } +} + +async function createWallet(walletName) { + const result = await callRpc("createwallet", [walletName]); + console.log("Wallet created:", result); + return result; +} + +async function generateAddress(walletName = null) { + const address = await callRpc("getnewaddress", [], walletName); + console.log("Generated Address:", address); + return address; +} + +async function checkWalletLoaded(walletName) { + const walletInfo = await callRpc("getwalletinfo", [], walletName); + console.log("Wallet Info:", walletInfo); + return walletInfo; +} + +async function walletExists(walletName) { + try { + await callRpc("getwalletinfo", [], walletName); + return true; + } catch (error) { + return false; + } +} + +async function dumpPrivateKey(address, walletName) { + const privateKey = await callRpc("dumpprivkey", [address], walletName); + console.log("Private Key:", privateKey); + return privateKey; +} + +async function setupWallet(walletName) { + const createResult = await createWallet(walletName); + const address = await generateAddress(walletName); + + console.log("Address:", address); + console.log("Wallet Name:", walletName); + console.log("Create Result:", createResult); + + const privKey = await dumpPrivateKey(address, walletName); + + console.log("Wallet Info:", privKey); + + + return { address, privKey }; +} + +export { setupWallet }; diff --git a/backend/utils/logger.js b/backend/utils/logger.js new file mode 100644 index 0000000..a600c6d --- /dev/null +++ b/backend/utils/logger.js @@ -0,0 +1,77 @@ +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + dim: "\x1b[2m", + underscore: "\x1b[4m", + blink: "\x1b[5m", + reverse: "\x1b[7m", + hidden: "\x1b[8m", + + fgBlack: "\x1b[30m", + fgRed: "\x1b[31m", + fgGreen: "\x1b[32m", + fgYellow: "\x1b[33m", + fgBlue: "\x1b[34m", + fgMagenta: "\x1b[35m", + fgCyan: "\x1b[36m", + fgWhite: "\x1b[37m", + + bgBlack: "\x1b[40m", + bgRed: "\x1b[41m", + bgGreen: "\x1b[42m", + bgYellow: "\x1b[43m", + bgBlue: "\x1b[44m", + bgMagenta: "\x1b[45m", + bgCyan: "\x1b[46m", + bgWhite: "\x1b[47m", +}; + +/** + * Formats a timestamp for logging. + * @returns {string} Formatted timestamp + */ +const getTimestamp = () => { + return new Date().toISOString(); +}; + +/** + * Logs an INFO message. + * @param {string} message - Log message + * @param {Object} [data] - Additional data + */ +const info = (message, data = null) => { + console.log(`${colors.fgGreen}[INFO] ${getTimestamp()} - ${message}${colors.reset}`); + if (data) console.log(data); +}; + +/** + * Logs a WARNING message. + * @param {string} message - Log message + * @param {Object} [data] - Additional data + */ +const warn = (message, data = null) => { + console.warn(`${colors.fgYellow}[WARN] ${getTimestamp()} - ${message}${colors.reset}`); + if (data) console.warn(data); +}; + +/** + * Logs an ERROR message. + * @param {string} message - Log message + * @param {Object} [data] - Additional data + */ +const error = (message, data = null) => { + console.error(`${colors.fgRed}[ERROR] ${getTimestamp()} - ${message}${colors.reset}`); + if (data) console.error(data); +}; + +/** + * Logs a DEBUG message. + * @param {string} message - Log message + * @param {Object} [data] - Additional data + */ +const debug = (message, data = null) => { + console.log(`${colors.fgBlue}[DEBUG] ${getTimestamp()} - ${message}${colors.reset}`); + if (data) console.log(data); +}; + +export default { info, warn, error, debug }; \ No newline at end of file diff --git a/backend/utils/telegramUtils.js b/backend/utils/telegramUtils.js new file mode 100644 index 0000000..efcf3d6 --- /dev/null +++ b/backend/utils/telegramUtils.js @@ -0,0 +1,171 @@ +import ky from "ky"; +import logger from "../utils/logger.js"; +import FormData from "form-data"; +import fs from "fs"; +import path from "path"; + +// Function to escape special characters for MarkdownV2 +const escapeMarkdown = (text) => { + return text.replace(/([_*\[\]()~`>#+=|{}.!\\-])/g, '\\$1'); +}; + +// Function to preserve markdown formatting while escaping other special characters +const preserveMarkdown = (text) => { + // First, temporarily replace valid markdown + text = text + .replace(/\*\*(.*?)\*\*/g, '%%%BOLD%%%$1%%%BOLD%%%') + .replace(/__(.*?)__/g, '%%%ITALIC%%%$1%%%ITALIC%%%') + .replace(/`(.*?)`/g, '%%%CODE%%%$1%%%CODE%%%') + .replace(/\[(.*?)\]\((.*?)\)/g, '%%%LINK_TEXT%%%$1%%%LINK_URL%%%$2%%%LINK%%%'); + + // Escape all special characters + text = escapeMarkdown(text); + + // Restore markdown formatting + return text + .replace(/%%%BOLD%%%(.*?)%%%BOLD%%%/g, '*$1*') + .replace(/%%%ITALIC%%%(.*?)%%%ITALIC%%%/g, '_$1_') + .replace(/%%%CODE%%%(.*?)%%%CODE%%%/g, '`$1`') + .replace(/%%%LINK_TEXT%%%(.*?)%%%LINK_URL%%%(.*?)%%%LINK%%%/g, '[$1]($2)'); +}; + +/** + * Sends a message via a Telegram bot. + * @param {string} telegramToken - The bot's API token. + * @param {number} chatId - The recipient's Telegram chat ID. + * @param {string} message - The message text. + * @param {string} [photoPath] - Optional path to photo file. + * @returns {Promise} - Returns `true` if the message is sent successfully, otherwise `false`. + */ +export const sendTelegramMessage = async (telegramToken, chatId, message, photoPath = null) => { + try { + // Process message text if it exists + const processedMessage = message?.trim() ? preserveMarkdown(message) : ''; + + if (photoPath) { + const formData = new FormData(); + formData.append('chat_id', chatId); + + // Debug information before reading file + logger.info('Attempting to read file:', { + photoPath, + exists: fs.existsSync(photoPath), + stats: fs.existsSync(photoPath) ? fs.statSync(photoPath) : null, + dirname: path.dirname(photoPath), + basename: path.basename(photoPath) + }); + + try { + // Ensure the file exists before reading + if (!fs.existsSync(photoPath)) { + throw new Error(`File does not exist at path: ${photoPath}`); + } + + // Create read stream and append to FormData + const photoStream = fs.createReadStream(photoPath); + formData.append('photo', photoStream); + + if (processedMessage) { + formData.append('caption', processedMessage); + formData.append('parse_mode', 'MarkdownV2'); + } + + // Debug the FormData contents + logger.info('FormData created:', { + boundaryUsed: formData.getBoundary?.() || 'No boundary found', + headers: formData.getHeaders?.() || 'No headers available', + messageLength: processedMessage.length + }); + + // Wait for the entire request to complete + await new Promise((resolve, reject) => { + formData.submit({ + host: 'api.telegram.org', + path: `/bot${telegramToken}/sendPhoto`, + protocol: 'https:', + }, (err, res) => { + if (err) { + reject(err); + return; + } + + let data = ''; + res.on('data', chunk => { + data += chunk; + }); + + res.on('end', () => { + try { + const response = JSON.parse(data); + if (!response.ok) { + reject(new Error(response.description || 'Failed to send photo')); + } else { + resolve(response); + } + } catch (e) { + reject(new Error('Failed to parse Telegram response')); + } + }); + }); + }); + + } catch (error) { + // Log detailed error information + logger.error(`❌ Telegram API Error for chat ${chatId}:`, { + error: error.message, + response: error.response ? await error.response.json().catch(() => ({})) : undefined, + photoPath, + messageLength: processedMessage.length, + fileExists: fs.existsSync(photoPath), + fileStats: fs.existsSync(photoPath) ? fs.statSync(photoPath) : null + }); + throw error; + } + } else { + try { + await ky.post(`https://api.telegram.org/bot${telegramToken}/sendMessage`, { + json: { + chat_id: chatId, + text: processedMessage, + parse_mode: "MarkdownV2", + }, + timeout: 5000, + }); + } catch (error) { + // Log detailed error information + logger.error(`❌ Telegram API Error for chat ${chatId}:`, { + error: error.message, + response: await error.response?.json().catch(() => ({})), + messageLength: processedMessage.length + }); + throw error; + } + } + + logger.info(`✅ Telegram message sent to ${chatId}`); + return true; + } catch (error) { + logger.error(`❌ Failed to send Telegram message to ${chatId}: ${error.message}`); + return false; + } +}; + +/** + * Sends messages in bulk while respecting Telegram's rate limits. + * @param {string} telegramToken - The bot's API token. + * @param {Array} chatIds - An array of chat IDs to send to. + * @param {string} message - The message text. + * @param {string} [photoPath] - Optional path to photo file. + * @returns {Promise} + */ +export const sendBulkTelegramMessages = async (telegramToken, chatIds, message, photoPath = null) => { + for (let i = 0; i < chatIds.length; i++) { + const chatId = chatIds[i]; + await sendTelegramMessage(telegramToken, chatId, message, photoPath); + + if ((i + 1) % 30 === 0) { + logger.info(`⏳ Rate limit pause (sent ${i + 1} messages)...`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } +}; \ No newline at end of file diff --git a/backend/utils/walletUtils.js b/backend/utils/walletUtils.js new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index 31df998..068e874 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,7 @@ services: - web environment: - NODE_ENV=production - - NEXT_PUBLIC_API_URL=https://internal-api.inboxi.ng/api + - NEXT_PUBLIC_API_URL=/api restart: unless-stopped networks: diff --git a/lib/.server-service.ts.swp b/lib/.server-service.ts.swp new file mode 100644 index 0000000000000000000000000000000000000000..7368ac4373096e0c4f72cb52797b8390add0646e GIT binary patch literal 12288 zcmeHNOKe+36rCdRr~JBLgTyV3cy>wbly-w@+B9isBef-@Nn43UJ$@eBuX%oBX6D6p zjVWTmrmDoYOI9pkQ6borEfV4*5M2ObgZQWmK0DZR=FPL6wu!n!0*#d;C-d&yd+wQg zuU&O&)29}X)AOZ046jESyY%GO&A%7EWzX(otSOZ0+4VcTnx$BVJidb!W3Oip%ZFu{ zwnbue`AB4rrnRzY*MypCbG0VSR71(ch@@VQqgq)DwT?&jEeb`+=sU*n23`TLz*Y)u zXM3O9Gi{&ldU_}Aczks$L%;47@CtYZyaHYUuYgy;E8rFI3j7ZhFr97eBJ#Q`&+SD1 zzU`Lx{7?Vp74Qmp1-t@Y0k42pz$@St@CtYZyaHYUufTs$0bXb9wYwSn@*a@K|NjsF z{r~+VjJ*%M2fPc^fhuqmV8EvjGxiB^8F&Ra13Urzu${4Mz+1qZz(rsQm<1jK{&|Plu z@oFlxAtPy`k2_}_86{>w4Iag!Ua3wx4@K)@Kp{6_iwc>CBK0%3N|;n7@S{uTv`h*x z&DNs^6?WQHxmL`{B%(_TmQbO(oQKWhAhtEthMQEci1TJp%|a4cH{(Y%Gc!Z`ruP&P zuO2B8nnF;HWiv{GVcbE!k-#==i3$aYoYF=r5lm*pkjU^CqEe|OJ0^lOrIz4zq4fCK zq13dbiY{>byh7DE!Ie;;%JE=Du8Cw&9a{(U*`+-*wbJW|0AYEj6Gu=Co6A<2QTRMi zB^Pib3Fpq9l(CA^4Dl|H3^fFVFQh7-%r|A33{MmSjLtK&#A#N*6ZRt=M;i=b8IG$H z{Y0uZkE08sKF76Kh4sd@g<&yNUHz2HG%wq=C=obL(Y6)S6oxp3vfYV=5wyB|EDN(C z)@>mWx#ohEP;L5jWLh}FphyRY>jbGM`nl1dJdVDQ=v0Z3ncYrZ><0xMbKSDF0-7dh+DZ1jb^OUb51tfVCZhuLnYvstj$IRxn*Ctk{a0|N9Vt3gvCSnBZJysBb6JP@f7pH7A)k z-;oOErF%(8jWh{u%eazvLu&sDHmAv2BD=|f0;An!fZVew9o2<1rmjNIp+j9!ph29M(!57($6 z71hPMh-JrK;DsfA+{DctS2uNxsnLnhVw+T$dtl4HJhU2A2jjYb35lpKTfpBAwZoAvu71ZKjXD39`h?;2LIvrYCv~$93HZ8uTf*NNt(eEf33av3K z)=Q@rmgg6aoyG2fO*0e7M&8_J%p@UL)YGi2CJC%UWCHDagffG+GO_3IH~ zbf%4+YPJTlUdm(I6!Whl`8b49wwH&Er(FX_poVtr!`OZP<6B8~7D0k<4dsNdN6qXb S7fn2}b(2fqY?^-x!|oqrUQhM_ literal 0 HcmV?d00001 diff --git a/lib/auth-helpers.ts b/lib/auth-helpers.ts new file mode 100644 index 0000000..bcb7b7f --- /dev/null +++ b/lib/auth-helpers.ts @@ -0,0 +1,33 @@ +'use client'; + +// Helper function to verify authentication with the local API +export async function verifyAuth(token: string) { + try { + // Use a properly formed URL with origin + const origin = typeof window !== 'undefined' ? window.location.origin : ''; + const authEndpoint = new URL('/api/auth/me', origin).toString(); + + console.log(`Verifying auth with ${authEndpoint} using token: ${token.substring(0, 10)}...`); + + const response = await fetch(authEndpoint, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + // Prevent caching of auth requests + cache: 'no-store' + }); + + if (!response.ok) { + console.error(`Auth verification failed: ${response.status}`); + return false; + } + + return await response.json(); + } catch (error) { + console.error("Authentication verification failed:", error); + console.error("Error details:", error instanceof Error ? error.message : 'Unknown error'); + return false; + } +} \ No newline at end of file diff --git a/lib/client-service.ts b/lib/client-service.ts index b6a2ebe..e28595b 100644 --- a/lib/client-service.ts +++ b/lib/client-service.ts @@ -21,65 +21,77 @@ function getAuthToken(): string | null { ?.split('=')[1] || null; } +/** + * Gets the API base URL for client-side fetch calls + */ +function getClientApiBaseUrl(): string { + // For client-side, we can access window.location if in browser + if (typeof window !== 'undefined') { + // Use the same origin (which includes the correct port) + return `${window.location.origin}/api`; + } + + // Fallback when window is not available + // For development mode, use port 3001 to match our server + if (process.env.NODE_ENV === 'development') { + return 'http://localhost:3001/api'; + } + + // Default fallback - relative URL + return '/api'; +} + export async function fetchClient( endpoint: string, options: FetchOptions = {} ): Promise { const { method = 'GET', body, headers = {}, ...rest } = options; - // Get the base API URL from environment or fallback - const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; - - // Ensure the endpoint starts with a slash - const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; - - // For the specific case of internal-api.inboxi.ng - remove duplicate /api - let url; - if (apiUrl.includes('internal-api.inboxi.ng')) { - // Special case for internal-api.inboxi.ng - if (normalizedEndpoint.startsWith('/api/')) { - url = `${apiUrl}${normalizedEndpoint.substring(4)}`; // Remove the /api part - } else { - url = `${apiUrl}${normalizedEndpoint}`; - } - } else { - // Normal case for other environments - url = `${apiUrl}${normalizedEndpoint}`; - } - - // Get auth token from cookies - const authToken = getAuthToken(); - - // Prepare headers with authentication if token exists - const requestHeaders: Record = { - 'Content-Type': 'application/json', - ...(headers as Record), - }; - - if (authToken) { - // Backend expects "Bearer TOKEN" format - requestHeaders['Authorization'] = `Bearer ${authToken}`; - console.log('Authorization header set to:', `Bearer ${authToken.substring(0, 10)}...`); - } - - console.log('API Request:', { - url, - method, - hasAuthToken: !!authToken - }); - - const fetchOptions: RequestInit = { - method, - credentials: 'include', - headers: requestHeaders, - ...rest, - }; - - if (body && method !== 'GET') { - fetchOptions.body = JSON.stringify(body); - } - try { + // Get the base API URL + const baseUrl = getClientApiBaseUrl(); + + // Ensure endpoint doesn't start with a slash if baseUrl ends with one + const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; + + // Ensure baseUrl ends with a slash if it doesn't already + const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + + // Combine them for the final URL + const url = `${normalizedBaseUrl}${normalizedEndpoint}`; + + // Get auth token from cookies + const authToken = getAuthToken(); + + // Prepare headers with authentication if token exists + const requestHeaders: Record = { + 'Content-Type': 'application/json', + ...(headers as Record), + }; + + if (authToken) { + // Backend expects "Bearer TOKEN" format + requestHeaders['Authorization'] = `Bearer ${authToken}`; + console.log('Authorization header set to:', `Bearer ${authToken.substring(0, 10)}...`); + } + + console.log('API Request:', { + url, + method, + hasAuthToken: !!authToken + }); + + const fetchOptions: RequestInit = { + method, + credentials: 'include', + headers: requestHeaders, + ...rest, + }; + + if (body && method !== 'GET') { + fetchOptions.body = JSON.stringify(body); + } + const response = await fetch(url, fetchOptions); if (!response.ok) { @@ -97,6 +109,7 @@ export async function fetchClient( return data; } catch (error) { console.error('API request failed:', error); + console.error('Error details:', error instanceof Error ? error.message : 'Unknown error'); // Only show toast if this is a client-side error (not during SSR) if (typeof window !== 'undefined') { diff --git a/lib/server-service.ts b/lib/server-service.ts index 26e27d5..51649c1 100644 --- a/lib/server-service.ts +++ b/lib/server-service.ts @@ -1,6 +1,36 @@ import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; +/** + * Gets the base URL for server API requests with proper fallbacks + */ +function getBaseUrl() { + // For server components, we normally would use environment variables + // But we need to be careful with how they're accessed + + // Try to get the API URL from environment variables + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + + // We need to get the host from somewhere to construct the URL + // In production, we can rely on the VERCEL_URL or similar + if (process.env.VERCEL_URL) { + return `https://${process.env.VERCEL_URL}/api`; + } + + // If we have a configured API URL, use that + if (apiUrl) { + // If it's already an absolute URL, use it + if (apiUrl.startsWith('http')) { + return apiUrl; + } + // Otherwise, it's likely a relative path like /api + return `http://localhost:3000${apiUrl.startsWith('/') ? apiUrl : `/${apiUrl}`}`; + } + + // Last resort fallback for development + return 'http://localhost:3000/api'; +} + /** * Server-side fetch wrapper with authentication. */ @@ -14,8 +44,20 @@ export async function fetchServer( if (!authToken) redirect('/login'); try { - console.log(`${endpoint}`) - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, { + const baseUrl = getBaseUrl(); + + // Ensure endpoint doesn't start with a slash if baseUrl ends with one + const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; + + // Ensure baseUrl ends with a slash if it doesn't already + const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + + // Combine them to get a complete URL + const url = `${normalizedBaseUrl}${normalizedEndpoint}`; + + console.log(`Server fetch to: ${url}`); + + const res = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', diff --git a/lib/server-utils.ts b/lib/server-utils.ts new file mode 100644 index 0000000..ae1319f --- /dev/null +++ b/lib/server-utils.ts @@ -0,0 +1,32 @@ +/** + * Helper function to construct proper URLs for server components. + * In Next.js server components, relative URLs don't work in fetch() calls. + */ +export function getServerApiUrl(endpoint: string): string { + // Base API URL - use a hardcoded value for server components + // This must be an absolute URL with protocol and domain + const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'; + + // Ensure endpoint doesn't start with a slash if baseUrl ends with one + const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; + + // Ensure the baseUrl ends with a slash if it doesn't already + const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + + // Combine them to get a complete URL + return `${normalizedBaseUrl}${normalizedEndpoint}`; +} + +/** + * Helper to detect if code is running on server or client + */ +export function isServer(): boolean { + return typeof window === 'undefined'; +} + +/** + * Helper to detect if code is running in development mode + */ +export function isDevelopment(): boolean { + return process.env.NODE_ENV === 'development'; +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index f285bed..39f9584 100644 --- a/middleware.ts +++ b/middleware.ts @@ -10,19 +10,30 @@ export async function middleware(req: NextRequest) { } try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/me`, { - method: "GET", + // Make sure we use a complete URL with protocol + // When running locally with integrated backend, we need to specify the full URL including protocol + const origin = req.nextUrl.origin; + const authEndpoint = new URL("/api/auth/me", origin).toString(); + + console.log("Verifying authentication with endpoint:", authEndpoint); + + const res = await fetch(authEndpoint, { + method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, + // Ensure we're not caching authentication checks + cache: 'no-store' }); if (!res.ok) { + console.error(`Auth check failed with status: ${res.status}`); return NextResponse.redirect(new URL("/auth/login", req.url)); } } catch (error) { console.error("Authentication validation failed:", error); + console.error("Error details:", error instanceof Error ? error.message : 'Unknown error'); return NextResponse.redirect(new URL("/auth/login", req.url)); } diff --git a/next.config.mjs b/next.config.mjs index 43d1187..ead5c16 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,6 +8,46 @@ const nextConfig = { }, ], }, + // Disable Next.js handling of API routes that our Express server will handle + async rewrites() { + return [ + { + // Don't rewrite actual Next.js API routes + source: "/api/_next/:path*", + destination: "/api/_next/:path*", + }, + { + // Rewrite API requests to be handled by our custom Express server + source: "/api/:path*", + destination: "/api/:path*", + }, + ]; + }, + // Make environment variables available to both client and server components + env: { + // For integrated backend, use http://localhost:3000/api + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api', + }, + // Prevent dynamic URLs from being hard-coded at build time + // This is important for Tor compatibility + experimental: { + // This prevents URLs from being hardcoded at build time + esmExternals: true, + }, + // Ensure server components can handle URL objects + serverComponentsExternalPackages: ['next/dist/compiled/path-to-regexp'], + // Allow server-side fetch calls to the local API + serverRuntimeConfig: { + apiUrl: 'http://localhost:3000/api', + }, + + // Special handling for server components + webpack: (config, { isServer }) => { + if (isServer) { + // Server-side specific config + } + return config; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index a3e9cc4..6e0dfd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,17 +38,28 @@ "@radix-ui/react-tooltip": "^1.1.6", "autoprefixer": "^10.4.20", "axios": "^1.8.1", + "bcrypt": "^5.1.1", + "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", + "cors": "^2.8.5", "date-fns": "4.1.0", + "dotenv": "^16.4.7", "embla-carousel-react": "8.5.1", + "express": "^4.21.2", "form-data": "^4.0.2", "input-otp": "1.4.1", + "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "ky": "^1.7.5", "lucide-react": "^0.454.0", + "mongoose": "^8.9.6", + "mongoose-sequence": "^6.0.1", + "multer": "^1.4.5-lts.1", "next": "14.2.16", "next-themes": "latest", + "openpgp": "^6.1.0", "react": "^18", "react-day-picker": "8.10.1", "react-dom": "^18", @@ -56,9 +67,11 @@ "react-markdown": "^10.0.0", "react-resizable-panels": "^2.1.7", "recharts": "2.15.0", + "sharp": "^0.33.5", "sonner": "^1.7.4", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.5", "vaul": "^0.9.6", "zod": "^3.24.1" }, @@ -97,6 +110,16 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -338,6 +361,367 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -430,6 +814,35 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", + "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@next/env": { "version": "14.2.16", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.16.tgz", @@ -2147,12 +2560,46 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "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": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -2176,6 +2623,18 @@ "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.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2236,6 +2695,46 @@ "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/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/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/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -2261,6 +2760,18 @@ "node": ">=10" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2331,6 +2842,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2343,11 +2874,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2398,6 +2967,27 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "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", @@ -2409,6 +2999,15 @@ "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", @@ -2422,6 +3021,22 @@ "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2564,6 +3179,15 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -2607,6 +3231,19 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2625,6 +3262,25 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2660,9 +3316,84 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2878,6 +3609,21 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "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/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2887,6 +3633,25 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -2928,6 +3693,18 @@ "csstype": "^3.0.2" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "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", @@ -2948,6 +3725,21 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "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/electron-to-chromium": { "version": "1.5.95", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.95.tgz", @@ -2988,6 +3780,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "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", @@ -3042,6 +3843,12 @@ "node": ">=6" } }, + "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/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3219,12 +4026,82 @@ "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/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3323,6 +4200,39 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3412,6 +4322,15 @@ "node": ">= 6" } }, + "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/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3425,6 +4344,45 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3448,18 +4406,65 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -3620,6 +4625,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3682,6 +4693,47 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3719,6 +4771,23 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -3744,6 +4813,15 @@ "node": ">=12" } }, + "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-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -3768,6 +4846,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3866,6 +4950,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -3939,6 +5029,49 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "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": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "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": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -3948,6 +5081,15 @@ "node": ">=18" } }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3958,6 +5100,18 @@ "json-buffer": "3.0.1" } }, + "node_modules/ky": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.7.5.tgz", + "integrity": "sha512-HzhziW6sc5m0pwi5M196+7cEBtbt0lCYi67wNsiwMUmz833wloE0gbzJPWKs1gliFKQb34huItDQX97LyOdPdA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4019,11 +5173,40 @@ "dev": true, "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==", - "dev": true, + "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.merge": { @@ -4033,6 +5216,12 @@ "dev": true, "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/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -4070,6 +5259,30 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4232,6 +5445,30 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4241,6 +5478,15 @@ "node": ">= 8" } }, + "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/micromark": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", @@ -4696,6 +5942,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4721,7 +5979,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4730,6 +5987,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -4739,12 +6005,179 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mongodb": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", + "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.3", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", + "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.3", + "kareem": "2.6.3", + "mongodb": "~6.14.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose-sequence": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mongoose-sequence/-/mongoose-sequence-6.0.1.tgz", + "integrity": "sha512-uXnLCW9pu2V49Xw8BmdXdeRugd2mv+ntu3nT2Bbm33pNRmmvHE2GKA+8BASKoQt960McLX4VL78wkb492f6MoQ==", + "license": "GPL-2.0", + "dependencies": { + "async": "^3.2.5", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "mongoose": ">=5" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/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": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -4781,6 +6214,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "14.2.16", "resolved": "https://registry.npmjs.org/next/-/next-14.2.16.tgz", @@ -4869,12 +6311,75 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4893,6 +6398,19 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4911,6 +6429,48 @@ "node": ">= 6" } }, + "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/openpgp": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-6.1.0.tgz", + "integrity": "sha512-fRTeitP+hoGJD3kbdUlAI++wE6MvfvXw1rBqHwmBMxIpLjowatJ2zb5ThkORpIkSz5F12wO+xCYRSTbT7M4qKA==", + "license": "LGPL-3.0+", + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4999,6 +6559,15 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "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-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5009,6 +6578,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5040,6 +6618,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5229,6 +6813,12 @@ "node": ">= 0.8.0" } }, + "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/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5250,6 +6840,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5260,12 +6863,26 @@ "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/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5286,6 +6903,30 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -5493,6 +7134,27 @@ "pify": "^2.3.0" } }, + "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", @@ -5622,6 +7284,43 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5645,6 +7344,32 @@ "queue-microtask": "^1.2.2" } }, + "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/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -5654,6 +7379,132 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "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/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5675,6 +7526,84 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "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", @@ -5687,6 +7616,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -5716,6 +7654,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -5724,6 +7680,21 @@ "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/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5976,6 +7947,44 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6015,6 +8024,27 @@ "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/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -6060,6 +8090,25 @@ "node": ">= 0.8.0" } }, + "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/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/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -6168,6 +8217,15 @@ "url": "https://opencollective.com/unified" } }, + "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/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -6266,6 +8324,37 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "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/vaul": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", @@ -6329,6 +8418,28 @@ "d3-timer": "^3.0.1" } }, + "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.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "license": "MIT", + "dependencies": { + "tr46": "^5.0.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", @@ -6344,6 +8455,35 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6448,6 +8588,27 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", diff --git a/package.json b/package.json index 7d9c13c..08e2034 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,16 @@ "name": "my-v0-project", "version": "0.1.0", "private": true, + "type": "module", "scripts": { "dev": "next dev", + "dev:custom": "node server.js", + "dev:tor": "NODE_ENV=production node -r dotenv/config server.js dotenv_config_path=.env.tor", "build": "next build", "start": "next start", + "start:custom": "NODE_ENV=production node server.js", + "start:tor": "NODE_ENV=production node -r dotenv/config server.js dotenv_config_path=.env.tor", + "setup-backend": "node setup-backend.js", "lint": "next lint" }, "dependencies": { @@ -39,17 +45,28 @@ "@radix-ui/react-tooltip": "^1.1.6", "autoprefixer": "^10.4.20", "axios": "^1.8.1", + "bcrypt": "^5.1.1", + "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", + "cors": "^2.8.5", "date-fns": "4.1.0", + "dotenv": "^16.4.7", "embla-carousel-react": "8.5.1", + "express": "^4.21.2", "form-data": "^4.0.2", "input-otp": "1.4.1", + "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "ky": "^1.7.5", "lucide-react": "^0.454.0", + "mongoose": "^8.9.6", + "mongoose-sequence": "^6.0.1", + "multer": "^1.4.5-lts.1", "next": "14.2.16", "next-themes": "latest", + "openpgp": "^6.1.0", "react": "^18", "react-day-picker": "8.10.1", "react-dom": "^18", @@ -57,9 +74,11 @@ "react-markdown": "^10.0.0", "react-resizable-panels": "^2.1.7", "recharts": "2.15.0", + "sharp": "^0.33.5", "sonner": "^1.7.4", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.5", "vaul": "^0.9.6", "zod": "^3.24.1" }, diff --git a/server.js b/server.js new file mode 100644 index 0000000..c227738 --- /dev/null +++ b/server.js @@ -0,0 +1,127 @@ +// Load environment variables first +import dotenv from 'dotenv'; + +// Load backend-specific environment variables +dotenv.config({ path: '.env.backend' }); +// Then load frontend environment variables (will override if there are duplicates) +dotenv.config(); + +import express from 'express'; +import { createServer } from 'http'; +import { parse } from 'url'; +import next from 'next'; +import cors from 'cors'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +// Import your backend routes +import connectDB from './backend/config/db.js'; +import authRoutes from './backend/routes/auth.routes.js'; +import inviteRoutes from './backend/routes/invites.routes.js'; +import staffAuthRoutes from './backend/routes/staffAuth.routes.js'; +import orderRoutes from './backend/routes/orders.routes.js'; +import productRoutes from './backend/routes/products.routes.js'; +import categoryRoutes from './backend/routes/categories.routes.js'; +import shippingRoutes from './backend/routes/shipping.routes.js'; +import storeRoutes from './backend/routes/storefront.routes.js'; +import cryptoRoutes from './backend/routes/crypto.routes.js'; +import blockedUsersRoutes from './backend/routes/blockedUsers.routes.js'; +import chatRoutes from './backend/routes/chat.routes.js'; +import stockRoutes from './backend/routes/stock.routes.js'; +import promotionRoutes from './backend/routes/promotion.routes.js'; +import { startCryptoPriceUpdater } from './backend/controllers/cryptoController.js'; +import { protectTelegramApi } from './backend/middleware/telegramAuthMiddleware.js'; +import { processTelegramMessage, createTelegramChat } from './backend/controllers/chat.controller.js'; +import logger from './backend/utils/logger.js'; + +const dev = process.env.NODE_ENV !== 'production'; +const hostname = 'localhost'; +// Use port 3000 for the integrated application +// Original backend was on 3001, which is causing the conflict +const port = process.env.PORT || 3000; + +// Initialize Next.js +const app = next({ dev, hostname, port }); +const handle = app.getRequestHandler(); + +app.prepare().then(() => { + const server = express(); + + // Connect to MongoDB + connectDB(); + + // Add security headers and handle CORS + server.use((req, res, next) => { + // Basic security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + + // Handle CORS + const origin = req.headers.origin; + const host = req.headers.host; + + // CORS handling (simplified for local development) + res.setHeader('Access-Control-Allow-Origin', origin || '*'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin'); + res.setHeader('Access-Control-Expose-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Max-Age', '86400'); + + // Log the request for debugging + logger.info(`Request from ${req.ip} - Origin: ${origin || 'null'} - Host: ${host}`); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(204).end(); + return; + } + + next(); + }); + + // Parse JSON for all routes + server.use(express.json({ limit: "15mb" })); + + // Direct routes for Telegram API to bypass JWT middleware + server.post("/api/telegram/message", protectTelegramApi, processTelegramMessage); + server.post("/api/telegram/create", protectTelegramApi, createTelegramChat); + server.get("/api/telegram/test-auth", protectTelegramApi, (req, res) => { + res.status(200).json({ + success: true, + message: "Authentication successful", + headers: { + authHeader: req.headers.authorization ? req.headers.authorization.substring(0, 10) + "..." : "undefined", + xApiKey: req.headers['x-api-key'] ? req.headers['x-api-key'].substring(0, 10) + "..." : "undefined" + } + }); + }); + + // Register API routes + server.use("/api/products", productRoutes); + server.use("/api/chats", chatRoutes); + server.use("/api/auth", authRoutes); + server.use("/api/staff/auth", staffAuthRoutes); + server.use("/api/invite", inviteRoutes); + server.use("/api/orders", orderRoutes); + server.use("/api/categories", categoryRoutes); + server.use("/api/shipping-options", shippingRoutes); + server.use("/api/storefront", storeRoutes); + server.use("/api/crypto", cryptoRoutes); + server.use("/api/blocked-users", blockedUsersRoutes); + server.use("/api/stock", stockRoutes); + server.use("/api/promotions", promotionRoutes); + + // Start crypto price updater + startCryptoPriceUpdater(60); + + // For all other routes, use Next.js + server.all('*', (req, res) => { + return handle(req, res); + }); + + server.listen(port, () => { + console.log(`> Ready on http://${hostname}:${port}`); + }); +}); \ No newline at end of file diff --git a/setup-backend.js b/setup-backend.js new file mode 100644 index 0000000..b99dc0d --- /dev/null +++ b/setup-backend.js @@ -0,0 +1,75 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const backendPath = path.join(__dirname, '..', 'ember-market-backend'); +const targetPath = path.join(__dirname, 'backend'); + +// Create backend directory if it doesn't exist +if (!fs.existsSync(targetPath)) { + fs.mkdirSync(targetPath, { recursive: true }); +} + +// Function to copy directory recursively +function copyDir(src, dest) { + // Create destination directory if it doesn't exist + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + // Read contents of source directory + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + // Skip node_modules and .git directories + if (entry.name === 'node_modules' || entry.name === '.git') { + console.log(`Skipping ${entry.name}`); + continue; + } + + if (entry.isDirectory()) { + // Recursive call for directories + copyDir(srcPath, destPath); + } else { + // Copy file + fs.copyFileSync(srcPath, destPath); + console.log(`Copied: ${srcPath} -> ${destPath}`); + } + } +} + +// Copy files from backend to frontend/backend +try { + // Copy main backend directories + const dirsToSync = ['config', 'controllers', 'middleware', 'models', 'routes', 'scripts', 'utils']; + + dirsToSync.forEach(dir => { + const sourcePath = path.join(backendPath, dir); + const destPath = path.join(targetPath, dir); + + if (fs.existsSync(sourcePath)) { + copyDir(sourcePath, destPath); + } else { + console.warn(`Warning: Directory ${sourcePath} does not exist`); + } + }); + + // Copy .env file (you might want to modify this for different environments) + const envSource = path.join(backendPath, '.env'); + const envDest = path.join(__dirname, '.env.backend'); + + if (fs.existsSync(envSource)) { + fs.copyFileSync(envSource, envDest); + console.log(`Copied: ${envSource} -> ${envDest}`); + } + + console.log('Backend setup completed successfully!'); +} catch (error) { + console.error('Error setting up backend:', error); +} \ No newline at end of file