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