Spaces:
Paused
Paused
| const express = require('express'); | |
| const router = express.Router(); | |
| const Product = require('../models/Product'); | |
| const Order = require('../models/Order'); | |
| const auth = require('./middleware'); | |
| // Public Health Check & Config | |
| router.get('/status', (req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| bot: 'active', | |
| time: new Date(), | |
| apk_url: `${req.protocol}://${req.get('host')}/public/app.apk` // Dynamic APK Link | |
| }); | |
| }); | |
| // --- IMAGE PROXY (To show Telegram images in App) --- | |
| const axios = require('axios'); | |
| const config = require('../config'); // Need BOT_TOKEN | |
| const telegramAgent = require('../utils/telegramAgent'); | |
| router.get('/image/:fileId', async (req, res) => { | |
| try { | |
| const fileId = req.params.fileId; | |
| // 1. Get File Path from Telegram | |
| const fileRes = await axios.get(`https://api.telegram.org/bot${config.BOT_TOKEN}/getFile?file_id=${fileId}`, { | |
| httpsAgent: telegramAgent | |
| }); | |
| const filePath = fileRes.data.result.file_path; | |
| // 2. Stream File to Client | |
| const imageUrl = `https://api.telegram.org/file/bot${config.BOT_TOKEN}/${filePath}`; | |
| const response = await axios({ | |
| url: imageUrl, | |
| method: 'GET', | |
| responseType: 'stream', | |
| httpsAgent: telegramAgent | |
| }); | |
| response.data.pipe(res); | |
| } catch (e) { | |
| console.error("Image Proxy Error:", e.message); | |
| res.status(404).send('Image not found'); | |
| } | |
| }); | |
| // --- PROTECTED ROUTES --- | |
| const Category = require('../models/Category'); | |
| const User = require('../models/User'); | |
| const Banner = require('../models/Banner'); | |
| // --- PROTECTED ROUTES --- | |
| router.use(auth); | |
| // --- AUTH ROUTES (Phase 13) --- | |
| const LoginRequest = require('../models/LoginRequest'); | |
| // Check Login Status (Polled by App) | |
| router.get('/auth/check', async (req, res) => { | |
| try { | |
| const { token } = req.query; | |
| if (!token) return res.status(400).json({ error: 'Token required' }); | |
| const request = await LoginRequest.findOne({ token }); | |
| if (request && request.status === 'approved') { | |
| // Find real user or create/update? | |
| // User checks if user exists in User model: | |
| let dbUser = await User.findOne({ id: request.userId }); | |
| res.json({ | |
| success: true, | |
| status: 'approved', | |
| user: { | |
| id: request.userId, | |
| name: request.firstName, | |
| username: request.username, | |
| language: dbUser ? dbUser.language : null | |
| } | |
| }); | |
| // Ideally delete request after success, but keep for a bit to avoid race conditions or use TTL | |
| // await LoginRequest.deleteOne({ token }); | |
| } else { | |
| res.json({ success: false, status: 'pending' }); | |
| } | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // --- FILE UPLOAD (New) --- | |
| const fs = require('fs'); | |
| // Ensure uploads directory exists | |
| const uploadDir = 'uploads/'; | |
| if (!fs.existsSync(uploadDir)) { | |
| fs.mkdirSync(uploadDir); | |
| } | |
| const multer = require('multer'); | |
| const upload = multer({ dest: uploadDir }); | |
| const FormData = require('form-data'); | |
| router.post('/upload', upload.single('image'), async (req, res) => { | |
| try { | |
| if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); | |
| // Upload to Telegram to get file_id | |
| const form = new FormData(); | |
| form.append('chat_id', config.ADMIN_IDS[0] || 12345); // Send to admin or dummy chat | |
| form.append('document', fs.createReadStream(req.file.path)); | |
| // Use a method that returns file_id. sendDocument is good. | |
| // NOTE: You need a chat_id where the bot can send messages. | |
| // We will try sending to the first ADMIN_ID. | |
| const tgRes = await axios.post( | |
| `https://api.telegram.org/bot${config.BOT_TOKEN}/sendDocument`, | |
| form, | |
| { | |
| headers: form.getHeaders(), | |
| httpsAgent: telegramAgent | |
| } | |
| ); | |
| // Cleanup local file | |
| fs.unlinkSync(req.file.path); | |
| if (tgRes.data && tgRes.data.ok) { | |
| const fileId = tgRes.data.result.document.file_id; | |
| res.json({ success: true, file_id: fileId }); | |
| } else { | |
| res.status(500).json({ error: 'Telegram upload failed' }); | |
| } | |
| } catch (e) { | |
| if (req.file) fs.unlinkSync(req.file.path); // Cleanup on error | |
| console.error("Upload Error:", e.message); | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // --- BANNER ROUTES (New Phase 12) --- | |
| // Get Banners | |
| router.get('/banners', async (req, res) => { | |
| try { | |
| const banners = await Banner.find().sort({ createdAt: -1 }); | |
| res.json({ success: true, count: banners.length, data: banners }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Add Banner (Admin) | |
| router.post('/admin/banners', async (req, res) => { | |
| try { | |
| const { title, image_url, file_id, color } = req.body; | |
| const newBanner = new Banner({ | |
| id: Date.now().toString(), | |
| title, | |
| image_url, | |
| file_id, | |
| color | |
| }); | |
| await newBanner.save(); | |
| res.json({ success: true, data: newBanner }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Delete Banner (Admin) | |
| router.delete('/admin/banners/:id', async (req, res) => { | |
| try { | |
| await Banner.findOneAndDelete({ id: req.params.id }); | |
| res.json({ success: true, message: 'Banner deleted' }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Get Categories (For App Sidebar) | |
| router.get('/categories', async (req, res) => { | |
| try { | |
| const categories = await Category.find(); | |
| res.json({ success: true, count: categories.length, data: categories }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Admin Stats (For App Admin Dashboard) | |
| router.get('/admin/stats', async (req, res) => { | |
| try { | |
| const userCount = await User.countDocuments(); | |
| const productCount = await Product.countDocuments(); | |
| const orderCount = await Order.countDocuments(); | |
| // Calculate Revenue & Chart Data | |
| const orders = await Order.find({ status: 'completed' }); | |
| const revenue = orders.reduce((sum, o) => sum + o.total, 0); | |
| // Chart Data (Last 7 Days) | |
| const chartData = {}; | |
| const today = new Date(); | |
| for (let i = 6; i >= 0; i--) { | |
| const d = new Date(today); | |
| d.setDate(today.getDate() - i); | |
| const dateStr = d.toISOString().split('T')[0]; | |
| chartData[dateStr] = 0; | |
| } | |
| orders.forEach(order => { | |
| // Assuming order.createdAt is a Date object or string | |
| const dateStr = new Date(order.createdAt).toISOString().split('T')[0]; | |
| if (chartData[dateStr] !== undefined) { | |
| chartData[dateStr] += order.total; | |
| } | |
| }); | |
| // Convert to array for Flutter | |
| const chartArray = Object.keys(chartData).map(date => ({ | |
| date, | |
| amount: chartData[date] | |
| })); | |
| res.json({ | |
| success: true, | |
| data: { | |
| users: userCount, | |
| products: productCount, | |
| orders: orderCount, | |
| revenue: revenue, | |
| chart: chartArray | |
| } | |
| }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Get All Products (with Filter, Search & Sort) | |
| router.get('/products', async (req, res) => { | |
| try { | |
| const query = {}; | |
| // Category Filter | |
| if (req.query.category && req.query.category !== 'Hammasi') { | |
| query.category = req.query.category; | |
| } | |
| // Search Filter | |
| if (req.query.search) { | |
| query.name = { $regex: req.query.search, $options: 'i' }; | |
| } | |
| // Price Range Filter | |
| if (req.query.minPrice || req.query.maxPrice) { | |
| query.price = {}; | |
| if (req.query.minPrice) query.price.$gte = Number(req.query.minPrice); | |
| if (req.query.maxPrice) query.price.$lte = Number(req.query.maxPrice); | |
| } | |
| let sortOption = {}; | |
| if (req.query.sort) { | |
| switch (req.query.sort) { | |
| case 'price_asc': sortOption = { price: 1 }; break; | |
| case 'price_desc': sortOption = { price: -1 }; break; | |
| case 'rating': sortOption = { 'reviews.rating': -1 }; break; // Simple sort, ideally avg | |
| default: sortOption = { createdAt: -1 }; | |
| } | |
| } | |
| const products = await Product.find(query).sort(sortOption); | |
| res.json({ success: true, count: products.length, data: products }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Add Review to Product | |
| router.post('/products/:id/reviews', async (req, res) => { | |
| try { | |
| const { userId, userName, rating, comment } = req.body; | |
| const product = await Product.findOne({ id: req.params.id }); | |
| if (!product) return res.status(404).json({ error: 'Product not found' }); | |
| const review = { | |
| userId: userId || 'anon', | |
| userName: userName || 'Anonim', | |
| rating: Number(rating), | |
| comment, | |
| date: new Date() | |
| }; | |
| product.reviews.push(review); | |
| await product.save(); | |
| res.json({ success: true, message: 'Review added', data: product }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Get User Orders | |
| router.get('/orders/:userId', async (req, res) => { | |
| try { | |
| const orders = await Order.find({ userId: req.params.userId }).sort({ createdAt: -1 }); | |
| res.json({ success: true, count: orders.length, data: orders }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // ADMIN: Get All Orders | |
| router.get('/admin/orders', async (req, res) => { | |
| try { | |
| const orders = await Order.find().sort({ createdAt: -1 }); | |
| res.json({ success: true, count: orders.length, data: orders }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // ADMIN: Update Order Status | |
| router.put('/admin/orders/:id', async (req, res) => { | |
| try { | |
| const { status } = req.body; | |
| const order = await Order.findOneAndUpdate( | |
| { id: req.params.id }, | |
| { status: status }, | |
| { new: true } | |
| ); | |
| if (!order) return res.status(404).json({ error: 'Order not found' }); | |
| // Trigger Push Notification | |
| const statusMessages = { | |
| 'accepted': 'Buyurtmangiz qabul qilindi! ✅', | |
| 'shipping': 'Buyurtmangiz yo\'lga chiqdi! 🚚', | |
| 'completed': 'Buyurtma yetkazildi. Xaridingiz uchun rahmat! 🎉', | |
| 'cancelled': 'Buyurtmangiz bekor qilindi. ❌' | |
| }; | |
| if (statusMessages[status]) { | |
| sendPushNotification(order.userId, 'Buyurtma Holati', statusMessages[status]); | |
| } | |
| res.json({ success: true, data: order }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // Create Order (from App) | |
| router.post('/orders', async (req, res) => { | |
| try { | |
| const { userId, items, total, location, phone, userName } = req.body; | |
| if (!userId || !items || !total) { | |
| return res.status(400).json({ error: 'Missing required fields' }); | |
| } | |
| const orderId = Date.now(); | |
| const newOrder = new Order({ | |
| id: orderId, | |
| userId, | |
| user: userName || 'App User', | |
| items, | |
| total, | |
| phone, | |
| location, | |
| status: 'new', | |
| deliveryMethod: location ? 'delivery' : 'pickup', | |
| paymentMethod: 'app_payment' | |
| }); | |
| await newOrder.save(); | |
| // Notify Admins | |
| const adminIds = config.ADMIN_IDS; | |
| if (adminIds && adminIds.length > 0) { | |
| const itemsList = items.map(i => `- ${i.name} x${i.quantity} (${i.price})`).join('\n'); | |
| const mapLink = location ? `https://www.google.com/maps?q=${location.latitude},${location.longitude}` : 'Joylashuvsiz'; | |
| const message = `🆕 <b>Yangi Buyurtma!</b>\n\n` + | |
| `👤 <b>Mijoz:</b> ${userName || 'Noma\'lum'}\n` + | |
| `📞 <b>Telefon:</b> ${phone}\n` + | |
| `📍 <b>Manzil:</b> ${mapLink}\n\n` + | |
| `🛒 <b>Mahsulotlar:</b>\n${itemsList}\n\n` + | |
| `💰 <b>Jami:</b> ${total} UZS\n` + | |
| `💳 <b>To'lov:</b> ${req.body.paymentMethod || 'Naqd'}\n` + | |
| `🆔 <b>ID:</b> <code>${orderId}</code>`; | |
| adminIds.forEach(async (adminId) => { | |
| try { | |
| await axios.post(`https://api.telegram.org/bot${config.BOT_TOKEN}/sendMessage`, { | |
| chat_id: adminId, | |
| text: message, | |
| parse_mode: 'HTML' | |
| }, { httpsAgent: telegramAgent }); | |
| } catch (err) { | |
| console.error(`Failed to notify admin ${adminId}:`, err.message); | |
| } | |
| }); | |
| } | |
| res.json({ success: true, orderId, message: 'Order created' }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // --- PUSH NOTIFICATION HELPERS --- | |
| const sendPushNotification = async (userId, title, body) => { | |
| try { | |
| const user = await User.findOne({ id: userId }); | |
| if (!user || !user.fcmToken) return; | |
| // Note: You need FCM Server Key in config.js (FIREBASE_SERVER_KEY) | |
| if (!config.FIREBASE_SERVER_KEY) { | |
| console.log('Skipping Push: No FIREBASE_SERVER_KEY in config'); | |
| return; | |
| } | |
| await axios.post('https://fcm.googleapis.com/fcm/send', { | |
| to: user.fcmToken, | |
| notification: { | |
| title: title, | |
| body: body, | |
| sound: "default" | |
| }, | |
| data: { | |
| click_action: "FLUTTER_NOTIFICATION_CLICK", | |
| status: "done" | |
| } | |
| }, { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `key=${config.FIREBASE_SERVER_KEY}` | |
| } | |
| }); | |
| console.log(`Push sent to ${userId}`); | |
| } catch (e) { | |
| console.error("Push Error:", e.message); | |
| } | |
| }; | |
| // Update FCM Token | |
| router.post('/auth/token', async (req, res) => { | |
| try { | |
| const { userId, token } = req.body; | |
| if (!userId || !token) return res.status(400).json({ error: 'Missing fields' }); | |
| await User.findOneAndUpdate({ id: userId }, { fcmToken: token }); | |
| res.json({ success: true, message: 'Token updated' }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // --- PHONE LOGIN ENDPOINTS --- | |
| const OTP_STORE = new Map(); // Store OTPs: "998901234567" -> "1234" | |
| router.post('/auth/send-code', async (req, res) => { | |
| try { | |
| const { phone } = req.body; | |
| if (!phone) return res.status(400).json({ success: false, message: "Phone required" }); | |
| // Clean phone | |
| const cleanPhone = phone.replace('+', ''); | |
| // Find user by phone | |
| const user = await User.findOne({ phone: cleanPhone }); | |
| if (!user) { | |
| return res.json({ | |
| success: false, | |
| status: 'not_found', | |
| message: "Ushbu raqam botda ro'yxatdan o'tmagan. Iltimos, botga kiring va raqamingizni yuboring." | |
| }); | |
| } | |
| // Generate Code | |
| const code = Math.floor(1000 + Math.random() * 9000).toString(); | |
| OTP_STORE.set(cleanPhone, code); | |
| // Send via Bot | |
| try { | |
| await req.bot.telegram.sendMessage(user.id, `🔐 <b>Tasdiqlash kodi:</b> <code>${code}</code>\n\nIlovaga kiriting.`, { parse_mode: 'HTML' }); | |
| res.json({ success: true, status: 'sent', message: "Kod yuborildi" }); | |
| } catch (botError) { | |
| console.error("Bot Send Error:", botError); | |
| res.json({ success: false, message: "Botga yuborib bo'lmadi. Botni bloklamaganmisiz?" }); | |
| } | |
| } catch (e) { | |
| console.error("Send Code Error:", e); | |
| res.status(500).json({ success: false, message: "Server Error" }); | |
| } | |
| }); | |
| router.post('/auth/verify-code', async (req, res) => { | |
| try { | |
| const { phone, code } = req.body; | |
| const cleanPhone = phone.replace('+', ''); | |
| if (OTP_STORE.get(cleanPhone) === code) { | |
| OTP_STORE.delete(cleanPhone); // Consume code | |
| const user = await User.findOne({ phone: cleanPhone }); | |
| if (!user) return res.status(404).json({ success: false, message: "User not found" }); | |
| res.json({ | |
| success: true, | |
| status: 'approved', | |
| user: { | |
| id: user.id.toString(), | |
| name: user.first_name, | |
| username: user.username | |
| } | |
| }); | |
| } else { | |
| res.json({ success: false, message: "Kod noto'g'ri" }); | |
| } | |
| } catch (e) { | |
| res.status(500).json({ success: false, message: "Server Error" }); | |
| } | |
| }); | |
| router.post('/auth/firebase', async (req, res) => { | |
| try { | |
| const { uid, phone, name, email } = req.body; | |
| if (!uid) return res.status(400).json({ success: false, message: "UID required" }); | |
| let user = await User.findOne({ firebase_uid: uid }); | |
| if (!user && phone) { | |
| // Try finding by phone if user existed before | |
| const cleanPhone = phone.replace('+', ''); | |
| user = await User.findOne({ phone: cleanPhone }); | |
| } | |
| if (!user) { | |
| // Create New User | |
| user = new User({ | |
| id: uid, // Use Firebase UID as ID | |
| firebase_uid: uid, | |
| first_name: name || 'Foydalanuvchi', | |
| phone: phone ? phone.replace('+', '') : null, | |
| username: email ? email.split('@')[0] : null | |
| }); | |
| await user.save(); | |
| } else { | |
| // Link existing user | |
| if (!user.firebase_uid) { | |
| user.firebase_uid = uid; | |
| await user.save(); | |
| } | |
| } | |
| res.json({ | |
| success: true, | |
| status: 'approved', | |
| user: { | |
| id: user.id.toString(), | |
| name: user.first_name, | |
| username: user.username, | |
| phone: user.phone | |
| } | |
| }); | |
| } catch (e) { | |
| console.error("Firebase Auth Error:", e); | |
| res.status(500).json({ success: false, message: "Server Error" }); | |
| } | |
| }); | |
| module.exports = router; | |