diff --git a/auth/middleware.py b/auth/middleware.py index 72737231..e1e64e1f 100644 --- a/auth/middleware.py +++ b/auth/middleware.py @@ -82,69 +82,91 @@ class AuthMiddleware: async def authenticate_user(self, token: str) -> tuple[AuthCredentials, AuthenticatedUser | UnauthenticatedUser]: """Аутентифицирует пользователя по токену""" if not token: + logger.debug("[auth.authenticate] Токен отсутствует") return AuthCredentials( author_id=None, scopes={}, logged_in=False, error_message="no token", email=None, token=None ), UnauthenticatedUser() # Проверяем сессию в Redis - payload = await TokenManager.verify_session(token) - if not payload: - logger.debug("[auth.authenticate] Недействительный токен") - return AuthCredentials( - author_id=None, scopes={}, logged_in=False, error_message="Invalid token", email=None, token=None - ), UnauthenticatedUser() + try: + payload = await TokenManager.verify_session(token) + if not payload: + logger.debug("[auth.authenticate] Недействительный токен или сессия не найдена") + return AuthCredentials( + author_id=None, + scopes={}, + logged_in=False, + error_message="Invalid token or session", + email=None, + token=None, + ), UnauthenticatedUser() - with local_session() as session: - try: - author = session.query(Author).filter(Author.id == payload.user_id).one() + with local_session() as session: + try: + author = session.query(Author).filter(Author.id == payload.user_id).one() - if author.is_locked(): - logger.debug(f"[auth.authenticate] Аккаунт заблокирован: {author.id}") + if author.is_locked(): + logger.debug(f"[auth.authenticate] Аккаунт заблокирован: {author.id}") + return AuthCredentials( + author_id=None, + scopes={}, + logged_in=False, + error_message="Account is locked", + email=None, + token=None, + ), UnauthenticatedUser() + + # Получаем разрешения из ролей + scopes = author.get_permissions() + + # Получаем роли для пользователя + roles = [role.id for role in author.roles] if author.roles else [] + + # Обновляем last_seen + author.last_seen = int(time.time()) + session.commit() + + # Создаем объекты авторизации с сохранением токена + credentials = AuthCredentials( + author_id=author.id, + scopes=scopes, + logged_in=True, + error_message="", + email=author.email, + token=token, + ) + + user = AuthenticatedUser( + user_id=str(author.id), + username=author.slug or author.email or "", + roles=roles, + permissions=scopes, + token=token, + ) + + logger.debug(f"[auth.authenticate] Успешная аутентификация: {author.email}") + return credentials, user + + except exc.NoResultFound: + logger.debug("[auth.authenticate] Пользователь не найден в базе данных") return AuthCredentials( author_id=None, scopes={}, logged_in=False, - error_message="Account is locked", + error_message="User not found", email=None, token=None, ), UnauthenticatedUser() - - # Получаем разрешения из ролей - scopes = author.get_permissions() - - # Получаем роли для пользователя - roles = [role.id for role in author.roles] if author.roles else [] - - # Обновляем last_seen - author.last_seen = int(time.time()) - session.commit() - - # Создаем объекты авторизации с сохранением токена - credentials = AuthCredentials( - author_id=author.id, - scopes=scopes, - logged_in=True, - error_message="", - email=author.email, - token=token, - ) - - user = AuthenticatedUser( - user_id=str(author.id), - username=author.slug or author.email or "", - roles=roles, - permissions=scopes, - token=token, - ) - - logger.debug(f"[auth.authenticate] Успешная аутентификация: {author.email}") - return credentials, user - - except exc.NoResultFound: - logger.debug("[auth.authenticate] Пользователь не найден") - return AuthCredentials( - author_id=None, scopes={}, logged_in=False, error_message="User not found", email=None, token=None - ), UnauthenticatedUser() + except Exception as e: + logger.error(f"[auth.authenticate] Ошибка при работе с базой данных: {e}") + return AuthCredentials( + author_id=None, scopes={}, logged_in=False, error_message=str(e), email=None, token=None + ), UnauthenticatedUser() + except Exception as e: + logger.error(f"[auth.authenticate] Ошибка при проверке сессии: {e}") + return AuthCredentials( + author_id=None, scopes={}, logged_in=False, error_message=str(e), email=None, token=None + ), UnauthenticatedUser() async def __call__( self, diff --git a/auth/oauth.py b/auth/oauth.py index 4d6526fe..a8ad21e6 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -15,7 +15,15 @@ from auth.tokens.storage import TokenStorage from resolvers.auth import generate_unique_slug from services.db import local_session from services.redis import redis -from settings import FRONTEND_URL, OAUTH_CLIENTS +from settings import ( + FRONTEND_URL, + OAUTH_CLIENTS, + SESSION_COOKIE_HTTPONLY, + SESSION_COOKIE_MAX_AGE, + SESSION_COOKIE_NAME, + SESSION_COOKIE_SAMESITE, + SESSION_COOKIE_SECURE, +) from utils.logger import root_logger as logger # Type для dependency injection сессии @@ -302,7 +310,10 @@ async def oauth_login(_: None, _info: GraphQLResolveInfo, provider: str, callbac async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse: - """Обрабатывает callback от OAuth провайдера""" + """ + Обработчик OAuth callback. + Создает или обновляет пользователя и устанавливает сессионный токен. + """ try: # Получаем state из query параметров state = request.query_params.get("state") @@ -341,12 +352,12 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse: redirect_url = f"{stored_redirect_uri}?state={state}&access_token={session_token}" response = RedirectResponse(url=redirect_url) response.set_cookie( - "session_token", + SESSION_COOKIE_NAME, session_token, - httponly=True, - secure=True, - samesite="lax", - max_age=30 * 24 * 60 * 60, # 30 days + httponly=SESSION_COOKIE_HTTPONLY, + secure=SESSION_COOKIE_SECURE, + samesite=SESSION_COOKIE_SAMESITE, + max_age=SESSION_COOKIE_MAX_AGE, ) return response @@ -460,12 +471,12 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon # Возвращаем redirect с cookie response = RedirectResponse(url="/auth/success", status_code=307) response.set_cookie( - "session_token", + SESSION_COOKIE_NAME, session_token, - httponly=True, - secure=True, - samesite="lax", - max_age=30 * 24 * 60 * 60, # 30 дней + httponly=SESSION_COOKIE_HTTPONLY, + secure=SESSION_COOKIE_SECURE, + samesite=SESSION_COOKIE_SAMESITE, + max_age=SESSION_COOKIE_MAX_AGE, ) return response diff --git a/auth/tokens/sessions.py b/auth/tokens/sessions.py index 0352794b..0c2ccae3 100644 --- a/auth/tokens/sessions.py +++ b/auth/tokens/sessions.py @@ -230,6 +230,10 @@ class SessionTokenManager(BaseTokenManager): """ Проверяет сессию по токену для совместимости с TokenStorage """ + if not token: + logger.debug("Пустой токен") + return None + logger.debug(f"Проверка сессии для токена: {token[:20]}...") # Декодируем токен для получения payload @@ -239,15 +243,23 @@ class SessionTokenManager(BaseTokenManager): logger.error("Не удалось декодировать токен") return None + if not hasattr(payload, "user_id"): + logger.error("В токене отсутствует user_id") + return None + logger.debug(f"Успешно декодирован токен, user_id={payload.user_id}") except Exception as e: logger.error(f"Ошибка при декодировании токена: {e}") return None # Проверяем валидность токена - valid, _ = await self.validate_session_token(token) - if valid: - logger.debug(f"Сессия найдена для пользователя {payload.user_id}") - return payload - logger.warning(f"Сессия не найдена: {payload.user_id}") - return None + try: + valid, error = await self.validate_session_token(token) + if valid: + logger.debug(f"Сессия найдена для пользователя {payload.user_id}") + return payload + logger.warning(f"Сессия не найдена: {payload.user_id}, ошибка: {error}") + return None + except Exception as e: + logger.error(f"Ошибка при валидации сессии: {e}") + return None diff --git a/main.py b/main.py index fd8df81b..685d22a0 100644 --- a/main.py +++ b/main.py @@ -38,28 +38,29 @@ schema = make_executable_schema(load_schema_from_path("schema/"), list(resolvers # Создаем middleware с правильным порядком middleware = [ + # Начинаем с обработки ошибок + Middleware(ExceptionHandlerMiddleware), # CORS должен быть перед другими middleware для корректной обработки preflight-запросов Middleware( CORSMiddleware, allow_origins=[ "https://localhost:3000", + "http://localhost:3000", "https://testing.discours.io", - "https://testing.dscrs.site", "https://testing3.discours.io", + "https://v3.dscrs.site", + "https://session-daily.vercel.app", "https://coretest.discours.io", - "https://core.discours.io", - "https://discours.io", "https://new.discours.io", ], allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS allow_headers=["*"], allow_credentials=True, ), - # извлечение токена + аутентификация + cookies + # Аутентификация должна быть после CORS Middleware(AuthMiddleware), ] - # Создаем экземпляр GraphQL с улучшенным обработчиком graphql_app = GraphQL(schema, debug=DEVMODE, http_handler=EnhancedGraphQLHTTPHandler()) @@ -224,26 +225,6 @@ async def lifespan(app: Starlette): print("[lifespan] Shutdown complete") -middleware = [ - # Начинаем с обработки ошибок - Middleware(ExceptionHandlerMiddleware), - # CORS должен быть перед другими middleware для корректной обработки preflight-запросов - Middleware( - CORSMiddleware, - allow_origins=[ - "https://localhost:3000", - "http://localhost:3000", - "https://testing.discours.io", - "https://testing3.discours.io", - "https://coretest.discours.io", - "https://session-daily.vercel.app", - ], - allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS - allow_headers=["*"], - allow_credentials=True, - ), -] - # Обновляем маршрут в Starlette app = Starlette( routes=[ @@ -253,7 +234,7 @@ app = Starlette( Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]), Mount("/", app=StaticFiles(directory=str(DIST_DIR), html=True)), ], - middleware=middleware, + middleware=middleware, # Используем единый список middleware lifespan=lifespan, debug=True, ) diff --git a/settings.py b/settings.py index cedc0ce0..91a3f14e 100644 --- a/settings.py +++ b/settings.py @@ -63,7 +63,7 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS = 30 # Настройки для HTTP cookies (используется в auth middleware) SESSION_COOKIE_NAME = "auth_token" -SESSION_COOKIE_SECURE = False +SESSION_COOKIE_SECURE = True # Включаем для HTTPS SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE: Literal["lax", "strict", "none"] = "lax" SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 дней