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."
}