From adadb892325368561ff8eea62554ff67613557b3 Mon Sep 17 00:00:00 2001 From: renato97 Date: Sun, 23 Nov 2025 22:10:21 +0100 Subject: [PATCH] Update v9.3.0: Enhanced Security with Telegram Integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env | 2 + .gitignore | 5 + README.md | 39 +- app/build.gradle | 4 +- .../java/com/streamplayer/DeviceRegistry.java | 19 +- .../java/com/streamplayer/MainActivity.java | 14 +- app/src/main/res/values/strings.xml | 1 + dashboard/config.example.json | 4 + dashboard/data/devices.json | 27 +- dashboard/node_modules/.package-lock.json | 367 ++++++++++++++++- dashboard/package-lock.json | 372 +++++++++++++++++- dashboard/package.json | 5 +- dashboard/public/app.js | 56 ++- dashboard/public/index.html | 3 + dashboard/server.js | 156 +++++++- update-manifest.json | 12 +- 16 files changed, 1039 insertions(+), 47 deletions(-) create mode 100644 dashboard/config.example.json diff --git a/.env b/.env index 2706416..354ceca 100644 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ GITEA_TOKEN=7921aa22187b39125d29399d26f527ba26a2fb5b GEMINI_API_KEY=AIzaSyDWOgyAJqscuPU6iSpS6gxupWBm4soNw5o +telegram_bot_token:8593525164:AAGCX9B_RJGN35_F7tSB72rEZhS_4Zpcszs +chat_id:692714536 diff --git a/.gitignore b/.gitignore index 69d1f68..de2f7d4 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,8 @@ lint/tmp/ app/release/ app/debug/ *.apk + +# Dashboard local files +dashboard/node_modules/ +dashboard/server.log +dashboard/config.json diff --git a/README.md b/README.md index 8fd3d78..41a28ee 100644 --- a/README.md +++ b/README.md @@ -113,13 +113,6 @@ StreamPlayer ahora consulta automáticamente las releases públicas del reposito Si por algún motivo olvidas subir el manifiesto, la app igualmente tomará el primer asset `.apk` de la release, pero no podrá forzar versiones mínimas. -### Flujo dentro de la app - -- Cada vez que se abre `MainActivity` se consulta `https://gitea.cbcren.online/api/v1/repos/renato97/app/releases/latest`. -- Si `versionCode` del servidor es mayor al instalado se muestra un diálogo para actualizar; el usuario puede abrir la release o descargarla directamente. -- Si `minSupportedVersionCode` es mayor al instalado la app bloqueará el uso hasta actualizar, cumpliendo con el requerimiento de controlar instalaciones. -- La descarga se gestiona con `DownloadManager` y, una vez completada, se lanza el instalador usando FileProvider. - ### Dashboard de Dispositivos y Bloqueo Remoto Para saber en qué equipo está instalada la app y bloquear el acceso cuando lo necesites, se incluye un dashboard liviano en `dashboard/`: @@ -132,19 +125,43 @@ npm install npm start # escucha en http://localhost:4000 ``` -2. Ajusta `DEVICE_REGISTRY_URL` en `app/build.gradle` para apuntar al dominio/puerto donde despliegues el servidor (por defecto `http://localhost:4000`). -3. Distribuye el APK; cada vez que se abra la app enviará un registro a `POST /api/devices/register` con su `ANDROID_ID`, modelo y versión. -4. Entra a `http://TU_HOST:4000/` para ver el listado, asignar alias o bloquear/desbloquear dispositivos. +2. Copia `dashboard/config.example.json` a `dashboard/config.json` y completa `telegramBotToken` + `telegramChatId` (o usa variables de entorno `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID`). +3. Ajusta `DEVICE_REGISTRY_URL` en `app/build.gradle` para apuntar al dominio/puerto donde despliegues el servidor (ya configurado como `http://194.163.191.200:4000`). +4. Distribuye el APK; cada instalación reportará `ANDROID_ID`, modelo, IP pública y país. +5. Entra a `http://TU_HOST:4000/` para ver el listado, asignar alias, bloquear/desbloquear o validar tokens. El servidor guarda los datos en `dashboard/data/devices.json`, por lo que puedes versionarlo o respaldarlo fácilmente. Cada registro almacena: - `deviceId`: `Settings.Secure.ANDROID_ID` del equipo - `deviceName`, `manufacturer`, `model`, `osVersion` - `appVersionName`/`Code` -- `firstSeen`, `lastSeen`, `blocked`, `notes` +- `ip`, `country` detectados automáticamente +- `firstSeen`, `lastSeen`, `blocked`, `notes`, `verification.status` Cuando presionas “Bloquear”, la app recibe la respuesta `{"blocked": true}` y muestra un diálogo irreversible hasta que lo habilites. Esto añade una capa adicional de control aparte del sistema de actualizaciones. +### Flujo dentro de la app y tokens divididos + +- Cada vez que se abre `MainActivity` se consulta `https://gitea.cbcren.online/api/v1/repos/renato97/app/releases/latest`. +- Si `versionCode` del servidor es mayor al instalado se muestra un diálogo para actualizar; el usuario puede abrir la release o descargarla directamente. +- Si `minSupportedVersionCode` es mayor al instalado la app bloqueará el uso hasta actualizar, cumpliendo con el requerimiento de controlar instalaciones. +- La descarga se gestiona con `DownloadManager` y, una vez completada, se lanza el instalador usando FileProvider. +- Mientras el dashboard mantenga un dispositivo "Pendiente" o "Bloqueado", la app muestra un diálogo con el motivo y la mitad del token que debe compartir la persona. + +Cada instalación genera un token interno dividido en dos: + +1. **Parte cliente**: se muestra en el diálogo del dispositivo bloqueado para que el usuario pueda copiarla. +2. **Parte admin**: llega al bot de Telegram configurado junto con la IP, país y datos del dispositivo. + +Para autorizar un dispositivo pendiente: + +1. Obtén la parte cliente desde el usuario (visible en pantalla). +2. Copia la parte admin del mensaje de Telegram. +3. En el dashboard presiona “Verificar token” e introduce ambas mitades. Si coinciden, el estado pasa a "Verificado" y la app se desbloquea automáticamente. +4. A partir de allí puedes bloquear/desbloquear manualmente cuando quieras. + +Cada nuevo registro también dispara una notificación de Telegram para que puedas reaccionar en tiempo real. + ## 📱 Estructura del Proyecto ``` diff --git a/app/build.gradle b/app/build.gradle index 5790bfa..1edb999 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.streamplayer" minSdk 21 targetSdk 33 - versionCode 92000 - versionName "9.2.0" + versionCode 93000 + versionName "9.3.0" buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"' } diff --git a/app/src/main/java/com/streamplayer/DeviceRegistry.java b/app/src/main/java/com/streamplayer/DeviceRegistry.java index ddbce02..bf286ed 100644 --- a/app/src/main/java/com/streamplayer/DeviceRegistry.java +++ b/app/src/main/java/com/streamplayer/DeviceRegistry.java @@ -31,7 +31,7 @@ public class DeviceRegistry { public interface Callback { void onAllowed(); - void onBlocked(String reason); + void onBlocked(String reason, String tokenPart); void onError(String message); } @@ -82,14 +82,21 @@ public class DeviceRegistry { } String responseText = response.body().string(); JSONObject json = new JSONObject(responseText); - boolean blocked = json.optBoolean("blocked", false); JSONObject deviceJson = json.optJSONObject("device"); + JSONObject verificationJson = json.optJSONObject("verification"); + boolean blocked = json.optBoolean("blocked", false); String reason = json.optString("message"); if (TextUtils.isEmpty(reason) && deviceJson != null) { reason = deviceJson.optString("notes", ""); } + String tokenPart = ""; + if (verificationJson != null) { + boolean verificationRequired = verificationJson.optBoolean("required", false); + blocked = blocked || verificationRequired; + tokenPart = verificationJson.optString("clientTokenPart", ""); + } if (blocked) { - postBlocked(callback, reason); + postBlocked(callback, reason, tokenPart); } else { postAllowed(callback); } @@ -139,11 +146,13 @@ public class DeviceRegistry { mainHandler.post(callback::onAllowed); } - private void postBlocked(Callback callback, String reason) { + private void postBlocked(Callback callback, String reason, String tokenPart) { if (callback == null) { return; } - mainHandler.post(() -> callback.onBlocked(reason)); + String reasonText = reason == null ? "" : reason; + String token = tokenPart == null ? "" : tokenPart; + mainHandler.post(() -> callback.onBlocked(reasonText, token)); } private void postError(Callback callback, String message) { diff --git a/app/src/main/java/com/streamplayer/MainActivity.java b/app/src/main/java/com/streamplayer/MainActivity.java index 12ccb20..d6214b6 100644 --- a/app/src/main/java/com/streamplayer/MainActivity.java +++ b/app/src/main/java/com/streamplayer/MainActivity.java @@ -97,8 +97,8 @@ public class MainActivity extends AppCompatActivity { } @Override - public void onBlocked(String reason) { - showBlockedDialog(reason); + public void onBlocked(String reason, String tokenPart) { + showBlockedDialog(reason, tokenPart); } @Override @@ -307,7 +307,7 @@ public class MainActivity extends AppCompatActivity { } } - private void showBlockedDialog(String reason) { + private void showBlockedDialog(String reason, String tokenPart) { if (isFinishing()) { return; } @@ -317,9 +317,15 @@ public class MainActivity extends AppCompatActivity { if (blockedDialog != null && blockedDialog.isShowing()) { blockedDialog.dismiss(); } + StringBuilder messageBuilder = new StringBuilder(); + messageBuilder.append(getString(R.string.device_blocked_message, finalReason)); + if (!TextUtils.isEmpty(tokenPart)) { + messageBuilder.append("\n\n") + .append(getString(R.string.device_blocked_token_hint, tokenPart)); + } blockedDialog = new AlertDialog.Builder(this) .setTitle(R.string.device_blocked_title) - .setMessage(getString(R.string.device_blocked_message, finalReason)) + .setMessage(messageBuilder.toString()) .setCancelable(false) .setPositiveButton(R.string.device_blocked_close, (dialog, which) -> finish()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4858131..56d49b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,6 +39,7 @@ Dispositivo bloqueado Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s Sin motivo especificado. + Comparte este código con el administrador para solicitar acceso: %1$s Salir No se pudo registrar el dispositivo (%1$s) diff --git a/dashboard/config.example.json b/dashboard/config.example.json new file mode 100644 index 0000000..a67051f --- /dev/null +++ b/dashboard/config.example.json @@ -0,0 +1,4 @@ +{ + "telegramBotToken": "123456:ABCDEF-TOKEN", + "telegramChatId": "123456789" +} diff --git a/dashboard/data/devices.json b/dashboard/data/devices.json index fe51488..2bd66b3 100644 --- a/dashboard/data/devices.json +++ b/dashboard/data/devices.json @@ -1 +1,26 @@ -[] +[ + { + "deviceId": "f91f2668e8dfb2a7", + "alias": "", + "deviceName": "SM-S928B", + "model": "SM-S928B", + "manufacturer": "Samsung", + "osVersion": "16 (API 36)", + "appVersionName": "9.2.0", + "appVersionCode": 92000, + "firstSeen": "2025-11-23T20:53:43.615Z", + "lastSeen": "2025-11-23T21:09:04.607Z", + "blocked": false, + "notes": "no pagó", + "installs": 8, + "blockedAt": "2025-11-23T20:54:05.413Z", + "ip": "181.23.253.20", + "country": "AR", + "verification": { + "clientPart": "6e05a220abe0ed05", + "adminPart": "19d6ee4c992ee1a0", + "status": "pending", + "createdAt": "2025-11-23T21:09:04.607Z" + } + } +] \ No newline at end of file diff --git a/dashboard/node_modules/.package-lock.json b/dashboard/node_modules/.package-lock.json index f9365e7..8ce28ee 100644 --- a/dashboard/node_modules/.package-lock.json +++ b/dashboard/node_modules/.package-lock.json @@ -17,6 +17,21 @@ "node": ">= 0.6" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -37,11 +52,36 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/basic-auth": { @@ -103,7 +143,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -123,6 +162,15 @@ "node": ">=8" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -161,6 +209,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -186,11 +271,40 @@ "fsevents": "~2.3.2" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -251,6 +365,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -329,6 +452,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -390,6 +528,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -421,6 +568,42 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -439,6 +622,12 @@ "node": ">= 0.6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -448,6 +637,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geoip-lite": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.4.10.tgz", + "integrity": "sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w==", + "license": "Apache-2.0", + "dependencies": { + "async": "2.1 - 2.6.4", + "chalk": "4.1 - 4.1.2", + "iconv-lite": "0.4.13 - 0.6.3", + "ip-address": "5.8.9 - 5.9.4", + "lazy": "1.0.11", + "rimraf": "2.5.2 - 2.7.1", + "yauzl": "2.9.2 - 2.10.0" + }, + "engines": { + "node": ">=10.3.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -485,6 +692,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -532,6 +760,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -579,12 +822,37 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-5.9.4.tgz", + "integrity": "sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "lodash": "^4.17.15", + "sprintf-js": "1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -640,6 +908,27 @@ "node": ">=0.12.0" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==", + "license": "MIT", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -713,7 +1002,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -871,6 +1159,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -880,12 +1177,27 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -912,6 +1224,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -971,6 +1289,25 @@ "node": ">=8.10.0" } }, + "node_modules/request-ip": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", + "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1155,6 +1492,12 @@ "node": ">=10" } }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1255,6 +1598,22 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 912d2dc..d98c3ca 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -9,9 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "axios": "^1.6.7", "cors": "^2.8.5", "express": "^4.18.2", - "morgan": "^1.10.0" + "geoip-lite": "^1.4.6", + "morgan": "^1.10.0", + "request-ip": "^3.3.0" }, "devDependencies": { "nodemon": "^3.0.1" @@ -30,6 +33,21 @@ "node": ">= 0.6" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -50,11 +68,36 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/basic-auth": { @@ -116,7 +159,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -136,6 +178,15 @@ "node": ">=8" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -174,6 +225,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -199,11 +287,40 @@ "fsevents": "~2.3.2" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -264,6 +381,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -342,6 +468,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -403,6 +544,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -434,6 +584,42 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -452,6 +638,12 @@ "node": ">= 0.6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -476,6 +668,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geoip-lite": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.4.10.tgz", + "integrity": "sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w==", + "license": "Apache-2.0", + "dependencies": { + "async": "2.1 - 2.6.4", + "chalk": "4.1 - 4.1.2", + "iconv-lite": "0.4.13 - 0.6.3", + "ip-address": "5.8.9 - 5.9.4", + "lazy": "1.0.11", + "rimraf": "2.5.2 - 2.7.1", + "yauzl": "2.9.2 - 2.10.0" + }, + "engines": { + "node": ">=10.3.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -513,6 +723,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -560,6 +791,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -607,12 +853,37 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-5.9.4.tgz", + "integrity": "sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "lodash": "^4.17.15", + "sprintf-js": "1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -668,6 +939,27 @@ "node": ">=0.12.0" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==", + "license": "MIT", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -741,7 +1033,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -899,6 +1190,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -908,12 +1208,27 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -940,6 +1255,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -999,6 +1320,25 @@ "node": ">=8.10.0" } }, + "node_modules/request-ip": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", + "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1183,6 +1523,12 @@ "node": ">=10" } }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1283,6 +1629,22 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/dashboard/package.json b/dashboard/package.json index 92814dd..09b7a17 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -10,9 +10,12 @@ "author": "StreamPlayer", "license": "MIT", "dependencies": { + "axios": "^1.6.7", "cors": "^2.8.5", "express": "^4.18.2", - "morgan": "^1.10.0" + "geoip-lite": "^1.4.6", + "morgan": "^1.10.0", + "request-ip": "^3.3.0" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/dashboard/public/app.js b/dashboard/public/app.js index 76b22ff..1febcb1 100644 --- a/dashboard/public/app.js +++ b/dashboard/public/app.js @@ -21,7 +21,7 @@ async function fetchDevices() { function renderTable(devices) { tableBody.innerHTML = ''; if (!devices.length) { - tableBody.innerHTML = 'Sin registros'; + tableBody.innerHTML = 'Sin registros'; return; } devices.sort((a, b) => (a.lastSeen || '').localeCompare(b.lastSeen || '') * -1); @@ -31,6 +31,21 @@ function renderTable(devices) { tr.classList.add('blocked'); } const alias = device.alias && device.alias.trim().length ? device.alias : 'Sin alias'; + const verificationStatus = device.verification && device.verification.status ? device.verification.status : 'pending'; + const needsVerification = verificationStatus !== 'verified'; + const verificationText = needsVerification + ? `Pendiente - Token cliente: ${device.verification && device.verification.clientPart ? device.verification.clientPart : 'N/A'}` + : `Verificado ${device.verification.verifiedAt ? `(${formatDate(device.verification.verifiedAt)})` : ''}`; + const statusLabel = device.blocked + ? 'Bloqueado' + : needsVerification ? 'Pendiente token' : 'Activo'; + + const actions = [``]; + if (needsVerification) { + actions.push(''); + } + actions.push(device.blocked ? '' : ''); + tr.innerHTML = `
${alias}
@@ -39,11 +54,13 @@ function renderTable(devices) { ${device.deviceId} ${[device.manufacturer, device.model].filter(Boolean).join(' ')} ${device.appVersionName || ''} (${device.appVersionCode || ''}) + ${device.ip || '-'} + ${formatCountry(device.country)} + ${verificationText} ${formatDate(device.lastSeen)} - ${device.blocked ? 'Bloqueado' : 'Activo'} + ${statusLabel} - - ${device.blocked ? '' : ''} + ${actions.join(' ')} `; tr.dataset.deviceId = device.deviceId; @@ -58,6 +75,13 @@ function formatDate(value) { return date.toLocaleString(); } +function formatCountry(value) { + if (!value || value === 'N/A') { + return '-'; + } + return value; +} + async function blockDevice(deviceId) { const reason = prompt('Motivo del bloqueo (opcional):'); await fetch(`/api/devices/${encodeURIComponent(deviceId)}/block`, { @@ -73,6 +97,28 @@ async function unblockDevice(deviceId) { await fetchDevices(); } +async function verifyDevice(deviceId) { + const clientTokenPart = prompt('Introduce el token que aparece en el dispositivo:'); + if (clientTokenPart === null) { + return; + } + const adminTokenPart = prompt('Introduce el token recibido en Telegram:'); + if (adminTokenPart === null) { + return; + } + const response = await fetch(`/api/devices/${encodeURIComponent(deviceId)}/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ clientTokenPart, adminTokenPart }) + }); + if (!response.ok) { + const payload = await response.json().catch(() => ({})); + alert(payload.error || 'No se pudo verificar el token'); + return; + } + await fetchDevices(); +} + async function updateAlias(deviceId) { const alias = prompt('Nuevo alias para el dispositivo:'); if (alias === null) { @@ -106,6 +152,8 @@ tableBody.addEventListener('click', async (event) => { await unblockDevice(deviceId); } else if (action === 'alias') { await updateAlias(deviceId); + } else if (action === 'verify') { + await verifyDevice(deviceId); } } catch (error) { console.error(error); diff --git a/dashboard/public/index.html b/dashboard/public/index.html index 0520fae..4a694b1 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -22,6 +22,9 @@ Device ID Modelo Versión app + IP Pública + País + Verificación Última vez visto Estado Acciones diff --git a/dashboard/server.js b/dashboard/server.js index a10da8e..d48ccf6 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -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 || {}; diff --git a/update-manifest.json b/update-manifest.json index 60bf2e9..d6872ff 100644 --- a/update-manifest.json +++ b/update-manifest.json @@ -1,10 +1,10 @@ { - "versionCode": 91010, - "versionName": "9.1.1", + "versionCode": 92000, + "versionName": "9.2.0", "minSupportedVersionCode": 91000, "forceUpdate": false, - "downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.1.1/StreamPlayer-v9.1.1.apk", - "fileName": "StreamPlayer-v9.1.1.apk", - "sizeBytes": 5940765, - "notes": "StreamPlayer v9.1.1 - Device Registry and Remote Blocking\n\nNovedades principales:\n- Device Registry para gestión remota de dispositivos\n- Dashboard web para monitoreo y bloqueo de dispositivos\n- Bloqueo remoto con control administrativo\n- Sistema de alias y notas para dispositivos\n- Mejoras en seguridad y control de acceso\n- Panel de control en tiempo real\n\nEsta versión incluye importantes mejoras de seguridad y permite un control centralizado sobre los dispositivos donde está instalada la aplicación." + "downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.2.0/StreamPlayer-v9.2.0.apk", + "fileName": "StreamPlayer-v9.2.0.apk", + "sizeBytes": 5940764, + "notes": "StreamPlayer v9.2.0\n\nMejoras en esta versión:\n\n- Interfaz de usuario optimizada para mayor claridad\n- Diálogos de actualización más intuitivos\n- Mejora general en la experiencia de uso\n- Mayor estabilidad y rendimiento\n- Correcciones de errores menores\n\nEsta actualización mejora la usabilidad y mantiene todas las funcionalidades de seguridad y gestión de dispositivos." }