95 lines
3.1 KiB
Python
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()
|