"""User-management use cases: admin CRUD plus self-service password change. Deletion is *soft* (deactivate) — likes, play history and playlists reference users via append-only event-logs, so rows must not vanish (plan §4 invariants). """ import uuid from app.domain.entities import User from app.domain.errors import AlreadyExistsError, AuthenticationError, NotFoundError from app.domain.ports import PasswordHasher, RefreshTokenRepository, UserRepository class UserService: def __init__( self, *, users: UserRepository, refresh_tokens: RefreshTokenRepository, hasher: PasswordHasher, ) -> None: self._users = users self._refresh_tokens = refresh_tokens self._hasher = hasher async def create_user( self, *, username: str, password: str, is_superuser: bool = False ) -> User: if await self._users.get_credentials_by_username(username) is not None: raise AlreadyExistsError(f"Username {username!r} is taken.") return await self._users.add( username=username, password_hash=self._hasher.hash(password), is_superuser=is_superuser, ) async def list_users(self, *, limit: int = 50, offset: int = 0) -> list[User]: return await self._users.list(limit=limit, offset=offset) async def get_user(self, user_id: uuid.UUID) -> User: user = await self._users.get_by_id(user_id) if user is None: raise NotFoundError("User not found.") return user async def set_superuser(self, user_id: uuid.UUID, *, is_superuser: bool) -> User: await self.get_user(user_id) return await self._users.set_superuser(user_id, is_superuser) async def set_active(self, user_id: uuid.UUID, *, is_active: bool) -> User: await self.get_user(user_id) user = await self._users.set_active(user_id, is_active) if not is_active: # Deactivating a user kills their sessions immediately. await self._refresh_tokens.revoke_all_for_user(user_id) return user async def reset_password(self, user_id: uuid.UUID, *, new_password: str) -> None: """Admin-driven password reset. Revokes all sessions.""" await self.get_user(user_id) await self._users.set_password_hash(user_id, self._hasher.hash(new_password)) await self._refresh_tokens.revoke_all_for_user(user_id) async def deactivate(self, user_id: uuid.UUID) -> User: """Soft delete: disable the account, keep the row for referential history.""" return await self.set_active(user_id, is_active=False) async def change_password( self, user_id: uuid.UUID, *, current_password: str, new_password: str ) -> None: """Self-service change: verify the current password first.""" user = await self.get_user(user_id) credentials = await self._users.get_credentials_by_username(user.username) assert credentials is not None # user exists; fetched by id above valid, _ = self._hasher.verify_and_update(current_password, credentials.password_hash) if not valid: raise AuthenticationError("Current password is incorrect.") await self._users.set_password_hash(user_id, self._hasher.hash(new_password)) await self._refresh_tokens.revoke_all_for_user(user_id)