7 Commits

Author SHA1 Message Date
77c417117a Update v9.4.3: Manual Events Refresh & Background Sync 2025-11-25 19:01:21 +00:00
renato97
2c65578bdd Update v9.4.2: Enhanced UI Theme & Visual Consistency
- Incremented version to 9.4.2 (versionCode: 94200)
- Added custom AlertDialog theme with white text styling
- Enhanced visual consistency for all dialog components
- Improved theme overlay for better readability
- Applied custom styling to update and blocked dialogs
- Better contrast and visual hierarchy in dialogs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:13:29 +01:00
renato97
6aef195f30 Update v9.4.1: Enhanced Playback & Device Management
- Incremented version to 9.4.1 (versionCode: 94100)
- Added keep screen on functionality during video playback
- Implemented device deletion in dashboard with confirmation
- Enhanced device management with delete capability
- Improved user experience during media playback
- Better device lifecycle management in dashboard
- Added confirmation dialog for device deletion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:07:04 +01:00
renato97
cc4696dec2 Update v9.4.0: Advanced Telegram Integration & Custom UI
- Incremented version to 9.4.0 (versionCode: 94000)
- Added custom blocked dialog layout with improved UX
- Implemented Telegram commands: /allow, /deny, /pending
- Enhanced Telegram polling for real-time device management
- Custom token display with click-to-copy functionality
- Improved device verification workflow via Telegram
- Better error handling and user feedback
- Enhanced documentation for Telegram integration
- Improved token validation and management system

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 23:15:37 +01:00
renato97
73a4f81341 Update v9.3.1: Enhanced UX and Environment Configuration
- Incremented version to 9.3.1 (versionCode: 93100)
- Added copy token button in blocked dialog for better UX
- Fixed environment variable configuration for Telegram integration
- Improved clipboard functionality for token sharing
- Enhanced dashboard environment handling with dotenv
- Corrected variable names for Telegram configuration
- Improved error handling for token copy operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:18:31 +01:00
renato97
adadb89232 Update v9.3.0: Enhanced Security with Telegram Integration
- Incremented version to 9.3.0 (versionCode: 93000)
- Added Telegram integration for device notifications
- Implemented token-based verification system
- Enhanced device registry with IP/country detection
- Added split token verification for admin/user validation
- Improved dashboard with real-time notifications
- Enhanced blocking system with token verification
- Added geo-location tracking for devices
- Improved device management interface
- Enhanced security controls and monitoring

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:10:21 +01:00
renato97
d326f4c549 Update v9.2.0: Enhanced UI and generic update messages
- Incremented version to 9.2.0 (versionCode: 92000)
- Simplified update dialog buttons: "Actualizar ahora" → "Actualizar"
- Changed "Cerrar aplicación" to "Salir" for better UX
- Maintained security and device management features
- Improved user interface text for better user experience

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:52:11 +01:00
22 changed files with 1482 additions and 63 deletions

3
.env
View File

@@ -1,3 +1,4 @@
GITEA_TOKEN=7921aa22187b39125d29399d26f527ba26a2fb5b
GEMINI_API_KEY=AIzaSyDWOgyAJqscuPU6iSpS6gxupWBm4soNw5o
TELEGRAM_BOT_TOKEN=8593525164:AAGCX9B_RJGN35_F7tSB72rEZhS_4Zpcszs
TELEGRAM_CHAT_ID=692714536

5
.gitignore vendored
View File

@@ -129,3 +129,8 @@ lint/tmp/
app/release/
app/debug/
*.apk
# Dashboard local files
dashboard/node_modules/
dashboard/server.log
dashboard/config.json

View File

@@ -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,49 @@ 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.
También puedes gestionar todo desde Telegram:
- `/allow <deviceId> <token_cliente>` autoriza el dispositivo (verifica el token y lo desbloquea).
- `/deny <deviceId> <token_cliente> [motivo]` lo bloquea con un motivo opcional.
- `/pending` lista los registros que aún esperan un token válido.
Cada nuevo registro dispara una notificación de Telegram con la parte admin del token y recordatorios de esos comandos.
## 📱 Estructura del Proyecto
```

View File

@@ -8,8 +8,8 @@ android {
applicationId "com.streamplayer"
minSdk 21
targetSdk 33
versionCode 91020
versionName "9.1.2"
versionCode 94300
versionName "9.4.3"
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
}

View File

@@ -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) {

View File

@@ -73,6 +73,21 @@ public class EventRepository {
}).start();
}
public void prefetchEvents(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
new Thread(() -> {
try {
String json = downloadJson();
parseEvents(json);
prefs.edit()
.putString(KEY_JSON, json)
.putLong(KEY_TIMESTAMP, System.currentTimeMillis())
.apply();
} catch (IOException | JSONException ignored) {
}
}).start();
}
private String downloadJson() throws IOException {
URL url = new URL(EVENTS_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

View File

@@ -1,14 +1,20 @@
package com.streamplayer;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.text.TextUtils;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
@@ -21,14 +27,18 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
private static final long EVENT_PREFETCH_INTERVAL_MS = TimeUnit.HOURS.toMillis(1);
private RecyclerView sectionList;
private RecyclerView contentList;
private ProgressBar loadingIndicator;
private TextView messageView;
private TextView contentTitle;
private Button eventsRefreshButton;
private ChannelAdapter channelAdapter;
private EventAdapter eventAdapter;
@@ -43,6 +53,9 @@ public class MainActivity extends AppCompatActivity {
private AlertDialog updateDialog;
private AlertDialog blockedDialog;
private DeviceRegistry deviceRegistry;
private Handler eventPrefetchHandler;
private Runnable eventPrefetchRunnable;
private boolean isEventsRefreshing;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -54,6 +67,8 @@ public class MainActivity extends AppCompatActivity {
loadingIndicator = findViewById(R.id.loading_indicator);
messageView = findViewById(R.id.message_view);
contentTitle = findViewById(R.id.content_title);
eventsRefreshButton = findViewById(R.id.events_refresh_button);
eventsRefreshButton.setOnClickListener(v -> manualRefreshEvents());
channelAdapter = new ChannelAdapter(
channel -> openPlayer(channel.getName(), channel.getPageUrl()));
@@ -97,8 +112,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
@@ -110,6 +125,8 @@ public class MainActivity extends AppCompatActivity {
}
}
});
startEventPrefetchScheduler();
}
@Override
@@ -135,6 +152,12 @@ public class MainActivity extends AppCompatActivity {
if (deviceRegistry != null) {
deviceRegistry.release();
}
stopEventPrefetchScheduler();
}
@Override
public void onBackPressed() {
closeAppCompletely();
}
private void selectSection(int index) {
@@ -154,6 +177,7 @@ public class MainActivity extends AppCompatActivity {
}
private void showChannels(SectionEntry section) {
updateEventsRefreshVisibility(false);
contentTitle.setText(section.title);
contentList.setLayoutManager(channelLayoutManager);
contentList.setAdapter(channelAdapter);
@@ -169,6 +193,7 @@ public class MainActivity extends AppCompatActivity {
}
private void showEvents() {
updateEventsRefreshVisibility(true);
contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events));
contentList.setLayoutManager(eventLayoutManager);
contentList.setAdapter(eventAdapter);
@@ -180,6 +205,9 @@ public class MainActivity extends AppCompatActivity {
}
private void loadEvents(boolean forceRefresh) {
if (currentSection != null && currentSection.type == SectionEntry.Type.EVENTS) {
setEventsRefreshing(true);
}
loadingIndicator.setVisibility(View.VISIBLE);
messageView.setVisibility(View.GONE);
eventRepository.loadEvents(this, forceRefresh, new EventRepository.Callback() {
@@ -193,6 +221,7 @@ public class MainActivity extends AppCompatActivity {
} else {
loadingIndicator.setVisibility(View.GONE);
}
setEventsRefreshing(false);
});
}
@@ -202,11 +231,48 @@ public class MainActivity extends AppCompatActivity {
loadingIndicator.setVisibility(View.GONE);
messageView.setVisibility(View.VISIBLE);
messageView.setText(getString(R.string.message_events_error, message));
setEventsRefreshing(false);
});
}
});
}
private void manualRefreshEvents() {
if (isEventsRefreshing) {
return;
}
if (currentSection == null || currentSection.type != SectionEntry.Type.EVENTS) {
return;
}
loadEvents(true);
}
private void updateEventsRefreshVisibility(boolean visible) {
if (eventsRefreshButton == null) {
return;
}
eventsRefreshButton.setVisibility(visible ? View.VISIBLE : View.GONE);
if (visible) {
eventsRefreshButton.setEnabled(!isEventsRefreshing);
eventsRefreshButton.setText(isEventsRefreshing
? getString(R.string.events_refreshing)
: getString(R.string.events_refresh_action));
}
}
private void setEventsRefreshing(boolean refreshing) {
isEventsRefreshing = refreshing;
if (eventsRefreshButton == null) {
return;
}
if (eventsRefreshButton.getVisibility() == View.VISIBLE) {
eventsRefreshButton.setEnabled(!refreshing);
eventsRefreshButton.setText(refreshing
? getString(R.string.events_refreshing)
: getString(R.string.events_refresh_action));
}
}
private void displayEvents() {
loadingIndicator.setVisibility(View.GONE);
if (cachedEvents.isEmpty()) {
@@ -241,7 +307,7 @@ public class MainActivity extends AppCompatActivity {
if (updateDialog != null && updateDialog.isShowing()) {
updateDialog.dismiss();
}
AlertDialog.Builder builder = new AlertDialog.Builder(this)
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
.setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title)
.setMessage(buildUpdateMessage(info))
.setPositiveButton(R.string.update_action_download,
@@ -251,7 +317,7 @@ public class MainActivity extends AppCompatActivity {
if (mandatory) {
builder.setCancelable(false);
builder.setNegativeButton(R.string.update_action_close_app,
(dialog, which) -> finish());
(dialog, which) -> closeAppCompletely());
} else {
builder.setNegativeButton(R.string.update_action_later, null);
}
@@ -307,7 +373,7 @@ public class MainActivity extends AppCompatActivity {
}
}
private void showBlockedDialog(String reason) {
private void showBlockedDialog(String reason, String tokenPart) {
if (isFinishing()) {
return;
}
@@ -317,13 +383,76 @@ public class MainActivity extends AppCompatActivity {
if (blockedDialog != null && blockedDialog.isShowing()) {
blockedDialog.dismiss();
}
blockedDialog = new AlertDialog.Builder(this)
View dialogView = getLayoutInflater().inflate(R.layout.dialog_blocked, null);
TextView messageText = dialogView.findViewById(R.id.blocked_message_text);
View tokenContainer = dialogView.findViewById(R.id.blocked_token_container);
TextView tokenValue = dialogView.findViewById(R.id.blocked_token_value);
messageText.setText(getString(R.string.device_blocked_message, finalReason));
boolean hasToken = !TextUtils.isEmpty(tokenPart);
if (hasToken) {
tokenContainer.setVisibility(View.VISIBLE);
tokenValue.setText(tokenPart);
tokenValue.setOnClickListener(v -> copyTokenToClipboard(tokenPart));
} else {
tokenContainer.setVisibility(View.GONE);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
.setTitle(R.string.device_blocked_title)
.setMessage(getString(R.string.device_blocked_message, finalReason))
.setView(dialogView)
.setCancelable(false)
.setPositiveButton(R.string.device_blocked_close,
(dialog, which) -> finish())
.show();
(dialog, which) -> closeAppCompletely());
if (hasToken) {
builder.setNeutralButton(R.string.device_blocked_copy_token,
(dialog, which) -> copyTokenToClipboard(tokenPart));
}
blockedDialog = builder.create();
blockedDialog.show();
}
private void copyTokenToClipboard(String tokenPart) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard == null) {
Toast.makeText(this, R.string.device_blocked_copy_error, Toast.LENGTH_SHORT).show();
return;
}
ClipData data = ClipData.newPlainText("token", tokenPart);
clipboard.setPrimaryClip(data);
Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show();
}
private void startEventPrefetchScheduler() {
if (eventPrefetchHandler == null) {
eventPrefetchHandler = new Handler(Looper.getMainLooper());
}
if (eventPrefetchRunnable == null) {
eventPrefetchRunnable = new Runnable() {
@Override
public void run() {
if (eventRepository != null) {
eventRepository.prefetchEvents(getApplicationContext());
}
if (eventPrefetchHandler != null) {
eventPrefetchHandler.postDelayed(this, EVENT_PREFETCH_INTERVAL_MS);
}
}
};
} else {
eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable);
}
eventPrefetchHandler.post(eventPrefetchRunnable);
}
private void stopEventPrefetchScheduler() {
if (eventPrefetchHandler != null && eventPrefetchRunnable != null) {
eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable);
}
}
private void closeAppCompletely() {
finishAffinity();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private int getSpanCount() {

View File

@@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.StrictMode;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -58,6 +59,7 @@ public class PlayerActivity extends AppCompatActivity {
);
setContentView(R.layout.activity_player);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
Intent intent = getIntent();
if (intent == null) {
@@ -254,6 +256,7 @@ public class PlayerActivity extends AppCompatActivity {
protected void onDestroy() {
super.onDestroy();
releasePlayer();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
private void toggleOverlay() {

View File

@@ -72,14 +72,32 @@
app:layout_constraintStart_toEndOf="@id/divider"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/content_title"
<LinearLayout
android:id="@+id/content_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Canales" />
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/content_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Canales" />
<Button
android:id="@+id/events_refresh_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/events_refresh_action"
android:textAllCaps="false"
android:visibility="gone" />
</LinearLayout>
<ProgressBar
android:id="@+id/loading_indicator"

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/blocked_message_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/blocked_token_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/blocked_token_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/device_blocked_token_label"
android:textColor="@android:color/black"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/blocked_token_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:padding="8dp"
android:textColor="@android:color/black"
android:textIsSelectable="true"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -9,9 +9,9 @@
<string name="message_events_error">No se pudieron cargar los eventos: %1$s</string>
<string name="update_required_title">Actualización obligatoria</string>
<string name="update_available_title">Actualización disponible</string>
<string name="update_action_download">Actualizar ahora</string>
<string name="update_action_download">Actualizar</string>
<string name="update_action_view_release">Ver detalles</string>
<string name="update_action_close_app">Cerrar aplicación</string>
<string name="update_action_close_app">Salir</string>
<string name="update_action_later">Más tarde</string>
<string name="update_current_version">Versión instalada: %1$s (%2$d)</string>
<string name="update_latest_version">Última versión publicada: %1$s (%2$d)</string>
@@ -39,6 +39,13 @@
<string name="device_blocked_title">Dispositivo bloqueado</string>
<string name="device_blocked_message">Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s</string>
<string name="device_blocked_default_reason">Sin motivo especificado.</string>
<string name="device_blocked_token_hint">Comparte este código con el administrador para solicitar acceso: %1$s</string>
<string name="device_blocked_token_label">Código de verificación</string>
<string name="device_blocked_close">Salir</string>
<string name="device_blocked_copy_token">Copiar código</string>
<string name="device_blocked_copy_success">Código copiado al portapapeles</string>
<string name="device_blocked_copy_error">No se pudo copiar el código</string>
<string name="device_registry_error">No se pudo registrar el dispositivo (%1$s)</string>
<string name="events_refresh_action">Actualizar ahora</string>
<string name="events_refreshing">Actualizando...</string>
</resources>

View File

@@ -6,4 +6,10 @@
<item name="android:statusBarColor">@color/black</item>
<item name="android:navigationBarColor">@color/black</item>
</style>
<style name="ThemeOverlay.StreamPlayer.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">@color/white</item>
<item name="colorAccent">@color/white</item>
</style>
</resources>

View File

@@ -0,0 +1,4 @@
{
"telegramBotToken": "123456:ABCDEF-TOKEN",
"telegramChatId": "123456789"
}

View File

@@ -1 +1,26 @@
[]
[
{
"deviceId": "f91f2668e8dfb2a7",
"alias": "",
"deviceName": "SM-S928B",
"model": "SM-S928B",
"manufacturer": "Samsung",
"osVersion": "16 (API 36)",
"appVersionName": "9.4.3",
"appVersionCode": 94300,
"firstSeen": "2025-11-23T22:31:13.359Z",
"lastSeen": "2025-11-23T23:11:07.215Z",
"blocked": false,
"notes": "",
"installs": 7,
"ip": "181.23.253.20",
"country": "AR",
"verification": {
"clientPart": "1714c2bb93670c3f",
"adminPart": "9924c7049211c58c",
"status": "verified",
"createdAt": "2025-11-23T22:31:13.359Z",
"verifiedAt": "2025-11-23T22:33:11.942Z"
}
}
]

View File

@@ -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",
@@ -270,6 +393,18 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -329,6 +464,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 +540,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 +580,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 +634,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 +649,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 +704,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 +772,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 +834,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 +920,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 +1014,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 +1171,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 +1189,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 +1236,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 +1301,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 +1504,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 +1610,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"
}
}
}
}

View File

@@ -9,9 +9,13 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"axios": "^1.6.7",
"cors": "^2.8.5",
"dotenv": "^16.4.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 +34,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 +69,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 +160,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 +179,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 +226,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 +288,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 +382,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",
@@ -283,6 +410,18 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -342,6 +481,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 +557,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 +597,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 +651,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 +681,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 +736,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 +804,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 +866,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 +952,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 +1046,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 +1203,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 +1221,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 +1268,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 +1333,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 +1536,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 +1642,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"
}
}
}
}

View File

@@ -10,9 +10,13 @@
"author": "StreamPlayer",
"license": "MIT",
"dependencies": {
"axios": "^1.6.7",
"dotenv": "^16.4.5",
"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"

View File

@@ -21,7 +21,7 @@ async function fetchDevices() {
function renderTable(devices) {
tableBody.innerHTML = '';
if (!devices.length) {
tableBody.innerHTML = '<tr><td colspan="7" class="empty">Sin registros</td></tr>';
tableBody.innerHTML = '<tr><td colspan="10" class="empty">Sin registros</td></tr>';
return;
}
devices.sort((a, b) => (a.lastSeen || '').localeCompare(b.lastSeen || '') * -1);
@@ -31,6 +31,22 @@ 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 = [`<button data-action="alias">Alias</button>`];
if (needsVerification) {
actions.push('<button data-action="verify" class="primary">Verificar token</button>');
}
actions.push(device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>');
actions.push('<button data-action="delete" class="danger ghost">Borrar</button>');
tr.innerHTML = `
<td>
<div class="alias">${alias}</div>
@@ -39,11 +55,13 @@ function renderTable(devices) {
<td>${device.deviceId}</td>
<td>${[device.manufacturer, device.model].filter(Boolean).join(' ')}</td>
<td>${device.appVersionName || ''} (${device.appVersionCode || ''})</td>
<td>${device.ip || '-'}</td>
<td>${formatCountry(device.country)}</td>
<td>${verificationText}</td>
<td>${formatDate(device.lastSeen)}</td>
<td>${device.blocked ? 'Bloqueado' : 'Activo'}</td>
<td>${statusLabel}</td>
<td class="actions-cell">
<button data-action="alias">Alias</button>
${device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>'}
${actions.join(' ')}
</td>
`;
tr.dataset.deviceId = device.deviceId;
@@ -58,6 +76,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 +98,41 @@ async function unblockDevice(deviceId) {
await fetchDevices();
}
async function deleteDevice(deviceId) {
const confirmation = confirm('¿Seguro que quieres borrar este dispositivo? Generará un nuevo token cuando se registre de nuevo.');
if (!confirmation) {
return;
}
const response = await fetch(`/api/devices/${encodeURIComponent(deviceId)}`, { method: 'DELETE' });
if (!response.ok) {
alert('No se pudo borrar el dispositivo');
return;
}
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 +166,10 @@ tableBody.addEventListener('click', async (event) => {
await unblockDevice(deviceId);
} else if (action === 'alias') {
await updateAlias(deviceId);
} else if (action === 'verify') {
await verifyDevice(deviceId);
} else if (action === 'delete') {
await deleteDevice(deviceId);
}
} catch (error) {
console.error(error);

View File

@@ -22,6 +22,9 @@
<th>Device ID</th>
<th>Modelo</th>
<th>Versión app</th>
<th>IP Pública</th>
<th>País</th>
<th>Verificación</th>
<th>Última vez visto</th>
<th>Estado</th>
<th>Acciones</th>

View File

@@ -3,10 +3,29 @@ const cors = require('cors');
const fs = require('fs');
const path = require('path');
const morgan = require('morgan');
const crypto = require('crypto');
const axios = require('axios');
const requestIp = require('request-ip');
const geoip = require('geoip-lite');
const dotenv = require('dotenv');
const envPath = path.resolve(__dirname, '..', '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
} else {
dotenv.config();
}
const app = express();
const PORT = process.env.PORT || 4000;
const DATA_PATH = path.join(__dirname, 'data', 'devices.json');
const CONFIG_PATH = path.join(__dirname, 'config.json');
const config = loadConfig();
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || config.telegramBotToken || '';
const TELEGRAM_CHAT_ID = (process.env.TELEGRAM_CHAT_ID || config.telegramChatId || '').toString();
let telegramOffset = 0;
let telegramPollingStarted = false;
app.use(cors());
app.use(express.json());
@@ -20,17 +39,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 +79,211 @@ function sanitizeId(input) {
return String(input || '').trim();
}
function sanitizeIp(ip) {
if (!ip) return '';
if (ip.startsWith('::ffff:')) {
return ip.replace('::ffff:', '');
}
return ip;
}
function lookupLocation(ip) {
if (!ip) {
return { country: 'N/A', region: '', city: '' };
}
const geo = geoip.lookup(ip);
if (!geo) {
return { country: 'N/A', region: '', city: '' };
}
return {
country: geo.country || 'N/A',
region: geo.region || '',
city: geo.city || ''
};
}
function generateTokenParts() {
const fullToken = crypto.randomBytes(16).toString('hex');
const mid = Math.floor(fullToken.length / 2);
return {
clientPart: fullToken.slice(0, mid),
adminPart: fullToken.slice(mid)
};
}
async function sendTelegramMessage(message, options = {}) {
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
return;
}
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
try {
const payload = {
chat_id: TELEGRAM_CHAT_ID,
text: message,
disable_web_page_preview: true
};
if (options.parseMode) {
payload.parse_mode = options.parseMode;
}
await axios.post(url, payload);
} catch (error) {
console.warn('Failed to send Telegram message', error.response ? error.response.data : error.message);
}
}
async function sendTelegramNotification(message) {
await sendTelegramMessage(message, { parseMode: 'Markdown' });
}
function formatTelegramMessage(device, verificationRequired) {
const lines = [
'*Nuevo registro de dispositivo*',
`ID: ${device.deviceId}`,
`Alias: ${device.alias || '-'}`,
`Modelo: ${device.manufacturer} ${device.model}`,
`Versión app: ${device.appVersionName} (${device.appVersionCode})`,
`IP: ${device.ip || '-'} / País: ${device.country || '-'}`,
`Última vez visto: ${device.lastSeen}`
];
if (verificationRequired && device.verification) {
lines.push('`Token Admin` (guárdalo): `' + device.verification.adminPart + '`');
lines.push('Autorizar: `/allow ' + device.deviceId + ' TOKEN_CLIENTE`');
lines.push('Rechazar: `/deny ' + device.deviceId + ' TOKEN_CLIENTE [motivo]`');
}
return lines.join('\n');
}
async function initializeTelegramOffset() {
if (!TELEGRAM_BOT_TOKEN) {
return;
}
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`;
try {
const { data } = await axios.get(url, { params: { timeout: 1 } });
if (data.ok && Array.isArray(data.result) && data.result.length > 0) {
telegramOffset = data.result[data.result.length - 1].update_id + 1;
}
} catch (error) {
console.warn('Failed to initialize Telegram offset', error.message);
}
}
async function pollTelegramUpdates() {
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
return;
}
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`;
const params = {
timeout: 25
};
if (telegramOffset) {
params.offset = telegramOffset;
}
try {
const { data } = await axios.get(url, { params });
if (data.ok && Array.isArray(data.result)) {
for (const update of data.result) {
telegramOffset = update.update_id + 1;
if (update.message) {
processTelegramMessage(update.message);
}
}
}
} catch (error) {
console.warn('Telegram polling error', error.message);
} finally {
setTimeout(pollTelegramUpdates, 3000);
}
}
async function startTelegramPolling() {
if (telegramPollingStarted || !TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
return;
}
telegramPollingStarted = true;
await initializeTelegramOffset();
pollTelegramUpdates();
}
function processTelegramMessage(message) {
const chatId = message.chat && message.chat.id ? message.chat.id.toString() : '';
if (chatId && TELEGRAM_CHAT_ID && chatId !== TELEGRAM_CHAT_ID.toString()) {
return;
}
const text = (message.text || '').trim();
if (!text.startsWith('/')) {
return;
}
const parts = text.split(/\s+/);
const command = parts[0].toLowerCase();
if (command === '/allow' && parts.length >= 3) {
handleTelegramAllow(parts[1], parts[2]);
} else if (command === '/deny' && parts.length >= 3) {
const reason = parts.slice(3).join(' ') || 'Bloqueado desde Telegram';
handleTelegramDeny(parts[1], parts[2], reason);
} else if (command === '/pending') {
handleTelegramPending();
} else {
sendTelegramMessage('Comandos disponibles:\n/allow <deviceId> <token_cliente>\n/deny <deviceId> <token_cliente> [motivo]\n/pending');
}
}
async function handleTelegramAllow(deviceId, clientToken) {
const devices = readDevices();
const device = devices.find(d => d.deviceId === deviceId);
if (!device || !device.verification) {
await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`);
return;
}
if (device.verification.status === 'verified') {
await sendTelegramMessage(` El dispositivo ${deviceId} ya está verificado.`);
return;
}
if (device.verification.clientPart !== clientToken.trim()) {
await sendTelegramMessage('❌ Token del cliente inválido.');
return;
}
device.verification.status = 'verified';
device.verification.verifiedAt = new Date().toISOString();
device.blocked = false;
device.notes = '';
writeDevices(devices);
await sendTelegramMessage(`✅ Dispositivo ${deviceId} autorizado correctamente.`);
}
async function handleTelegramDeny(deviceId, clientToken, reason) {
const devices = readDevices();
const device = devices.find(d => d.deviceId === deviceId);
if (!device || !device.verification) {
await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`);
return;
}
if (device.verification.clientPart !== clientToken.trim()) {
await sendTelegramMessage('❌ Token del cliente inválido.');
return;
}
device.verification.status = 'denied';
device.verification.deniedAt = new Date().toISOString();
device.blocked = true;
device.notes = reason;
writeDevices(devices);
await sendTelegramMessage(`🚫 Dispositivo ${deviceId} bloqueado. Motivo: ${reason}`);
}
async function handleTelegramPending() {
const devices = readDevices();
const pending = devices.filter(d => !d.verification || d.verification.status !== 'verified');
if (!pending.length) {
await sendTelegramMessage('No hay dispositivos pendientes.');
return;
}
const lines = pending.slice(0, 10).map(device => {
const token = device.verification ? device.verification.clientPart : 'N/A';
return `${device.deviceId} - Token cliente: ${token}`;
});
await sendTelegramMessage('Pendientes:\n' + lines.join('\n'));
}
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
@@ -75,8 +311,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 +331,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 +352,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 +409,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 || {};
@@ -162,4 +460,5 @@ app.delete('/api/devices/:deviceId', (req, res) => {
app.listen(PORT, () => {
ensureDataFile();
console.log(`StreamPlayer dashboard server listening on port ${PORT}`);
startTelegramPolling();
});

View File

@@ -0,0 +1,10 @@
{
"versionCode": 91020,
"versionName": "9.1.2",
"minSupportedVersionCode": 91000,
"forceUpdate": false,
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.1.2/StreamPlayer-v9.1.2.apk",
"fileName": "StreamPlayer-v9.1.2.apk",
"sizeBytes": 5940784,
"notes": "StreamPlayer v9.1.2 - Device Registry IP Fix\n\nCorrecciones importantes:\n- Fix de Device Registry URL: ahora apunta a IP pública 194.163.191.200:4000\n- Resuelve problema de conexión con el dashboard de dispositivos\n- Habilita registro de dispositivos remotos correctamente\n- Mejora funcionalidad completa de Device Registry\n- Mayor estabilidad en el sistema de gestión remota\n\nEsta actualización es recomendada para asegurar el funcionamiento correcto del sistema de registro y bloqueo remoto de dispositivos."
}

View File

@@ -1,10 +1,10 @@
{
"versionCode": 91010,
"versionName": "9.1.1",
"versionCode": 94300,
"versionName": "9.4.3",
"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.4.3/StreamPlayer-v9.4.3.apk",
"fileName": "StreamPlayer-v9.4.3.apk",
"sizeBytes": 5947283,
"notes": "StreamPlayer v9.4.3\n\nNovedades destacadas:\n\n- Botón \"Actualizar ahora\" en la sección de Eventos para refrescar manualmente la grilla.\n- Sincronización silenciosa cada 60 minutos para precargar nuevos eventos sin interrumpir la reproducción.\n- Cache persiste y se aplica automáticamente al reiniciar la app para asegurar datos frescos.\n- Cierre completo al salir para garantizar que cada inicio recargue con la última información disponible.\n- Correcciones y mejoras generales de estabilidad en la sección de eventos."
}