181 lines
7.5 KiB
Python
181 lines
7.5 KiB
Python
# -*- 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`.
|
||
|
||
Структура размещения:
|
||
<project_root>/dispatch/hub/my_account/auth_service.py
|
||
<project_root>/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
|