/** * GoldRate Engine — Express Server * * Serves Gold 24K and Silver 999 prices from two sources: * - SLN Bullion Chennai (local Chennai rates) * - IBJA India via goldratetodaylive.in (national rates) * * Endpoints: * GET /api/rates → Prices based on API key's source (SLN or IBJA) * GET /api/sln-rates → SLN Bullion Chennai rates (for frontend) * GET /api/ibja-rates → IBJA India rates (for frontend) * GET /api/history → SLN historical prices * GET /api/ibja-history → IBJA historical prices * GET /health → Server health check * * Authentication: * Header: x-api-key: YOUR_API_KEY * OR Query: ?apikey=YOUR_API_KEY */ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const { getGoldRate, getIBJARate } = require('./priceFetcher'); const { authMiddleware } = require('./auth'); const { connect: connectDB, getIsConnected, getClient } = require('./db'); const { initTelegramBot } = require('./telegramBot'); // Middleware to secure public endpoints (allows same-origin referers, custom headers, or valid API keys) const dashboardAuthMiddleware = (req, res, next) => { const apiKey = req.headers['x-api-key'] || req.query.apikey; if (apiKey) { return authMiddleware(req, res, next); } // Allow requests from RapidAPI proxy (strictly authenticated using the proxy secret key) if (req.headers['x-rapidapi-proxy-secret'] && req.headers['x-rapidapi-proxy-secret'] === process.env.RAPIDAPI_PROXY_SECRET) { req.priceSource = 'SLN'; // Default to SLN return next(); } // Allow requests containing our secure frontend dashboard verification header or query param if (req.headers['x-app-request'] === 'goldscrape-dashboard' || req.query.dashboard === 'true') { req.priceSource = 'SLN'; // Default to SLN return next(); } const referer = req.headers.referer || req.headers.referrer; const host = req.headers.host; // Allow same-origin, Hugging Face Space iframes, Render domains, and localhost const isAllowedOrigin = referer && ( (host && referer.includes(host)) || referer.includes('huggingface.co') || referer.includes('onrender.com') || referer.includes('localhost') ); if (isAllowedOrigin) { req.priceSource = 'SLN'; // Default to SLN return next(); } return res.status(401).json({ success: false, error: 'API key required for external requests.' }); }; // Connect to MongoDB (if URI present) connectDB(); const app = express(); const PORT = process.env.PORT || 3000; // Trust proxy header to allow correct IP extraction behind Hugging Face/Render proxies app.set('trust proxy', 1); // ─── Security ──────────────────────────────────────── app.use(helmet({ contentSecurityPolicy: false, frameguard: false })); app.use(cors()); app.use(express.json()); // ─── Serve Frontend ─────────────────────────────────── app.use(express.static('public')); // Explicitly serve index.html for the root route app.get('/', (req, res) => { const path = require('path'); res.sendFile(path.join(__dirname, '../public/index.html')); }); // ─── Rate Limiting ─────────────────────────────────── const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: parseInt(process.env.RATE_LIMIT_MAX) || 500, skip: (req) => { return req.headers['x-app-request'] === 'goldscrape-dashboard' || req.query.dashboard === 'true' || (req.headers['x-rapidapi-proxy-secret'] && req.headers['x-rapidapi-proxy-secret'] === process.env.RAPIDAPI_PROXY_SECRET); }, message: { success: false, error: 'Too many requests. Please try again later.' } }); app.use('/api', limiter); // ─── Track server start time ───────────────────────── const serverStartTime = new Date(); let lastSlnSaveTime = 0; let lastIbjaSaveTime = 0; // Warm rate caches in the background every 30 seconds setInterval(async () => { try { await getGoldRate(); await getIBJARate(); } catch (err) { console.warn('⚠️ Background cache warming failed:', err.message); } }, 30000); // ─── Health Check (No API key needed) ──────────────── app.get('/health', (req, res) => { const uptime = Math.floor((Date.now() - serverStartTime.getTime()) / 1000); res.json({ success: true, status: 'running', uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`, serverTime: new Date().toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' }), sources: ['SLN Bullion Chennai', 'IBJA India'], version: '2.0.0' }); }); // ─── Helper: Save price to DB ──────────────────────── const DB_SAVE_INTERVAL = 30 * 60 * 1000; // 30 minutes async function saveSLNToDb(data) { if (!getIsConnected() || (Date.now() - lastSlnSaveTime) < DB_SAVE_INTERVAL) return; try { const supabase = getClient(); const today = data.rateDate || new Date().toISOString().split('T')[0]; // Explicitly round to 2 decimals const goldRounded = data.gold24k ? Math.round(data.gold24k * 100) / 100 : null; const silverPerGram = data.silver999 ? Math.round((data.silver999 / 1000) * 100) / 100 : null; await supabase .from('price_records') .upsert({ date: today, gold24k: goldRounded, silver999: silverPerGram, source: 'SLN' }); lastSlnSaveTime = Date.now(); console.log(`💾 SLN DB saved: ₹${goldRounded}/g`); } catch (e) { console.warn('⚠️ SLN DB save failed:', e.message); } } async function saveIBJAToDb(data) { if (!getIsConnected() || (Date.now() - lastIbjaSaveTime) < DB_SAVE_INTERVAL) return; try { const supabase = getClient(); const today = data.rateDate || new Date().toISOString().split('T')[0]; // Explicitly round to 2 decimals const goldRounded = data.gold24k ? Math.round(data.gold24k * 100) / 100 : null; const rawSilver = data.silver999; const silverPerGram = rawSilver ? (rawSilver > 500 ? Math.round((rawSilver / 1000) * 100) / 100 : Math.round(rawSilver * 100) / 100) : null; await supabase .from('ibja_price_records') .upsert({ date: today, gold24k: goldRounded, silver999: silverPerGram, source: 'IBJA' }); lastIbjaSaveTime = Date.now(); console.log(`💾 IBJA DB saved: ₹${goldRounded}/g`); } catch (e) { console.warn('⚠️ IBJA DB save failed:', e.message); } } // ─── Main API: Rates based on API key source ───────── app.get('/api/rates', authMiddleware, async (req, res) => { try { const source = req.priceSource || 'SLN'; // If master key or explicitly configured for BOTH sources if (req.isMasterKey || source === 'BOTH') { const slnData = await getGoldRate(); const ibjaData = await getIBJARate(); saveSLNToDb(slnData); saveIBJAToDb(ibjaData); const slnSilver = slnData.silver999 ? Math.round((slnData.silver999 / 1000) * 100) / 100 : null; const ibjaSilver = ibjaData.silver999 ? (ibjaData.silver999 > 500 ? Math.round((ibjaData.silver999 / 1000) * 100) / 100 : ibjaData.silver999) : null; res.set('Cache-Control', 'public, max-age=15'); return res.json({ success: true, rates: { sln: { gold24k_1gram: slnData.gold24k, silver999_1gram: slnSilver, currency: 'INR', source: slnData.source, session: slnData.session, date: slnData.rateDate, updatedAt: slnData.fetchedAt }, ibja: { gold24k_1gram: ibjaData.gold24k, silver999_1gram: ibjaSilver, currency: 'INR', source: ibjaData.source, session: ibjaData.session, date: ibjaData.rateDate, updatedAt: ibjaData.fetchedAt } } }); } // Default: Single source rates based on key configuration let data; if (source === 'IBJA') { data = await getIBJARate(); saveIBJAToDb(data); } else { data = await getGoldRate(); saveSLNToDb(data); } const silverPerGram = data.silver999 ? (data.silver999 > 500 ? Math.round((data.silver999 / 1000) * 100) / 100 : data.silver999) : null; res.set('Cache-Control', 'public, max-age=15'); res.json({ success: true, gold24k_1gram: data.gold24k, silver999_1gram: silverPerGram, currency: 'INR', source: data.source, session: data.session, date: data.rateDate, updatedAt: data.fetchedAt }); } catch (err) { console.error('❌ API Error:', err.message); res.status(503).json({ success: false, error: 'Unable to fetch prices right now. Please try again.', details: err.message }); } }); // ─── SLN Rates (for frontend) ──────────────────────── app.get('/api/sln-rates', dashboardAuthMiddleware, async (req, res) => { try { const data = await getGoldRate(); saveSLNToDb(data); const silverPerGram = data.silver999 ? Math.round((data.silver999 / 1000) * 100) / 100 : null; res.set('Cache-Control', 'public, max-age=15'); res.json({ success: true, gold24k_1gram: data.gold24k, silver999_1gram: silverPerGram, currency: 'INR', source: data.source, session: data.session, date: data.rateDate, updatedAt: data.fetchedAt }); } catch (err) { res.status(503).json({ success: false, error: err.message }); } }); // ─── IBJA Rates (for frontend) ─────────────────────── app.get('/api/ibja-rates', dashboardAuthMiddleware, async (req, res) => { try { const data = await getIBJARate(); saveIBJAToDb(data); const silverPerGram = data.silver999 ? (data.silver999 > 500 ? Math.round((data.silver999 / 1000) * 100) / 100 : data.silver999) : null; res.set('Cache-Control', 'public, max-age=15'); res.json({ success: true, gold24k_1gram: data.gold24k, silver999_1gram: silverPerGram, currency: 'INR', source: data.source, session: data.session, date: data.rateDate, updatedAt: data.fetchedAt }); } catch (err) { res.status(503).json({ success: false, error: 'IBJA source unavailable', details: err.message }); } }); // Memory cache for historical records const slnHistoryCache = {}; const ibjaHistoryCache = {}; const HISTORY_CACHE_DURATION = 60 * 60 * 1000; // 1 hour // ─── Historical Prices (SLN) ───────────────────────── const RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365, '3y': 1095 }; app.get('/api/history', dashboardAuthMiddleware, async (req, res) => { if (!getIsConnected()) { return res.status(503).json({ success: false, error: 'History not available — database not configured' }); } try { const range = (req.query.range || '3m').toLowerCase(); const now = Date.now(); // Check memory cache if (slnHistoryCache[range] && (now - slnHistoryCache[range].time) < HISTORY_CACHE_DURATION) { return res.json(slnHistoryCache[range].data); } const days = RANGE_DAYS[range] || 90; const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const startStr = startDate.toISOString().split('T')[0]; const supabase = getClient(); const { data: records } = await supabase .from('price_records') .select('date, gold24k, silver999') .gte('date', startStr) .order('date', { ascending: true }); // Calculate performance let performance = null; if (records.length >= 2) { const first = records[0].gold24k; const last = records[records.length - 1].gold24k; const change = last - first; const percent = ((change / first) * 100).toFixed(2); performance = { startPrice: first, endPrice: last, change: Math.round(change * 100) / 100, percentChange: parseFloat(percent), direction: change >= 0 ? 'UP' : 'DOWN' }; } const responseJson = { success: true, source: 'SLN Bullion Chennai', range: range, totalDays: records.length, performance: performance, data: records }; // Save to memory cache slnHistoryCache[range] = { time: now, data: responseJson }; res.json(responseJson); } catch (err) { console.error('❌ History Error:', err.message); res.status(500).json({ success: false, error: 'Failed to fetch history' }); } }); // ─── Historical Prices (IBJA) ──────────────────────── app.get('/api/ibja-history', dashboardAuthMiddleware, async (req, res) => { if (!getIsConnected()) { return res.status(503).json({ success: false, error: 'History not available — database not configured' }); } try { const range = (req.query.range || '3m').toLowerCase(); const now = Date.now(); // Check memory cache if (ibjaHistoryCache[range] && (now - ibjaHistoryCache[range].time) < HISTORY_CACHE_DURATION) { return res.json(ibjaHistoryCache[range].data); } const days = RANGE_DAYS[range] || 90; const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const startStr = startDate.toISOString().split('T')[0]; const supabase = getClient(); const { data: records } = await supabase .from('ibja_price_records') .select('date, gold24k, silver999') .gte('date', startStr) .order('date', { ascending: true }); let performance = null; if (records && records.length >= 2) { const first = records[0].gold24k; const last = records[records.length - 1].gold24k; const change = last - first; const percent = ((change / first) * 100).toFixed(2); performance = { startPrice: first, endPrice: last, change: Math.round(change * 100) / 100, percentChange: parseFloat(percent), direction: change >= 0 ? 'UP' : 'DOWN' }; } const responseJson = { success: true, source: 'IBJA India', range: range, totalDays: records ? records.length : 0, performance: performance, data: records || [] }; // Save to memory cache ibjaHistoryCache[range] = { time: now, data: responseJson }; res.json(responseJson); } catch (err) { console.error('❌ IBJA History Error:', err.message); res.status(500).json({ success: false, error: 'Failed to fetch IBJA history' }); } }); // ─── SEO Landing Pages Engine ──────────────────────── const serveSEOPage = (req, res, seoData) => { try { const htmlPath = path.join(__dirname, '../public/index.html'); if (!fs.existsSync(htmlPath)) { return res.status(404).send('Dashboard template not found'); } let html = fs.readFileSync(htmlPath, 'utf8'); // Replace Meta Title if (seoData.title) { html = html.replace(/
Your request has been received. Redirecting you to WhatsApp to complete your activation...