635 lines
25 KiB
Python
635 lines
25 KiB
Python
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Form, Request
|
||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, delete
|
||
from app.core import security
|
||
from app.api import schemas
|
||
from app.db import models
|
||
from jose import JWTError, jwt
|
||
from app.core.security import get_current_user
|
||
import pyotp
|
||
import qrcode
|
||
import base64
|
||
from io import BytesIO
|
||
from fastapi.responses import StreamingResponse
|
||
from typing import Optional
|
||
from datetime import datetime, timezone
|
||
from app.core.security import _parse_device_name
|
||
from app.websocket.connection_manager import manager
|
||
from app.services.notification_service import send_system_notification
|
||
|
||
|
||
async def get_db():
|
||
async with models.AsyncSessionLocal() as db:
|
||
try:
|
||
yield db
|
||
finally:
|
||
await db.close()
|
||
|
||
authRouter = APIRouter(prefix="/auth", tags=[])
|
||
|
||
# Helper для создания сессии при логинах
|
||
|
||
|
||
async def _register_new_login_session(user_id: int, request: Request, db: AsyncSession, device_name: Optional[str] = None):
|
||
if not device_name:
|
||
device_name = request.headers.get(
|
||
"X-Device-Name") or _parse_device_name(request)
|
||
ip_address = request.client.host if request.client else "0.0.0.0"
|
||
|
||
new_session = models.Session(
|
||
user_id=user_id,
|
||
session_token="",
|
||
device_name=device_name,
|
||
ip_address=ip_address,
|
||
created_at=datetime.now(timezone.utc),
|
||
last_active=datetime.now(timezone.utc)
|
||
)
|
||
db.add(new_session)
|
||
await db.commit()
|
||
await db.refresh(new_session)
|
||
return new_session
|
||
|
||
|
||
@authRouter.post("/register")
|
||
async def register(username: str, password: str, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||
if current_user.id != 1:
|
||
raise HTTPException(status_code=403, detail='Forbidden')
|
||
if len(password.encode('utf-8')) > 72:
|
||
raise HTTPException(
|
||
status_code=400, detail="Пароль слишком длинный (макс. 72 байта)")
|
||
|
||
result = await db.execute(select(models.User).where(models.User.username == username))
|
||
if result.scalars().first():
|
||
raise HTTPException(
|
||
status_code=400, detail="Пользователь уже существует")
|
||
|
||
hashed_pwd = security.get_password_hash(password)
|
||
new_user = models.User(username=username, hashed_password=hashed_pwd)
|
||
db.add(new_user)
|
||
await db.commit()
|
||
return {"status": "ok", "message": "User created", "id": new_user.id}
|
||
|
||
|
||
@authRouter.post("/hash")
|
||
async def register_hash(password: str):
|
||
if len(password.encode('utf-8')) > 72:
|
||
raise HTTPException(
|
||
status_code=400, detail="Пароль слишком длинный (макс. 72 байта)")
|
||
hashed_pwd = security.get_password_hash(password)
|
||
return {"password": hashed_pwd}
|
||
|
||
|
||
@authRouter.post("/login")
|
||
async def login(data: schemas.LoginRequest, request: Request, db: AsyncSession = Depends(get_db)):
|
||
result = await db.execute(select(models.User).where(models.User.username == data.username))
|
||
user = result.scalars().first()
|
||
|
||
if not user or not security.verify_password(data.password, user.hashed_password):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Неверный логин или пароль",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
|
||
if user.totp_secret:
|
||
if not data.totp_code:
|
||
raise HTTPException(status_code=400, detail="TOTP код требуется")
|
||
totp = pyotp.TOTP(user.totp_secret)
|
||
if not totp.verify(data.totp_code):
|
||
raise HTTPException(status_code=400, detail="Неверный TOTP код")
|
||
|
||
user_id = user.id
|
||
|
||
explicit_device_name = getattr(
|
||
data, "device_name", None) or request.headers.get("X-Device-Name")
|
||
|
||
new_session = await _register_new_login_session(user_id, request, db, device_name=explicit_device_name)
|
||
|
||
access_token = security.create_access_token(
|
||
data={"sub": str(user_id)}, session_id=new_session.id)
|
||
refresh_token = security.create_refresh_token(
|
||
data={"sub": str(user_id)}, session_id=new_session.id)
|
||
|
||
new_session.session_token = access_token
|
||
await db.commit()
|
||
|
||
device_name = _parse_device_name(request)
|
||
ip_address = request.client.host if request.client else "0.0.0.0"
|
||
current_time = datetime.now(timezone.utc).strftime("%H:%M:%S")
|
||
|
||
security_alert_text = (
|
||
f"Обнаружен новый вход в ваш аккаунт.\n\n"
|
||
f"Устройство: {device_name}\n"
|
||
f"IP-адрес: {ip_address}\n"
|
||
f"Время: {current_time}\n\n"
|
||
f"Если это не вы, немедленно перейдите в Настройки -> Устройства и завершите подозрительную сессию."
|
||
)
|
||
|
||
await send_system_notification(
|
||
db=db,
|
||
receiver_id=user_id,
|
||
plain_text=security_alert_text
|
||
)
|
||
|
||
return {
|
||
"access_token": access_token,
|
||
"refresh_token": refresh_token,
|
||
"token_type": "bearer",
|
||
"user_id": user_id
|
||
}
|
||
|
||
|
||
@authRouter.post("/login-oauth")
|
||
async def login_oauth(request: Request, form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):
|
||
totp_code = form_data.client_secret if form_data.client_secret else None
|
||
|
||
result = await db.execute(select(models.User).where(models.User.username == form_data.username))
|
||
user = result.scalars().first()
|
||
|
||
if not user or not security.verify_password(form_data.password, user.hashed_password):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Неверный логин или пароль",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
|
||
if user.totp_secret:
|
||
if not totp_code:
|
||
raise HTTPException(status_code=400, detail="TOTP код требуется")
|
||
totp = pyotp.TOTP(user.totp_secret)
|
||
if not totp.verify(totp_code):
|
||
raise HTTPException(status_code=400, detail="Неверный TOTP код")
|
||
|
||
# Логируем сессию в базу данных
|
||
|
||
oauth_device_name = request.headers.get("X-Device-Name")
|
||
|
||
new_session = await _register_new_login_session(user.id, request, db, device_name=oauth_device_name)
|
||
|
||
access_token = security.create_access_token(
|
||
data={"sub": str(user.id)}, session_id=new_session.id)
|
||
refresh_token = security.create_refresh_token(
|
||
data={"sub": str(user.id)}, session_id=new_session.id)
|
||
|
||
new_session.session_token = access_token
|
||
await db.commit()
|
||
|
||
return {
|
||
"access_token": access_token,
|
||
"refresh_token": refresh_token,
|
||
"token_type": "bearer",
|
||
"user_id": user.id
|
||
}
|
||
|
||
|
||
@authRouter.post("/totp/enable")
|
||
async def enable_totp(current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||
result = await db.execute(select(models.User).where(models.User.id == current_user.id))
|
||
user = result.scalars().first()
|
||
if not user:
|
||
raise HTTPException(status_code=400, detail="Пользователь не найден")
|
||
|
||
secret = pyotp.random_base32()
|
||
user.totp_temp_secret = secret
|
||
await db.commit()
|
||
|
||
totp = pyotp.TOTP(secret)
|
||
uri = totp.provisioning_uri(name=user.username, issuer_name="Chepuhagram")
|
||
img = qrcode.make(uri)
|
||
buf = BytesIO()
|
||
img.save(buf, format='PNG')
|
||
buf.seek(0)
|
||
qr_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
|
||
qr_data_url = f"data:image/png;base64,{qr_base64}"
|
||
|
||
return {"secret": secret, "qr_code": qr_data_url}
|
||
|
||
|
||
@authRouter.post("/totp/verify")
|
||
async def verify_totp(data: schemas.TOTPVerifyRequest, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||
result = await db.execute(select(models.User).where(models.User.id == current_user.id))
|
||
user = result.scalars().first()
|
||
if not user:
|
||
raise HTTPException(status_code=400, detail="Пользователь не найден")
|
||
|
||
if not user.totp_temp_secret:
|
||
raise HTTPException(status_code=400, detail="TOTP не включен")
|
||
|
||
try:
|
||
totp = pyotp.TOTP(user.totp_temp_secret)
|
||
code_str = str(data.code).strip()
|
||
is_valid = totp.verify(code_str)
|
||
|
||
if is_valid:
|
||
user.totp_secret = user.totp_temp_secret
|
||
user.totp_temp_secret = None
|
||
await db.commit()
|
||
return {"status": "ok"}
|
||
else:
|
||
raise HTTPException(status_code=400, detail="Неверный код")
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(
|
||
status_code=500, detail=f"Ошибка верификации: {str(e)}")
|
||
|
||
|
||
@authRouter.post("/totp/disable")
|
||
async def disable_totp(current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||
result = await db.execute(select(models.User).where(models.User.id == current_user.id))
|
||
user = result.scalars().first()
|
||
if user:
|
||
user.totp_secret = None
|
||
await db.commit()
|
||
return {"status": "ok"}
|
||
|
||
|
||
@authRouter.post("/refresh")
|
||
async def refresh_token(data: schemas.RefreshRequest, request: Request, db: AsyncSession = Depends(get_db)):
|
||
try:
|
||
# 1. Декодируем переданный REFRESH токен
|
||
payload = jwt.decode(data.refresh_token, security.SECRET_KEY, algorithms=[
|
||
security.ALGORITHM])
|
||
user_id = str(payload.get("sub"))
|
||
|
||
# ДОСТАЕМ SESSION_ID ПРЯМО ИЗ ТОКЕНА (для новых клиентов)
|
||
session_id = payload.get("session_id")
|
||
|
||
if user_id is None:
|
||
raise HTTPException(
|
||
status_code=401, detail="Invalid token payload")
|
||
|
||
session = None
|
||
|
||
# 2. Ищем сессию в базе
|
||
if session_id:
|
||
# === СТРОГИЙ РЕЖИМ (Новые клиенты) ===
|
||
# Ищем мгновенно по ID сессии
|
||
session_res = await db.execute(
|
||
select(models.Session).where(
|
||
models.Session.id == int(session_id))
|
||
)
|
||
session = session_res.scalars().first()
|
||
else:
|
||
# === РЕЖИМ СОВМЕСТИМОСТИ (Старые клиенты) ===
|
||
# Пытаемся вытащить старый access_token из заголовка, СНАЧАЛА объявляя переменную
|
||
auth_header = request.headers.get("authorization", "")
|
||
old_access_token = auth_header.split(
|
||
" ")[1] if "Bearer " in auth_header else None
|
||
|
||
if old_access_token:
|
||
session_res = await db.execute(
|
||
select(models.Session).where(
|
||
models.Session.session_token == old_access_token)
|
||
)
|
||
session = session_res.scalars().first()
|
||
|
||
# 3. ЕСЛИ СЕССИИ НЕТ — ВЫКИДЫВАЕМ НА ЭКРАН ЛОГИНА!
|
||
if not session:
|
||
raise HTTPException(
|
||
status_code=401,
|
||
detail="Сессия была отозвана или устарела. Требуется повторная авторизация."
|
||
)
|
||
|
||
# 4. Генерируем новые токены, ОБЯЗАТЕЛЬНО ПРОШИВАЯ В НИХ ID СЕССИИ
|
||
new_access_token = security.create_access_token(
|
||
data={"sub": user_id}, session_id=session.id
|
||
)
|
||
new_refresh_token = security.create_refresh_token(
|
||
data={"sub": user_id}, session_id=session.id
|
||
)
|
||
|
||
# 5. Обновляем время жизни текущей сессии
|
||
# Оставляем чисто для совместимости со старыми клиентами
|
||
session.session_token = new_access_token
|
||
session.last_active = datetime.now(timezone.utc)
|
||
await db.commit()
|
||
|
||
return {
|
||
"access_token": new_access_token,
|
||
"refresh_token": new_refresh_token,
|
||
"token_type": "bearer"
|
||
}
|
||
|
||
except JWTError:
|
||
raise HTTPException(
|
||
status_code=401, detail="Refresh token expired or invalid")
|
||
|
||
|
||
# --- СИСТЕМА УПРАВЛЕНИЯ СЕССИЯМИ ---
|
||
|
||
# 1. Списочный эндпоинт активных устройств
|
||
@authRouter.get("/sessions")
|
||
async def get_active_sessions(
|
||
request: Request,
|
||
current_user: models.User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
auth_header = request.headers.get("authorization", "")
|
||
current_token = auth_header.split(
|
||
" ")[1] if "Bearer " in auth_header else ""
|
||
|
||
# Достаем ID текущей сессии из токена
|
||
current_session_id = None
|
||
if current_token:
|
||
try:
|
||
payload = jwt.decode(current_token, security.SECRET_KEY, algorithms=[
|
||
security.ALGORITHM])
|
||
current_session_id = payload.get("session_id")
|
||
except:
|
||
pass
|
||
|
||
result = await db.execute(
|
||
select(models.Session)
|
||
.where(models.Session.user_id == current_user.id)
|
||
.order_by(models.Session.last_active.desc())
|
||
)
|
||
sessions = result.scalars().all()
|
||
|
||
return [
|
||
{
|
||
"id": s.id,
|
||
"device_name": s.device_name,
|
||
"ip_address": s.ip_address,
|
||
"created_at": s.created_at.replace(tzinfo=timezone.utc).isoformat() if s.created_at else None,
|
||
"last_active": s.last_active.replace(tzinfo=timezone.utc).isoformat() if s.last_active else None,
|
||
"is_current": (s.id == current_session_id) if current_session_id else (s.session_token == current_token)
|
||
}
|
||
for s in sessions
|
||
]
|
||
|
||
|
||
# 2. Завершить все сессии, кроме текущей (как в Telegram)
|
||
@authRouter.delete("/sessions/clear-others")
|
||
async def revoke_all_other_sessions(
|
||
request: Request,
|
||
current_user: models.User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
auth_header = request.headers.get("authorization", "")
|
||
current_token = auth_header.split(
|
||
" ")[1] if "Bearer " in auth_header else ""
|
||
|
||
# Достаем ID текущей сессии из токена
|
||
try:
|
||
payload = jwt.decode(current_token, security.SECRET_KEY,
|
||
algorithms=[security.ALGORITHM])
|
||
current_session_id = payload.get("session_id")
|
||
except:
|
||
raise HTTPException(status_code=401, detail="Невалидный токен")
|
||
|
||
if not current_session_id:
|
||
raise HTTPException(
|
||
status_code=400, detail="Невозможно определить текущую сессию (старый клиент)")
|
||
|
||
# 1. Находим все ЧУЖИЕ сессии пользователя
|
||
result = await db.execute(
|
||
select(models.Session)
|
||
.where(
|
||
(models.Session.user_id == current_user.id) &
|
||
(models.Session.id != int(current_session_id)) # Исключаем ТЕКУЩИЙ ID
|
||
)
|
||
)
|
||
other_sessions = result.scalars().all()
|
||
|
||
# 2. Удаляем их из базы и МГНОВЕННО убиваем их вебсокеты
|
||
for session in other_sessions:
|
||
await db.delete(session)
|
||
await manager.kill_session_socket(user_id=current_user.id, session_id=session.id)
|
||
|
||
await db.commit()
|
||
return {"status": "ok", "message": f"Завершено других сессий: {len(other_sessions)}"}
|
||
|
||
|
||
# 3. Отзыв (удаление) конкретной сессии по ID
|
||
@authRouter.delete("/sessions/{session_id}")
|
||
async def revoke_session(
|
||
session_id: int,
|
||
request: Request,
|
||
current_user: models.User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
auth_header = request.headers.get("authorization", "")
|
||
current_token = auth_header.split(
|
||
" ")[1] if "Bearer " in auth_header else ""
|
||
|
||
# Достаем ID текущей сессии
|
||
try:
|
||
payload = jwt.decode(current_token, security.SECRET_KEY,
|
||
algorithms=[security.ALGORITHM])
|
||
current_session_id = payload.get("session_id")
|
||
except:
|
||
current_session_id = None
|
||
|
||
result = await db.execute(
|
||
select(models.Session)
|
||
.where((models.Session.id == session_id) & (models.Session.user_id == current_user.id))
|
||
)
|
||
session = result.scalars().first()
|
||
|
||
if not session:
|
||
raise HTTPException(status_code=404, detail="Сессия не найдена")
|
||
|
||
# Безопасность: нельзя удалить сессию, с которой прямо сейчас сделан этот запрос
|
||
if current_session_id and session.id == current_session_id:
|
||
raise HTTPException(
|
||
status_code=400, detail="Нельзя отозвать текущую сессию. Используйте Выход из аккаунта (Logout)."
|
||
)
|
||
# Фолбек для старых клиентов:
|
||
elif not current_session_id and session.session_token == current_token:
|
||
raise HTTPException(
|
||
status_code=400, detail="Нельзя отозвать текущую сессию."
|
||
)
|
||
|
||
await db.delete(session)
|
||
await db.commit()
|
||
|
||
# Убиваем сокет именно этого устройства
|
||
await manager.kill_session_socket(user_id=current_user.id, session_id=session_id)
|
||
|
||
return {"status": "ok", "message": "Сессия успешно отозвана"}
|
||
|
||
|
||
@authRouter.post("/logout")
|
||
async def logout(
|
||
request: Request,
|
||
current_user: models.User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
# 1. Извлекаем токен из заголовка Authorization
|
||
auth_header = request.headers.get("authorization", "")
|
||
current_token = auth_header.split(
|
||
" ")[1] if "Bearer " in auth_header else ""
|
||
|
||
if not current_token:
|
||
raise HTTPException(status_code=401, detail="Токен не найден")
|
||
|
||
# 2. Декодируем токен, чтобы узнать session_id текущего устройства
|
||
try:
|
||
payload = jwt.decode(current_token, security.SECRET_KEY,
|
||
algorithms=[security.ALGORITHM])
|
||
current_session_id = payload.get("session_id")
|
||
except jwt.JWTError:
|
||
raise HTTPException(
|
||
status_code=401, detail="Невалидный или истекший токен")
|
||
|
||
if not current_session_id:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="Невозможно завершить сессию: старая версия приложения без session_id"
|
||
)
|
||
|
||
# 3. Ищем сессию в базе данных
|
||
result = await db.execute(
|
||
select(models.Session).where(
|
||
(models.Session.id == int(current_session_id)) &
|
||
(models.Session.user_id == current_user.id)
|
||
)
|
||
)
|
||
session = result.scalars().first()
|
||
|
||
if not session:
|
||
raise HTTPException(status_code=404, detail="Сессия уже не существует")
|
||
|
||
# 4. Мгновенно обрываем WebSocket-соединение для ЭТОЙ сессии
|
||
# (Клиент сразу получит событие закрытия сокета и запустит локальную очистку)
|
||
await manager.kill_session_socket(user_id=current_user.id, session_id=session.id)
|
||
|
||
# 5. Удаляем сессию из базы данных
|
||
await db.delete(session)
|
||
await db.commit()
|
||
|
||
return {"status": "ok", "message": "Сессия успешно завершена, устройство разлогинено"}
|
||
|
||
|
||
@authRouter.post("/setup-account")
|
||
async def setup_account(data: schemas.SetupAccount, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||
user_to_update = await db.merge(current_user)
|
||
user_to_update.first_name = data.first_name
|
||
user_to_update.last_name = data.last_name
|
||
user_to_update.public_key = data.public_key
|
||
user_to_update.encrypted_private_key = data.encrypted_private_key
|
||
await db.commit()
|
||
await db.refresh(user_to_update)
|
||
return {"status": "ok", "message": "Account setup completed"}
|
||
|
||
|
||
@authRouter.post("/update-fcm")
|
||
async def update_fcm(token: str, request: Request, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||
user_to_update = await db.merge(current_user)
|
||
user_to_update.fcm_token = token
|
||
await db.commit()
|
||
await db.refresh(user_to_update)
|
||
# 1. Извлекаем токен из заголовка Authorization
|
||
auth_header = request.headers.get("authorization", "")
|
||
current_token = auth_header.split(
|
||
" ")[1] if "Bearer " in auth_header else ""
|
||
|
||
if not current_token:
|
||
raise HTTPException(status_code=401, detail="Токен не найден")
|
||
|
||
# 2. Декодируем токен, чтобы узнать session_id текущего устройства
|
||
try:
|
||
payload = jwt.decode(current_token, security.SECRET_KEY,
|
||
algorithms=[security.ALGORITHM])
|
||
current_session_id = payload.get("session_id")
|
||
except jwt.JWTError:
|
||
raise HTTPException(
|
||
status_code=401, detail="Невалидный или истекший токен")
|
||
|
||
if not current_session_id:
|
||
return {"status": "ok"}
|
||
# 3. Ищем сессию в базе данных
|
||
result = await db.execute(
|
||
select(models.Session).where(
|
||
(models.Session.id == int(current_session_id)) &
|
||
(models.Session.user_id == current_user.id)
|
||
)
|
||
)
|
||
session = result.scalars().first()
|
||
session.fcm_token = token
|
||
await db.commit()
|
||
await db.refresh(session)
|
||
|
||
return {"status": "ok"}
|
||
|
||
|
||
@authRouter.post("/qr/approve")
|
||
async def qr_approve(
|
||
data: schemas.QRApproveRequest,
|
||
request: Request,
|
||
current_user: models.User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
# 1. Проверяем, есть ли активное вебсокет-соединение для этой комнаты (room_id)
|
||
if data.room_id not in manager.qr_connections:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Сессия сопряжения не найдена или истекла"
|
||
)
|
||
|
||
# 2. Регистрируем новую сессию для сопряженного устройства
|
||
device_name = data.device_name or "Новое устройство (QR)"
|
||
new_session = await _register_new_login_session(
|
||
user_id=current_user.id,
|
||
request=request,
|
||
db=db,
|
||
device_name=device_name
|
||
)
|
||
|
||
# 3. Генерируем JWT-токены авторизации для новой сессии
|
||
access_token = security.create_access_token(
|
||
data={"sub": str(current_user.id)},
|
||
session_id=new_session.id
|
||
)
|
||
refresh_token = security.create_refresh_token(
|
||
data={"sub": str(current_user.id)},
|
||
session_id=new_session.id
|
||
)
|
||
|
||
new_session.session_token = access_token
|
||
await db.commit()
|
||
|
||
# 4. Отправляем все необходимые данные новому устройству через WebSocket
|
||
payload = {
|
||
"type": "qr_authorized",
|
||
"access_token": access_token,
|
||
"refresh_token": refresh_token,
|
||
"phone_temp_pub": data.phone_temp_pub,
|
||
"enc_private_key_payload": data.enc_private_key_payload,
|
||
"user_id": current_user.id
|
||
}
|
||
|
||
# Отправляем сообщение
|
||
sent = await manager.send_qr_message(data.room_id, payload)
|
||
|
||
# Закрываем сокет комнаты сопряжения
|
||
if sent:
|
||
ws = manager.qr_connections.get(data.room_id)
|
||
if ws:
|
||
try:
|
||
await ws.close()
|
||
except Exception:
|
||
pass
|
||
manager.disconnect_qr(data.room_id)
|
||
|
||
# Отправляем системное оповещение на телефон о новом входе
|
||
current_time = datetime.now(timezone.utc).strftime("%H:%M:%S")
|
||
ip_address = request.client.host if request.client else "0.0.0.0"
|
||
security_alert_text = (
|
||
f"Обнаружен новый вход в ваш аккаунт через QR-код.\n\n"
|
||
f"Устройство: {device_name}\n"
|
||
f"IP-адрес: {ip_address}\n"
|
||
f"Время: {current_time}\n\n"
|
||
f"Если это не вы, немедленно перейдите в Настройки -> Устройства и завершите подозрительную сессию."
|
||
)
|
||
|
||
await send_system_notification(
|
||
db=db,
|
||
receiver_id=current_user.id,
|
||
plain_text=security_alert_text
|
||
)
|
||
|
||
return {"status": "ok", "message": "Вход через QR-код успешно одобрен"}
|