Spaces:
Running
Running
| /** | |
| * 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(/<title>[^<]*<\/title>/, `<title>${seoData.title}</title>`); | |
| html = html.replace(/<meta property="og:title" content="[^"]*"/, `<meta property="og:title" content="${seoData.title}"`); | |
| html = html.replace(/<meta name="twitter:title" content="[^"]*"/, `<meta name="twitter:title" content="${seoData.title}"`); | |
| } | |
| // Replace Meta Description | |
| if (seoData.description) { | |
| html = html.replace(/<meta name="description" content="[^"]*"/, `<meta name="description" content="${seoData.description}"`); | |
| html = html.replace(/<meta property="og:description" content="[^"]*"/, `<meta property="og:description" content="${seoData.description}"`); | |
| html = html.replace(/<meta name="twitter:description" content="[^"]*"/, `<meta name="twitter:description" content="${seoData.description}"`); | |
| } | |
| // Replace H1 tag | |
| if (seoData.h1) { | |
| html = html.replace(/<h1 class="hero-title">[\s\S]*?<\/h1>/, `<h1 class="hero-title" style="font-size: 38px; line-height: 1.3; margin-bottom: 16px;">${seoData.h1}</h1>`); | |
| } | |
| res.send(html); | |
| } catch (err) { | |
| console.error('β SEO Render Error:', err.message); | |
| res.status(500).send('Error rendering page'); | |
| } | |
| }; | |
| const seoRoutes = [ | |
| { | |
| path: '/gold-rate-api', | |
| title: 'Gold Rate API India | Real-time Gold Price Feed', | |
| description: 'Free and premium Gold Rate API for India. Get live 22K and 24K gold prices and developer documentation.', | |
| h1: 'Gold Rate API India' | |
| }, | |
| { | |
| path: '/live-gold-api', | |
| title: 'Live Gold Price API | Real-Time Bullion Prices', | |
| description: 'High-speed live gold price API for developers. Integrate spot gold rates with under 10ms response times.', | |
| h1: 'Live Gold Price API' | |
| }, | |
| { | |
| path: '/chennai-gold-rate-api', | |
| title: 'Chennai Gold Rate API | Live SLN Bullion Price Feed', | |
| description: 'Get real-time retail and wholesale gold spot rates in Chennai directly from SLN Bullion Chennai.', | |
| h1: 'Chennai Gold Rate API' | |
| }, | |
| { | |
| path: '/india-gold-api', | |
| title: 'India Gold API | Official IBJA Bullion Price Feed', | |
| description: 'Access official gold and silver price feeds for India based on IBJA national bullion standards.', | |
| h1: 'India Gold API & Silver Price API' | |
| }, | |
| { | |
| path: '/gold-price-api', | |
| title: 'Gold Price API | Real-Time Gold Price JSON Feed', | |
| description: 'Reliable B2B Gold Price API for jewellery stores, finance apps, and ERP systems.', | |
| h1: 'Gold Price API for Developers' | |
| }, | |
| { | |
| path: '/silver-price-api', | |
| title: 'Silver Price API | Live Silver Spot Rates', | |
| description: 'Get live Silver 999 price API feeds for India. Under 10ms cached proxy responses.', | |
| h1: 'Silver Price API' | |
| }, | |
| { | |
| path: '/historical-gold-api', | |
| title: 'Historical Gold Price API India | Historical Bullion Data', | |
| description: 'Retrieve historical gold and silver price records for Chennai and India. Includes 1-month to 3-year history.', | |
| h1: 'Historical Gold Price API India' | |
| }, | |
| { | |
| path: '/rest-api', | |
| title: 'Gold Rate JSON API | Gold Price REST API', | |
| description: 'Easy integration using our RESTful JSON API. Complete with code examples in Node.js, Python, and cURL.', | |
| h1: 'Gold Rate JSON API' | |
| }, | |
| { | |
| path: '/json-api', | |
| title: 'Gold Rate JSON API | Gold Price REST API', | |
| description: 'Easy integration using our RESTful JSON API. Complete with code examples in Node.js, Python, and cURL.', | |
| h1: 'Gold Rate JSON API' | |
| }, | |
| { | |
| path: '/docs', | |
| title: 'GoldScrape API Documentation | Developer Guides', | |
| description: 'Developer guides, JSON response templates, and implementation code snippets in Node.js, Python, and PHP.', | |
| h1: 'GoldScrape API Documentation' | |
| }, | |
| { | |
| path: '/pricing', | |
| title: 'GoldScrape API Pricing | Gold API Subscription Plans', | |
| description: 'Choose between Monthly License (βΉ599/mo) and Yearly License (βΉ3,999/yr) plans. Start a 7-day free trial.', | |
| h1: 'GoldScrape API Pricing' | |
| } | |
| ]; | |
| seoRoutes.forEach(route => { | |
| app.get(route.path, (req, res) => { | |
| serveSEOPage(req, res, route); | |
| }); | |
| }); | |
| // Dynamic City Specific Landing Pages | |
| app.get('/city/:cityName', (req, res) => { | |
| const citySlug = req.params.cityName; | |
| let cityName = citySlug.split('-')[0]; | |
| cityName = cityName.charAt(0).toUpperCase() + cityName.slice(1); | |
| const seoData = { | |
| title: `${cityName} Gold Rate API | Live Gold Price in ${cityName}`, | |
| description: `Get real-time gold and silver spot prices in ${cityName}, Tamil Nadu. Live updates, fallbacks, and developer guides.`, | |
| h1: `${cityName} Gold Rate API` | |
| }; | |
| serveSEOPage(req, res, seoData); | |
| }); | |
| // βββ Google Ads Conversion Page ββββββββββββββββββββββ | |
| app.get('/thank-you', (req, res) => { | |
| const plan = req.query.plan || 'general'; | |
| let waText = 'Hi, I want to inquire about GoldScrape API.'; | |
| if (plan === 'monthly') { | |
| waText = 'Hi, I want to subscribe to the Monthly Plan (βΉ599/mo) for GoldScrape API.'; | |
| } else if (plan === 'yearly') { | |
| waText = 'Hi, I want to subscribe to the Yearly Plan (βΉ3,999/yr) for GoldScrape API.'; | |
| } else if (plan === 'enterprise') { | |
| waText = 'Hi, I want to inquire about the Enterprise Plan for GoldScrape API.'; | |
| } | |
| const waUrl = `https://wa.me/919360345770?text=${encodeURIComponent(waText)}`; | |
| res.send(` | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Thank You - GoldScrape API</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Outfit', sans-serif; | |
| background: #0B0F19; | |
| color: white; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| text-align: center; | |
| } | |
| .container { | |
| max-width: 450px; | |
| padding: 40px 24px; | |
| background: #111827; | |
| border: 1px solid #1f2937; | |
| border-radius: 24px; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.4); | |
| } | |
| .icon { | |
| font-size: 48px; | |
| margin-bottom: 20px; | |
| } | |
| h1 { | |
| font-size: 28px; | |
| margin-bottom: 12px; | |
| background: linear-gradient(135deg, #F59E0B, #6366F1); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| p { | |
| color: #9CA3AF; | |
| font-size: 16px; | |
| margin-bottom: 24px; | |
| line-height: 1.6; | |
| } | |
| .spinner { | |
| border: 4px solid rgba(255,255,255,0.1); | |
| border-top: 4px solid #6366F1; | |
| border-radius: 50%; | |
| width: 30px; | |
| height: 30px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| </style> | |
| <script> | |
| setTimeout(() => { | |
| window.location.href = "${waUrl}"; | |
| }, 1500); | |
| </script> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="icon">β </div> | |
| <h1>Thank You!</h1> | |
| <p>Your request has been received. Redirecting you to WhatsApp to complete your activation...</p> | |
| <div class="spinner"></div> | |
| </div> | |
| </body> | |
| </html> | |
| `); | |
| }); | |
| // βββ 404 Handler βββββββββββββββββββββββββββββββββββββ | |
| app.use((req, res) => { | |
| res.status(404).json({ | |
| success: false, | |
| error: 'Endpoint not found', | |
| availableEndpoints: { | |
| rates: 'GET /api/rates?apikey=YOUR_KEY (source based on key)', | |
| slnRates: 'GET /api/sln-rates?apikey=YOUR_KEY', | |
| ibjaRates: 'GET /api/ibja-rates?apikey=YOUR_KEY', | |
| history: 'GET /api/history?range=1m|3m|6m|1y|3y&apikey=YOUR_KEY', | |
| ibjaHistory: 'GET /api/ibja-history?range=1m|3m|6m|1y|3y&apikey=YOUR_KEY', | |
| health: 'GET /health' | |
| } | |
| }); | |
| }); | |
| // βββ Start Server ββββββββββββββββββββββββββββββββββββ | |
| app.listen(PORT, () => { | |
| console.log(''); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log('β πͺ GoldRate Engine v2.0.0 πͺ β'); | |
| console.log('β SLN Bullion Chennai + IBJA India Live API β'); | |
| console.log('β βββββββββββββββββββββββββββββββββββββββββββββββββββ£'); | |
| console.log(`β Server: http://localhost:${PORT} β`); | |
| console.log(`β SLN Rates: /api/sln-rates β`); | |
| console.log(`β IBJA Rates: /api/ibja-rates β`); | |
| console.log(`β SLN History: /api/history β`); | |
| console.log(`β IBJA History: /api/ibja-history β`); | |
| console.log(`β Health: /health β`); | |
| console.log('β βββββββββββββββββββββββββββββββββββββββββββββββββββ£'); | |
| console.log(`β Master Key: ${process.env.API_KEY ? 'β Loaded' : 'β Missing!'} β`); | |
| console.log(`β Sources: SLN Bullion + IBJA β`); | |
| console.log(`β Key Expiry: 30 days (Supabase api_keys table) β`); | |
| console.log(`β Telegram: ${process.env.TELEGRAM_BOT_TOKEN ? 'β Bot Active' : 'β Not configured'} β`); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log(''); | |
| console.log('π± API Key Management:'); | |
| console.log(' π€ Telegram: /create_sln or /create_ibja'); | |
| console.log(' π» CLI: node scripts/manage-keys.js create --name "Shop" --source SLN'); | |
| console.log(''); | |
| // Start Telegram Bot | |
| initTelegramBot(); | |
| // Start background price saver to guarantee historical records without client traffic | |
| startBackgroundSaver(); | |
| }); | |
| // βββ Background Price Saver (no traffic required) βββββ | |
| function startBackgroundSaver() { | |
| console.log('β° Starting background price saver (interval: 1 hour)...'); | |
| const saveRates = async () => { | |
| try { | |
| const slnData = await getGoldRate(); | |
| const slnToday = slnData.rateDate || new Date().toISOString().split('T')[0]; | |
| const slnGoldRounded = slnData.gold24k ? Math.round(slnData.gold24k * 100) / 100 : null; | |
| const slnSilverPerGram = slnData.silver999 ? Math.round((slnData.silver999 / 1000) * 100) / 100 : null; | |
| const supabase = getClient(); | |
| if (getIsConnected()) { | |
| await supabase | |
| .from('price_records') | |
| .upsert({ date: slnToday, gold24k: slnGoldRounded, silver999: slnSilverPerGram, source: 'SLN' }); | |
| console.log(`β° Background Auto-Save: SLN Bullion saved (βΉ${slnGoldRounded}/g)`); | |
| try { | |
| const ibjaData = await getIBJARate(); | |
| const ibjaToday = ibjaData.rateDate || new Date().toISOString().split('T')[0]; | |
| const ibjaGoldRounded = ibjaData.gold24k ? Math.round(ibjaData.gold24k * 100) / 100 : null; | |
| const rawSilver = ibjaData.silver999; | |
| const ibjaSilverPerGram = rawSilver | |
| ? (rawSilver > 500 ? Math.round((rawSilver / 1000) * 100) / 100 : Math.round(rawSilver * 100) / 100) | |
| : null; | |
| await supabase | |
| .from('ibja_price_records') | |
| .upsert({ date: ibjaToday, gold24k: ibjaGoldRounded, silver999: ibjaSilverPerGram, source: 'IBJA' }); | |
| console.log(`β° Background Auto-Save: IBJA India saved (βΉ${ibjaGoldRounded}/g)`); | |
| } catch (e) { | |
| console.warn('β οΈ Background IBJA save failed:', e.message); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('β οΈ Background price saver failed:', err.message); | |
| } | |
| }; | |
| // Run initial save after 10 seconds | |
| setTimeout(saveRates, 10000); | |
| // Repeat every 1 hour | |
| const ONE_HOUR = 60 * 60 * 1000; | |
| setInterval(saveRates, ONE_HOUR); | |
| } | |
| module.exports = app; | |