Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""User service for VoIdea."""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.models.user import User
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def _check_owner(target: User) -> None:
|
||||
"""Protect owner from deletion, suspension, or role change."""
|
||||
if target.is_owner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Владелец системы защищён от изменений",
|
||||
)
|
||||
|
||||
|
||||
class UserService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_by_id(self, user_id: str) -> Optional[User]:
|
||||
result = await self.db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_email(self, email: str) -> Optional[User]:
|
||||
result = await self.db.execute(
|
||||
select(User).where(User.email == email)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def update_profile(
|
||||
self, user_id: str, display_name: Optional[str] = None,
|
||||
avatar_url: Optional[str] = None,
|
||||
) -> Optional[User]:
|
||||
user = await self.get_by_id(user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if display_name is not None:
|
||||
user.display_name = display_name
|
||||
if avatar_url is not None:
|
||||
user.avatar_url = avatar_url
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(user)
|
||||
return user
|
||||
|
||||
async def delete(self, user_id: str) -> bool:
|
||||
user = await self.get_by_id(user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
_check_owner(user)
|
||||
user.is_active = False
|
||||
await self.db.commit()
|
||||
return True
|
||||
|
||||
async def hard_delete(self, user_id: str) -> bool:
|
||||
"""Permanently delete user from DB (admin only, never for owner)."""
|
||||
user = await self.get_by_id(user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
_check_owner(user)
|
||||
await self.db.delete(user)
|
||||
await self.db.commit()
|
||||
return True
|
||||
|
||||
async def list_users(
|
||||
self, skip: int = 0, limit: int = 100, search: Optional[str] = None
|
||||
) -> list[User]:
|
||||
query = select(User)
|
||||
if search:
|
||||
query = query.where(
|
||||
or_(
|
||||
User.display_name.ilike(f"%{search}%"),
|
||||
User.email.ilike(f"%{search}%"),
|
||||
)
|
||||
)
|
||||
result = await self.db.execute(
|
||||
query.order_by(User.created_at.desc()).offset(skip).limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def update_role(self, user_id: str, is_superuser: bool) -> Optional[User]:
|
||||
user = await self.get_by_id(user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
_check_owner(user)
|
||||
user.is_superuser = is_superuser
|
||||
await self.db.commit()
|
||||
await self.db.refresh(user)
|
||||
return user
|
||||
|
||||
# ── v2.0 admin methods ──
|
||||
|
||||
async def admin_update_user(
|
||||
self, user_id: str, updates: dict[str, Any]
|
||||
) -> Optional[User]:
|
||||
"""Admin: update user fields with owner protection."""
|
||||
user = await self.get_by_id(user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
is_owner_change = any(k in updates for k in ("is_owner", "role"))
|
||||
if is_owner_change or "is_active" in updates:
|
||||
_check_owner(user)
|
||||
|
||||
for key, value in updates.items():
|
||||
if hasattr(user, key) and key not in ("id", "password_hash", "created_at"):
|
||||
if key == "permissions" and value is not None:
|
||||
value = {k: bool(v) for k, v in value.items()}
|
||||
setattr(user, key, value)
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(user)
|
||||
return user
|
||||
|
||||
async def set_owner_by_email(self, email: str) -> Optional[User]:
|
||||
"""Mark a user as system owner by email (called at startup / registration)."""
|
||||
if not settings.system_owner_email:
|
||||
return None
|
||||
if email != settings.system_owner_email:
|
||||
return None
|
||||
user = await self.get_by_email(email)
|
||||
if not user:
|
||||
return None
|
||||
if user.is_owner:
|
||||
return user
|
||||
|
||||
user.is_owner = True
|
||||
user.role = "owner"
|
||||
user.is_superuser = True
|
||||
user.is_active = True
|
||||
await self.db.commit()
|
||||
await self.db.refresh(user)
|
||||
return user
|
||||
Reference in New Issue
Block a user