from __future__ import annotations from datetime import datetime, timedelta from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.exceptions import ConflictException, UnauthorizedException from app.core.security import ( create_access_token, create_refresh_token, decode_token, hash_password, verify_password, ) from app.models.mariadb.auth import RefreshToken from app.models.mariadb.user import User from app.repositories.auth_repo import AuthRepository from app.repositories.user_repo import UserRepository from app.schemas.auth import TokenResponse class AuthService: def __init__(self, session: AsyncSession): self.user_repo = UserRepository(session) self.auth_repo = AuthRepository(session) async def register( self, email: str, password: str, full_name: str = "" ) -> User: existing = await self.user_repo.get_by_email(email) if existing: raise ConflictException("Email already registered") user = User( email=email, hashed_password=hash_password(password), ) return await self.user_repo.create_with_profile(user, full_name=full_name) async def login(self, email: str, password: str) -> TokenResponse: user = await self.user_repo.get_by_email(email) if not user or not verify_password(password, user.hashed_password): raise UnauthorizedException("Invalid email or password") if not user.is_active: raise UnauthorizedException("Account is deactivated") user.last_login_at = datetime.utcnow() return await self._create_tokens(user) async def refresh(self, refresh_token_str: str) -> TokenResponse: payload = decode_token(refresh_token_str) if not payload or payload.get("type") != "refresh": raise UnauthorizedException("Invalid refresh token") stored = await self.auth_repo.get_by_token(refresh_token_str) if not stored: raise UnauthorizedException("Refresh token not found or expired") stored.is_revoked = True user = await self.user_repo.get_by_id(stored.user_id) if not user or not user.is_active: raise UnauthorizedException("User not found or deactivated") return await self._create_tokens(user) async def logout(self, user_id: int) -> None: await self.auth_repo.revoke_all_for_user(user_id) async def _create_tokens(self, user: User) -> TokenResponse: access = create_access_token(user.id, user.role) # type: ignore[arg-type] refresh = create_refresh_token(user.id) # type: ignore[arg-type] token_obj = RefreshToken( user_id=user.id, # type: ignore[arg-type] token=refresh, expires_at=datetime.utcnow() + timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS), ) await self.auth_repo.create(token_obj) return TokenResponse(access_token=access, refresh_token=refresh)