WOOHOO
This commit is contained in:
@@ -1,19 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,521 +0,0 @@
|
|||||||
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" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// 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" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
112
backend/index.js
112
backend/index.js
@@ -1,112 +0,0 @@
|
|||||||
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}`);
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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" });
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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" });
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import ky from "ky"
|
|
||||||
import { getCryptoPrices } from "../controllers/cryptoController.js";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/", getCryptoPrices);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,598 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/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);
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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!' };
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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 <staffEmail>");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
createInvitation(staffEmail);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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<string>} - 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<string>} - 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
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<boolean>} - 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<number>} 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<void>}
|
|
||||||
*/
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
125
server.js
125
server.js
@@ -1,125 +0,0 @@
|
|||||||
// 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';
|
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
http:
|
|
||||||
middlewares:
|
|
||||||
ip-whitelist:
|
|
||||||
ipWhiteList:
|
|
||||||
sourceRange:
|
|
||||||
- "212.113.116.6"
|
|
||||||
- "194.26.229.41"
|
|
||||||
- "138.124.13.13"
|
|
||||||
Reference in New Issue
Block a user