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 = `🆕 Yangi Buyurtma!\n\n` +
`👤 Mijoz: ${userName || 'Noma\'lum'}\n` +
`📞 Telefon: ${phone}\n` +
`📍 Manzil: ${mapLink}\n\n` +
`🛒 Mahsulotlar:\n${itemsList}\n\n` +
`💰 Jami: ${total} UZS\n` +
`💳 To'lov: ${req.body.paymentMethod || 'Naqd'}\n` +
`🆔 ID: ${orderId}`;
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, `🔐 Tasdiqlash kodi: ${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;