2025-07-02 21:20:10 +00:00
|
|
|
|
"""
|
|
|
|
|
Сервис аутентификации с бизнес-логикой для регистрации,
|
|
|
|
|
входа и управления сессиями и декорраторами для GraphQL.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import secrets
|
|
|
|
|
import time
|
2024-01-25 19:41:27 +00:00
|
|
|
|
from functools import wraps
|
2025-06-01 23:56:11 +00:00
|
|
|
|
from typing import Any, Callable, Optional
|
2024-04-08 07:38:58 +00:00
|
|
|
|
|
2025-05-29 09:37:39 +00:00
|
|
|
|
from sqlalchemy import exc
|
2025-05-21 15:29:46 +00:00
|
|
|
|
from starlette.requests import Request
|
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
from auth.email import send_auth_email
|
|
|
|
|
from auth.exceptions import InvalidPassword, InvalidToken, ObjectNotExist
|
|
|
|
|
from auth.identity import Identity, Password
|
2025-05-29 09:37:39 +00:00
|
|
|
|
from auth.internal import verify_internal_auth
|
2025-07-02 21:20:10 +00:00
|
|
|
|
from auth.jwtcodec import JWTCodec
|
2025-07-02 19:30:21 +00:00
|
|
|
|
from auth.orm import Author
|
2025-07-02 21:20:10 +00:00
|
|
|
|
from auth.tokens.storage import TokenStorage
|
2025-05-22 01:34:30 +00:00
|
|
|
|
from cache.cache import get_cached_author_by_id
|
2025-07-02 21:20:10 +00:00
|
|
|
|
from orm.community import Community, CommunityAuthor, CommunityFollower
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from services.db import local_session
|
2025-07-02 21:20:10 +00:00
|
|
|
|
from services.redis import redis
|
|
|
|
|
from settings import (
|
|
|
|
|
ADMIN_EMAILS,
|
|
|
|
|
SESSION_COOKIE_NAME,
|
|
|
|
|
SESSION_TOKEN_HEADER,
|
|
|
|
|
)
|
|
|
|
|
from utils.generate_slug import generate_unique_slug
|
2025-05-29 09:37:39 +00:00
|
|
|
|
from utils.logger import root_logger as logger
|
2024-01-13 08:49:12 +00:00
|
|
|
|
|
2024-12-24 11:04:52 +00:00
|
|
|
|
# Список разрешенных заголовков
|
2025-01-21 07:09:28 +00:00
|
|
|
|
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
|
|
|
|
|
|
2024-02-21 07:27:16 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
class AuthService:
|
|
|
|
|
"""Сервис аутентификации с бизнес-логикой"""
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
async def check_auth(self, req: Request) -> tuple[int, list[str], bool]:
|
|
|
|
|
"""
|
|
|
|
|
Проверка авторизации пользователя.
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
Проверяет токен и получает данные из локальной БД.
|
|
|
|
|
"""
|
|
|
|
|
logger.debug("[check_auth] Проверка авторизации...")
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Получаем заголовок авторизации
|
|
|
|
|
token = None
|
|
|
|
|
|
|
|
|
|
# Если req is None (в тестах), возвращаем пустые данные
|
|
|
|
|
if not req:
|
|
|
|
|
logger.debug("[check_auth] Запрос отсутствует (тестовое окружение)")
|
|
|
|
|
return 0, [], False
|
|
|
|
|
|
|
|
|
|
# Проверяем заголовок с учетом регистра
|
|
|
|
|
headers_dict = dict(req.headers.items())
|
|
|
|
|
logger.debug(f"[check_auth] Все заголовки: {headers_dict}")
|
|
|
|
|
|
|
|
|
|
# Ищем заголовок Authorization независимо от регистра
|
|
|
|
|
for header_name, header_value in headers_dict.items():
|
|
|
|
|
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
|
|
|
|
|
token = header_value
|
|
|
|
|
logger.debug(f"[check_auth] Найден заголовок {header_name}: {token[:10]}...")
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not token:
|
|
|
|
|
logger.debug("[check_auth] Токен не найден в заголовках")
|
|
|
|
|
return 0, [], False
|
|
|
|
|
|
|
|
|
|
# Очищаем токен от префикса Bearer если он есть
|
|
|
|
|
if token.startswith("Bearer "):
|
|
|
|
|
token = token.split("Bearer ")[-1].strip()
|
|
|
|
|
|
|
|
|
|
# Проверяем авторизацию внутренним механизмом
|
|
|
|
|
logger.debug("[check_auth] Вызов verify_internal_auth...")
|
|
|
|
|
user_id, user_roles, is_admin = await verify_internal_auth(token)
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"[check_auth] Результат verify_internal_auth: user_id={user_id}, roles={user_roles}, is_admin={is_admin}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Если в ролях нет админа, но есть ID - проверяем в БД
|
|
|
|
|
if user_id and not is_admin:
|
|
|
|
|
try:
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
# Преобразуем user_id в число
|
|
|
|
|
try:
|
|
|
|
|
if isinstance(user_id, str):
|
|
|
|
|
user_id_int = int(user_id.strip())
|
|
|
|
|
else:
|
|
|
|
|
user_id_int = int(user_id)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
|
|
|
|
else:
|
|
|
|
|
# Проверяем наличие админских прав через новую RBAC систему
|
|
|
|
|
from orm.community import get_user_roles_in_community
|
|
|
|
|
|
|
|
|
|
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1)
|
|
|
|
|
is_admin = any(role in ["admin", "super"] for role in user_roles_in_community)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
|
|
|
|
|
|
|
|
|
return user_id, user_roles, is_admin
|
|
|
|
|
|
|
|
|
|
async def add_user_role(self, user_id: str, roles: Optional[list[str]] = None) -> Optional[str]:
|
|
|
|
|
"""
|
|
|
|
|
Добавление ролей пользователю в локальной БД через CommunityAuthor.
|
|
|
|
|
"""
|
|
|
|
|
if not roles:
|
|
|
|
|
roles = ["author", "reader"]
|
|
|
|
|
|
|
|
|
|
logger.info(f"Adding roles {roles} to user {user_id}")
|
|
|
|
|
|
|
|
|
|
from orm.community import assign_role_to_user
|
|
|
|
|
|
|
|
|
|
logger.debug("Using local authentication with new RBAC system")
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
try:
|
|
|
|
|
author = session.query(Author).filter(Author.id == user_id).one()
|
|
|
|
|
|
|
|
|
|
# Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1)
|
|
|
|
|
for role_name in roles:
|
|
|
|
|
success = assign_role_to_user(int(user_id), role_name, community_id=1)
|
|
|
|
|
if success:
|
|
|
|
|
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
|
|
|
|
|
else:
|
|
|
|
|
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
|
|
|
|
|
|
|
|
|
|
return user_id
|
|
|
|
|
|
|
|
|
|
except exc.NoResultFound:
|
|
|
|
|
logger.error(f"Author {user_id} not found")
|
|
|
|
|
return None
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
def create_user(self, user_dict: dict[str, Any], community_id: int | None = None) -> Author:
|
|
|
|
|
"""Создает нового пользователя с дефолтными ролями"""
|
|
|
|
|
user = Author(**user_dict)
|
|
|
|
|
target_community_id = community_id or 1
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
with local_session() as session:
|
|
|
|
|
session.add(user)
|
|
|
|
|
session.flush()
|
2025-06-01 23:56:11 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Получаем сообщество для назначения ролей
|
|
|
|
|
community = session.query(Community).filter(Community.id == target_community_id).first()
|
|
|
|
|
if not community:
|
|
|
|
|
logger.warning(f"Сообщество {target_community_id} не найдено, используем ID=1")
|
|
|
|
|
target_community_id = 1
|
|
|
|
|
community = session.query(Community).filter(Community.id == target_community_id).first()
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
if community:
|
|
|
|
|
# Инициализируем права сообщества
|
|
|
|
|
try:
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
loop.run_until_complete(community.initialize_role_permissions())
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"Не удалось инициализировать права сообщества: {e}")
|
|
|
|
|
|
|
|
|
|
# Получаем дефолтные роли
|
|
|
|
|
try:
|
|
|
|
|
default_roles = community.get_default_roles()
|
|
|
|
|
if not default_roles:
|
|
|
|
|
default_roles = ["reader", "author"]
|
|
|
|
|
except AttributeError:
|
|
|
|
|
default_roles = ["reader", "author"]
|
|
|
|
|
|
|
|
|
|
# Создаем CommunityAuthor с ролями
|
|
|
|
|
community_author = CommunityAuthor(
|
|
|
|
|
community_id=target_community_id,
|
|
|
|
|
author_id=user.id,
|
|
|
|
|
roles=",".join(default_roles),
|
|
|
|
|
)
|
|
|
|
|
session.add(community_author)
|
|
|
|
|
|
|
|
|
|
# Создаем подписку на сообщество
|
|
|
|
|
follower = CommunityFollower(community=target_community_id, follower=int(user.id))
|
|
|
|
|
session.add(follower)
|
|
|
|
|
|
|
|
|
|
logger.info(f"Пользователь {user.id} создан с ролями {default_roles}")
|
|
|
|
|
|
|
|
|
|
session.commit()
|
|
|
|
|
return user
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
async def get_session(self, token: str) -> dict[str, Any]:
|
|
|
|
|
"""Получает информацию о текущей сессии по токену"""
|
|
|
|
|
try:
|
|
|
|
|
# Проверяем токен
|
|
|
|
|
payload = JWTCodec.decode(token)
|
|
|
|
|
if not payload:
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Невалидный токен"}
|
|
|
|
|
|
|
|
|
|
token_verification = await TokenStorage.verify_session(token)
|
|
|
|
|
if not token_verification:
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Токен истек"}
|
|
|
|
|
|
|
|
|
|
user_id = payload.user_id
|
|
|
|
|
|
|
|
|
|
# Получаем автора
|
|
|
|
|
author = await get_cached_author_by_id(int(user_id), lambda x: x)
|
|
|
|
|
if not author:
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
|
|
|
|
|
|
|
|
|
return {"success": True, "token": token, "author": author, "error": None}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка получения сессии: {e}")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
async def register_user(self, email: str, password: str = "", name: str = "") -> dict[str, Any]:
|
|
|
|
|
"""Регистрирует нового пользователя"""
|
|
|
|
|
email = email.lower()
|
|
|
|
|
logger.info(f"Попытка регистрации для {email}")
|
|
|
|
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
user = session.query(Author).filter(Author.email == email).first()
|
|
|
|
|
if user:
|
|
|
|
|
logger.warning(f"Пользователь {email} уже существует")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Пользователь уже существует"}
|
|
|
|
|
|
|
|
|
|
slug = generate_unique_slug(name if name else email.split("@")[0])
|
|
|
|
|
user_dict = {
|
|
|
|
|
"email": email,
|
|
|
|
|
"username": email,
|
|
|
|
|
"name": name if name else email.split("@")[0],
|
|
|
|
|
"slug": slug,
|
|
|
|
|
}
|
|
|
|
|
if password:
|
|
|
|
|
user_dict["password"] = Password.encode(password)
|
|
|
|
|
|
|
|
|
|
new_user = self.create_user(user_dict)
|
2025-01-21 07:09:28 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
try:
|
|
|
|
|
await self.send_verification_link(email)
|
|
|
|
|
logger.info(f"Пользователь {email} зарегистрирован, ссылка отправлена")
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"token": None,
|
|
|
|
|
"author": new_user,
|
|
|
|
|
"error": "Требуется подтверждение email.",
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка отправки ссылки для {email}: {e}")
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"token": None,
|
|
|
|
|
"author": new_user,
|
|
|
|
|
"error": f"Пользователь зарегистрирован, но ошибка отправки ссылки: {e}",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async def send_verification_link(self, email: str, lang: str = "ru", template: str = "confirm") -> Author:
|
|
|
|
|
"""Отправляет ссылку подтверждения на email"""
|
|
|
|
|
email = email.lower()
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
user = session.query(Author).filter(Author.email == email).first()
|
|
|
|
|
if not user:
|
|
|
|
|
raise ObjectNotExist("User not found")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from auth.tokens.verification import VerificationTokenManager
|
|
|
|
|
|
|
|
|
|
verification_manager = VerificationTokenManager()
|
|
|
|
|
token = await verification_manager.create_verification_token(
|
|
|
|
|
str(user.id), "email_confirmation", {"email": user.email, "template": template}
|
|
|
|
|
)
|
|
|
|
|
except (AttributeError, ImportError):
|
|
|
|
|
token = await TokenStorage.create_session(
|
|
|
|
|
user_id=str(user.id),
|
|
|
|
|
username=str(user.username or user.email or user.slug or ""),
|
|
|
|
|
device_info={"email": user.email} if hasattr(user, "email") else None,
|
|
|
|
|
)
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
await send_auth_email(user, token, lang, template)
|
|
|
|
|
return user
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
async def confirm_email(self, token: str) -> dict[str, Any]:
|
|
|
|
|
"""Подтверждает email по токену"""
|
2025-05-21 15:29:46 +00:00
|
|
|
|
try:
|
2025-07-02 21:20:10 +00:00
|
|
|
|
logger.info("Начало подтверждения email по токену")
|
|
|
|
|
payload = JWTCodec.decode(token)
|
|
|
|
|
if not payload:
|
|
|
|
|
logger.warning("Невалидный токен")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Невалидный токен"}
|
|
|
|
|
|
|
|
|
|
token_verification = await TokenStorage.verify_session(token)
|
|
|
|
|
if not token_verification:
|
|
|
|
|
logger.warning("Токен не найден в системе или истек")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Токен не найден или истек"}
|
|
|
|
|
|
|
|
|
|
user_id = payload.user_id
|
|
|
|
|
username = payload.username
|
|
|
|
|
|
2025-05-21 15:29:46 +00:00
|
|
|
|
with local_session() as session:
|
2025-07-02 21:20:10 +00:00
|
|
|
|
user = session.query(Author).where(Author.id == user_id).first()
|
|
|
|
|
if not user:
|
|
|
|
|
logger.warning(f"Пользователь с ID {user_id} не найден")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
|
|
|
|
|
|
|
|
|
device_info = {"email": user.email} if hasattr(user, "email") else None
|
|
|
|
|
session_token = await TokenStorage.create_session(
|
|
|
|
|
user_id=str(user_id),
|
|
|
|
|
username=user.username or user.email or user.slug or username,
|
|
|
|
|
device_info=device_info,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
user.email_verified = True
|
|
|
|
|
user.last_seen = int(time.time())
|
|
|
|
|
session.add(user)
|
|
|
|
|
session.commit()
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
logger.info(f"Email для пользователя {user_id} подтвержден")
|
|
|
|
|
return {"success": True, "token": session_token, "author": user, "error": None}
|
|
|
|
|
|
|
|
|
|
except InvalidToken as e:
|
|
|
|
|
logger.warning(f"Невалидный токен - {e.message}")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": f"Невалидный токен: {e.message}"}
|
2025-05-21 15:29:46 +00:00
|
|
|
|
except Exception as e:
|
2025-07-02 21:20:10 +00:00
|
|
|
|
logger.error(f"Ошибка подтверждения email: {e}")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": f"Ошибка подтверждения email: {e}"}
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
async def login(self, email: str, password: str, request=None) -> dict[str, Any]:
|
|
|
|
|
"""Авторизация пользователя"""
|
|
|
|
|
email = email.lower()
|
|
|
|
|
logger.info(f"Попытка входа для {email}")
|
2023-10-23 14:47:11 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
try:
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
author = session.query(Author).filter(Author.email == email).first()
|
|
|
|
|
if not author:
|
|
|
|
|
logger.warning(f"Пользователь {email} не найден")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
|
|
|
|
|
|
|
|
|
# Проверяем роли (упрощенная версия)
|
|
|
|
|
has_reader_role = False
|
|
|
|
|
if hasattr(author, "roles") and author.roles:
|
|
|
|
|
for role in author.roles:
|
|
|
|
|
if role.id == "reader":
|
|
|
|
|
has_reader_role = True
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not has_reader_role and author.email not in ADMIN_EMAILS.split(","):
|
|
|
|
|
logger.warning(f"У пользователя {email} нет роли 'reader'")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Нет прав для входа"}
|
|
|
|
|
|
|
|
|
|
# Проверяем пароль
|
|
|
|
|
try:
|
|
|
|
|
valid_author = Identity.password(author, password)
|
|
|
|
|
except (InvalidPassword, Exception) as e:
|
|
|
|
|
logger.warning(f"Неверный пароль для {email}: {e}")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
# Создаем токен
|
|
|
|
|
username = str(valid_author.username or valid_author.email or valid_author.slug or "")
|
|
|
|
|
token = await TokenStorage.create_session(
|
|
|
|
|
user_id=str(valid_author.id),
|
|
|
|
|
username=username,
|
|
|
|
|
device_info={"email": valid_author.email} if hasattr(valid_author, "email") else None,
|
|
|
|
|
)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Обновляем время входа
|
|
|
|
|
valid_author.last_seen = int(time.time())
|
|
|
|
|
session.commit()
|
2024-01-25 19:41:27 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Устанавливаем cookie если есть request
|
|
|
|
|
if request and token:
|
|
|
|
|
self._set_auth_cookie(request, token)
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
try:
|
|
|
|
|
author_dict = valid_author.dict(True)
|
|
|
|
|
except Exception:
|
|
|
|
|
author_dict = {
|
|
|
|
|
"id": valid_author.id,
|
|
|
|
|
"email": valid_author.email,
|
|
|
|
|
"name": getattr(valid_author, "name", ""),
|
|
|
|
|
"slug": getattr(valid_author, "slug", ""),
|
|
|
|
|
"username": getattr(valid_author, "username", ""),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(f"Успешный вход для {email}")
|
|
|
|
|
return {"success": True, "token": token, "author": author_dict, "error": None}
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка входа для {email}: {e}")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": f"Ошибка авторизации: {e}"}
|
2025-07-02 19:30:21 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
def _set_auth_cookie(self, request, token: str) -> bool:
|
|
|
|
|
"""Устанавливает cookie аутентификации"""
|
2025-05-16 06:23:48 +00:00
|
|
|
|
try:
|
2025-07-02 21:20:10 +00:00
|
|
|
|
if hasattr(request, "cookies"):
|
|
|
|
|
request.cookies[SESSION_COOKIE_NAME] = token
|
|
|
|
|
return True
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка установки cookie: {e}")
|
|
|
|
|
return False
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
async def logout(self, user_id: str, token: str = None) -> dict[str, Any]:
|
|
|
|
|
"""Выход из системы"""
|
|
|
|
|
try:
|
|
|
|
|
if token:
|
|
|
|
|
await TokenStorage.revoke_session(token)
|
|
|
|
|
logger.info(f"Пользователь {user_id} вышел из системы")
|
|
|
|
|
return {"success": True, "message": "Успешный выход"}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка выхода для {user_id}: {e}")
|
|
|
|
|
return {"success": False, "message": f"Ошибка выхода: {e}"}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
async def refresh_token(self, user_id: str, old_token: str, device_info: dict = None) -> dict[str, Any]:
|
|
|
|
|
"""Обновление токена"""
|
|
|
|
|
try:
|
|
|
|
|
new_token = await TokenStorage.refresh_session(int(user_id), old_token, device_info or {})
|
|
|
|
|
if not new_token:
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Не удалось обновить токен"}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Получаем данные пользователя
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
author = session.query(Author).filter(Author.id == int(user_id)).first()
|
|
|
|
|
if not author:
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
try:
|
|
|
|
|
author_dict = author.dict(True)
|
|
|
|
|
except Exception:
|
|
|
|
|
author_dict = {
|
|
|
|
|
"id": author.id,
|
|
|
|
|
"email": author.email,
|
|
|
|
|
"name": getattr(author, "name", ""),
|
|
|
|
|
"slug": getattr(author, "slug", ""),
|
|
|
|
|
}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
return {"success": True, "token": new_token, "author": author_dict, "error": None}
|
2024-12-11 22:04:11 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка обновления токена для {user_id}: {e}")
|
|
|
|
|
return {"success": False, "token": None, "author": None, "error": str(e)}
|
2025-05-20 22:34:02 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
async def request_password_reset(self, email: str, lang: str = "ru") -> dict[str, Any]:
|
|
|
|
|
"""Запрос сброса пароля"""
|
|
|
|
|
try:
|
|
|
|
|
email = email.lower()
|
|
|
|
|
logger.info(f"Запрос сброса пароля для {email}")
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
with local_session() as session:
|
|
|
|
|
author = session.query(Author).filter(Author.email == email).first()
|
|
|
|
|
if not author:
|
|
|
|
|
logger.warning(f"Пользователь {email} не найден")
|
|
|
|
|
return {"success": True} # Для безопасности
|
2025-06-01 23:56:11 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
try:
|
|
|
|
|
from auth.tokens.verification import VerificationTokenManager
|
|
|
|
|
|
|
|
|
|
verification_manager = VerificationTokenManager()
|
|
|
|
|
token = await verification_manager.create_verification_token(
|
|
|
|
|
str(author.id), "password_reset", {"email": author.email}
|
2025-06-02 18:50:58 +00:00
|
|
|
|
)
|
2025-07-02 21:20:10 +00:00
|
|
|
|
except (AttributeError, ImportError):
|
|
|
|
|
token = await TokenStorage.create_session(
|
|
|
|
|
user_id=str(author.id),
|
|
|
|
|
username=str(author.username or author.email or author.slug or ""),
|
|
|
|
|
device_info={"email": author.email} if hasattr(author, "email") else None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await send_auth_email(author, token, lang, "password_reset")
|
|
|
|
|
logger.info(f"Письмо сброса пароля отправлено для {email}")
|
|
|
|
|
|
|
|
|
|
return {"success": True}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка запроса сброса пароля для {email}: {e}")
|
|
|
|
|
return {"success": False}
|
|
|
|
|
|
|
|
|
|
def is_email_used(self, email: str) -> bool:
|
|
|
|
|
"""Проверяет, используется ли email"""
|
|
|
|
|
email = email.lower()
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
user = session.query(Author).filter(Author.email == email).first()
|
|
|
|
|
return user is not None
|
|
|
|
|
|
|
|
|
|
async def update_security(
|
|
|
|
|
self, user_id: int, old_password: str, new_password: str = None, email: str = None
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Обновление пароля и email"""
|
|
|
|
|
try:
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
author = session.query(Author).filter(Author.id == user_id).first()
|
|
|
|
|
if not author:
|
|
|
|
|
return {"success": False, "error": "NOT_AUTHENTICATED", "author": None}
|
|
|
|
|
|
|
|
|
|
if not author.verify_password(old_password):
|
|
|
|
|
return {"success": False, "error": "incorrect old password", "author": None}
|
|
|
|
|
|
|
|
|
|
if email and email != author.email:
|
|
|
|
|
existing_user = session.query(Author).filter(Author.email == email).first()
|
|
|
|
|
if existing_user:
|
|
|
|
|
return {"success": False, "error": "email already exists", "author": None}
|
|
|
|
|
|
|
|
|
|
changes_made = []
|
|
|
|
|
|
|
|
|
|
if new_password:
|
|
|
|
|
author.set_password(new_password)
|
|
|
|
|
changes_made.append("password")
|
|
|
|
|
|
|
|
|
|
if email and email != author.email:
|
|
|
|
|
# Создаем запрос на смену email через Redis
|
|
|
|
|
token = secrets.token_urlsafe(32)
|
|
|
|
|
email_change_data = {
|
|
|
|
|
"user_id": user_id,
|
|
|
|
|
"old_email": author.email,
|
|
|
|
|
"new_email": email,
|
|
|
|
|
"token": token,
|
|
|
|
|
"expires_at": int(time.time()) + 3600, # 1 час
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
redis_key = f"email_change:{user_id}"
|
|
|
|
|
await redis.execute("SET", redis_key, json.dumps(email_change_data))
|
|
|
|
|
await redis.execute("EXPIRE", redis_key, 3600)
|
|
|
|
|
|
|
|
|
|
changes_made.append("email_pending")
|
|
|
|
|
logger.info(f"Email смена инициирована для пользователя {user_id}")
|
|
|
|
|
|
|
|
|
|
session.commit()
|
|
|
|
|
logger.info(f"Безопасность обновлена для {user_id}: {changes_made}")
|
|
|
|
|
|
|
|
|
|
return {"success": True, "error": None, "author": author}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка обновления безопасности для {user_id}: {e}")
|
|
|
|
|
return {"success": False, "error": str(e), "author": None}
|
|
|
|
|
|
|
|
|
|
async def confirm_email_change(self, user_id: int, token: str) -> dict[str, Any]:
|
|
|
|
|
"""Подтверждение смены email по токену"""
|
|
|
|
|
try:
|
|
|
|
|
# Получаем данные смены email из Redis
|
|
|
|
|
redis_key = f"email_change:{user_id}"
|
|
|
|
|
cached_data = await redis.execute("GET", redis_key)
|
|
|
|
|
|
|
|
|
|
if not cached_data:
|
|
|
|
|
return {"success": False, "error": "NO_PENDING_EMAIL", "author": None}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
email_change_data = json.loads(cached_data)
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
return {"success": False, "error": "INVALID_TOKEN", "author": None}
|
|
|
|
|
|
|
|
|
|
# Проверяем токен
|
|
|
|
|
if email_change_data.get("token") != token:
|
|
|
|
|
return {"success": False, "error": "INVALID_TOKEN", "author": None}
|
|
|
|
|
|
|
|
|
|
# Проверяем срок действия
|
|
|
|
|
if email_change_data.get("expires_at", 0) < int(time.time()):
|
|
|
|
|
await redis.execute("DEL", redis_key)
|
|
|
|
|
return {"success": False, "error": "TOKEN_EXPIRED", "author": None}
|
|
|
|
|
|
|
|
|
|
new_email = email_change_data.get("new_email")
|
|
|
|
|
if not new_email:
|
|
|
|
|
return {"success": False, "error": "INVALID_TOKEN", "author": None}
|
|
|
|
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
author = session.query(Author).filter(Author.id == user_id).first()
|
|
|
|
|
if not author:
|
|
|
|
|
return {"success": False, "error": "NOT_AUTHENTICATED", "author": None}
|
|
|
|
|
|
|
|
|
|
# Проверяем, что новый email не занят
|
|
|
|
|
existing_user = session.query(Author).filter(Author.email == new_email).first()
|
|
|
|
|
if existing_user and existing_user.id != author.id:
|
|
|
|
|
await redis.execute("DEL", redis_key)
|
|
|
|
|
return {"success": False, "error": "email already exists", "author": None}
|
|
|
|
|
|
|
|
|
|
# Применяем смену email
|
|
|
|
|
author.email = new_email
|
|
|
|
|
author.email_verified = True
|
|
|
|
|
author.updated_at = int(time.time())
|
|
|
|
|
|
|
|
|
|
session.add(author)
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
# Удаляем данные из Redis
|
|
|
|
|
await redis.execute("DEL", redis_key)
|
|
|
|
|
|
|
|
|
|
logger.info(f"Email изменен для пользователя {user_id}")
|
|
|
|
|
return {"success": True, "error": None, "author": author}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка подтверждения смены email: {e}")
|
|
|
|
|
return {"success": False, "error": str(e), "author": None}
|
|
|
|
|
|
|
|
|
|
async def cancel_email_change(self, user_id: int) -> dict[str, Any]:
|
|
|
|
|
"""Отмена смены email"""
|
|
|
|
|
try:
|
|
|
|
|
redis_key = f"email_change:{user_id}"
|
|
|
|
|
cached_data = await redis.execute("GET", redis_key)
|
|
|
|
|
|
|
|
|
|
if not cached_data:
|
|
|
|
|
return {"success": False, "error": "NO_PENDING_EMAIL", "author": None}
|
|
|
|
|
|
|
|
|
|
# Удаляем данные из Redis
|
|
|
|
|
await redis.execute("DEL", redis_key)
|
|
|
|
|
|
|
|
|
|
# Получаем текущие данные пользователя
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
author = session.query(Author).filter(Author.id == user_id).first()
|
|
|
|
|
if not author:
|
|
|
|
|
return {"success": False, "error": "NOT_AUTHENTICATED", "author": None}
|
|
|
|
|
|
|
|
|
|
logger.info(f"Смена email отменена для пользователя {user_id}")
|
|
|
|
|
return {"success": True, "error": None, "author": author}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка отмены смены email: {e}")
|
|
|
|
|
return {"success": False, "error": str(e), "author": None}
|
|
|
|
|
|
|
|
|
|
def login_required(self, f: Callable) -> Callable:
|
|
|
|
|
"""Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'."""
|
|
|
|
|
|
|
|
|
|
@wraps(f)
|
|
|
|
|
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
from graphql.error import GraphQLError
|
|
|
|
|
|
|
|
|
|
info = args[1]
|
|
|
|
|
req = info.context.get("request")
|
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.debug(
|
2025-07-02 21:20:10 +00:00
|
|
|
|
f"[login_required] Проверка авторизации для запроса: {req.method if req else 'unknown'} {req.url.path if req and hasattr(req, 'url') else 'unknown'}"
|
2025-06-01 23:56:11 +00:00
|
|
|
|
)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Извлекаем токен из заголовков
|
|
|
|
|
token = None
|
|
|
|
|
if req:
|
|
|
|
|
headers_dict = dict(req.headers.items())
|
|
|
|
|
for header_name, header_value in headers_dict.items():
|
|
|
|
|
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
|
|
|
|
|
token = header_value
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if token and token.startswith("Bearer "):
|
|
|
|
|
token = token.split("Bearer ")[-1].strip()
|
|
|
|
|
|
|
|
|
|
# Для тестового режима
|
|
|
|
|
if not req and info.context.get("author") and info.context.get("roles"):
|
|
|
|
|
logger.debug("[login_required] Тестовый режим")
|
|
|
|
|
user_id = info.context["author"]["id"]
|
|
|
|
|
user_roles = info.context["roles"]
|
|
|
|
|
is_admin = info.context.get("is_admin", False)
|
|
|
|
|
if not token:
|
|
|
|
|
token = info.context.get("token")
|
|
|
|
|
else:
|
|
|
|
|
# Обычный режим
|
|
|
|
|
user_id, user_roles, is_admin = await self.check_auth(req)
|
|
|
|
|
|
|
|
|
|
if not user_id:
|
|
|
|
|
msg = "Требуется авторизация"
|
|
|
|
|
raise GraphQLError(msg)
|
2023-10-23 14:47:11 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Проверяем роль reader
|
|
|
|
|
if "reader" not in user_roles and not is_admin:
|
|
|
|
|
msg = "У вас нет необходимых прав для доступа"
|
|
|
|
|
raise GraphQLError(msg)
|
2024-11-12 14:56:20 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
|
|
|
|
|
info.context["roles"] = user_roles
|
|
|
|
|
info.context["is_admin"] = is_admin
|
2024-11-12 14:56:20 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
if token:
|
|
|
|
|
info.context["token"] = token
|
2024-12-11 22:04:11 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Получаем автора если его нет в контексте
|
|
|
|
|
if not info.context.get("author") or not isinstance(info.context["author"], dict):
|
|
|
|
|
author = await get_cached_author_by_id(int(user_id), lambda x: x)
|
|
|
|
|
if not author:
|
|
|
|
|
logger.error(f"Профиль автора не найден для пользователя {user_id}")
|
|
|
|
|
info.context["author"] = author
|
2024-11-18 08:31:19 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
return await f(*args, **kwargs)
|
2024-11-18 08:31:19 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
return decorated_function
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
def login_accepted(self, f: Callable) -> Callable:
|
|
|
|
|
"""Декоратор для добавления данных авторизации в контекст."""
|
2024-11-12 14:56:20 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
@wraps(f)
|
|
|
|
|
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
info = args[1]
|
|
|
|
|
req = info.context.get("request")
|
|
|
|
|
|
|
|
|
|
logger.debug("login_accepted: Проверка авторизации пользователя.")
|
|
|
|
|
user_id, user_roles, is_admin = await self.check_auth(req)
|
|
|
|
|
|
|
|
|
|
if user_id and user_roles:
|
|
|
|
|
logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}")
|
|
|
|
|
info.context["roles"] = user_roles
|
|
|
|
|
info.context["is_admin"] = is_admin
|
|
|
|
|
|
|
|
|
|
author = await get_cached_author_by_id(int(user_id), lambda x: x)
|
|
|
|
|
if author:
|
|
|
|
|
is_owner = True
|
|
|
|
|
info.context["author"] = author.dict(is_owner or is_admin)
|
|
|
|
|
else:
|
|
|
|
|
logger.error(f"login_accepted: Профиль автора не найден для пользователя {user_id}")
|
2024-11-14 11:00:33 +00:00
|
|
|
|
else:
|
2025-07-02 21:20:10 +00:00
|
|
|
|
logger.debug("login_accepted: Пользователь не авторизован")
|
|
|
|
|
info.context["roles"] = None
|
|
|
|
|
info.context["author"] = None
|
|
|
|
|
info.context["is_admin"] = False
|
|
|
|
|
|
|
|
|
|
return await f(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
return decorated_function
|
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Синглтон сервиса
|
|
|
|
|
auth_service = AuthService()
|
2024-11-12 14:56:20 +00:00
|
|
|
|
|
2025-07-02 21:20:10 +00:00
|
|
|
|
# Экспортируем функции для обратной совместимости
|
|
|
|
|
check_auth = auth_service.check_auth
|
|
|
|
|
add_user_role = auth_service.add_user_role
|
|
|
|
|
login_required = auth_service.login_required
|
|
|
|
|
login_accepted = auth_service.login_accepted
|