85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
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)
|