Askhat777 commited on
Commit
e1fca83
·
verified ·
1 Parent(s): 9b04107

Update proxy_server.py

Browse files
Files changed (1) hide show
  1. proxy_server.py +409 -160
proxy_server.py CHANGED
@@ -1,34 +1,52 @@
1
  """
2
- PROXY SERVER - Публичный прокси для MT5
 
3
  """
4
- from fastapi import FastAPI, HTTPException, Request
5
  from fastapi.middleware.cors import CORSMiddleware
6
- import httpx
 
7
  import os
8
  import time
9
- import uvicorn
 
10
  from datetime import datetime
 
 
11
 
12
- # ==================== КОНФИГУРАЦИЯ ИЗ СЕКРЕТОВ ====================
13
- # Эти значения берутся из Secrets Hugging Face
14
- SAFE_BACKEND_URL = os.environ.get("SAFE_BACKEND_URL", "")
15
- SAFE_API_TOKEN = os.environ.get("SAFE_API_TOKEN", "")
16
-
17
- # Проверяем, что секреты установлены
18
- if not SAFE_BACKEND_URL or not SAFE_API_TOKEN:
19
- print("❌ ОШИБКА: Не настроены секреты в Hugging Face!")
20
- print("❌ SAFE_BACKEND_URL:", "установлен" if SAFE_BACKEND_URL else "НЕ УСТАНОВЛЕН")
21
- print("❌ SAFE_API_TOKEN:", "установлен" if SAFE_API_TOKEN else "НЕ УСТАНОВЛЕН")
22
- print("❌ Инструкция: Зайдите в Settings → Variables and secrets")
23
- print("❌ Добавьте секреты:")
24
- print("❌ SAFE_BACKEND_URL = https://askhat777-gold-backend-secure.hf.space")
25
- print("❌ SAFE_API_TOKEN = GOLD_SAFE_2026_u7x9A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P")
 
 
 
 
 
 
26
 
27
- app = FastAPI(title="Gold Options Proxy", version="1.0")
 
28
 
29
- # Клиент HTTP для запросов к Safe Backend
30
- client = None
 
 
 
 
 
31
 
 
32
  app.add_middleware(
33
  CORSMiddleware,
34
  allow_origins=["*"],
@@ -37,181 +55,412 @@ app.add_middleware(
37
  allow_headers=["*"],
38
  )
39
 
40
- # ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
41
- def log_request(license_key: str, dte: int, ip: str):
42
- """Логирование запроса"""
43
- timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
44
- print(f"[{timestamp}] 📥 Запрос от {ip}: key={license_key[:8]}..., dte={dte}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- def log_response(status: int, response_time: float):
47
- """Логирование ответа"""
48
- print(f"📤 Ответ: {status} ({response_time:.2f} сек)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  # ==================== API ЭНДПОИНТЫ ====================
51
  @app.get("/")
52
  def root():
53
  return {
54
- "status": "Gold Options Proxy",
55
- "version": "1.0",
 
 
56
  "endpoints": {
57
  "data": "/api/mt5/data?license_key=TEST_KEY_12345&dte=0",
58
  "health": "/health",
59
- "config": "/config"
60
- },
61
- "note": "Публичный прокси-сервер для MT5",
62
- "config_status": {
63
- "safe_backend_configured": bool(SAFE_BACKEND_URL),
64
- "api_token_configured": bool(SAFE_API_TOKEN)
65
  }
66
  }
67
 
68
  @app.get("/health")
69
  def health_check():
70
- """Проверка здоровья прокси"""
71
  return {
72
  "status": "healthy",
73
  "timestamp": datetime.now().isoformat(),
74
- "proxy_version": "1.0",
75
- "uptime": time.time() - startup_time,
76
- "config": {
77
- "safe_backend": "configured" if SAFE_BACKEND_URL else "not_configured",
78
- "api_token": "configured" if SAFE_API_TOKEN else "not_configured"
79
- }
80
  }
81
 
82
- @app.get("/config")
83
- def show_config():
84
- """Показать конфигурацию (без секретов)"""
85
  return {
86
- "safe_backend_url": SAFE_BACKEND_URL,
87
- "safe_api_token_configured": bool(SAFE_API_TOKEN),
88
- "proxy_version": "1.0",
89
- "request_timeout": 10.0
90
  }
91
 
92
  @app.get("/api/mt5/data")
93
- async def proxy_mt5_data(
94
- request: Request,
95
  license_key: str,
96
- dte: int = 0
 
97
  ):
98
  """
99
- Прокси-эндпоинт для MT5.
100
- Принимает запрос от MT5, добавляет токен и пересылает в Safe Backend.
 
 
 
 
101
  """
102
 
103
- # Логирование входящего запроса
104
- client_ip = request.client.host if request.client else "unknown"
105
- log_request(license_key, dte, client_ip)
106
- start_time = time.time()
107
-
108
- # Проверяем конфигурацию
109
- if not SAFE_BACKEND_URL or not SAFE_API_TOKEN:
110
- error_msg = "Proxy не настроен. Отсутствуют SAFE_BACKEND_URL или SAFE_API_TOKEN в секретах."
111
- print(f"❌ {error_msg}")
112
- log_response(500, time.time() - start_time)
113
- raise HTTPException(status_code=500, detail=error_msg)
114
-
115
- # Формируем URL для запроса к Safe Backend
116
- safe_url = f"{SAFE_BACKEND_URL.rstrip('/')}/api/mt5/data"
117
-
118
- # Заголовки для Safe Backend
119
- headers = {
120
- "X-API-Token": SAFE_API_TOKEN,
121
- "User-Agent": "Gold-Proxy/1.0",
122
- "X-Forwarded-For": client_ip,
123
- "X-Client-License": license_key[:8] + "..." # Только начало для логирования
124
- }
125
 
126
- # Параметры запроса
127
- query_params = {
128
- "license_key": license_key,
129
- "dte": dte
130
- }
131
 
132
- # Максимальное количество попыток
133
- max_retries = 3
134
- last_error = None
 
 
 
 
 
 
 
135
 
136
- for attempt in range(max_retries):
137
- try:
138
- print(f"🔄 Попытка #{attempt + 1} к Safe Backend")
139
-
140
- async with httpx.AsyncClient(timeout=10.0) as client:
141
- response = await client.get(
142
- safe_url,
143
- params=query_params,
144
- headers=headers
145
- )
146
-
147
- # Если успешно
148
- if response.status_code == 200:
149
- response_time = time.time() - start_time
150
- print(f"✅ Успешный ответ от Safe Backend ({response_time:.2f} сек)")
151
- log_response(200, response_time)
152
- return response.json()
153
- else:
154
- last_error = f"Safe Backend вернул {response.status_code}: {response.text[:100]}"
155
- print(f"❌ {last_error}")
156
-
157
- except httpx.TimeoutException:
158
- last_error = f"Timeout при попытке #{attempt + 1}"
159
- print(f"⌛ {last_error}")
160
- except httpx.RequestError as e:
161
- last_error = f"Ошибка сети: {e}"
162
- print(f"🌐 {last_error}")
163
- except Exception as e:
164
- last_error = f"Неизвестная ошибка: {e}"
165
- print(f"⚠️ {last_error}")
166
-
167
- # Ждем перед повторной попыткой (экспоненциальная задержка)
168
- if attempt < max_retries - 1:
169
- wait_time = 0.5 * (2 ** attempt) # 0.5, 1.0, 2.0 секунды
170
- print(f"⏳ Ждем {wait_time:.1f} сек перед повторной попыткой...")
171
- await asyncio.sleep(wait_time)
172
-
173
- # Если все попытки неудачны
174
- error_detail = f"Не удалось подключиться к Safe Backend после {max_retries} попыток. Последняя ошибка: {last_error}"
175
- print(f"❌ {error_detail}")
176
- log_response(503, time.time() - start_time)
177
-
178
- raise HTTPException(
179
- status_code=503,
180
- detail=error_detail
181
- )
182
 
183
- @app.exception_handler(Exception)
184
- async def global_exception_handler(request: Request, exc: Exception):
185
- """Глобальный обработчик исключений"""
186
- error_time = time.time()
187
- error_msg = str(exc)
 
 
 
 
 
188
 
189
- print(f"❌ Глобальная ошибка: {error_msg}")
 
190
 
 
191
  return {
192
- "status": "error",
193
- "detail": error_msg,
194
- "timestamp": error_time,
195
- "path": request.url.path,
196
- "proxy_version": "1.0"
197
  }
198
 
199
  # ==================== ЗАПУСК ====================
200
- startup_time = time.time()
201
-
202
  @app.on_event("startup")
203
- async def startup_event():
204
- print("=" * 60)
205
- print("🚀 GOLD OPTIONS PROXY ЗАПУЩЕН")
206
- print("=" * 60)
207
- print(f"Safe Backend URL: {SAFE_BACKEND_URL}")
208
- print(f"API Token настроен: {'✅ Да' if SAFE_API_TOKEN else '❌ Нет'}")
209
- print(f"Текущее время: {datetime.now().isoformat()}")
210
- print("=" * 60)
211
-
212
- if not SAFE_BACKEND_URL or not SAFE_API_TOKEN:
213
- print("⚠️ ВНИМАНИЕ: Секреты не настроены!")
214
- print("👉 Зайдите в Settings → Variables and secrets")
215
- print("👉 Добавьте:")
216
- print(" SAFE_BACKEND_URL = https://askhat777-gold-backend-secure.hf.space")
217
- print(" SAFE_API_TOKEN = GOLD_SAFE_2026_u7x9A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P")
 
1
  """
2
+ SAFE BACKEND - Приватный сервер с проверкой лицензий из файла
3
+ Схема: MT5 → PROXY → SAFE_BACKEND
4
  """
5
+ from fastapi import FastAPI, HTTPException, Request, Header
6
  from fastapi.middleware.cors import CORSMiddleware
7
+ import firebase_admin
8
+ from firebase_admin import db, credentials
9
  import os
10
  import time
11
+ import threading
12
+ import json
13
  from datetime import datetime
14
+ from typing import Optional, Tuple, Dict
15
+ import uvicorn
16
 
17
+ # ==================== КОНФИГУРАЦИЯ ====================
18
+ class Config:
19
+ # Токены из секретов HF
20
+ SAFE_API_TOKEN = os.environ.get(
21
+ "SAFE_API_TOKEN",
22
+ "GOLD_SAFE_2026_u7x9A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P"
23
+ )
24
+ ADMIN_TOKEN = os.environ.get(
25
+ "ADMIN_TOKEN",
26
+ "GOLD_ADMIN_2026_98765ZYXWVUTSRQPONMLKJIHGFEDCBA"
27
+ )
28
+
29
+ # Пути
30
+ LICENSES_FILE = "licenses.json"
31
+ FIREBASE_CRED_FILE = "firebase-creds.json"
32
+ DATABASE_URL = "https://cmeparser-default-rtdb.europe-west1.firebasedatabase.app"
33
+
34
+ # Кэширование
35
+ PRICE_CACHE_TTL = 3
36
+ LEVELS_CACHE_TTL = 180
37
 
38
+ # ==================== ИНИЦИАЛИЗАЦИЯ ====================
39
+ app = FastAPI(title="Gold Safe Backend", version="2.2")
40
 
41
+ print("\n" + "=" * 70)
42
+ print("⚠️ ПРОВЕРКА КОНФИГУРАЦИИ SAFE BACKEND")
43
+ print("=" * 70)
44
+ print(f"✅ SAFE_API_TOKEN установлен: {bool(os.environ.get('SAFE_API_TOKEN'))}")
45
+ print(f"✅ ADMIN_TOKEN установлен: {bool(os.environ.get('ADMIN_TOKEN'))}")
46
+ print(f"📄 Файл лицензий: {Config.LICENSES_FILE}")
47
+ print("=" * 70 + "\n")
48
 
49
+ # CORS
50
  app.add_middleware(
51
  CORSMiddleware,
52
  allow_origins=["*"],
 
55
  allow_headers=["*"],
56
  )
57
 
58
+ # ==================== МЕНЕДЖЕР ЛИЦЕНЗИЙ ====================
59
+ class LicenseManager:
60
+ """Управление лицензиями из файла"""
61
+
62
+ def __init__(self, file_path: str):
63
+ self.file_path = file_path
64
+ self.licenses = {}
65
+ self.last_load_time = 0
66
+ self.load_licenses()
67
+
68
+ def load_licenses(self):
69
+ """Загружает лицензии из файла"""
70
+ try:
71
+ if os.path.exists(self.file_path):
72
+ with open(self.file_path, 'r', encoding='utf-8') as f:
73
+ data = json.load(f)
74
+ self.licenses = data.get('licenses', {})
75
+ self.last_load_time = time.time()
76
+ print(f"✅ Загружено {len(self.licenses)} лицензий из {self.file_path}")
77
+ self._print_licenses_summary()
78
+ else:
79
+ print(f"⚠️ Файл лицензий не найден: {self.file_path}")
80
+ self.licenses = {}
81
+ except Exception as e:
82
+ print(f"❌ Ошибка загрузки лицензий: {e}")
83
+ self.licenses = {}
84
+
85
+ def _print_licenses_summary(self):
86
+ """Печатает сводку лицензий"""
87
+ for key, lic in self.licenses.items():
88
+ status = "✅ АКТИВНА" if lic.get('active') else "❌ ОТКЛЮЧЕНА"
89
+ days = lic.get('days_remaining', 0)
90
+ plan = lic.get('plan', 'UNKNOWN')
91
+ print(f" {key[:15]}... | {plan:6} | {days:3} дней | {status}")
92
+
93
+ def verify_license(self, license_key: str) -> Tuple[bool, Optional[Dict]]:
94
+ """
95
+ Проверяет лицензию
96
+ Возвращает (валидна ли, данные лицензии)
97
+ """
98
+ # Перезагружаем файл каждые 5 секунд
99
+ if time.time() - self.last_load_time > 5:
100
+ self.load_licenses()
101
+
102
+ if license_key not in self.licenses:
103
+ return False, None
104
+
105
+ lic = self.licenses[license_key]
106
+
107
+ # Проверка активности
108
+ if not lic.get('active', False):
109
+ return False, None
110
+
111
+ # Проверка срока действия
112
+ days_remaining = lic.get('days_remaining', 0)
113
+ if days_remaining <= 0:
114
+ return False, None
115
+
116
+ return True, lic
117
+
118
+ def add_license(self, license_key: str, plan: str, days: int) -> bool:
119
+ """Добавляет новую лицензию"""
120
+ if license_key in self.licenses:
121
+ return False
122
+
123
+ self.licenses[license_key] = {
124
+ "active": True,
125
+ "plan": plan,
126
+ "days_remaining": days,
127
+ "max_daily_requests": 1000 if plan == "PRO" else 100,
128
+ "created_at": datetime.now().isoformat() + "Z",
129
+ "expires_at": (datetime.now().replace(hour=23, minute=59, second=59)).isoformat() + "Z"
130
+ }
131
+
132
+ self._save_licenses()
133
+ return True
134
+
135
+ def _save_licenses(self):
136
+ """Сохраняет лицензии в файл"""
137
+ try:
138
+ data = {
139
+ "licenses": self.licenses,
140
+ "last_updated": datetime.now().isoformat() + "Z"
141
+ }
142
+ with open(self.file_path, 'w', encoding='utf-8') as f:
143
+ json.dump(data, f, indent=2, ensure_ascii=False)
144
+ print(f"✅ Лицензии сохранены в {self.file_path}")
145
+ except Exception as e:
146
+ print(f"❌ Ошибка сохранения лицензий: {e}")
147
+
148
+ # Создаем менеджер лицензий
149
+ license_manager = LicenseManager(Config.LICENSES_FILE)
150
+
151
+ # ==================== MIDDLEWARE ПРОВЕРКИ ТОКЕНА ====================
152
+ @app.middleware("http")
153
+ async def verify_api_token(request: Request, call_next):
154
+ """
155
+ Проверяет токен для /api/* эндпоинтов
156
+ Логирует все попытки доступа
157
+ """
158
+ # Пропускаем публичные эндпоинты
159
+ if request.url.path in ["/", "/health", "/docs", "/openapi.json", "/redoc"]:
160
+ return await call_next(request)
161
+
162
+ if request.url.path.startswith("/api/"):
163
+ token_from_header = request.headers.get("X-API-Token", "")
164
+ client_ip = request.client.host if request.client else "unknown"
165
+
166
+ print(f"\n🔐 API ЗАПРОС")
167
+ print(f" IP: {client_ip}")
168
+ print(f" Path: {request.url.path}")
169
+ print(f" Токен из заголовка: {'✓ НАЙДЕН' if token_from_header else '✗ НЕ НАЙДЕН'}")
170
+
171
+ if not token_from_header:
172
+ print(f" ❌ ОШИБКА: Токен отсутствует!")
173
+ raise HTTPException(
174
+ status_code=401,
175
+ detail="Missing X-API-Token header"
176
+ )
177
+
178
+ if token_from_header != Config.SAFE_API_TOKEN:
179
+ print(f" ❌ ОШИБКА: Неверный токен!")
180
+ print(f" Получен: {token_from_header[:20]}...")
181
+ print(f" Ожидается: {Config.SAFE_API_TOKEN[:20]}...")
182
+ raise HTTPException(
183
+ status_code=401,
184
+ detail="Invalid X-API-Token"
185
+ )
186
+
187
+ print(f" ✅ Токен верный!")
188
+
189
+ response = await call_next(request)
190
+ return response
191
 
192
+ # ==================== FIREBASE ====================
193
+ print("\n🔥 ИНИЦИАЛИЗАЦИЯ FIREBASE...")
194
+ try:
195
+ if not firebase_admin._apps:
196
+ if os.path.exists(Config.FIREBASE_CRED_FILE):
197
+ cred = credentials.Certificate(Config.FIREBASE_CRED_FILE)
198
+ firebase_admin.initialize_app(cred, {
199
+ 'databaseURL': Config.DATABASE_URL
200
+ })
201
+ print("✅ Firebase подключен успешно!")
202
+ else:
203
+ print("⚠️ Firebase credentials не найдены - работаем в демо-режиме")
204
+ except Exception as e:
205
+ print(f"❌ Ошибка Firebase: {e}")
206
+
207
+ # ==================== КЛАСС КЭША ====================
208
+ class DataCache:
209
+ """Умный кэш с автообновлением"""
210
+
211
+ def __init__(self):
212
+ self.price_cache = {
213
+ 'data': 4323.5,
214
+ 'timestamp': time.time(),
215
+ 'ttl': Config.PRICE_CACHE_TTL
216
+ }
217
+ self.levels_cache = {}
218
+ self.start_background_updater()
219
+
220
+ def start_background_updater(self):
221
+ """Фоновое обновление данных"""
222
+ def updater():
223
+ while True:
224
+ try:
225
+ self._update_price_if_needed()
226
+ for dte in list(self.levels_cache.keys()):
227
+ self._update_levels_if_needed(dte)
228
+ except Exception as e:
229
+ print(f"⚠️ Ошибка фонового обновления: {e}")
230
+ time.sleep(1)
231
+
232
+ thread = threading.Thread(target=updater, daemon=True)
233
+ thread.start()
234
+ print("✅ Фоновое обновление запущено")
235
+
236
+ def _update_price_if_needed(self):
237
+ """Обновляет цену каждые 3 секунды"""
238
+ now = time.time()
239
+ if (now - self.price_cache['timestamp']) > self.price_cache['ttl']:
240
+ try:
241
+ ref = db.reference('current_price/price')
242
+ price = ref.get()
243
+ if price:
244
+ self.price_cache['data'] = float(price)
245
+ self.price_cache['timestamp'] = now
246
+ print(f"💰 Цена обновлена: {price}")
247
+ except Exception as e:
248
+ pass # Firebase может быть недоступен
249
+
250
+ def _update_levels_if_needed(self, dte: int):
251
+ """Обновляет уровни каждые 3 минуты"""
252
+ now = time.time()
253
+ cache_key = f"dte_{dte}"
254
+
255
+ if cache_key not in self.levels_cache:
256
+ self.levels_cache[cache_key] = {'data': {'calls': [], 'puts': []}, 'timestamp': 0}
257
+
258
+ if (now - self.levels_cache[cache_key]['timestamp']) > Config.LEVELS_CACHE_TTL:
259
+ try:
260
+ data = self._fetch_levels_from_firebase(dte)
261
+ self.levels_cache[cache_key]['data'] = data
262
+ self.levels_cache[cache_key]['timestamp'] = now
263
+ print(f"📊 Уровни DTE={dte} обновлены")
264
+ except Exception as e:
265
+ pass
266
+
267
+ def _fetch_levels_from_firebase(self, dte: int) -> dict:
268
+ """Получает данные из Firebase"""
269
+ try:
270
+ ref = db.reference(f'gc/mt5/breakevens/dte_{dte}')
271
+ data = ref.get()
272
+
273
+ if not data:
274
+ return {'calls': [], 'puts': []}
275
+
276
+ calls = []
277
+ if 'calls_0dte' in data:
278
+ raw = data['calls_0dte']
279
+ items = list(raw.values()) if isinstance(raw, dict) else raw
280
+ for item in items:
281
+ if isinstance(item, dict) and item.get('v', 0) > 0:
282
+ calls.append({
283
+ 'be': float(item.get('be', 0)),
284
+ 's': float(item.get('s', 0)),
285
+ 'v': int(item.get('v', 0)),
286
+ 'oi': int(item.get('oi', 0)),
287
+ 'pr': float(item.get('pr', 0))
288
+ })
289
+
290
+ puts = []
291
+ if 'puts_0dte' in data:
292
+ raw = data['puts_0dte']
293
+ items = list(raw.values()) if isinstance(raw, dict) else raw
294
+ for item in items:
295
+ if isinstance(item, dict) and item.get('v', 0) > 0:
296
+ puts.append({
297
+ 'be': float(item.get('be', 0)),
298
+ 's': float(item.get('s', 0)),
299
+ 'v': int(item.get('v', 0)),
300
+ 'oi': int(item.get('oi', 0)),
301
+ 'pr': float(item.get('pr', 0))
302
+ })
303
+
304
+ calls.sort(key=lambda x: x['v'], reverse=True)
305
+ puts.sort(key=lambda x: x['v'], reverse=True)
306
+
307
+ return {'calls': calls[:10], 'puts': puts[:10]}
308
+ except Exception as e:
309
+ return {'calls': [], 'puts': []}
310
+
311
+ def get_price(self) -> float:
312
+ return self.price_cache['data']
313
+
314
+ def get_levels(self, dte: int) -> dict:
315
+ cache_key = f"dte_{dte}"
316
+ if cache_key not in self.levels_cache:
317
+ self.levels_cache[cache_key] = {'data': {'calls': [], 'puts': []}, 'timestamp': 0}
318
+ return self.levels_cache[cache_key]['data']
319
+
320
+ cache = DataCache()
321
 
322
  # ==================== API ЭНДПОИНТЫ ====================
323
  @app.get("/")
324
  def root():
325
  return {
326
+ "name": "Gold Safe Backend",
327
+ "version": "2.2",
328
+ "status": "online",
329
+ "licenses_loaded": len(license_manager.licenses),
330
  "endpoints": {
331
  "data": "/api/mt5/data?license_key=TEST_KEY_12345&dte=0",
332
  "health": "/health",
333
+ "stats": "/api/stats"
 
 
 
 
 
334
  }
335
  }
336
 
337
  @app.get("/health")
338
  def health_check():
339
+ """Healthcheck (без проверки токена)"""
340
  return {
341
  "status": "healthy",
342
  "timestamp": datetime.now().isoformat(),
343
+ "price": cache.get_price(),
344
+ "licenses_count": len(license_manager.licenses)
 
 
 
 
345
  }
346
 
347
+ @app.get("/api/stats")
348
+ def get_stats():
349
+ """Статистика (требует токен в заголовке)"""
350
  return {
351
+ "price": cache.get_price(),
352
+ "total_licenses": len(license_manager.licenses),
353
+ "active_licenses": sum(1 for l in license_manager.licenses.values() if l.get('active')),
354
+ "timestamp": datetime.now().isoformat()
355
  }
356
 
357
  @app.get("/api/mt5/data")
358
+ def get_mt5_data(
 
359
  license_key: str,
360
+ dte: int = 0,
361
+ x_api_token: Optional[str] = Header(None)
362
  ):
363
  """
364
+ Основной эндпоинт
365
+ MT5 PROXY SAFE_BACKEND
366
+
367
+ Требует:
368
+ - X-API-Token в заголовке (для Proxy)
369
+ - Валидный license_key
370
  """
371
 
372
+ print(f"\n📥 ЗАПРОС MT5 DATA")
373
+ print(f" License: {license_key}")
374
+ print(f" DTE: {dte}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
+ # 1. Проверка лицензии
377
+ is_valid, lic_data = license_manager.verify_license(license_key)
 
 
 
378
 
379
+ if not is_valid:
380
+ print(f" ❌ Лицензия НЕВАЛИДНА или ИСТЕКЛА!")
381
+ raise HTTPException(
382
+ status_code=401,
383
+ detail={
384
+ "error": "INVALID_OR_EXPIRED_LICENSE",
385
+ "license_key": license_key,
386
+ "message": "Лицензия невалидна, отключена или истекла"
387
+ }
388
+ )
389
 
390
+ print(f" ✅ Лицензия ВАЛИДНА ({lic_data.get('plan')})")
391
+
392
+ try:
393
+ # 2. Получаем данные
394
+ price = cache.get_price()
395
+ levels = cache.get_levels(dte)
396
+
397
+ # 3. Формируем ответ
398
+ response = {
399
+ "status": "ok",
400
+ "price": price,
401
+ "dte": dte,
402
+ "license_plan": lic_data.get("plan"),
403
+ "days_remaining": lic_data.get("days_remaining"),
404
+ "calls": levels.get("calls", []),
405
+ "puts": levels.get("puts", []),
406
+ "timestamp": datetime.now().isoformat(),
407
+ "calls_count": len(levels.get("calls", [])),
408
+ "puts_count": len(levels.get("puts", []))
409
+ }
410
+
411
+ print(f" 📤 Ответ отправлен ({len(response['calls'])} calls, {len(response['puts'])} puts)")
412
+ return response
413
+
414
+ except Exception as e:
415
+ print(f"Ошибка: {e}")
416
+ raise HTTPException(status_code=500, detail=str(e))
417
+
418
+ # ==================== АДМИН ЭНДПОИНТЫ ====================
419
+ @app.get("/api/admin/licenses")
420
+ def admin_get_licenses(admin_token: str):
421
+ """Получить все лицензии"""
422
+ if admin_token != Config.ADMIN_TOKEN:
423
+ raise HTTPException(status_code=403, detail="Invalid admin token")
424
+
425
+ return {
426
+ "licenses": license_manager.licenses,
427
+ "total": len(license_manager.licenses),
428
+ "active": sum(1 for l in license_manager.licenses.values() if l.get('active')),
429
+ "timestamp": datetime.now().isoformat()
430
+ }
 
 
 
 
 
431
 
432
+ @app.post("/api/admin/add-license")
433
+ def admin_add_license(
434
+ admin_token: str,
435
+ license_key: str,
436
+ plan: str = "PRO",
437
+ days: int = 30
438
+ ):
439
+ """Добавить лицензию"""
440
+ if admin_token != Config.ADMIN_TOKEN:
441
+ raise HTTPException(status_code=403, detail="Invalid admin token")
442
 
443
+ if not license_manager.add_license(license_key, plan, days):
444
+ raise HTTPException(status_code=400, detail="License already exists")
445
 
446
+ print(f"✅ Новая лицензия добавлена: {license_key} ({plan})")
447
  return {
448
+ "status": "created",
449
+ "license_key": license_key,
450
+ "plan": plan,
451
+ "days": days
 
452
  }
453
 
454
  # ==================== ЗАПУСК ====================
 
 
455
  @app.on_event("startup")
456
+ async def startup():
457
+ print("\n" + "=" * 70)
458
+ print("🚀 SAFE BACKEND ЗАПУЩЕН")
459
+ print("=" * 70)
460
+ print(f"Время: {datetime.now().isoformat()}")
461
+ print(f"API Token: {Config.SAFE_API_TOKEN[:15]}...")
462
+ print(f"Лицензий загружено: {len(license_manager.licenses)}")
463
+ print("=" * 70 + "\n")
464
+
465
+ if __name__ == "__main__":
466
+ uvicorn.run(app, host="0.0.0.0", port=8000)