""" Единый модуль аутентификации и создания HTTP-клиента. Раньше _client(), _client_id(), _stand(), _token_info() были продублированы в main.py и api_test.py с идентичным или почти идентичным кодом. Теперь всё здесь — один источник правды для всех роутов. Режимы работы (2026-08-03): - Эмуляция (mode=polygon): всё в полигон, стенд из селектора (dev/test/prod) - Облако (mode=cloud): реальный API, стенд автоопределяется по токену Cookie (устанавливаются main.py:action=set_mode): - mode — "polygon" (по умолчанию) или "cloud" - polygon_stand — "test" (по умолчанию), "dev", "prod" Функции (все без аргументов — берут данные из Flask request/current_app): get_mode() — "polygon" / "cloud" get_polygon_stand() — "dev" / "test" / "prod" get_token() — токен: cookie → env (в эмуляции только env) get_client() — HttpClient с учётом режима и стенда get_real_client() — алиас get_client() (историческая совместимость) get_client_id() — ClientID из JWT (base64url, без проверки подписи) get_stand() — "polygon_test" / "dev" / "test" get_token_info() — {email, company, client_id} из JWT для UI get_token_masked() — маскированный токен (abc...xyz) для placeholder """ import base64 import json from flask import request, current_app from api.http_client import HttpClient, detect_endpoint, stand_name, STANDS def get_mode(): """Режим: 'polygon' (эмуляция) или 'cloud' (реальное облако). Если POLYGON_ENDPOINT не задан в конфиге — всегда облако (селектор не нужен). Иначе — из cookie, по умолчанию эмуляция.""" if not current_app.config.get("POLYGON_ENDPOINT", ""): return "cloud" return request.cookies.get("mode", "polygon") def get_polygon_stand(): """Стенд полигона: 'test' (по умолчанию), 'dev', 'prod'. Используется ТОЛЬКО в режиме эмуляции — для построения URL и load_service_ids.""" return request.cookies.get("polygon_stand", "test") def get_token(): """Получить активный токен. В эмуляции — ТОЛЬКО env-токен (пользовательский ввод дезактивирован). В облаке — cookie → env (как раньше).""" if get_mode() == "polygon": return current_app.config["NUBES_API_TOKEN"] return request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"] def get_client(): """HttpClient с учётом режима и стенда. Эмуляция: polygon_url + "/" + stand → https://polygon.../{stand}/api/v1/svc Облако: реальный API с автоопределением стенда (dev/test по токену).""" token = get_token() mode = get_mode() if mode == "polygon": polygon = current_app.config.get("POLYGON_ENDPOINT", "") stand = get_polygon_stand() # POLYGON_ENDPOINT = "https://polygon.../api/v1/svc" # Стенд ВСТАВЛЯЕТСЯ перед /api/v1/svc: # "https://polygon.../api/v1/svc" → "https://polygon.../test/api/v1/svc" # Простая замена "/api/v1/svc" → "/{stand}/api/v1/svc" return HttpClient(polygon.replace("/api/v1/svc", f"/{stand}/api/v1/svc"), token) # Облако: NUBES_API_ENDPOINT с автоопределением endpoint = current_app.config["NUBES_API_ENDPOINT"] if endpoint in STANDS: endpoint = detect_endpoint(token) or endpoint return HttpClient(endpoint, token) def get_real_client(): """Алиас get_client() — историческая совместимость. Раньше get_real_client() всегда ходил в реальный API (для сервисов). Теперь в эмуляции сервисы тоже из полигона → обе функции идентичны. Оставлено чтобы не ломать все места вызова.""" return get_client() def get_client_id(): """Извлечь ClientID из payload JWT-токена (base64url, без проверки подписи). JWT состоит из трёх частей: header.payload.signature, разделённых точкой. Нам нужен ТОЛЬКО payload — он в base64url (не base64!). ClientID используется для: - Изоляции данных в БД (runs.client_id) - Изоляции трекера инстансов (/tmp/instances-{clientId}-{stand}.json) - Отображения в UI Безопасность: мы НЕ проверяем подпись — это не нужно. Токен уже проверен Nubes API (detect_endpoint делает реальный запрос).""" token = get_token() try: parts = token.split(".") # [header, payload, signature] if len(parts) >= 2: # base64url → добавляем padding ("==") на случай если длина не кратна 4 payload = base64.urlsafe_b64decode(parts[1] + "==") return json.loads(payload).get("ClientID", "") except Exception: pass # битый токен — не критично, вернём пустую строку return "" def get_stand(): """Определить стенд. Эмуляция: "polygon_{dev|test|prod}" — для load_service_ids и трекера. Облако: автоопределение или stand_name (dev/test/mock).""" mode = get_mode() if mode == "polygon": return f"polygon_{get_polygon_stand()}" token = get_token() endpoint = current_app.config["NUBES_API_ENDPOINT"] if endpoint not in STANDS: s = stand_name(endpoint) return s if s != "?" else "mock" endpoint = detect_endpoint(token) or endpoint return stand_name(endpoint) def get_token_info(): """Извлечь {email, company, client_id} из JWT — для отображения в топбаре UI. Возвращает dict с ключами: email, company, client_id. Если JWT невалиден — возвращает пустой {}.""" token = get_token() try: parts = token.split(".") if len(parts) >= 2: payload = base64.urlsafe_b64decode(parts[1] + "==") d = json.loads(payload) return { "email": d.get("email", ""), "company": d.get("company_name", ""), "client_id": d.get("ClientID", ""), } except Exception: pass return {} def get_token_masked(): """Маскированный env-токен для placeholder в форме: abc...xyz. Используется ТОЛЬКО env-токен (не пользовательский!). Если токен короче 8 символов — возвращает пустую строку.""" token = current_app.config["NUBES_API_TOKEN"] if not token or len(token) < 8: return "" # Первые 4 символа + звёздочки + последние 4 символа return token[:4] + "*" * (len(token) - 8) + token[-4:]