import type { Express } from "express"; import { createServer, type Server } from "http"; import { storage } from "./storage"; import { authenticateToken, requireRole, optionalAuth, generateToken, type AuthRequest } from "./middleware/auth"; import { upload } from "./middleware/multer"; import bcrypt from "bcrypt"; import express from "express"; import path from "path"; import { insertUserSchema, insertSellerSchema, insertCategorySchema, insertStoreSchema, insertProductSchema, insertOrderSchema, insertCartItemSchema, type Order } from "@shared/schema"; import { z } from "zod"; import { addPlaceholderImages } from "./add-placeholder-images"; export async function registerRoutes(app: Express): Promise { // Serve uploaded files app.use('/uploads', express.static(path.join(process.cwd(), 'uploads'))); // Serve attached assets app.use('/attached_assets', express.static(path.join(process.cwd(), 'attached_assets'))); // Auth Routes app.post("/api/auth/register", async (req, res) => { try { const userData = insertUserSchema.parse(req.body); // Check if user already exists const existingUser = await storage.getUserByEmail(userData.email) || await storage.getUserByUsername(userData.username); if (existingUser) { return res.status(400).json({ message: "User already exists" }); } // Hash password const hashedPassword = await bcrypt.hash(userData.password, 10); // Convert latitude/longitude numbers to strings for database insertion const processedUserData = { ...userData, password: hashedPassword, // Convert numbers to strings for decimal database fields latitude: userData.latitude ? userData.latitude.toString() : undefined, longitude: userData.longitude ? userData.longitude.toString() : undefined, }; const user = await storage.createUser(processedUserData); const token = generateToken({ id: user.id, type: 'user', username: user.username }); res.json({ token, user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName } }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } console.error("Registration error:", error); res.status(500).json({ message: "Registration failed" }); } }); app.post("/api/auth/login", async (req, res) => { try { const { email, password } = req.body; const user = await storage.getUserByEmail(email); if (!user || !await bcrypt.compare(password, user.password)) { return res.status(401).json({ message: "Invalid credentials" }); } const token = generateToken({ id: user.id, type: 'user', username: user.username }); res.json({ token, user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName } }); } catch (error) { res.status(500).json({ message: "Login failed" }); } }); app.post("/api/auth/seller-login", async (req, res) => { try { const { username, password } = req.body; const seller = await storage.getSellerByUsername(username); if (!seller || !await bcrypt.compare(password, seller.password)) { return res.status(401).json({ message: "Invalid credentials" }); } const token = generateToken({ id: seller.id, type: 'seller', username: seller.username }); res.json({ token, seller: { id: seller.id, username: seller.username } }); } catch (error) { res.status(500).json({ message: "Login failed" }); } }); // User Routes app.get("/api/user/profile", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const user = await storage.getUser(req.user!.id); if (!user) { return res.status(404).json({ message: "User not found" }); } const { password, ...userWithoutPassword } = user; res.json(userWithoutPassword); } catch (error) { res.status(500).json({ message: "Failed to fetch profile" }); } }); app.put("/api/user/profile", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const updateData = req.body; // Remove password from update data if present if (updateData.password) { delete updateData.password; } // Remove id from update data if present if (updateData.id) { delete updateData.id; } const updatedUser = await storage.updateUser(req.user!.id, updateData); const { password, ...userWithoutPassword } = updatedUser; res.json(userWithoutPassword); } catch (error) { res.status(500).json({ message: "Failed to update profile" }); } }); // Categories Routes app.get("/api/categories", async (req, res) => { try { const categories = await storage.getAllCategories(); res.json(categories); } catch (error) { res.status(500).json({ message: "Failed to fetch categories" }); } }); app.post("/api/categories", async (req, res) => { try { const categoryData = insertCategorySchema.parse(req.body); const category = await storage.createCategory(categoryData); res.json(category); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } res.status(500).json({ message: "Failed to create category" }); } }); app.put("/api/categories/:id", async (req, res) => { try { const categoryData = insertCategorySchema.partial().parse(req.body); const category = await storage.updateCategory(req.params.id, categoryData); res.json(category); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } res.status(500).json({ message: "Failed to update category" }); } }); app.delete("/api/categories/:id", async (req, res) => { try { await storage.deleteCategory(req.params.id); res.json({ message: "Category deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete category" }); } }); // Products Routes app.get("/api/products", optionalAuth, async (req: AuthRequest, res) => { try { const { category, search, seller } = req.query; let products; if (search) { products = await storage.searchProducts(search as string); } else if (category) { products = await storage.getProductsByCategory(category as string); } else if (seller) { products = await storage.getProductsBySeller(seller as string); } else { products = await storage.getAllProducts(); } // Add seller and category information const enrichedProducts = await Promise.all(products.map(async (product) => { const seller = await storage.getSeller(product.sellerId); const store = seller ? await storage.getStoreBysellerId(seller.id) : null; const category = await storage.getCategory(product.categoryId); return { ...product, seller: seller ? { id: seller.id, username: seller.username } : null, store: store ? { name: store.name, faceImage: store.faceImage } : null, category: category ? { id: category.id, name: category.name } : null, }; })); res.json(enrichedProducts); } catch (error) { res.status(500).json({ message: "Failed to fetch products" }); } }); app.get("/api/products/featured", async (req, res) => { try { let products = await storage.getFeaturedProductsByCategories(); // Check if we need to create more products for categories with less than 5 const categories = await storage.getAllCategories(); const sellers = await storage.getAllSellers(); if (sellers.length === 0) { return res.json([]); } for (const category of categories) { const categoryProducts = products.filter(p => p.categoryId === category.id); const needed = 5 - categoryProducts.length; if (needed > 0) { // Create missing products with random images for (let i = 0; i < needed; i++) { const randomSeller = sellers[Math.floor(Math.random() * sellers.length)]; const productNumber = categoryProducts.length + i + 1; const newProduct = await storage.createProduct({ title: `${category.name} Product ${productNumber}`, description: `High-quality ${category.name.toLowerCase()} product featuring premium materials and exceptional craftsmanship. Perfect for everyday use with modern design aesthetics.`, price: (Math.floor(Math.random() * 500) + 100).toString(), // Random price between 100-600 stock: Math.floor(Math.random() * 50) + 10, // Random stock between 10-60 images: [ `https://picsum.photos/400/400?random=${Date.now()}&${i}`, `https://picsum.photos/400/400?random=${Date.now()}&${i}&sig=2`, `https://picsum.photos/400/400?random=${Date.now()}&${i}&sig=3` ], categoryId: category.id, sellerId: randomSeller.id, isActive: true }); products.push(newProduct); } } } // Add seller and category information const enrichedProducts = await Promise.all(products.map(async (product) => { const seller = await storage.getSeller(product.sellerId); const store = seller ? await storage.getStoreBysellerId(seller.id) : null; const category = await storage.getCategory(product.categoryId); return { ...product, seller: seller ? { id: seller.id, username: seller.username } : null, store: store ? { name: store.name, faceImage: store.faceImage } : null, category: category ? { id: category.id, name: category.name } : null, }; })); res.json(enrichedProducts); } catch (error) { console.error('Featured products error:', error); res.status(500).json({ message: "Failed to fetch featured products" }); } }); app.get("/api/products/:id", async (req, res) => { try { const product = await storage.getProduct(req.params.id); if (!product) { return res.status(404).json({ message: "Product not found" }); } const seller = await storage.getSeller(product.sellerId); const store = seller ? await storage.getStoreBysellerId(seller.id) : null; const category = await storage.getCategory(product.categoryId); const enrichedProduct = { ...product, seller: seller ? { id: seller.id, username: seller.username } : null, store: store ? { id: store.id, name: store.name, description: store.description, faceImage: store.faceImage, bannerImage: store.bannerImage } : null, category: category ? { id: category.id, name: category.name } : null, }; res.json(enrichedProduct); } catch (error) { res.status(500).json({ message: "Failed to fetch product" }); } }); app.post("/api/products", authenticateToken, requireRole(['seller']), upload.array('images', 5), async (req: AuthRequest, res) => { try { // Check if seller has a store first const store = await storage.getStoreBysellerId(req.user!.id); if (!store) { return res.status(400).json({ message: "You must create a store before adding products" }); } // Convert form data strings to appropriate types const formData = { ...req.body }; // Keep price and originalPrice as strings (decimal fields) // Only convert stock to integer if (formData.stock) formData.stock = parseInt(formData.stock, 10); const productData = insertProductSchema.parse({ ...formData, sellerId: req.user!.id, images: req.files ? (req.files as Express.Multer.File[]).map(file => `/uploads/${file.filename}`) : [], }); const product = await storage.createProduct(productData); res.json(product); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } res.status(500).json({ message: "Failed to create product" }); } }); app.put("/api/products/:id", authenticateToken, requireRole(['seller']), upload.array('images', 5), async (req: AuthRequest, res) => { try { const product = await storage.getProduct(req.params.id); if (!product) { return res.status(404).json({ message: "Product not found" }); } if (product.sellerId !== req.user!.id) { return res.status(403).json({ message: "You can only update your own products" }); } const updateData = { ...req.body }; // Convert form data strings to appropriate types // Keep price and originalPrice as strings (decimal fields) // Only convert stock to integer if (updateData.stock) updateData.stock = parseInt(updateData.stock, 10); if (req.files && (req.files as Express.Multer.File[]).length > 0) { updateData.images = (req.files as Express.Multer.File[]).map(file => `/uploads/${file.filename}`); } const updatedProduct = await storage.updateProduct(req.params.id, updateData); res.json(updatedProduct); } catch (error) { res.status(500).json({ message: "Failed to update product" }); } }); app.delete("/api/products/:id", authenticateToken, requireRole(['seller']), async (req: AuthRequest, res) => { try { const product = await storage.getProduct(req.params.id); if (!product) { return res.status(404).json({ message: "Product not found" }); } if (product.sellerId !== req.user!.id) { return res.status(403).json({ message: "You can only delete your own products" }); } await storage.deleteProduct(req.params.id); res.json({ message: "Product deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete product" }); } }); // Seller Orders Route app.get("/api/seller/orders", authenticateToken, requireRole(['seller']), async (req: AuthRequest, res) => { try { // Get all orders that contain products from this seller const sellerProducts = await storage.getProductsBySeller(req.user!.id); const sellerProductIds = sellerProducts.map(p => p.id); if (sellerProductIds.length === 0) { return res.json([]); } const orders = await storage.getOrdersForSeller(sellerProductIds); // Enrich orders with items (only seller's items) and user info const enrichedOrders = await Promise.all(orders.map(async (order: Order) => { const allItems = await storage.getOrderItems(order.id); const sellerItems = allItems.filter(item => sellerProductIds.includes(item.productId)); const enrichedItems = await Promise.all(sellerItems.map(async (item) => { const product = await storage.getProduct(item.productId); return { ...item, product }; })); // Get user details for this order const user = await storage.getUser(order.userId); return { ...order, items: enrichedItems, user: user ? { firstName: user.firstName, lastName: user.lastName, email: user.email, phone: user.phone } : null }; })); res.json(enrichedOrders); } catch (error) { res.status(500).json({ message: "Failed to fetch seller orders" }); } }); // Update order status by seller app.patch("/api/seller/orders/:id", authenticateToken, requireRole(['seller']), async (req: AuthRequest, res) => { try { const orderId = req.params.id; const { status, paymentStatus } = req.body; // Verify the order contains the seller's products const sellerProducts = await storage.getProductsBySeller(req.user!.id); const sellerProductIds = sellerProducts.map(p => p.id); const orderItems = await storage.getOrderItems(orderId); const hasSellerItems = orderItems.some(item => sellerProductIds.includes(item.productId)); if (!hasSellerItems) { return res.status(403).json({ message: "You can only update orders containing your products" }); } const updates: { status?: string; paymentStatus?: string } = {}; if (status) updates.status = status; if (paymentStatus) updates.paymentStatus = paymentStatus; const updatedOrder = await storage.updateOrderStatusAndPayment(orderId, updates); res.json(updatedOrder); } catch (error) { res.status(500).json({ message: "Failed to update order status" }); } }); // Stores Routes app.get("/api/stores", async (req, res) => { try { const stores = await storage.getAllStores(); // Enrich stores with seller information const enrichedStores = await Promise.all(stores.map(async (store) => { const seller = await storage.getSeller(store.sellerId); return { ...store, seller: seller ? { id: seller.id, username: seller.username } : null, }; })); res.json(enrichedStores); } catch (error) { res.status(500).json({ message: "Failed to fetch stores" }); } }); app.get("/api/stores/:id", async (req, res) => { try { const store = await storage.getStore(req.params.id); if (!store) { return res.status(404).json({ message: "Store not found" }); } const seller = await storage.getSeller(store.sellerId); const products = await storage.getProductsBySeller(store.sellerId); const enrichedProducts = await Promise.all(products.map(async (product) => { const category = await storage.getCategory(product.categoryId); return { ...product, category: category ? { id: category.id, name: category.name } : null, }; })); res.json({ ...store, seller: seller ? { id: seller.id, username: seller.username } : null, products: enrichedProducts }); } catch (error) { res.status(500).json({ message: "Failed to fetch store" }); } }); app.post("/api/stores", authenticateToken, requireRole(['seller']), upload.fields([ { name: 'bannerImage', maxCount: 1 }, { name: 'faceImage', maxCount: 1 } ]), async (req: AuthRequest, res) => { try { // Check if seller already has a store const existingStore = await storage.getStoreBysellerId(req.user!.id); if (existingStore) { return res.status(400).json({ message: "You can only create one store per seller account" }); } const files = req.files as { [fieldname: string]: Express.Multer.File[] } || {}; const storeData = insertStoreSchema.parse({ ...req.body, sellerId: req.user!.id, bannerImage: files.bannerImage ? `/uploads/${files.bannerImage[0].filename}` : null, faceImage: files.faceImage ? `/uploads/${files.faceImage[0].filename}` : null, }); const store = await storage.createStore(storeData); res.json(store); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } res.status(500).json({ message: "Failed to create store" }); } }); app.put("/api/stores/:id", authenticateToken, requireRole(['seller']), upload.fields([ { name: 'bannerImage', maxCount: 1 }, { name: 'faceImage', maxCount: 1 } ]), async (req: AuthRequest, res) => { try { const store = await storage.getStore(req.params.id); if (!store) { return res.status(404).json({ message: "Store not found" }); } if (store.sellerId !== req.user!.id) { return res.status(403).json({ message: "You can only update your own store" }); } const files = req.files as { [fieldname: string]: Express.Multer.File[] }; const updateData = { ...req.body }; if (files.bannerImage) { updateData.bannerImage = `/uploads/${files.bannerImage[0].filename}`; } if (files.faceImage) { updateData.faceImage = `/uploads/${files.faceImage[0].filename}`; } const updatedStore = await storage.updateStore(req.params.id, updateData); res.json(updatedStore); } catch (error) { res.status(500).json({ message: "Failed to update store" }); } }); // Cart Routes app.get("/api/cart", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const cartItems = await storage.getCartItems(req.user!.id); // Enrich cart items with product details const enrichedItems = await Promise.all(cartItems.map(async (item) => { const product = await storage.getProduct(item.productId); const seller = product ? await storage.getSeller(product.sellerId) : null; const store = seller ? await storage.getStoreBysellerId(seller.id) : null; return { ...item, product: product ? { ...product, seller: seller ? { id: seller.id, username: seller.username } : null, store: store ? { name: store.name } : null, } : null, }; })); res.json(enrichedItems.filter(item => item.product !== null)); } catch (error) { res.status(500).json({ message: "Failed to fetch cart" }); } }); app.post("/api/cart", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const cartItemData = insertCartItemSchema.parse({ ...req.body, userId: req.user!.id, }); const cartItem = await storage.addToCart(cartItemData); res.json(cartItem); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } res.status(500).json({ message: "Failed to add to cart" }); } }); app.put("/api/cart/:id", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const { quantity } = req.body; const cartItem = await storage.updateCartItem(req.params.id, quantity); res.json(cartItem); } catch (error) { res.status(500).json({ message: "Failed to update cart item" }); } }); app.delete("/api/cart/:id", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { await storage.removeFromCart(req.params.id); res.json({ message: "Item removed from cart" }); } catch (error) { res.status(500).json({ message: "Failed to remove from cart" }); } }); // Orders Routes app.get("/api/orders", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const orders = await storage.getOrdersByUser(req.user!.id); // Enrich orders with items const enrichedOrders = await Promise.all(orders.map(async (order) => { const items = await storage.getOrderItems(order.id); const enrichedItems = await Promise.all(items.map(async (item) => { const product = await storage.getProduct(item.productId); return { ...item, product }; })); return { ...order, items: enrichedItems }; })); res.json(enrichedOrders); } catch (error) { res.status(500).json({ message: "Failed to fetch orders" }); } }); app.post("/api/orders", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const orderData = insertOrderSchema.parse({ ...req.body, userId: req.user!.id, }); const order = await storage.createOrder(orderData); // Create order items from cart const cartItems = await storage.getCartItems(req.user!.id); await Promise.all(cartItems.map(async (cartItem) => { await storage.createOrderItem({ orderId: order.id, productId: cartItem.productId, quantity: cartItem.quantity, price: req.body.items.find((item: any) => item.productId === cartItem.productId)?.price || "0", }); })); // Clear cart after order await storage.clearCart(req.user!.id); res.json(order); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } res.status(500).json({ message: "Failed to create order" }); } }); // Cancel order endpoint app.patch("/api/orders/:id/cancel", authenticateToken, requireRole(['user']), async (req: AuthRequest, res) => { try { const orderId = req.params.id; // Get the order to verify ownership and check if it can be cancelled const order = await storage.getOrder(orderId); if (!order) { return res.status(404).json({ message: "Order not found" }); } // Verify the order belongs to the authenticated user if (order.userId !== req.user!.id) { return res.status(403).json({ message: "You can only cancel your own orders" }); } // Check if the order can be cancelled (only pending orders can be cancelled) if (order.status !== 'pending') { return res.status(400).json({ message: "Order cannot be cancelled. Only pending orders can be cancelled." }); } // Update order status to cancelled const updatedOrder = await storage.updateOrderStatus(orderId, 'cancelled'); res.json(updatedOrder); } catch (error) { res.status(500).json({ message: "Failed to cancel order" }); } }); // Admin Dev Routes (no auth required as per requirements) app.get("/api/admin/sellers", async (req, res) => { try { const sellers = await storage.getAllSellers(); res.json(sellers); } catch (error) { res.status(500).json({ message: "Failed to fetch sellers" }); } }); app.post("/api/admin/sellers", async (req, res) => { try { const sellerData = insertSellerSchema.parse(req.body); // Check if seller already exists const existingSeller = await storage.getSellerByUsername(sellerData.username); if (existingSeller) { return res.status(400).json({ message: "Seller already exists" }); } // Hash password const hashedPassword = await bcrypt.hash(sellerData.password, 10); const seller = await storage.createSeller({ ...sellerData, password: hashedPassword, plainTextPassword: sellerData.password, // Store for admin access recovery }); res.json(seller); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid input data", errors: error.errors }); } res.status(500).json({ message: "Failed to create seller" }); } }); app.get("/api/admin/users", async (req, res) => { try { const users = await storage.getAllUsers(); res.json(users); } catch (error) { res.status(500).json({ message: "Failed to fetch users" }); } }); app.delete("/api/admin/sellers/:id", async (req, res) => { try { // First delete all products by this seller const products = await storage.getProductsBySeller(req.params.id); for (const product of products) { await storage.deleteProduct(product.id); } // Delete all stores by this seller const stores = await storage.getStoresBySeller(req.params.id); for (const store of stores) { await storage.deleteStore(store.id); } // Finally delete the seller await storage.deleteSeller(req.params.id); res.json({ message: "Seller and all associated data deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete seller" }); } }); // Admin product management app.delete("/api/admin/products/:id", async (req, res) => { try { await storage.deleteProduct(req.params.id); res.json({ message: "Product deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete product" }); } }); app.put("/api/admin/products/:id", async (req, res) => { try { const updatedProduct = await storage.updateProduct(req.params.id, req.body); res.json(updatedProduct); } catch (error) { res.status(500).json({ message: "Failed to update product" }); } }); // Admin Orders Route app.get("/api/admin/orders", async (req, res) => { try { // Get all orders for admin const orders = await storage.getAllOrders(); // Enrich orders with items and user info const enrichedOrders = await Promise.all(orders.map(async (order: Order) => { const items = await storage.getOrderItems(order.id); const enrichedItems = await Promise.all(items.map(async (item) => { const product = await storage.getProduct(item.productId); return { ...item, product }; })); // Get user details for this order const user = await storage.getUser(order.userId); return { ...order, items: enrichedItems, user: user ? { firstName: user.firstName, lastName: user.lastName, email: user.email, phone: user.phone } : null }; })); res.json(enrichedOrders); } catch (error) { res.status(500).json({ message: "Failed to fetch admin orders" }); } }); // Update order status by admin app.patch("/api/admin/orders/:id", async (req, res) => { try { const orderId = req.params.id; const { status, paymentStatus } = req.body; const updates: { status?: string; paymentStatus?: string } = {}; if (status) updates.status = status; if (paymentStatus) updates.paymentStatus = paymentStatus; const updatedOrder = await storage.updateOrderStatusAndPayment(orderId, updates); res.json(updatedOrder); } catch (error) { res.status(500).json({ message: "Failed to update order status" }); } }); // Seller Dashboard Routes app.get("/api/seller/store", authenticateToken, requireRole(['seller']), async (req: AuthRequest, res) => { try { const store = await storage.getStoreBysellerId(req.user!.id); res.json(store); } catch (error) { res.status(500).json({ message: "Failed to fetch store" }); } }); app.get("/api/seller/products", authenticateToken, requireRole(['seller']), async (req: AuthRequest, res) => { try { const products = await storage.getProductsBySeller(req.user!.id); // Add category information const enrichedProducts = await Promise.all(products.map(async (product) => { const category = await storage.getCategory(product.categoryId); return { ...product, category: category ? { id: category.id, name: category.name } : null, }; })); res.json(enrichedProducts); } catch (error) { res.status(500).json({ message: "Failed to fetch products" }); } }); const httpServer = createServer(app); return httpServer; }