Files
Solo_bot/database/identity_sessions.py
T
2026-04-19 20:59:15 +00:00

150 lines
4.8 KiB
Python

from datetime import datetime, timedelta
from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from config import API_TOKEN_TTL_DAYS
from database.models import Identity, IdentitySession
_LAST_SEEN_TOUCH_SECONDS = 60
def _device_label_from_user_agent(user_agent: str | None) -> str:
if not user_agent:
return "Unknown device"
ua = user_agent.lower()
browser = "Browser"
for needle, label in (
("edg/", "Edge"),
("opr/", "Opera"),
("yabrowser", "Yandex"),
("firefox", "Firefox"),
("chrome", "Chrome"),
("safari", "Safari"),
):
if needle in ua:
browser = label
break
platform = "Desktop"
if "iphone" in ua or "ipad" in ua or "ios" in ua:
platform = "iOS"
elif "android" in ua:
platform = "Android"
elif "macintosh" in ua or "mac os" in ua:
platform = "macOS"
elif "windows" in ua:
platform = "Windows"
elif "linux" in ua:
platform = "Linux"
return f"{browser} · {platform}"[:128]
async def create_identity_session(
session: AsyncSession,
*,
identity: Identity,
token_hash: str,
user_agent: str | None = None,
ip: str | None = None,
device_label: str | None = None,
) -> IdentitySession:
"""Создаёт новую сессию. TTL — из API_TOKEN_TTL_DAYS."""
now = datetime.utcnow()
expires_at = now + timedelta(days=API_TOKEN_TTL_DAYS) if API_TOKEN_TTL_DAYS else None
label = device_label or _device_label_from_user_agent(user_agent)
obj = IdentitySession(
identity_id=identity.id,
token_hash=token_hash,
device_label=label,
user_agent=user_agent,
ip=ip,
created_at=now,
last_seen_at=now,
expires_at=expires_at,
)
session.add(obj)
await session.flush()
return obj
async def get_session_by_token_hash(
session: AsyncSession, token_hash: str
) -> IdentitySession | None:
result = await session.execute(
select(IdentitySession).where(IdentitySession.token_hash == token_hash)
)
return result.scalar_one_or_none()
async def list_sessions_for_identity(
session: AsyncSession, identity_id: str
) -> list[IdentitySession]:
now = datetime.utcnow()
result = await session.execute(
select(IdentitySession)
.where(IdentitySession.identity_id == identity_id)
.where(
(IdentitySession.expires_at.is_(None)) | (IdentitySession.expires_at > now)
)
.order_by(IdentitySession.last_seen_at.desc())
)
return list(result.scalars().all())
async def delete_session_by_id(
session: AsyncSession, *, session_id: str, identity_id: str
) -> bool:
"""Удаляет сессию по id при условии, что она принадлежит identity_id."""
result = await session.execute(
delete(IdentitySession)
.where(IdentitySession.id == session_id)
.where(IdentitySession.identity_id == identity_id)
)
return (result.rowcount or 0) > 0
async def delete_session_by_token_hash(session: AsyncSession, token_hash: str) -> bool:
result = await session.execute(
delete(IdentitySession).where(IdentitySession.token_hash == token_hash)
)
return (result.rowcount or 0) > 0
async def delete_other_sessions(
session: AsyncSession, *, identity_id: str, keep_token_hash: str
) -> int:
"""Удаляет все сессии identity кроме той, которая соответствует keep_token_hash."""
result = await session.execute(
delete(IdentitySession)
.where(IdentitySession.identity_id == identity_id)
.where(IdentitySession.token_hash != keep_token_hash)
)
return int(result.rowcount or 0)
async def touch_session_last_seen(
session: AsyncSession, sess: IdentitySession
) -> None:
"""Обновляет last_seen_at, но только если прошло ≥60 секунд — чтобы не писать в БД на каждый запрос."""
now = datetime.utcnow()
if (now - sess.last_seen_at).total_seconds() < _LAST_SEEN_TOUCH_SECONDS:
return
await session.execute(
update(IdentitySession)
.where(IdentitySession.id == sess.id)
.values(last_seen_at=now)
)
sess.last_seen_at = now
async def cleanup_expired_sessions(session: AsyncSession) -> int:
"""Удаляет все сессии с expires_at в прошлом. Возвращает количество удалённых."""
now = datetime.utcnow()
result = await session.execute(
delete(IdentitySession)
.where(IdentitySession.expires_at.is_not(None))
.where(IdentitySession.expires_at <= now)
)
return int(result.rowcount or 0)