Update v9.3.0: Enhanced Security with Telegram Integration
- Incremented version to 9.3.0 (versionCode: 93000) - Added Telegram integration for device notifications - Implemented token-based verification system - Enhanced device registry with IP/country detection - Added split token verification for admin/user validation - Improved dashboard with real-time notifications - Enhanced blocking system with token verification - Added geo-location tracking for devices - Improved device management interface - Enhanced security controls and monitoring 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,19 @@ 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 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 || '';
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@@ -20,17 +29,29 @@ app.use((req, res, next) => {
|
||||
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));
|
||||
fs.writeFileSync(DATA_PATH, JSON.stringify([], null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function readDevices() {
|
||||
ensureDataFile();
|
||||
const raw = fs.readFileSync(DATA_PATH, 'utf-8');
|
||||
try {
|
||||
const raw = fs.readFileSync(DATA_PATH, 'utf-8');
|
||||
const devices = JSON.parse(raw);
|
||||
return Array.isArray(devices) ? devices : [];
|
||||
} catch (err) {
|
||||
@@ -48,6 +69,71 @@ 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 sendTelegramNotification(message) {
|
||||
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
|
||||
return;
|
||||
}
|
||||
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
|
||||
try {
|
||||
await axios.post(url, {
|
||||
chat_id: TELEGRAM_CHAT_ID,
|
||||
text: message,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to send Telegram notification', error.response ? error.response.data : error.message);
|
||||
}
|
||||
}
|
||||
|
||||
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('Comparte el token del cliente y este admin para autorizar.');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
@@ -75,8 +161,13 @@ app.post('/api/devices/register', (req, res) => {
|
||||
|
||||
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: '',
|
||||
@@ -90,9 +181,18 @@ app.post('/api/devices/register', (req, res) => {
|
||||
lastSeen: now,
|
||||
blocked: false,
|
||||
notes: '',
|
||||
installs: 1
|
||||
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;
|
||||
@@ -102,10 +202,34 @@ app.post('/api/devices/register', (req, res) => {
|
||||
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);
|
||||
res.json({ blocked: existing.blocked, device: existing });
|
||||
|
||||
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) => {
|
||||
@@ -135,6 +259,30 @@ app.post('/api/devices/:deviceId/unblock', (req, res) => {
|
||||
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 || {};
|
||||
|
||||
Reference in New Issue
Block a user