from __future__ import annotations import hashlib import logging from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.core.exceptions import ConflictError, UnauthorizedError from app.core.security import create_access_token, get_password_hash, verify_password from app.models.user import RoleEnum, User from app.schemas.auth import LoginRequest, UserCreate audit_logger = logging.getLogger("app.audit") class AuthService: def __init__(self, db: Session) -> None: self.db = db def register_user(self, payload: UserCreate) -> User: existing_user = self.db.scalar(select(User).where(User.username == payload.username)) if existing_user is not None: raise ConflictError("Username already exists") user = User( username=payload.username, password_hash=get_password_hash(payload.password), role=RoleEnum.client, is_active=True, ) self.db.add(user) try: self.db.commit() self.db.refresh(user) except IntegrityError as exc: self.db.rollback() raise ConflictError("Username already exists") from exc audit_logger.info("user_registered user_id=%s role=%s", user.id, user.role.value) return user def authenticate_user(self, payload: LoginRequest) -> str: user = self.db.scalar(select(User).where(User.username == payload.username)) subject_hash = self._hash_subject(payload.username) if user is None or not verify_password(payload.password, user.password_hash): audit_logger.warning("login_failed subject_hash=%s", subject_hash) raise UnauthorizedError("Invalid username or password") if not user.is_active: audit_logger.warning("login_rejected_inactive user_id=%s", user.id) raise UnauthorizedError("Invalid username or password") access_token = create_access_token(subject=str(user.id)) audit_logger.info("login_success user_id=%s role=%s", user.id, user.role.value) return access_token def get_user_by_id(self, user_id: int) -> User | None: return self.db.get(User, user_id) @staticmethod def _hash_subject(subject: str) -> str: return hashlib.sha256(subject.encode("utf-8")).hexdigest()[:12]