utp_service/backend/app/services/auth.py
2026-04-07 00:00:50 +03:00

95 lines
3.1 KiB
Python

import secrets
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.user import User, UserSession
ALGORITHM = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(user_id: int) -> str:
expire = datetime.now(timezone.utc) + timedelta(seconds=settings.ACCESS_TOKEN_EXPIRE_SECONDS)
return jwt.encode({"sub": str(user_id), "exp": expire}, settings.SECRET_KEY, algorithm=ALGORITHM)
def decode_access_token(token: str) -> int | None:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
sub = payload.get("sub")
if sub is None:
return None
return int(sub)
except (JWTError, ValueError):
return None
async def authenticate_user(db: AsyncSession, username: str, password: str) -> User | None:
result = await db.execute(select(User).where(User.username == username, User.is_active == True))
user = result.scalar_one_or_none()
if user is None or not verify_password(password, user.password_hash):
return None
return user
async def create_session(
db: AsyncSession, user_id: int, ip_address: str | None = None, user_agent: str | None = None
) -> str:
token = secrets.token_urlsafe(32)
now = datetime.now(timezone.utc)
session = UserSession(
user_id=user_id,
refresh_token=token,
expires_at=now + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),
created_at=now,
ip_address=ip_address,
user_agent=user_agent,
)
db.add(session)
await db.flush()
return token
async def refresh_session(
db: AsyncSession, refresh_token: str, ip_address: str | None = None
) -> tuple[User, str] | None:
now = datetime.now(timezone.utc)
result = await db.execute(
select(UserSession)
.where(UserSession.refresh_token == refresh_token, UserSession.expires_at > now)
)
session = result.scalar_one_or_none()
if session is None:
return None
user_result = await db.execute(select(User).where(User.id == session.user_id, User.is_active == True))
user = user_result.scalar_one_or_none()
if user is None:
return None
# Rotate: delete old session, create new
await db.delete(session)
new_token = await create_session(db, user.id, ip_address=ip_address)
return user, new_token
async def revoke_session(db: AsyncSession, refresh_token: str) -> None:
await db.execute(delete(UserSession).where(UserSession.refresh_token == refresh_token))
async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None:
result = await db.execute(select(User).where(User.id == user_id, User.is_active == True))
return result.scalar_one_or_none()