other
This commit is contained in:
158
backend/routes/auth.routes.js
Normal file
158
backend/routes/auth.routes.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import express from "express";
|
||||
import bcrypt from "bcryptjs";
|
||||
import jwt from "jsonwebtoken";
|
||||
import Vendor from "../models/Vendor.model.js";
|
||||
import Store from "../models/Store.model.js";
|
||||
import Invitation from "../models/Invitation.model.js";
|
||||
import { protectVendor } from "../middleware/vendorAuthMiddleware.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Register a New Vendor (and create a corresponding Store)
|
||||
* @route POST /api/auth/register
|
||||
*/
|
||||
router.post("/register", async (req, res) => {
|
||||
const { username, password, invitationCode } = req.body;
|
||||
|
||||
try {
|
||||
if (!username || !password || !invitationCode) {
|
||||
return res.status(400).json({ error: "All fields are required." });
|
||||
}
|
||||
|
||||
// Verify invitation code
|
||||
const invitation = await Invitation.findOne({
|
||||
code: invitationCode,
|
||||
isUsed: false,
|
||||
});
|
||||
|
||||
console.log(`Invitation: ${invitation}`);
|
||||
|
||||
if (!invitation) {
|
||||
return res.status(400).json({ error: "Invalid or used invitation code." });
|
||||
}
|
||||
|
||||
// Check if vendor already exists
|
||||
const existingVendor = await Vendor.findOne({ username });
|
||||
if (existingVendor) {
|
||||
return res.status(400).json({ error: "Vendor already exists." });
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create the vendor
|
||||
const vendor = new Vendor({
|
||||
username,
|
||||
passwordHash: hashedPassword,
|
||||
});
|
||||
await vendor.save();
|
||||
|
||||
// Create a store for this vendor
|
||||
const store = new Store({
|
||||
vendorId: vendor._id,
|
||||
storeName: `${username}'s Store`,
|
||||
welcomeMessage: "Welcome to my store!",
|
||||
categories: [],
|
||||
});
|
||||
await store.save();
|
||||
|
||||
// Attach `storeId` to vendor
|
||||
vendor.storeId = store._id;
|
||||
await vendor.save();
|
||||
|
||||
// Mark invitation as used
|
||||
invitation.isUsed = true;
|
||||
invitation.usedBy = vendor._id;
|
||||
invitation.usedAt = new Date();
|
||||
await invitation.save();
|
||||
|
||||
return res
|
||||
.status(201)
|
||||
.json({ message: "Vendor registered successfully", store });
|
||||
} catch (error) {
|
||||
console.error("Error registering vendor:", error);
|
||||
return res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Vendor Login
|
||||
* @route POST /api/auth/login
|
||||
*/
|
||||
router.post("/login", async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: "Username and password are required." });
|
||||
}
|
||||
|
||||
const vendor = await Vendor.findOne({ username });
|
||||
if (!vendor) {
|
||||
return res.status(401).json({ error: "Invalid credentials." });
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, vendor.passwordHash);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: "Invalid credentials." });
|
||||
}
|
||||
|
||||
// Generate a JWT
|
||||
const token = jwt.sign(
|
||||
{ id: vendor._id, role: "vendor" },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
|
||||
// Store the token in the DB to identify the current session
|
||||
vendor.currentToken = token;
|
||||
await vendor.save();
|
||||
|
||||
return res.json({ token, role: "vendor" });
|
||||
} catch (error) {
|
||||
console.error("Error logging in vendor:", error);
|
||||
return res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Vendor Logout
|
||||
* @route POST /api/auth/logout
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.post("/logout", protectVendor, async (req, res) => {
|
||||
try {
|
||||
await Vendor.findByIdAndUpdate(req.user._id, { currentToken: null });
|
||||
return res.json({ message: "Successfully logged out." });
|
||||
} catch (error) {
|
||||
console.error("Error logging out vendor:", error);
|
||||
return res.status(500).json({ error: "Failed to log out." });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Vendor Info
|
||||
* @route GET /api/auth/me
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.get("/me", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const vendor = await Vendor.findById(req.user._id).select("-passwordHash -currentToken");
|
||||
|
||||
if (!vendor) {
|
||||
return res.status(404).json({ error: "Vendor not found." });
|
||||
}
|
||||
|
||||
vendor.lastOnline = new Date();
|
||||
await vendor.save();
|
||||
|
||||
const store = await Store.findOne({ vendorId: vendor._id });
|
||||
return res.json({ vendor, store });
|
||||
} catch (error) {
|
||||
console.error("Error fetching vendor info:", error);
|
||||
return res.status(500).json({ error: "Failed to fetch vendor data." });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
75
backend/routes/blockedUsers.routes.js
Normal file
75
backend/routes/blockedUsers.routes.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import express from "express";
|
||||
import { protectStaff } from "../middleware/staffAuthMiddleware.js";
|
||||
import BlockedUser from "../models/BlockedUser.model.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Get all blocked users
|
||||
* @route GET /api/blocked-users
|
||||
* @access Private (Staff only)
|
||||
*/
|
||||
router.get("/", protectStaff, async (req, res) => {
|
||||
try {
|
||||
const blockedUsers = await BlockedUser.find()
|
||||
.sort({ blockedAt: -1 });
|
||||
res.json(blockedUsers);
|
||||
} catch (error) {
|
||||
console.error("Error fetching blocked users:", error);
|
||||
res.status(500).json({ error: "Failed to fetch blocked users" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Block a user
|
||||
* @route POST /api/blocked-users
|
||||
* @access Private (Staff only)
|
||||
*/
|
||||
router.post("/", protectStaff, async (req, res) => {
|
||||
try {
|
||||
const { telegramUserId, reason } = req.body;
|
||||
|
||||
if (!telegramUserId) {
|
||||
return res.status(400).json({ error: "Telegram user ID is required" });
|
||||
}
|
||||
|
||||
const existingBlock = await BlockedUser.findOne({ telegramUserId });
|
||||
if (existingBlock) {
|
||||
return res.status(400).json({ error: "User is already blocked" });
|
||||
}
|
||||
|
||||
const blockedUser = await BlockedUser.create({
|
||||
telegramUserId,
|
||||
reason,
|
||||
blockedBy: req.user._id
|
||||
});
|
||||
|
||||
res.status(201).json(blockedUser);
|
||||
} catch (error) {
|
||||
console.error("Error blocking user:", error);
|
||||
res.status(500).json({ error: "Failed to block user" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Unblock a user
|
||||
* @route DELETE /api/blocked-users/:telegramUserId
|
||||
* @access Private (Staff only)
|
||||
*/
|
||||
router.delete("/:telegramUserId", protectStaff, async (req, res) => {
|
||||
try {
|
||||
const { telegramUserId } = req.params;
|
||||
|
||||
const result = await BlockedUser.findOneAndDelete({ telegramUserId });
|
||||
if (!result) {
|
||||
return res.status(404).json({ error: "User is not blocked" });
|
||||
}
|
||||
|
||||
res.json({ message: "User unblocked successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error unblocking user:", error);
|
||||
res.status(500).json({ error: "Failed to unblock user" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
154
backend/routes/categories.routes.js
Normal file
154
backend/routes/categories.routes.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import express from "express";
|
||||
import Store from "../models/Store.model.js";
|
||||
import { protectVendor } from "../middleware/vendorAuthMiddleware.js";
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 📌 Fetch all categories for the vendor's store
|
||||
*/
|
||||
router.get("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const store = await Store.findById(req.user.storeId);
|
||||
if (!store) return res.status(404).json({ message: "Store not found" });
|
||||
|
||||
res.json(store.categories);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch categories", error });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Add a new category
|
||||
*/
|
||||
router.post("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { name, parentId } = req.body;
|
||||
const store = await Store.findById(req.user.storeId);
|
||||
|
||||
if (!store) return res.status(404).json({ message: "Store not found" });
|
||||
|
||||
// Check if the category name already exists at the same level
|
||||
const categoryExists = store.categories.some(category =>
|
||||
category.name === name &&
|
||||
(!parentId ? !category.parentId : category.parentId?.toString() === parentId)
|
||||
);
|
||||
|
||||
if (categoryExists) {
|
||||
return res.status(400).json({ message: "Category already exists at this level" });
|
||||
}
|
||||
|
||||
// If parentId is provided, verify it exists
|
||||
if (parentId) {
|
||||
const parentExists = store.categories.some(cat => cat._id.toString() === parentId);
|
||||
if (!parentExists) {
|
||||
return res.status(400).json({ message: "Parent category not found" });
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new category with a unique _id, name, and optional parentId
|
||||
const newCategory = {
|
||||
name,
|
||||
...(parentId && { parentId }) // Only add parentId if it exists
|
||||
};
|
||||
store.categories.push(newCategory);
|
||||
await store.save();
|
||||
|
||||
// Get the newly created category with its _id
|
||||
const createdCategory = store.categories[store.categories.length - 1];
|
||||
|
||||
res.status(201).json({
|
||||
_id: createdCategory._id,
|
||||
name: createdCategory.name,
|
||||
parentId: createdCategory.parentId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error adding category:", error);
|
||||
res.status(400).json({ message: "Failed to add category", error });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Update a category
|
||||
*/
|
||||
router.put("/:categoryId", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { categoryId } = req.params;
|
||||
const { name } = req.body;
|
||||
const store = await Store.findById(req.user.storeId);
|
||||
|
||||
if (!store) return res.status(404).json({ message: "Store not found" });
|
||||
|
||||
// Find the category
|
||||
const category = store.categories.id(categoryId);
|
||||
if (!category) {
|
||||
return res.status(404).json({ message: "Category not found" });
|
||||
}
|
||||
|
||||
// Check if the new name already exists at the same level (excluding this category)
|
||||
const categoryExists = store.categories.some(cat =>
|
||||
cat.name === name &&
|
||||
cat._id.toString() !== categoryId &&
|
||||
(!category.parentId ? !cat.parentId : cat.parentId?.toString() === category.parentId.toString())
|
||||
);
|
||||
|
||||
if (categoryExists) {
|
||||
return res.status(400).json({ message: "Category name already exists at this level" });
|
||||
}
|
||||
|
||||
// Update the category
|
||||
category.name = name;
|
||||
await store.save();
|
||||
|
||||
res.json({
|
||||
_id: category._id,
|
||||
name: category.name,
|
||||
parentId: category.parentId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating category:", error);
|
||||
res.status(400).json({ message: "Failed to update category", error });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Delete a category and its subcategories
|
||||
*/
|
||||
router.delete("/:categoryId", protectVendor, async (req, res) => {
|
||||
const { categoryId } = req.params;
|
||||
|
||||
try {
|
||||
const store = await Store.findById(req.user.storeId);
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found" });
|
||||
}
|
||||
|
||||
// Find all subcategories recursively
|
||||
const getAllSubcategoryIds = (categoryId) => {
|
||||
const subcategories = store.categories.filter(cat =>
|
||||
cat.parentId?.toString() === categoryId
|
||||
);
|
||||
|
||||
return [
|
||||
categoryId,
|
||||
...subcategories.flatMap(subcat => getAllSubcategoryIds(subcat._id.toString()))
|
||||
];
|
||||
};
|
||||
|
||||
const categoryIdsToDelete = getAllSubcategoryIds(categoryId);
|
||||
|
||||
// Remove all categories and their subcategories
|
||||
store.categories = store.categories.filter(
|
||||
cat => !categoryIdsToDelete.includes(cat._id.toString())
|
||||
);
|
||||
|
||||
// Save the updated store document
|
||||
await store.save();
|
||||
|
||||
res.status(200).json({ message: "Category and subcategories deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting category:", error);
|
||||
res.status(500).json({ message: "Failed to delete category", error });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
41
backend/routes/chat.routes.js
Normal file
41
backend/routes/chat.routes.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import express from "express";
|
||||
import {
|
||||
getVendorChats,
|
||||
getChatMessages,
|
||||
sendVendorMessage,
|
||||
processTelegramMessage,
|
||||
getVendorUnreadCounts,
|
||||
createChat,
|
||||
createTelegramChat,
|
||||
markMessagesAsRead
|
||||
} from "../controllers/chat.controller.js";
|
||||
import { protectVendor as vendorAuth } from "../middleware/vendorAuthMiddleware.js";
|
||||
import { protectTelegramApi } from "../middleware/telegramAuthMiddleware.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Routes that require vendor authentication
|
||||
router.get("/vendor/:vendorId", vendorAuth, getVendorChats);
|
||||
router.get("/vendor/:vendorId/unread", vendorAuth, getVendorUnreadCounts);
|
||||
router.get("/:chatId", vendorAuth, getChatMessages);
|
||||
router.post("/:chatId/message", vendorAuth, sendVendorMessage);
|
||||
router.post("/:chatId/mark-read", vendorAuth, markMessagesAsRead);
|
||||
router.post("/create", vendorAuth, createChat);
|
||||
|
||||
// Routes for Telegram client (secured with API key)
|
||||
router.post("/telegram/message", protectTelegramApi, processTelegramMessage);
|
||||
router.post("/telegram/create", protectTelegramApi, createTelegramChat);
|
||||
|
||||
// Test route for Telegram API auth
|
||||
router.get("/telegram/test-auth", protectTelegramApi, (req, res) => {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Authentication successful",
|
||||
headers: {
|
||||
authHeader: req.headers.authorization ? req.headers.authorization.substring(0, 10) + "..." : "undefined",
|
||||
xApiKey: req.headers['x-api-key'] ? req.headers['x-api-key'].substring(0, 10) + "..." : "undefined"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
9
backend/routes/crypto.routes.js
Normal file
9
backend/routes/crypto.routes.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import express from "express";
|
||||
import ky from "ky"
|
||||
import { getCryptoPrices } from "../controllers/cryptoController.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", getCryptoPrices);
|
||||
|
||||
export default router;
|
||||
21
backend/routes/invites.routes.js
Normal file
21
backend/routes/invites.routes.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import express from "express";
|
||||
import crypto from "crypto";
|
||||
import { protectStaff } from "../middleware/staffAuthMiddleware.js";
|
||||
import Invitation from "../models/Invitation.model.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/generate", protectStaff, async (req, res) => {
|
||||
try {
|
||||
const invitationCode = crypto.randomBytes(6).toString("hex");
|
||||
|
||||
const invitation = new Invitation({ code: invitationCode, createdBy: req.user._id });
|
||||
await invitation.save();
|
||||
|
||||
res.status(201).json({ invitationCode });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
598
backend/routes/orders.routes.js
Normal file
598
backend/routes/orders.routes.js
Normal file
@@ -0,0 +1,598 @@
|
||||
import express from "express";
|
||||
import { protectVendor } from "../middleware/vendorAuthMiddleware.js";
|
||||
import { protectCrypto } from "../middleware/apiAuthMiddleware.js";
|
||||
import Order from "../models/Order.model.js";
|
||||
import Wallet from "../models/Wallet.model.js";
|
||||
|
||||
import crypto from "crypto";
|
||||
import { setupWallet } from "../utils/litecoin/index.js";
|
||||
import { returnCryptoPrices } from "../controllers/cryptoController.js";
|
||||
import Store from "../models/Store.model.js";
|
||||
import { sendTelegramMessage } from "../utils/telegramUtils.js"
|
||||
import Product from "../models/Product.model.js";
|
||||
import mongoose from "mongoose";
|
||||
import { decreaseStockOnOrder, restoreStockOnCancel } from "../controllers/stock.controller.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 📌 Get Orders for Vendor
|
||||
* @route GET /api/orders
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.get("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
// Extract query params for filtering and navigation
|
||||
const { status, page = 1, limit = 25, before, after, storeId } = req.query;
|
||||
|
||||
// Initialize query object
|
||||
let query = {};
|
||||
|
||||
// Use storeId from query parameter if provided, otherwise use the one from user object
|
||||
query.storeId = storeId || req.user.storeId;
|
||||
|
||||
// If for some reason there's no storeId in user object and none provided in query
|
||||
if (!query.storeId) {
|
||||
return res.status(400).json({ error: "No store ID found. Please specify a storeId parameter." });
|
||||
}
|
||||
// Filter by order status if provided
|
||||
if (status) {
|
||||
query.status = status;
|
||||
}
|
||||
|
||||
// Add orderId filters for navigation
|
||||
if (before) {
|
||||
query.orderId = { $lt: before };
|
||||
} else if (after) {
|
||||
query.orderId = { $gt: after };
|
||||
}
|
||||
|
||||
// Count total orders for pagination
|
||||
const totalOrders = await Order.countDocuments(query);
|
||||
|
||||
// Fetch orders with pagination and sorting
|
||||
const orders = await Order.find(query)
|
||||
.sort({ orderId: -1 }) // Always sort by newest first
|
||||
.limit(parseInt(limit))
|
||||
.skip((parseInt(page) - 1) * parseInt(limit));
|
||||
|
||||
res.json({
|
||||
orders,
|
||||
page: parseInt(page),
|
||||
totalPages: Math.ceil(totalOrders / limit),
|
||||
totalOrders,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching orders:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve orders" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/stats", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const vendorId = req.user._id;
|
||||
|
||||
const totalOrders = await Order.countDocuments({ vendorId });
|
||||
const pendingOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: { $in: ["unpaid", "confirming"] },
|
||||
});
|
||||
const ongoingOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: { $in: ["paid", "shipped"] },
|
||||
});
|
||||
const cancelledOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: "cancelled",
|
||||
});
|
||||
const completedOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: "completed",
|
||||
});
|
||||
|
||||
res.json({ totalOrders, pendingOrders, ongoingOrders, cancelledOrders, completedOrders });
|
||||
} catch (error) {
|
||||
console.error("Error fetching order stats:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve order statistics" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Get Vendor's Best Selling Products
|
||||
* @route GET /api/orders/top-products
|
||||
* @access Private (Vendors only)
|
||||
* @returns Most sold products for the logged-in vendor
|
||||
*/
|
||||
router.get("/top-products", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const vendorId = req.user._id;
|
||||
|
||||
// Find the vendor's store
|
||||
const store = await Store.findOne({ vendorId });
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found for this vendor" });
|
||||
}
|
||||
|
||||
// Aggregate orders to find the vendor's best-selling products
|
||||
const topProducts = await Order.aggregate([
|
||||
// Match only orders for this vendor
|
||||
{ $match: { vendorId: new mongoose.Types.ObjectId(vendorId) } },
|
||||
// Unwind the products array
|
||||
{ $unwind: "$products" },
|
||||
// Group by product ID and count occurrences
|
||||
{
|
||||
$group: {
|
||||
_id: "$products.productId",
|
||||
count: { $sum: 1 },
|
||||
revenue: { $sum: "$products.price" }
|
||||
}
|
||||
},
|
||||
// Sort by count in descending order
|
||||
{ $sort: { count: -1 } },
|
||||
// Limit to top 10 products
|
||||
{ $limit: 10 }
|
||||
]);
|
||||
|
||||
// Get the actual product details
|
||||
const productIds = topProducts.map(item => item._id);
|
||||
const products = await Product.find({
|
||||
_id: { $in: productIds },
|
||||
storeId: store._id
|
||||
}).select('name price image');
|
||||
|
||||
// Combine the count with product details
|
||||
const result = topProducts.map(item => {
|
||||
const productDetails = products.find(p => p._id.toString() === item._id.toString());
|
||||
return {
|
||||
id: item._id,
|
||||
name: productDetails?.name || 'Unknown Product',
|
||||
price: productDetails?.price || 0,
|
||||
image: productDetails?.image || '',
|
||||
count: item.count,
|
||||
revenue: item.revenue
|
||||
};
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error("Error fetching top products:", error);
|
||||
res.status(500).json({ error: "Failed to fetch top products data" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/create", protectCrypto, async (req, res) => {
|
||||
try {
|
||||
const { vendorId, storeId, products, totalPrice, shippingMethod, telegramChatId, telegramBuyerId, telegramUsername, cryptoCurrency, pgpAddress } = req.body;
|
||||
|
||||
console.log("Create order request:", req.body);
|
||||
|
||||
if (!vendorId || !storeId || !products || !totalPrice || !shippingMethod || !cryptoCurrency || !pgpAddress) {
|
||||
console.log("Missing required fields");
|
||||
return res.status(400).json({ error: "Missing required fields" });
|
||||
}
|
||||
|
||||
/*
|
||||
// TEMPORARILY DISABLED: Stock check disabled to give vendors time to add stock
|
||||
// Check stock levels before creating order
|
||||
const outOfStockProducts = [];
|
||||
for (const item of products) {
|
||||
const product = await Product.findOne({ _id: item.productId, storeId });
|
||||
|
||||
if (product && product.stockTracking && product.currentStock < item.quantity) {
|
||||
outOfStockProducts.push({
|
||||
productId: item.productId,
|
||||
name: product.name,
|
||||
requested: item.quantity,
|
||||
available: product.currentStock
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If any products are out of stock, return an error
|
||||
if (outOfStockProducts.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: "Some products are out of stock or have insufficient quantity",
|
||||
outOfStockProducts
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
const cryptoPrices = returnCryptoPrices();
|
||||
const cryptoRate = cryptoPrices[cryptoCurrency.toLowerCase()];
|
||||
|
||||
if (!cryptoRate) {
|
||||
console.log("Invalid or unsupported cryptocurrency");
|
||||
return res.status(400).json({ error: "Invalid or unsupported cryptocurrency" });
|
||||
}
|
||||
|
||||
const cryptoTotal = (totalPrice / cryptoRate).toFixed(8);
|
||||
|
||||
const walletName = "order_" + crypto.randomBytes(8).toString("hex");
|
||||
const walletData = await setupWallet(walletName);
|
||||
|
||||
if (!walletData || !walletData.address || !walletData.privKey) {
|
||||
return res.status(500).json({ error: "Failed to generate payment address" });
|
||||
}
|
||||
|
||||
// ✅ Create Order with `cryptoTotal`
|
||||
const newOrder = await Order.create({
|
||||
vendorId,
|
||||
storeId,
|
||||
products,
|
||||
pgpAddress,
|
||||
totalPrice,
|
||||
cryptoTotal,
|
||||
shippingMethod,
|
||||
telegramChatId,
|
||||
telegramUsername,
|
||||
telegramBuyerId,
|
||||
status: "unpaid",
|
||||
paymentAddress: walletData.address,
|
||||
});
|
||||
|
||||
// ✅ Link the Wallet to the Order
|
||||
const newWallet = await Wallet.create({
|
||||
walletName,
|
||||
orderId: newOrder._id,
|
||||
address: walletData.address,
|
||||
encryptedPrivateKey: walletData.privKey,
|
||||
});
|
||||
|
||||
// ✅ Update Order with Wallet ID
|
||||
newOrder.wallet = newWallet._id;
|
||||
await newOrder.save();
|
||||
|
||||
// TEMPORARILY DISABLED: Stock decrease disabled to give vendors time to add stock
|
||||
// Decrease stock for ordered products
|
||||
// await decreaseStockOnOrder(newOrder);
|
||||
|
||||
res.status(201).json({
|
||||
message: "Order created successfully",
|
||||
order: newOrder,
|
||||
paymentAddress: walletData.address,
|
||||
walletName: walletName,
|
||||
cryptoTotal,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating order:", error);
|
||||
res.status(500).json({ error: "Failed to create order", details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id/status", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!status || !["acknowledged", "paid", "shipped", "completed", "cancelled"].includes(status)) {
|
||||
return res.status(400).json({ message: "Invalid status" });
|
||||
}
|
||||
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ message: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Find order by ID and storeId
|
||||
const order = await Order.findOne({ _id: id, storeId });
|
||||
if (!order) {
|
||||
return res.status(404).json({ message: "Order not found" });
|
||||
}
|
||||
|
||||
const previousStatus = order.status;
|
||||
|
||||
// TEMPORARILY DISABLED: Stock restoration disabled to give vendors time to add stock
|
||||
// Handle stock changes based on status transitions
|
||||
/*
|
||||
if (status === "cancelled" && previousStatus !== "cancelled") {
|
||||
// Restore stock quantities if order is cancelled
|
||||
await restoreStockOnCancel(order);
|
||||
}
|
||||
*/
|
||||
|
||||
const store = await Store.findById(order.storeId);
|
||||
if (!store) return res.status(404).json({ message: "Store not found" });
|
||||
|
||||
order.status = status;
|
||||
await order.save();
|
||||
|
||||
if (store.telegramToken && order.telegramChatId) {
|
||||
let message = '';
|
||||
|
||||
switch (status) {
|
||||
case 'acknowledged':
|
||||
message = `✅ Your order ${order.orderId} has been acknowledged.`;
|
||||
break;
|
||||
case 'paid':
|
||||
message = `💰 Your order ${order.orderId} has been marked as paid.`;
|
||||
break;
|
||||
case 'shipped':
|
||||
message = `🚚 Your order ${order.orderId} has been shipped!`;
|
||||
break;
|
||||
case 'completed':
|
||||
message = `🎉 Your order ${order.orderId} has been completed.`;
|
||||
break;
|
||||
case 'cancelled':
|
||||
message = `❌ Your order ${order.orderId} has been cancelled.`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message);
|
||||
if (!sent) {
|
||||
console.error(`Failed to notify user ${order.telegramChatId} about status update to ${status}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({ message: "Order status updated successfully" });
|
||||
} catch (error) {
|
||||
console.error("❌ Error updating order status:", error);
|
||||
return res.status(500).json({ error: "Failed to update order status", details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", protectVendor, async (req, res) => {
|
||||
if (!req.params.id) return res.status(400).json({ message: "Missing order ID" });
|
||||
|
||||
try {
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ message: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Find order by ID and storeId
|
||||
const order = await Order.findOne({ _id: req.params.id, storeId: storeId });
|
||||
|
||||
if(!order) return res.status(404).json({ message: "Order not found for this store" });
|
||||
|
||||
return res.status(200).json(order);
|
||||
} catch (error) {
|
||||
console.error("Error fetching order:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve order" });
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Mark Multiple Orders as Shipped
|
||||
* @route POST /api/orders/mark-shipped
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.post("/mark-shipped", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { orderIds } = req.body;
|
||||
|
||||
if (!Array.isArray(orderIds) || orderIds.length === 0) {
|
||||
return res.status(400).json({ error: "Order IDs array is required" });
|
||||
}
|
||||
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ error: "No store associated with this user" });
|
||||
}
|
||||
|
||||
const updatedOrders = [];
|
||||
const failedOrders = [];
|
||||
|
||||
// Get store for notifications
|
||||
const store = await Store.findById(storeId);
|
||||
if (!store) {
|
||||
return res.status(404).json({ error: "Store not found" });
|
||||
}
|
||||
|
||||
for (const orderId of orderIds) {
|
||||
try {
|
||||
// Find and update order using storeId instead of vendorId
|
||||
const order = await Order.findOne({ _id: orderId, storeId: storeId });
|
||||
|
||||
if (!order) {
|
||||
failedOrders.push({ orderId, reason: "Order not found or not associated with this store" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (order.status !== "paid") {
|
||||
failedOrders.push({ orderId, reason: "Order must be in paid status to be marked as shipped" });
|
||||
continue;
|
||||
}
|
||||
|
||||
order.status = "shipped";
|
||||
await order.save();
|
||||
updatedOrders.push(order);
|
||||
|
||||
// Send notification if possible
|
||||
if (store.telegramToken && order.telegramChatId) {
|
||||
const message = `🚚 Your order ${order.orderId} has been marked as shipped!`;
|
||||
const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message);
|
||||
|
||||
if (!sent) {
|
||||
console.error(`Failed to notify user ${order.telegramChatId} about shipping update.`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating order ${orderId}:`, error);
|
||||
failedOrders.push({ orderId, reason: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: "Orders processed",
|
||||
success: {
|
||||
count: updatedOrders.length,
|
||||
orders: updatedOrders.map(o => ({
|
||||
id: o._id,
|
||||
orderId: o.orderId,
|
||||
status: o.status
|
||||
}))
|
||||
},
|
||||
failed: {
|
||||
count: failedOrders.length,
|
||||
orders: failedOrders
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error marking orders as shipped:", error);
|
||||
return res.status(500).json({
|
||||
error: "Failed to mark orders as shipped",
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update Order Tracking Number
|
||||
* @route PUT /api/orders/:id/tracking
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.put("/:id/tracking", protectVendor, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { trackingNumber } = req.body;
|
||||
|
||||
if (!id) return res.status(400).json({ error: "Missing order ID" });
|
||||
if (!trackingNumber) return res.status(400).json({ error: "Missing tracking number" });
|
||||
|
||||
try {
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ error: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Use storeId for lookup instead of vendorId
|
||||
const order = await Order.findOne({ _id: id, storeId: storeId });
|
||||
if (!order) return res.status(404).json({ error: "Order not found for this store" });
|
||||
|
||||
// Only allow tracking updates for paid or shipped orders
|
||||
if (order.status !== "paid" && order.status !== "shipped") {
|
||||
return res.status(400).json({ error: "Can only add tracking to paid or shipped orders" });
|
||||
}
|
||||
|
||||
// Get store details for notification
|
||||
const store = await Store.findById(order.storeId);
|
||||
if (!store) return res.status(404).json({ error: "Store not found" });
|
||||
|
||||
// Update the order with tracking number
|
||||
order.trackingNumber = trackingNumber;
|
||||
await order.save();
|
||||
|
||||
// Send tracking notification
|
||||
if (store.telegramToken && order.telegramChatId) {
|
||||
const message = `📦 Tracking added for order ${order.orderId}\!`;
|
||||
const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message);
|
||||
|
||||
if (!sent) {
|
||||
console.error(`Failed to notify user ${order.telegramChatId} about tracking update.`);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: "Tracking number updated successfully",
|
||||
order: {
|
||||
id: order._id,
|
||||
orderId: order.orderId,
|
||||
status: order.status,
|
||||
trackingNumber: order.trackingNumber
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating tracking number:", error);
|
||||
return res.status(500).json({
|
||||
error: "Failed to update tracking number",
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete Order
|
||||
* @route DELETE /api/orders/:id
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.delete("/:id", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ message: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Find order by ID and storeId
|
||||
const order = await Order.findOne({ _id: id, storeId: storeId });
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({ message: "Order not found for this store" });
|
||||
}
|
||||
|
||||
// Delete associated wallet if exists
|
||||
if (order.wallet) {
|
||||
await Wallet.findByIdAndDelete(order.wallet);
|
||||
}
|
||||
|
||||
// Delete the order
|
||||
await Order.findByIdAndDelete(id);
|
||||
|
||||
res.status(200).json({ message: "Order deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting order:", error);
|
||||
res.status(500).json({ error: "Failed to delete order" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Get Adjacent Orders (for navigation)
|
||||
* @route GET /api/orders/adjacent/:orderId
|
||||
* @access Private (Vendors only)
|
||||
* @description Returns the immediately previous and next orders by orderId
|
||||
*/
|
||||
router.get("/adjacent/:orderId", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
const numericOrderId = parseInt(orderId);
|
||||
|
||||
if (isNaN(numericOrderId)) {
|
||||
return res.status(400).json({ error: "Invalid order ID format" });
|
||||
}
|
||||
|
||||
// Get the store ID from user or query parameter
|
||||
const storeId = req.query.storeId || req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ error: "No store ID found. Please specify a storeId parameter." });
|
||||
}
|
||||
|
||||
// Find the next newer order (higher orderId)
|
||||
const newerOrder = await Order.findOne({
|
||||
storeId,
|
||||
orderId: { $gt: numericOrderId }
|
||||
})
|
||||
.sort({ orderId: 1 }) // Ascending to get the immediately next one
|
||||
.select('_id orderId')
|
||||
.lean();
|
||||
|
||||
// Find the next older order (lower orderId)
|
||||
const olderOrder = await Order.findOne({
|
||||
storeId,
|
||||
orderId: { $lt: numericOrderId }
|
||||
})
|
||||
.sort({ orderId: -1 }) // Descending to get the immediately previous one
|
||||
.select('_id orderId')
|
||||
.lean();
|
||||
|
||||
res.json({
|
||||
current: { orderId: numericOrderId },
|
||||
newer: newerOrder || null,
|
||||
older: olderOrder || null
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching adjacent orders:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve adjacent orders" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
456
backend/routes/products.routes.js
Normal file
456
backend/routes/products.routes.js
Normal file
@@ -0,0 +1,456 @@
|
||||
import express from "express";
|
||||
import Product from "../models/Product.model.js";
|
||||
import multer from "multer";
|
||||
import sharp from "sharp";
|
||||
import { protectVendor } from "../middleware/authMiddleware.js";
|
||||
import fs from "fs";
|
||||
import path, { dirname } from "path";
|
||||
import Store from "../models/Store.model.js";
|
||||
import mongoose from "mongoose";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get the current directory and set up a relative uploads path
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads');
|
||||
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 15 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith("image/")) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error("Only image files are allowed!"), false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleMulterError = (err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === "LIMIT_FILE_SIZE") {
|
||||
return res.status(400).json({ message: "File size too large. Maximum allowed size is 15 MB." });
|
||||
}
|
||||
return res.status(400).json({ message: "Multer error occurred.", error: err.message });
|
||||
}
|
||||
next(err);
|
||||
};
|
||||
|
||||
router.use(handleMulterError)
|
||||
|
||||
// 📌 Upload Image for Product
|
||||
router.put("/:id/image", protectVendor, upload.single("file"), handleMulterError, async (req, res) => {
|
||||
try {
|
||||
const productId = req.params.id;
|
||||
if (!productId) return res.status(400).json({ message: "Missing product ID." });
|
||||
|
||||
const product = await Product.findOne({ _id: productId, storeId: req.user.storeId });
|
||||
if (!product) {
|
||||
return res.status(404).json({ message: "Product not found." });
|
||||
}
|
||||
|
||||
const file = req.file;
|
||||
if (!file) return res.status(400).json({ message: "No file uploaded." });
|
||||
|
||||
// Generate a new filename for the uploaded image
|
||||
const outputFileName = `${Date.now()}-${file.originalname.split('.').slice(0, -1).join('.')}.jpg`;
|
||||
const outputFilePath = path.join(uploadsDir, outputFileName);
|
||||
|
||||
await sharp(file.buffer)
|
||||
.jpeg({ quality: 80 })
|
||||
.toFile(outputFilePath);
|
||||
|
||||
// Check the final file size
|
||||
const stats = fs.statSync(outputFilePath);
|
||||
if (stats.size > 10 * 1024 * 1024) {
|
||||
fs.unlinkSync(outputFilePath);
|
||||
return res.status(400).json({ message: "File size too large after processing." });
|
||||
}
|
||||
|
||||
// Update the product in the database
|
||||
const updatedProduct = await Product.findOneAndUpdate(
|
||||
{ _id: productId, storeId: req.user.storeId },
|
||||
{ image: outputFileName },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedProduct) {
|
||||
fs.unlinkSync(outputFilePath); // Clean up uploaded file if product not found
|
||||
return res.status(404).json({ message: "Product not found." });
|
||||
}
|
||||
|
||||
res.json(updatedProduct);
|
||||
} catch (error) {
|
||||
if(req.file){
|
||||
const outputFileName = `${Date.now()}-${req.file.originalname.split('.').slice(0, -1).join('.')}.jpg`;
|
||||
const outputFilePath = path.join(uploadsDir, outputFileName);
|
||||
|
||||
if (fs.existsSync(outputFilePath)) {
|
||||
fs.unlinkSync(outputFilePath);
|
||||
}
|
||||
}
|
||||
res.status(500).json({ message: "Failed to upload image.", error });
|
||||
}
|
||||
});
|
||||
|
||||
router.use(express.json({ limit: "50mb" }));
|
||||
|
||||
router.get("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.storeId) {
|
||||
return res.status(400).json({ message: "Store not found for this vendor" });
|
||||
}
|
||||
|
||||
const products = await Product.find({ storeId: req.user.storeId }).select("-base64Image");
|
||||
res.json(products);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch products", error });
|
||||
}
|
||||
});
|
||||
|
||||
// 📌 Add New Product
|
||||
router.post("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.storeId) {
|
||||
return res.status(400).json({ message: "Store not found for this vendor" });
|
||||
}
|
||||
|
||||
const { name, description, category, unitType, pricing } = req.body;
|
||||
|
||||
if (!name || !category || !unitType || !pricing || !pricing.length) {
|
||||
return res.status(400).json({ message: "Missing required fields" });
|
||||
}
|
||||
|
||||
const formattedPricing = pricing.map((tier) => ({
|
||||
minQuantity: tier.minQuantity,
|
||||
pricePerUnit: tier.pricePerUnit,
|
||||
}));
|
||||
|
||||
const newProduct = new Product({
|
||||
storeId: req.user.storeId,
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
unitType,
|
||||
pricing: formattedPricing,
|
||||
});
|
||||
|
||||
const savedProduct = await newProduct.save();
|
||||
res.status(201).json(savedProduct);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to add product", error });
|
||||
}
|
||||
});
|
||||
|
||||
// 📌 Edit a Product by ID
|
||||
router.put("/:id", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const productId = req.params.id;
|
||||
|
||||
const updatableFields = ["name", "description", "category", "unitType"];
|
||||
|
||||
const updateFields = {};
|
||||
|
||||
for (const field of updatableFields) {
|
||||
if (req.body[field] !== undefined) {
|
||||
updateFields[field] = req.body[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.pricing !== undefined) {
|
||||
if (!Array.isArray(req.body.pricing)) {
|
||||
return res.status(400).json({ message: "Pricing must be an array" });
|
||||
}
|
||||
|
||||
updateFields.pricing = req.body.pricing.map((tier) => ({
|
||||
minQuantity: tier.minQuantity,
|
||||
pricePerUnit: tier.pricePerUnit,
|
||||
}));
|
||||
}
|
||||
|
||||
const updatedProduct = await Product.findOneAndUpdate(
|
||||
{ _id: productId, storeId: req.user.storeId },
|
||||
updateFields,
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedProduct) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ message: "Product not found or not owned by this store" });
|
||||
}
|
||||
|
||||
return res.json(updatedProduct);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to update product", error });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id/image", async (req, res) => {
|
||||
try {
|
||||
const productId = req.params.id;
|
||||
const product = await Product.findById(productId);
|
||||
|
||||
if (!product || !product.image) {
|
||||
return res.status(404).json({ message: "Image not found" });
|
||||
}
|
||||
|
||||
const imagePath = path.join(uploadsDir, product.image);
|
||||
console.log("Image path:", imagePath);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
res.sendFile(imagePath);
|
||||
} else {
|
||||
res.status(404).json({ message: "Image file does not exist" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching image:", error);
|
||||
res.status(400).json({ message: "Failed to fetch image", error });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const productId = req.params.id;
|
||||
|
||||
const deletedProduct = await Product.findByIdAndDelete({ _id: productId, storeId: req.user.storeId });
|
||||
|
||||
if (!deletedProduct) {
|
||||
return res.status(404).json({ message: "Product not found" });
|
||||
}
|
||||
|
||||
res.json({ message: "Product deleted successfully" });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to delete product", error });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const productId = req.params.id;
|
||||
const product = await Product.findById(productId);
|
||||
|
||||
if (!product) {
|
||||
return res.status(404).json({ message: "Product not found" });
|
||||
}
|
||||
|
||||
res.status(200).json(product);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to fetch product", error });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to escape special regex characters
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Helper function to standardize unit types
|
||||
function standardizeUnitType(unit) {
|
||||
const unitMap = {
|
||||
'gram': 'gr',
|
||||
'grams': 'gr',
|
||||
'g': 'gr',
|
||||
'gr': 'gr',
|
||||
'piece': 'pcs',
|
||||
'pieces': 'pcs',
|
||||
'pcs': 'pcs',
|
||||
'kilo': 'kg',
|
||||
'kilos': 'kg',
|
||||
'kilogram': 'kg',
|
||||
'kilograms': 'kg',
|
||||
'kg': 'kg'
|
||||
};
|
||||
|
||||
return unitMap[unit.toLowerCase()] || unit.toLowerCase();
|
||||
}
|
||||
|
||||
// 📌 Batch Add Products
|
||||
router.post("/batch", protectVendor, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.storeId) {
|
||||
return res.status(400).json({ message: "Store not found for this vendor" });
|
||||
}
|
||||
|
||||
const { products } = req.body;
|
||||
|
||||
if (!Array.isArray(products) || products.length === 0) {
|
||||
return res.status(400).json({ message: "Products array is required and cannot be empty" });
|
||||
}
|
||||
|
||||
// Get store to validate categories
|
||||
const store = await Store.findById(req.user.storeId);
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found" });
|
||||
}
|
||||
|
||||
const processedProducts = [];
|
||||
const skippedProducts = [];
|
||||
let completedCount = 0;
|
||||
|
||||
console.log(`Starting batch processing of ${products.length} products...`);
|
||||
|
||||
for (const product of products) {
|
||||
const { name, category, subcategory, prices } = product;
|
||||
completedCount++;
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !category || !prices || !Array.isArray(prices) || prices.length === 0) {
|
||||
console.log(`Skipping product ${completedCount}/${products.length}: Missing required fields`);
|
||||
skippedProducts.push({ name: name || 'Unknown', reason: 'Missing required fields' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if product already exists - with escaped special characters
|
||||
const escapedName = escapeRegExp(name);
|
||||
const existingProduct = await Product.findOne({
|
||||
storeId: req.user.storeId,
|
||||
name: { $regex: new RegExp(`^${escapedName}$`, 'i') }
|
||||
});
|
||||
|
||||
if (existingProduct) {
|
||||
console.log(`Skipping product ${completedCount}/${products.length}: "${name}" already exists`);
|
||||
skippedProducts.push({ name, reason: 'Product already exists' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find or create main category
|
||||
let targetCategory;
|
||||
const existingMainCategory = store.categories.find(cat =>
|
||||
cat.name.toLowerCase() === category.toLowerCase() && !cat.parentId
|
||||
);
|
||||
|
||||
if (!existingMainCategory) {
|
||||
const newCategory = {
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
name: category,
|
||||
parentId: null
|
||||
};
|
||||
store.categories.push(newCategory);
|
||||
targetCategory = newCategory;
|
||||
console.log(`Created new main category: ${category}`);
|
||||
} else {
|
||||
targetCategory = existingMainCategory;
|
||||
}
|
||||
|
||||
// If subcategory is provided, find or create it
|
||||
if (subcategory) {
|
||||
const existingSubcategory = store.categories.find(cat =>
|
||||
cat.name.toLowerCase() === subcategory.toLowerCase() &&
|
||||
cat.parentId?.toString() === targetCategory._id.toString()
|
||||
);
|
||||
|
||||
if (existingSubcategory) {
|
||||
targetCategory = existingSubcategory;
|
||||
} else {
|
||||
const newSubcategory = {
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
name: subcategory,
|
||||
parentId: targetCategory._id
|
||||
};
|
||||
store.categories.push(newSubcategory);
|
||||
targetCategory = newSubcategory;
|
||||
console.log(`Created new subcategory: ${subcategory} under ${category}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(targetCategory._id)
|
||||
|
||||
// Convert prices array to pricing format
|
||||
const pricing = prices.map(price => ({
|
||||
minQuantity: price.quantity,
|
||||
pricePerUnit: price.pricePerUnit
|
||||
}));
|
||||
|
||||
const standardizedUnitType = standardizeUnitType(prices[0].unit);
|
||||
|
||||
// Validate unit type
|
||||
if (!["pcs", "gr", "kg"].includes(standardizedUnitType)) {
|
||||
console.log(`Skipping product ${completedCount}/${products.length}: Invalid unit type "${prices[0].unit}"`);
|
||||
skippedProducts.push({ name, reason: `Invalid unit type: ${prices[0].unit}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const newProduct = new Product({
|
||||
storeId: req.user.storeId,
|
||||
name,
|
||||
category: targetCategory._id,
|
||||
unitType: standardizedUnitType,
|
||||
pricing
|
||||
});
|
||||
|
||||
try {
|
||||
await store.save();
|
||||
const savedProduct = await newProduct.save();
|
||||
processedProducts.push(savedProduct);
|
||||
console.log(`Processed ${completedCount}/${products.length}: Successfully added "${name}"`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to save product ${completedCount}/${products.length}: "${name}"`, error);
|
||||
skippedProducts.push({ name, reason: `Save error: ${error.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Batch processing completed. Processed: ${processedProducts.length}, Skipped: ${skippedProducts.length}`);
|
||||
|
||||
if (processedProducts.length === 0) {
|
||||
return res.status(400).json({
|
||||
message: "No valid products were processed",
|
||||
totalSubmitted: products.length,
|
||||
totalProcessed: 0,
|
||||
skippedProducts
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
message: "Products batch processed successfully",
|
||||
totalSubmitted: products.length,
|
||||
totalProcessed: processedProducts.length,
|
||||
totalSkipped: skippedProducts.length,
|
||||
products: processedProducts,
|
||||
skippedProducts,
|
||||
categories: store.categories
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Batch processing error:", error);
|
||||
res.status(500).json({
|
||||
message: "Failed to process product batch",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 📌 Disable stock tracking for all products (one-time setup route)
|
||||
router.post("/disable-stock-tracking", protectVendor, async (req, res) => {
|
||||
try {
|
||||
// Ensure the user has admin privileges or apply appropriate restrictions here
|
||||
if (!req.user.storeId) {
|
||||
return res.status(400).json({ message: "Store not found for this vendor" });
|
||||
}
|
||||
|
||||
// Update all products for this store to disable stock tracking
|
||||
const result = await Product.updateMany(
|
||||
{ storeId: req.user.storeId },
|
||||
{
|
||||
stockTracking: false,
|
||||
stockStatus: "in_stock" // Set a default status to avoid UI issues
|
||||
}
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Stock tracking disabled for all products",
|
||||
modifiedCount: result.modifiedCount,
|
||||
matchedCount: result.matchedCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error disabling stock tracking:", error);
|
||||
res.status(500).json({ message: "Failed to disable stock tracking", error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
24
backend/routes/promotion.routes.js
Normal file
24
backend/routes/promotion.routes.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import express from "express";
|
||||
import {
|
||||
getPromotions,
|
||||
getPromotionById,
|
||||
createPromotion,
|
||||
updatePromotion,
|
||||
deletePromotion,
|
||||
validatePromotion
|
||||
} from "../controllers/promotion.controller.js";
|
||||
import { protectVendor } from "../middleware/vendorAuthMiddleware.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Vendor routes for managing their own promotions (protected)
|
||||
router.get("/", protectVendor, getPromotions);
|
||||
router.get("/:id", protectVendor, getPromotionById);
|
||||
router.post("/", protectVendor, createPromotion);
|
||||
router.put("/:id", protectVendor, updatePromotion);
|
||||
router.delete("/:id", protectVendor, deletePromotion);
|
||||
|
||||
// Public route for validating a promotion code
|
||||
router.post("/validate/:storeId", validatePromotion);
|
||||
|
||||
export default router;
|
||||
151
backend/routes/shipping.routes.js
Normal file
151
backend/routes/shipping.routes.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import express from "express";
|
||||
import Store from "../models/Store.model.js";
|
||||
import { protectVendor } from "../middleware/authMiddleware.js"; // Protect the vendor
|
||||
import mongoose from "mongoose"; // Import mongoose to use ObjectId
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 📌 Get Shipping Options for Vendor's Store
|
||||
* @route GET /api/shipping-options
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.get("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.storeId) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "Store not found for this vendor" });
|
||||
}
|
||||
|
||||
const store = await Store.findOne({ vendorId: req.user._id }).select(
|
||||
"shippingOptions"
|
||||
);
|
||||
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found" });
|
||||
}
|
||||
|
||||
res.json(store.shippingOptions);
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to fetch shipping options", error });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Add New Shipping Option for Vendor's Store
|
||||
* @route POST /api/shipping-options
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.post("/", protectVendor, async (req, res) => {
|
||||
const { name, price } = req.body;
|
||||
|
||||
if (!name || !price) {
|
||||
return res.status(400).json({ message: "Missing required fields" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Find the store by vendorId (user)
|
||||
const store = await Store.findOne({ vendorId: req.user._id });
|
||||
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found" });
|
||||
}
|
||||
|
||||
// Add the new shipping option to the store's shippingOptions array
|
||||
store.shippingOptions.push({ name, price });
|
||||
|
||||
// Save the store with the new shipping option
|
||||
await store.save();
|
||||
|
||||
res.status(201).json(store.shippingOptions);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to add shipping option", error });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Delete Shipping Option for Vendor's Store
|
||||
* @route DELETE /api/shipping-options/:id
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.delete("/:id", protectVendor, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const store = await Store.findOne({ vendorId: req.user._id });
|
||||
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found" });
|
||||
}
|
||||
|
||||
// Use find to find the shipping option by its 'id' field
|
||||
const shippingOption = store.shippingOptions.find(
|
||||
(option) => option.id.toString() === id
|
||||
);
|
||||
|
||||
if (!shippingOption) {
|
||||
return res.status(404).json({ message: "Shipping option not found" });
|
||||
}
|
||||
|
||||
// Remove the shipping option
|
||||
const index = store.shippingOptions.indexOf(shippingOption);
|
||||
store.shippingOptions.splice(index, 1); // Remove the shipping option from the array
|
||||
|
||||
await store.save(); // Save the updated store
|
||||
|
||||
res.status(204).json({ message: "Shipping option deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to delete shipping option", error });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Edit Shipping Option for Vendor's Store
|
||||
* @route PUT /api/shipping-options/:id
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.put("/:id", protectVendor, async (req, res) => {
|
||||
const { name, price } = req.body;
|
||||
const { id } = req.params;
|
||||
|
||||
if (!name || !price) {
|
||||
return res.status(400).json({ message: "Missing required fields" });
|
||||
}
|
||||
|
||||
try {
|
||||
const store = await Store.findOne({ vendorId: req.user._id });
|
||||
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found" });
|
||||
}
|
||||
|
||||
// Use find to find the shipping option by its 'id' field
|
||||
const shippingOption = store.shippingOptions.find(
|
||||
(option) => option.id.toString() === id
|
||||
);
|
||||
|
||||
if (!shippingOption) {
|
||||
return res.status(404).json({ message: "Shipping option not found" });
|
||||
}
|
||||
|
||||
shippingOption.name = name; // Update the name
|
||||
shippingOption.price = price; // Update the price
|
||||
|
||||
await store.save(); // Save the updated store
|
||||
|
||||
res.json(shippingOption); // Return the updated shipping option
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to update shipping option", error });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
84
backend/routes/staffAuth.routes.js
Normal file
84
backend/routes/staffAuth.routes.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import express from "express";
|
||||
import bcrypt from "bcryptjs";
|
||||
import jwt from "jsonwebtoken";
|
||||
import Staff from "../models/Staff.model.js";
|
||||
import { protectStaff, logoutStaff } from "../middleware/staffAuthMiddleware.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 📌 Staff Login - Store JWT in Database
|
||||
*/
|
||||
router.post("/login", async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const staff = await Staff.findOne({ username });
|
||||
if (!staff) {
|
||||
return res.status(401).json({ error: "Staff user not found" });
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, staff.passwordHash);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Generate JWT for Staff/Admin
|
||||
const token = jwt.sign(
|
||||
{ id: staff._id, role: staff.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
|
||||
// Store token in database
|
||||
staff.currentToken = token;
|
||||
await staff.save();
|
||||
|
||||
res.json({ token, role: staff.role });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Staff Logout - Remove JWT from Database
|
||||
*/
|
||||
router.post("/logout", protectStaff, logoutStaff);
|
||||
|
||||
/**
|
||||
* 📌 Force Logout All Staff Users (Admin Only)
|
||||
*/
|
||||
router.post("/logout/all", protectStaff, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== "admin") {
|
||||
return res.status(403).json({ error: "Access restricted to admins only" });
|
||||
}
|
||||
|
||||
await Staff.updateMany({}, { currentToken: null });
|
||||
|
||||
res.json({ message: "All staff users have been logged out" });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to log out all staff users" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Check Staff Sessions (Admin Only)
|
||||
*/
|
||||
router.get("/sessions", protectStaff, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== "admin") {
|
||||
return res.status(403).json({ error: "Access restricted to admins only" });
|
||||
}
|
||||
|
||||
const activeSessions = await Staff.find({ currentToken: { $ne: null } })
|
||||
.select("username role currentToken createdAt");
|
||||
|
||||
res.json({ activeSessions });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to fetch active sessions" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
13
backend/routes/stock.routes.js
Normal file
13
backend/routes/stock.routes.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import express from "express";
|
||||
import { protectVendor } from "../middleware/authMiddleware.js";
|
||||
import { updateStock, getStoreStock } from "../controllers/stock.controller.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get all product stock information for a store
|
||||
router.get("/", protectVendor, getStoreStock);
|
||||
|
||||
// Update stock for a specific product
|
||||
router.put("/:productId", protectVendor, updateStock);
|
||||
|
||||
export default router;
|
||||
265
backend/routes/storefront.routes.js
Normal file
265
backend/routes/storefront.routes.js
Normal file
@@ -0,0 +1,265 @@
|
||||
import express from "express";
|
||||
import { protectVendor } from "../middleware/vendorAuthMiddleware.js";
|
||||
import Store from "../models/Store.model.js";
|
||||
import TelegramUser from "../models/TelegramUser.model.js";
|
||||
import { sendBulkTelegramMessages } from "../utils/telegramUtils.js";
|
||||
import multer from "multer";
|
||||
import sharp from "sharp";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get the current directory and set up a relative path for uploads
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const uploadsBaseDir = path.join(process.cwd(), 'uploads');
|
||||
const broadcastsDir = path.join(uploadsBaseDir, 'broadcasts');
|
||||
|
||||
console.log('Upload directory:', {
|
||||
path: broadcastsDir,
|
||||
exists: fs.existsSync(broadcastsDir),
|
||||
absolutePath: path.resolve(broadcastsDir)
|
||||
});
|
||||
|
||||
// Create both the base uploads directory and the broadcasts subdirectory
|
||||
if (!fs.existsSync(uploadsBaseDir)) {
|
||||
fs.mkdirSync(uploadsBaseDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(broadcastsDir)) {
|
||||
fs.mkdirSync(broadcastsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit for Telegram
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith("image/")) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error("Only image files are allowed!"), false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleMulterError = (err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === "LIMIT_FILE_SIZE") {
|
||||
return res.status(400).json({ error: "File size too large. Maximum allowed size is 10MB." });
|
||||
}
|
||||
return res.status(400).json({ error: "Upload error occurred.", details: err.message });
|
||||
}
|
||||
next(err);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Storefront Details
|
||||
* @route GET /api/storefront
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.get("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const storeId = req.user.storeId;
|
||||
const store = await Store.findById(storeId);
|
||||
|
||||
if (!store) {
|
||||
return res.status(404).json({ error: "Storefront not found" });
|
||||
}
|
||||
|
||||
return res.json(store);
|
||||
} catch (error) {
|
||||
console.error("Error fetching storefront:", error);
|
||||
return res.status(500).json({ error: "Failed to retrieve storefront" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update Storefront Settings
|
||||
* @route PUT /api/storefront
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.put("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const storeId = req.user.storeId;
|
||||
const { pgpKey, welcomeMessage, telegramToken, shipsFrom, shipsTo, wallets } = req.body;
|
||||
|
||||
const updatedStore = await Store.findByIdAndUpdate(
|
||||
storeId,
|
||||
{
|
||||
...(pgpKey !== undefined && { pgpKey }),
|
||||
...(welcomeMessage !== undefined && { welcomeMessage }),
|
||||
...(telegramToken !== undefined && { telegramToken }),
|
||||
...(shipsFrom !== undefined && { shipsFrom }),
|
||||
...(shipsTo !== undefined && { shipsTo }),
|
||||
...(wallets !== undefined && { wallets })
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedStore) {
|
||||
return res.status(404).json({ error: "Storefront not found" });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: "Storefront updated successfully",
|
||||
store: updatedStore,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating storefront:", error);
|
||||
return res.status(500).json({ error: "Failed to update storefront" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Broadcast a Message
|
||||
* @route POST /api/storefront/broadcast
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.post("/broadcast", protectVendor, upload.single("file"), handleMulterError, async (req, res) => {
|
||||
let photoPath = null;
|
||||
try {
|
||||
const storeId = req.user.storeId;
|
||||
const { message } = req.body;
|
||||
|
||||
// Handle image upload if present
|
||||
if (req.file) {
|
||||
const outputFileName = `${Date.now()}-${req.file.originalname.split('.').slice(0, -1).join('.')}.jpg`;
|
||||
const outputFilePath = path.join(broadcastsDir, outputFileName);
|
||||
|
||||
// Ensure upload directory exists
|
||||
if (!fs.existsSync(broadcastsDir)) {
|
||||
fs.mkdirSync(broadcastsDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log('Processing upload:', {
|
||||
fileName: outputFileName,
|
||||
filePath: outputFilePath,
|
||||
absolutePath: path.resolve(outputFilePath),
|
||||
uploadDirExists: fs.existsSync(broadcastsDir),
|
||||
uploadDirContents: fs.readdirSync(broadcastsDir)
|
||||
});
|
||||
|
||||
try {
|
||||
// Process and save the image
|
||||
await sharp(req.file.buffer)
|
||||
.resize(1280, 1280, { fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 80 })
|
||||
.toFile(outputFilePath);
|
||||
|
||||
// Verify file was saved and get stats
|
||||
await new Promise((resolve, reject) => {
|
||||
fs.stat(outputFilePath, (err, stats) => {
|
||||
if (err) {
|
||||
console.error('Error verifying file:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('File processed and verified:', {
|
||||
path: outputFilePath,
|
||||
size: stats.size,
|
||||
exists: fs.existsSync(outputFilePath),
|
||||
stats: stats
|
||||
});
|
||||
resolve(stats);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Additional verification
|
||||
if (!fs.existsSync(outputFilePath)) {
|
||||
throw new Error('File was not saved successfully');
|
||||
}
|
||||
|
||||
const stats = fs.statSync(outputFilePath);
|
||||
if (stats.size > 10 * 1024 * 1024) {
|
||||
fs.unlinkSync(outputFilePath);
|
||||
return res.status(400).json({ error: "File size too large after processing." });
|
||||
}
|
||||
|
||||
// Set the photo path for sending
|
||||
photoPath = outputFilePath;
|
||||
} catch (err) {
|
||||
console.error("Error processing image:", {
|
||||
error: err,
|
||||
outputFilePath,
|
||||
exists: fs.existsSync(outputFilePath)
|
||||
});
|
||||
|
||||
// Cleanup if file exists
|
||||
if (outputFilePath && fs.existsSync(outputFilePath)) {
|
||||
fs.unlinkSync(outputFilePath);
|
||||
}
|
||||
|
||||
return res.status(500).json({ error: "Failed to process image file." });
|
||||
}
|
||||
} else if (!message || !message.trim()) {
|
||||
return res.status(400).json({ error: "Either message or image must be provided" });
|
||||
}
|
||||
|
||||
const users = await TelegramUser.find({ "stores.store": storeId });
|
||||
|
||||
if (!users.length) {
|
||||
return res.status(404).json({ error: "No users found to broadcast to." });
|
||||
}
|
||||
|
||||
const store = await Store.findById(storeId);
|
||||
if (!store || !store.telegramToken) {
|
||||
return res.status(400).json({ error: "Store has no Telegram bot token configured." });
|
||||
}
|
||||
|
||||
// Flatten out each store entry to gather all chat IDs belonging to this store
|
||||
const chatIds = users.flatMap((user) =>
|
||||
user.stores
|
||||
.filter((s) => s.store.toString() === storeId.toString())
|
||||
.map((s) => s.chatId)
|
||||
);
|
||||
|
||||
console.log("Broadcasting to chat IDs:", chatIds);
|
||||
|
||||
try {
|
||||
if (photoPath) {
|
||||
console.log('Sending photo broadcast:', {
|
||||
photoPath,
|
||||
exists: fs.existsSync(photoPath),
|
||||
stats: fs.existsSync(photoPath) ? fs.statSync(photoPath) : null
|
||||
});
|
||||
|
||||
await sendBulkTelegramMessages(store.telegramToken, chatIds, message, photoPath);
|
||||
} else {
|
||||
await sendBulkTelegramMessages(store.telegramToken, chatIds, message);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: "Broadcast sent successfully",
|
||||
totalUsers: chatIds.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending broadcast:", {
|
||||
error: error.message,
|
||||
photoPath,
|
||||
messageLength: message?.length,
|
||||
fileExists: photoPath ? fs.existsSync(photoPath) : null
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error broadcasting message:", error);
|
||||
return res.status(500).json({
|
||||
error: "Failed to broadcast message",
|
||||
details: error.message,
|
||||
});
|
||||
} finally {
|
||||
// Clean up the file after everything is done
|
||||
if (photoPath && fs.existsSync(photoPath)) {
|
||||
try {
|
||||
fs.unlinkSync(photoPath);
|
||||
} catch (err) {
|
||||
console.error("Error cleaning up file:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user