const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); const morgan = require('morgan'); const crypto = require('crypto'); const axios = require('axios'); const requestIp = require('request-ip'); const geoip = require('geoip-lite'); const dotenv = require('dotenv'); const envPath = path.resolve(__dirname, '..', '.env'); if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }); } else { dotenv.config(); } const app = express(); const PORT = process.env.PORT || 4000; const DATA_PATH = path.join(__dirname, 'data', 'devices.json'); const CONFIG_PATH = path.join(__dirname, 'config.json'); const config = loadConfig(); const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || config.telegramBotToken || ''; const TELEGRAM_CHAT_ID = (process.env.TELEGRAM_CHAT_ID || config.telegramChatId || '').toString(); let telegramOffset = 0; let telegramPollingStarted = false; app.use(cors()); app.use(express.json()); app.use(morgan('dev')); app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] })); app.use((req, res, next) => { res.setHeader('Cache-Control', 'no-store'); next(); }); function loadConfig() { try { if (fs.existsSync(CONFIG_PATH)) { const raw = fs.readFileSync(CONFIG_PATH, 'utf-8'); return JSON.parse(raw); } } catch (error) { console.warn('Could not parse config.json', error); } return {}; } function ensureDataFile() { if (!fs.existsSync(DATA_PATH)) { fs.mkdirSync(path.dirname(DATA_PATH), { recursive: true }); fs.writeFileSync(DATA_PATH, JSON.stringify([], null, 2)); } } function readDevices() { ensureDataFile(); try { const raw = fs.readFileSync(DATA_PATH, 'utf-8'); const devices = JSON.parse(raw); return Array.isArray(devices) ? devices : []; } catch (err) { console.error('Error parsing devices.json', err); return []; } } function writeDevices(devices) { ensureDataFile(); fs.writeFileSync(DATA_PATH, JSON.stringify(devices, null, 2)); } function sanitizeId(input) { return String(input || '').trim(); } function sanitizeIp(ip) { if (!ip) return ''; if (ip.startsWith('::ffff:')) { return ip.replace('::ffff:', ''); } return ip; } function lookupLocation(ip) { if (!ip) { return { country: 'N/A', region: '', city: '' }; } const geo = geoip.lookup(ip); if (!geo) { return { country: 'N/A', region: '', city: '' }; } return { country: geo.country || 'N/A', region: geo.region || '', city: geo.city || '' }; } function generateTokenParts() { const fullToken = crypto.randomBytes(16).toString('hex'); const mid = Math.floor(fullToken.length / 2); return { clientPart: fullToken.slice(0, mid), adminPart: fullToken.slice(mid) }; } async function sendTelegramMessage(message, options = {}) { if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) { return; } const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`; try { const payload = { chat_id: TELEGRAM_CHAT_ID, text: message, disable_web_page_preview: true }; if (options.parseMode) { payload.parse_mode = options.parseMode; } await axios.post(url, payload); } catch (error) { console.warn('Failed to send Telegram message', error.response ? error.response.data : error.message); } } async function sendTelegramNotification(message) { await sendTelegramMessage(message, { parseMode: 'Markdown' }); } function formatTelegramMessage(device, verificationRequired) { const lines = [ '*Nuevo registro de dispositivo*', `ID: ${device.deviceId}`, `Alias: ${device.alias || '-'}`, `Modelo: ${device.manufacturer} ${device.model}`, `Versión app: ${device.appVersionName} (${device.appVersionCode})`, `IP: ${device.ip || '-'} / País: ${device.country || '-'}`, `Última vez visto: ${device.lastSeen}` ]; if (verificationRequired && device.verification) { lines.push('`Token Admin` (guárdalo): `' + device.verification.adminPart + '`'); lines.push('Autorizar: `/allow ' + device.deviceId + ' TOKEN_CLIENTE`'); lines.push('Rechazar: `/deny ' + device.deviceId + ' TOKEN_CLIENTE [motivo]`'); } return lines.join('\n'); } async function initializeTelegramOffset() { if (!TELEGRAM_BOT_TOKEN) { return; } const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`; try { const { data } = await axios.get(url, { params: { timeout: 1 } }); if (data.ok && Array.isArray(data.result) && data.result.length > 0) { telegramOffset = data.result[data.result.length - 1].update_id + 1; } } catch (error) { console.warn('Failed to initialize Telegram offset', error.message); } } async function pollTelegramUpdates() { if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) { return; } const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`; const params = { timeout: 25 }; if (telegramOffset) { params.offset = telegramOffset; } try { const { data } = await axios.get(url, { params }); if (data.ok && Array.isArray(data.result)) { for (const update of data.result) { telegramOffset = update.update_id + 1; if (update.message) { processTelegramMessage(update.message); } } } } catch (error) { console.warn('Telegram polling error', error.message); } finally { setTimeout(pollTelegramUpdates, 3000); } } async function startTelegramPolling() { if (telegramPollingStarted || !TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) { return; } telegramPollingStarted = true; await initializeTelegramOffset(); pollTelegramUpdates(); } function processTelegramMessage(message) { const chatId = message.chat && message.chat.id ? message.chat.id.toString() : ''; if (chatId && TELEGRAM_CHAT_ID && chatId !== TELEGRAM_CHAT_ID.toString()) { return; } const text = (message.text || '').trim(); if (!text.startsWith('/')) { return; } const parts = text.split(/\s+/); const command = parts[0].toLowerCase(); if (command === '/allow' && parts.length >= 3) { handleTelegramAllow(parts[1], parts[2]); } else if (command === '/deny' && parts.length >= 3) { const reason = parts.slice(3).join(' ') || 'Bloqueado desde Telegram'; handleTelegramDeny(parts[1], parts[2], reason); } else if (command === '/pending') { handleTelegramPending(); } else { sendTelegramMessage('Comandos disponibles:\n/allow \n/deny [motivo]\n/pending'); } } async function handleTelegramAllow(deviceId, clientToken) { const devices = readDevices(); const device = devices.find(d => d.deviceId === deviceId); if (!device || !device.verification) { await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`); return; } if (device.verification.status === 'verified') { await sendTelegramMessage(`ℹ️ El dispositivo ${deviceId} ya está verificado.`); return; } if (device.verification.clientPart !== clientToken.trim()) { await sendTelegramMessage('❌ Token del cliente inválido.'); return; } device.verification.status = 'verified'; device.verification.verifiedAt = new Date().toISOString(); device.blocked = false; device.notes = ''; writeDevices(devices); await sendTelegramMessage(`✅ Dispositivo ${deviceId} autorizado correctamente.`); } async function handleTelegramDeny(deviceId, clientToken, reason) { const devices = readDevices(); const device = devices.find(d => d.deviceId === deviceId); if (!device || !device.verification) { await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`); return; } if (device.verification.clientPart !== clientToken.trim()) { await sendTelegramMessage('❌ Token del cliente inválido.'); return; } device.verification.status = 'denied'; device.verification.deniedAt = new Date().toISOString(); device.blocked = true; device.notes = reason; writeDevices(devices); await sendTelegramMessage(`🚫 Dispositivo ${deviceId} bloqueado. Motivo: ${reason}`); } async function handleTelegramPending() { const devices = readDevices(); const pending = devices.filter(d => !d.verification || d.verification.status !== 'verified'); if (!pending.length) { await sendTelegramMessage('No hay dispositivos pendientes.'); return; } const lines = pending.slice(0, 10).map(device => { const token = device.verification ? device.verification.clientPart : 'N/A'; return `${device.deviceId} - Token cliente: ${token}`; }); await sendTelegramMessage('Pendientes:\n' + lines.join('\n')); } app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); app.get('/api/devices', (req, res) => { const devices = readDevices(); res.json({ devices }); }); app.post('/api/devices/register', (req, res) => { const { deviceId, deviceName, model, manufacturer, osVersion, appVersionName, appVersionCode } = req.body || {}; const trimmedId = sanitizeId(deviceId); if (!trimmedId) { return res.status(400).json({ error: 'deviceId is required' }); } const devices = readDevices(); const now = new Date().toISOString(); const clientIp = sanitizeIp(requestIp.getClientIp(req) || req.ip || ''); const location = lookupLocation(clientIp); let existing = devices.find(d => d.deviceId === trimmedId); let isNew = false; if (!existing) { const tokenParts = generateTokenParts(); existing = { deviceId: trimmedId, alias: '', deviceName: deviceName || '', model: model || '', manufacturer: manufacturer || '', osVersion: osVersion || '', appVersionName: appVersionName || '', appVersionCode: appVersionCode || 0, firstSeen: now, lastSeen: now, blocked: false, notes: '', installs: 1, ip: clientIp, country: location.country, verification: { clientPart: tokenParts.clientPart, adminPart: tokenParts.adminPart, status: 'pending', createdAt: now } }; devices.push(existing); isNew = true; } else { existing.deviceName = deviceName || existing.deviceName; existing.model = model || existing.model; existing.manufacturer = manufacturer || existing.manufacturer; existing.osVersion = osVersion || existing.osVersion; existing.appVersionName = appVersionName || existing.appVersionName; existing.appVersionCode = appVersionCode || existing.appVersionCode; existing.lastSeen = now; existing.installs = (existing.installs || 1) + 1; existing.ip = clientIp || existing.ip; const hasValidCountry = location.country && location.country !== 'N/A'; existing.country = hasValidCountry ? location.country : (existing.country || 'N/A'); existing.verification = existing.verification || { ...generateTokenParts(), status: 'pending', createdAt: now }; } const verificationRequired = !existing.verification || existing.verification.status !== 'verified'; const blocked = existing.blocked || verificationRequired; writeDevices(devices); if (isNew || verificationRequired) { sendTelegramNotification(formatTelegramMessage(existing, verificationRequired)); } res.json({ blocked, device: existing, message: verificationRequired ? 'Instalación pendiente de verificación.' : (existing.notes || ''), verification: { required: verificationRequired, clientTokenPart: verificationRequired ? existing.verification.clientPart : '' } }); }); app.post('/api/devices/:deviceId/block', (req, res) => { const { deviceId } = req.params; const { reason } = req.body || {}; const devices = readDevices(); const existing = devices.find(d => d.deviceId === deviceId); if (!existing) { return res.status(404).json({ error: 'Device not found' }); } existing.blocked = true; existing.notes = reason || existing.notes; existing.blockedAt = new Date().toISOString(); writeDevices(devices); res.json({ blocked: true, device: existing }); }); app.post('/api/devices/:deviceId/unblock', (req, res) => { const { deviceId } = req.params; const devices = readDevices(); const existing = devices.find(d => d.deviceId === deviceId); if (!existing) { return res.status(404).json({ error: 'Device not found' }); } existing.blocked = false; writeDevices(devices); res.json({ blocked: false, device: existing }); }); app.post('/api/devices/:deviceId/verify', (req, res) => { const { deviceId } = req.params; const { clientTokenPart, adminTokenPart } = req.body || {}; const devices = readDevices(); const existing = devices.find(d => d.deviceId === deviceId); if (!existing || !existing.verification) { return res.status(404).json({ error: 'Device not found' }); } if (!clientTokenPart || !adminTokenPart) { return res.status(400).json({ error: 'Both token parts are required' }); } if ( existing.verification.clientPart !== clientTokenPart.trim() || existing.verification.adminPart !== adminTokenPart.trim() ) { return res.status(400).json({ error: 'Invalid token parts' }); } existing.verification.status = 'verified'; existing.verification.verifiedAt = new Date().toISOString(); existing.blocked = false; writeDevices(devices); res.json({ verified: true, device: existing }); }); app.put('/api/devices/:deviceId/alias', (req, res) => { const { deviceId } = req.params; const { alias } = req.body || {}; const devices = readDevices(); const existing = devices.find(d => d.deviceId === deviceId); if (!existing) { return res.status(404).json({ error: 'Device not found' }); } existing.alias = alias || ''; writeDevices(devices); res.json({ device: existing }); }); app.delete('/api/devices/:deviceId', (req, res) => { const { deviceId } = req.params; const devices = readDevices(); const filtered = devices.filter(d => d.deviceId !== deviceId); if (filtered.length === devices.length) { return res.status(404).json({ error: 'Device not found' }); } writeDevices(filtered); res.json({ removed: true }); }); app.listen(PORT, () => { ensureDataFile(); console.log(`StreamPlayer dashboard server listening on port ${PORT}`); startTelegramPolling(); });