# -*- coding: utf-8 -*- # hub/my_account/auth_service.py """Сервис аутентификации Dispatch: проверка логина/пароля и запись сессии. Назначение модуля: Полностью повторяет контракт сервиса USMS `hub/my_account/auth_service`, но обращается к локальному каталогу `DB_dispatch` независимого приложения Dispatch. Источники данных: - `DB_dispatch/0_users.py` — список учётных записей диспетчеров и руководителей сервисной службы; - `DB_dispatch/1_actual_state.py` — текущая активная сессия. Архитектурные ограничения: - Каталог `DB_dispatch` располагается на одном уровне с каталогом `dispatch`, поэтому путь вычисляется относительно текущего файла. - Файлы данных читаются через `importlib.util` без подключения к фреймворку USMS. Это сохраняет независимость дистрибутива. - Запись сессии перезаписывает файл целиком, без частичных правок. """ from __future__ import annotations import importlib.util import os import uuid from datetime import datetime, timezone def _resolve_db_dir() -> str: """Вернуть абсолютный путь к каталогу `DB_dispatch`. Структура размещения: /dispatch/hub/my_account/auth_service.py /DB_dispatch/0_users.py Поэтому переход — четыре уровня вверх от текущего файла, затем спуск в каталог `DB_dispatch`. """ here = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(os.path.dirname(os.path.dirname(here))) return os.path.join(project_root, "DB_dispatch") _DB_DIR = _resolve_db_dir() def _load_users() -> list[dict]: """Загрузить список пользователей из `DB_dispatch/0_users.py`.""" path = os.path.join(_DB_DIR, "0_users.py") spec = importlib.util.spec_from_file_location("_dispatch_db_users", path) if spec is None or spec.loader is None: return [] mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return list(getattr(mod, "users", [])) def load_facility_locations() -> dict[str, str]: """Загрузить таблицу `facility_id → строка локации` из `DB_dispatch/2_customer_facility_list.py`.""" path = os.path.join(_DB_DIR, "2_customer_facility_list.py") if not os.path.exists(path): return {} spec = importlib.util.spec_from_file_location("_dispatch_db_facilities", path) if spec is None or spec.loader is None: return {} mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) raw = getattr(mod, "DEFAULT_BUTTON_LOCATIONS", {}) return {str(k): str(v) for k, v in dict(raw).items()} def load_software_list() -> dict[str, str]: """Загрузить таблицу `software_id → наименование` из `DB_dispatch/3_software_list.py`.""" path = os.path.join(_DB_DIR, "3_software_list.py") if not os.path.exists(path): return {} spec = importlib.util.spec_from_file_location("_dispatch_db_software", path) if spec is None or spec.loader is None: return {} mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) raw = getattr(mod, "software_list_ru", {}) return {str(k): str(v) for k, v in dict(raw).items()} def load_malfunction_list() -> dict[str, list[str]]: """Загрузить таблицу `software_id → список заголовков` из `DB_dispatch/4_malfunction_list.py`.""" path = os.path.join(_DB_DIR, "4_malfunction_list.py") if not os.path.exists(path): return {} spec = importlib.util.spec_from_file_location("_dispatch_db_malfunction", path) if spec is None or spec.loader is None: return {} mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) raw = getattr(mod, "malfunction_list_ru", {}) return {str(k): [str(item) for item in list(values)] for k, values in dict(raw).items()} def get_user_facility(user: dict | None) -> str: """Вернуть строку локации, привязанную к пользователю. Источник связи — поле `facility_id` в `DB_dispatch/0_users.py` и реестр `DEFAULT_BUTTON_LOCATIONS` из `DB_dispatch/2_customer_facility_list.py`. Возвращает пустую строку, если пользователь не передан или связь не настроена. """ if not user: return "" facility_id = str(user.get("facility_id", "")).strip() if not facility_id: return "" locations = load_facility_locations() return locations.get(facility_id, "") def authenticate(login: str, password: str) -> tuple[dict | None, str]: """Проверить пару логин/пароль. Возвращает ``(user_dict, "")`` при успехе или ``(None, error_code)``: * ``"no_user"`` — логин не найден; * ``"bad_password"`` — логин верный, пароль не совпадает. """ users = _load_users() matched = [u for u in users if u.get("login") == login] if not matched: return None, "no_user" if matched[0].get("password") == password: return matched[0], "" return None, "bad_password" def write_session(user: dict) -> dict: """Записать сессию в `DB_dispatch/1_actual_state.py` и вернуть её.""" state = { "state_id": str(uuid.uuid4()), "user_id": user["user_id"], "is_online": True, "current_module": "dispatch", "last_activity_ts": datetime.now(timezone.utc).isoformat(), } path = os.path.join(_DB_DIR, "1_actual_state.py") with open(path, "w", encoding="utf-8") as fh: fh.write("# -*- coding: utf-8 -*-\n") fh.write("# DB_dispatch/1_actual_state.py\n\n") fh.write(f"actual_state = [{repr(state)}]\n") return state def clear_session() -> None: """Сбросить активную сессию (выход из системы).""" path = os.path.join(_DB_DIR, "1_actual_state.py") with open(path, "w", encoding="utf-8") as fh: fh.write("# -*- coding: utf-8 -*-\n") fh.write("# DB_dispatch/1_actual_state.py\n\n") fh.write("actual_state = []\n") def load_session() -> dict | None: """Прочитать активную сессию и вернуть user-dict или None.""" path = os.path.join(_DB_DIR, "1_actual_state.py") if not os.path.exists(path): return None spec = importlib.util.spec_from_file_location("_dispatch_db_state", path) if spec is None or spec.loader is None: return None mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) states = getattr(mod, "actual_state", []) if not states: return None user_id = states[0].get("user_id") if not user_id: return None users = _load_users() matched = [u for u in users if u.get("user_id") == user_id] return matched[0] if matched else None