""" đŸ‘€ Admin Users API VĂ©gpontok: GET /admin/users/stats — FelhasznĂĄlĂłi statisztikĂĄk (dashboard) GET /admin/users/{user_id} — FelhasznĂĄlĂł rĂ©szletes adatai (User + Person + Address) PATCH /admin/users/{user_id} — FelhasznĂĄlĂł szerkesztĂ©se admin ĂĄltal GET /admin/users/{user_id}/memberships — FelhasznĂĄlĂł szervezeti tagsĂĄgai DELETE /admin/users/{user_id}/hard — [SUPERADMIN] VĂ©gleges felhasznĂĄlĂł törlĂ©s MegjegyzĂ©s: A GET /admin/users (lista) Ă©s POST /admin/users/bulk-action vĂ©gpontok a admin.py modulban talĂĄlhatĂłk, mivel az a /admin prefix alĂĄ van regisztrĂĄlva, Ă©s a /users, /users/bulk-action Ăștvonalakat kezeli. """ from __future__ import annotations import logging from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel, EmailStr, Field from sqlalchemy import select, func, update as sa_update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload from app.api import deps from app.db.session import get_db from app.models.identity import User, Person, UserRole from app.models.identity.address import Address from app.models.identity.identity import Wallet from app.models.marketplace.organization import Organization, OrganizationMember from app.models.system.audit import SecurityAuditLog, FinancialLedger from app.models.vehicle.history import AuditLog, LogSeverity from app.schemas.user import UserResponse from app.schemas.organization import OrganizationMemberResponse logger = logging.getLogger("admin-users") router = APIRouter() # ============================================================================= # Pydantic Schemas # ============================================================================= class RegistrationTrendItem(BaseModel): """Napi regisztrĂĄciĂłs trend elem.""" date: str = Field(..., description="DĂĄtum (YYYY-MM-DD)") count: int = Field(..., description="RegisztrĂĄciĂłk szĂĄma") class UserStatsResponse(BaseModel): """FelhasznĂĄlĂłi statisztikĂĄk a dashboard szĂĄmĂĄra.""" total_users: int = Field(..., description="Összes felhasznĂĄlĂł") active_users: int = Field(..., description="AktĂ­v felhasznĂĄlĂłk") deleted_users: int = Field(..., description="Törölt felhasznĂĄlĂłk") banned_users: int = Field(..., description="Kitiltott felhasznĂĄlĂłk") new_users_today: int = Field(..., description="Ma regisztrĂĄlt felhasznĂĄlĂłk") new_users_this_week: int = Field(..., description="Ezen a hĂ©ten regisztrĂĄlt felhasznĂĄlĂłk") new_users_this_month: int = Field(..., description="Ebben a hĂłnapban regisztrĂĄlt felhasznĂĄlĂłk") users_by_role: Dict[str, int] = Field(..., description="FelhasznĂĄlĂłk szerepkör szerint") users_by_plan: Dict[str, int] = Field(..., description="FelhasznĂĄlĂłk elƑfizetĂ©si csomag szerint") users_by_language: Dict[str, int] = Field(..., description="FelhasznĂĄlĂłk nyelv szerint") users_with_person: int = Field(..., description="Person rekorddal rendelkezƑ felhasznĂĄlĂłk") users_without_person: int = Field(..., description="Person rekord nĂ©lkĂŒli felhasznĂĄlĂłk") registration_trend: List[RegistrationTrendItem] = Field( default_factory=list, description="RegisztrĂĄciĂłs trend (utolsĂł 30 nap)" ) active_organizations_count: int = Field(..., description="AktĂ­v szervezetek szĂĄma") total_memberships: int = Field(..., description="Összes szervezeti tagsĂĄg") class AdminUserUpdate(BaseModel): """Admin ĂĄltal szerkeszthetƑ felhasznĂĄlĂłi mezƑk. Csak a megadott mezƑk mĂłdosĂ­thatĂłk admin felĂŒletrƑl. A role_id mĂłdosĂ­tĂĄsa csak SUPERADMIN szĂĄmĂĄra engedĂ©lyezett. """ email: Optional[EmailStr] = Field(default=None, description="Email cĂ­m (csak admin/superadmin)") is_active: Optional[bool] = Field(default=None, description="AktĂ­v stĂĄtusz (letiltĂĄs/feloldĂĄs)") is_vip: Optional[bool] = Field(default=None, description="VIP stĂĄtusz") preferred_language: Optional[str] = Field(default=None, max_length=5, description="Nyelv") region_code: Optional[str] = Field(default=None, max_length=5, description="RĂ©giĂł kĂłd") preferred_currency: Optional[str] = Field(default=None, max_length=3, description="PĂ©nznem") subscription_plan: Optional[str] = Field(default=None, max_length=30, description="ElƑfizetĂ©si csomag") subscription_expires_at: Optional[datetime] = Field(default=None, description="ElƑfizetĂ©s lejĂĄrata") scope_level: Optional[str] = Field(default=None, max_length=30, description="Scope szint") scope_id: Optional[str] = Field(default=None, max_length=50, description="Scope azonosĂ­tĂł") role_id: Optional[int] = Field(default=None, description="RBAC szerepkör ID (csak superadmin)") custom_permissions: Optional[Dict[str, Any]] = Field(default=None, description="Egyedi jogosultsĂĄgok") # Person almzƑk person: Optional["AdminPersonUpdate"] = Field(default=None, description="SzemĂ©lyes adatok") class AdminPersonUpdate(BaseModel): """Admin ĂĄltal szerkeszthetƑ szemĂ©lyes adatok.""" last_name: Optional[str] = Field(default=None, description="VezetĂ©knĂ©v") first_name: Optional[str] = Field(default=None, description="KeresztnĂ©v") phone: Optional[str] = Field(default=None, description="TelefonszĂĄm") mothers_last_name: Optional[str] = Field(default=None, description="Anyja szĂŒletĂ©si vezetĂ©kneve") mothers_first_name: Optional[str] = Field(default=None, description="Anyja szĂŒletĂ©si keresztneve") birth_place: Optional[str] = Field(default=None, description="SzĂŒletĂ©si hely") birth_date: Optional[datetime] = Field(default=None, description="SzĂŒletĂ©si dĂĄtum") identity_docs: Optional[Dict[str, Any]] = Field(default=None, description="SzemĂ©lyi okmĂĄny adatok") # ============================================================================= # Endpoints # ============================================================================= @router.get( "/stats", response_model=UserStatsResponse, summary="FelhasznĂĄlĂłi statisztikĂĄk lekĂ©rĂ©se", description=( "Visszaadja a felhasznĂĄlĂłk összesĂ­tett statisztikĂĄit a dashboard szĂĄmĂĄra, " "beleĂ©rtve a szerepkör, elƑfizetĂ©si csomag Ă©s nyelv szerinti megoszlĂĄst, " "valamint a regisztrĂĄciĂłs trendet. Csak admin/staff jogosultsĂĄggal Ă©rhetƑ el." ), ) async def get_user_stats( db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("user:view")), ) -> UserStatsResponse: """ GET /admin/users/stats ÖsszesĂ­tett felhasznĂĄlĂłi statisztikĂĄk a dashboard szĂĄmĂĄra. Az összes lekĂ©rdezĂ©s egyetlen tranzakciĂłban fut le. """ now = datetime.now(timezone.utc) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_start = today_start - timedelta(days=today_start.weekday()) month_start = today_start.replace(day=1) # ── 1. Alap statisztikĂĄk ── total_users_result = await db.execute( select(func.count(User.id)) ) total_users = total_users_result.scalar() or 0 active_users_result = await db.execute( select(func.count(User.id)).where(User.is_active.is_(True), User.is_deleted.is_(False)) ) active_users = active_users_result.scalar() or 0 deleted_users_result = await db.execute( select(func.count(User.id)).where(User.is_deleted.is_(True)) ) deleted_users = deleted_users_result.scalar() or 0 banned_users_result = await db.execute( select(func.count(User.id)).where( User.is_active.is_(False), User.is_deleted.is_(False), ) ) banned_users = banned_users_result.scalar() or 0 # ── 2. Új felhasznĂĄlĂłk ── new_today_result = await db.execute( select(func.count(User.id)).where(User.created_at >= today_start) ) new_users_today = new_today_result.scalar() or 0 new_week_result = await db.execute( select(func.count(User.id)).where(User.created_at >= week_start) ) new_users_this_week = new_week_result.scalar() or 0 new_month_result = await db.execute( select(func.count(User.id)).where(User.created_at >= month_start) ) new_users_this_month = new_month_result.scalar() or 0 # ── 3. FelhasznĂĄlĂłk szerepkör szerint ── role_rows = await db.execute( select(User.role, func.count(User.id).label("cnt")) .group_by(User.role) ) users_by_role: Dict[str, int] = {} for row in role_rows: role_name = row.role.value if hasattr(row.role, 'value') else str(row.role) users_by_role[role_name.lower()] = row.cnt # ── 4. FelhasznĂĄlĂłk elƑfizetĂ©si csomag szerint ── plan_rows = await db.execute( select(User.subscription_plan, func.count(User.id).label("cnt")) .group_by(User.subscription_plan) ) users_by_plan: Dict[str, int] = {} for row in plan_rows: users_by_plan[row.subscription_plan.lower()] = row.cnt # ── 5. FelhasznĂĄlĂłk nyelv szerint ── lang_rows = await db.execute( select(User.preferred_language, func.count(User.id).label("cnt")) .group_by(User.preferred_language) ) users_by_language: Dict[str, int] = {} for row in lang_rows: users_by_language[row.preferred_language] = row.cnt # ── 6. Person kapcsolat ── with_person_result = await db.execute( select(func.count(User.id)).where(User.person_id.isnot(None)) ) users_with_person = with_person_result.scalar() or 0 without_person_result = await db.execute( select(func.count(User.id)).where(User.person_id.is_(None)) ) users_without_person = without_person_result.scalar() or 0 # ── 7. RegisztrĂĄciĂłs trend (utolsĂł 30 nap) ── thirty_days_ago = today_start - timedelta(days=30) day_trunc = func.date_trunc("day", User.created_at) trend_rows = await db.execute( select( day_trunc.label("day"), func.count(User.id).label("cnt"), ) .where(User.created_at >= thirty_days_ago) .group_by(day_trunc) .order_by(day_trunc) ) registration_trend: List[RegistrationTrendItem] = [] for row in trend_rows: day_str = row.day.strftime("%Y-%m-%d") if row.day else "" registration_trend.append(RegistrationTrendItem(date=day_str, count=row.cnt)) # ── 8. Szervezeti statisztikĂĄk ── active_orgs_result = await db.execute( select(func.count(Organization.id)) .where(Organization.is_anonymized.is_(False)) ) active_organizations_count = active_orgs_result.scalar() or 0 total_memberships_result = await db.execute( select(func.count(OrganizationMember.id)) ) total_memberships = total_memberships_result.scalar() or 0 return UserStatsResponse( total_users=total_users, active_users=active_users, deleted_users=deleted_users, banned_users=banned_users, new_users_today=new_users_today, new_users_this_week=new_users_this_week, new_users_this_month=new_users_this_month, users_by_role=users_by_role, users_by_plan=users_by_plan, users_by_language=users_by_language, users_with_person=users_with_person, users_without_person=users_without_person, registration_trend=registration_trend, active_organizations_count=active_organizations_count, total_memberships=total_memberships, ) @router.get( "/{user_id}", response_model=UserResponse, summary="FelhasznĂĄlĂł rĂ©szletes adatainak lekĂ©rĂ©se", description=( "Visszaadja egy felhasznĂĄlĂł összes adatĂĄt, beleĂ©rtve a Person rekordot " "Ă©s a beĂĄgyazott Address adatokat. Csak admin/staff jogosultsĂĄggal Ă©rhetƑ el." ), ) async def get_admin_user( user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("user:view")), ) -> Dict[str, Any]: """ GET /admin/users/{user_id} LekĂ©rdezi a felhasznĂĄlĂłt a megadott ID alapjĂĄn, eager loading-gal betölti a Person Ă©s Address kapcsolatokat, majd visszaadja a teljes UserResponse-t. Args: user_id: A lekĂ©rdezni kĂ­vĂĄnt felhasznĂĄlĂł ID-ja. Returns: UserResponse sĂ©ma szerinti felhasznĂĄlĂłi adatok. Raises: HTTPException 404: Ha a felhasznĂĄlĂł nem talĂĄlhatĂł vagy törölve lett. """ stmt = ( select(User) .where(User.id == user_id) .options( joinedload(User.person) .joinedload(Person.address) .joinedload(Address.postal_code) ) ) result = await db.execute(stmt) user = result.unique().scalar_one_or_none() if not user or user.is_deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="FelhasznĂĄlĂł nem talĂĄlhatĂł.", ) # Reuse the existing _build_user_response helper from users.py from app.api.v1.endpoints.users import _build_user_response response_data = await _build_user_response(user, db=db) return UserResponse.model_validate(response_data) @router.patch( "/{user_id}", response_model=Dict[str, Any], summary="FelhasznĂĄlĂł szerkesztĂ©se", description=( "Admin ĂĄltali felhasznĂĄlĂł szerkesztĂ©s. Csak a megadott mezƑk mĂłdosĂ­thatĂłk. " "A role_id mĂłdosĂ­tĂĄsa csak SUPERADMIN szĂĄmĂĄra engedĂ©lyezett. " "Szuperadmin felhasznĂĄlĂł nem mĂłdosĂ­thatĂł." ), ) async def update_admin_user( user_id: int, update_data: AdminUserUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("user:edit")), ) -> Dict[str, Any]: """ PATCH /admin/users/{user_id} Admin ĂĄltali felhasznĂĄlĂł szerkesztĂ©s. Csak a megadott mezƑk mĂłdosĂ­thatĂłk. A role_id mĂłdosĂ­tĂĄsa csak SUPERADMIN szĂĄmĂĄra engedĂ©lyezett. Szuperadmin felhasznĂĄlĂł nem mĂłdosĂ­thatĂł. Args: user_id: A mĂłdosĂ­tandĂł felhasznĂĄlĂł ID-ja. update_data: A mĂłdosĂ­tandĂł mezƑk. Returns: UserResponse sĂ©ma szerinti frissĂ­tett felhasznĂĄlĂłi adatok. Raises: HTTPException 404: Ha a felhasznĂĄlĂł nem talĂĄlhatĂł vagy törölve lett. HTTPException 403: Ha a mĂłdosĂ­tĂĄs nem engedĂ©lyezett. """ # ── 1. LekĂ©rdezzĂŒk a felhasznĂĄlĂłt ── stmt = ( select(User) .where(User.id == user_id) .options( joinedload(User.person) .joinedload(Person.address) .joinedload(Address.postal_code) ) ) result = await db.execute(stmt) user = result.unique().scalar_one_or_none() if not user or user.is_deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="FelhasznĂĄlĂł nem talĂĄlhatĂł.", ) # ── 2. BiztonsĂĄgi ellenƑrzĂ©sek ── # Szuperadmin felhasznĂĄlĂł nem mĂłdosĂ­thatĂł (mĂ©g mĂĄsik superadmin ĂĄltal sem) if user.role == UserRole.SUPERADMIN: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Szuperadmin felhasznĂĄlĂł nem mĂłdosĂ­thatĂł.", ) # role_id mĂłdosĂ­tĂĄsa csak SUPERADMIN szĂĄmĂĄra engedĂ©lyezett if update_data.role_id is not None and current_user.role != UserRole.SUPERADMIN: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Csak SUPERADMIN mĂłdosĂ­thatja a szerepkört.", ) # ── 3. ÖsszegyƱjtjĂŒk a vĂĄltozĂĄsokat audit loghoz ── old_data: Dict[str, Any] = {} new_data: Dict[str, Any] = {} # ── 4. User mezƑk frissĂ­tĂ©se ── update_fields: Dict[str, Any] = {} if update_data.email is not None: # Email egyedisĂ©g ellenƑrzĂ©s existing = await db.execute( select(User).where(User.email == update_data.email, User.id != user_id) ) if existing.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Ez az email cĂ­m mĂĄr hasznĂĄlatban van.", ) old_data["email"] = user.email new_data["email"] = update_data.email update_fields["email"] = update_data.email if update_data.is_active is not None: old_data["is_active"] = user.is_active new_data["is_active"] = update_data.is_active update_fields["is_active"] = update_data.is_active if update_data.is_vip is not None: old_data["is_vip"] = user.is_vip new_data["is_vip"] = update_data.is_vip update_fields["is_vip"] = update_data.is_vip if update_data.preferred_language is not None: old_data["preferred_language"] = user.preferred_language new_data["preferred_language"] = update_data.preferred_language update_fields["preferred_language"] = update_data.preferred_language if update_data.region_code is not None: old_data["region_code"] = user.region_code new_data["region_code"] = update_data.region_code update_fields["region_code"] = update_data.region_code if update_data.preferred_currency is not None: old_data["preferred_currency"] = user.preferred_currency new_data["preferred_currency"] = update_data.preferred_currency update_fields["preferred_currency"] = update_data.preferred_currency if update_data.subscription_plan is not None: old_data["subscription_plan"] = user.subscription_plan new_data["subscription_plan"] = update_data.subscription_plan update_fields["subscription_plan"] = update_data.subscription_plan if update_data.subscription_expires_at is not None: old_data["subscription_expires_at"] = ( user.subscription_expires_at.isoformat() if user.subscription_expires_at else None ) new_data["subscription_expires_at"] = update_data.subscription_expires_at.isoformat() update_fields["subscription_expires_at"] = update_data.subscription_expires_at if update_data.scope_level is not None: old_data["scope_level"] = user.scope_level new_data["scope_level"] = update_data.scope_level update_fields["scope_level"] = update_data.scope_level if update_data.scope_id is not None: old_data["scope_id"] = user.scope_id new_data["scope_id"] = update_data.scope_id update_fields["scope_id"] = update_data.scope_id if update_data.role_id is not None: old_data["role_id"] = user.role_id new_data["role_id"] = update_data.role_id update_fields["role_id"] = update_data.role_id if update_data.custom_permissions is not None: old_data["custom_permissions"] = user.custom_permissions new_data["custom_permissions"] = update_data.custom_permissions update_fields["custom_permissions"] = update_data.custom_permissions # ── 5. Person mezƑk frissĂ­tĂ©se ── person_updated = False if update_data.person is not None and user.person is not None: person = user.person person_update_fields: Dict[str, Any] = {} if update_data.person.last_name is not None: old_data["person.last_name"] = person.last_name new_data["person.last_name"] = update_data.person.last_name person_update_fields["last_name"] = update_data.person.last_name if update_data.person.first_name is not None: old_data["person.first_name"] = person.first_name new_data["person.first_name"] = update_data.person.first_name person_update_fields["first_name"] = update_data.person.first_name if update_data.person.phone is not None: old_data["person.phone"] = person.phone new_data["person.phone"] = update_data.person.phone person_update_fields["phone"] = update_data.person.phone if update_data.person.mothers_last_name is not None: old_data["person.mothers_last_name"] = person.mothers_last_name new_data["person.mothers_last_name"] = update_data.person.mothers_last_name person_update_fields["mothers_last_name"] = update_data.person.mothers_last_name if update_data.person.mothers_first_name is not None: old_data["person.mothers_first_name"] = person.mothers_first_name new_data["person.mothers_first_name"] = update_data.person.mothers_first_name person_update_fields["mothers_first_name"] = update_data.person.mothers_first_name if update_data.person.birth_place is not None: old_data["person.birth_place"] = person.birth_place new_data["person.birth_place"] = update_data.person.birth_place person_update_fields["birth_place"] = update_data.person.birth_place if update_data.person.birth_date is not None: old_data["person.birth_date"] = ( person.birth_date.isoformat() if person.birth_date else None ) new_data["person.birth_date"] = update_data.person.birth_date.isoformat() person_update_fields["birth_date"] = update_data.person.birth_date if update_data.person.identity_docs is not None: old_data["person.identity_docs"] = person.identity_docs new_data["person.identity_docs"] = update_data.person.identity_docs person_update_fields["identity_docs"] = update_data.person.identity_docs if person_update_fields: for field, value in person_update_fields.items(): setattr(person, field, value) person.updated_at = datetime.now(timezone.utc) person_updated = True # ── 6. Ha nincs vĂĄltozĂĄs, dobjunk hibĂĄt ── if not update_fields and not person_updated: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nincs mĂłdosĂ­tandĂł mezƑ.", ) # ── 7. VĂ©grehajtjuk a frissĂ­tĂ©st ── if update_fields: stmt_update = ( sa_update(User) .where(User.id == user_id) .values(**update_fields) ) await db.execute(stmt_update) await db.commit() # ── 8. Audit log ── try: audit_entry = AuditLog( user_id=current_user.id, severity=LogSeverity.info, action="ADMIN_USER_UPDATE", target_type="user", target_id=str(user_id), old_data=old_data if old_data else None, new_data=new_data if new_data else None, ) db.add(audit_entry) await db.commit() except Exception as e: logger.warning(f"Failed to write audit log for user update {user_id}: {e}") await db.rollback() # ── 9. Visszaadjuk a frissĂ­tett adatokat ── # ÚjratöltjĂŒk a frissĂ­tett adatokat stmt_refresh = ( select(User) .where(User.id == user_id) .options( joinedload(User.person) .joinedload(Person.address) .joinedload(Address.postal_code) ) ) result_refresh = await db.execute(stmt_refresh) updated_user = result_refresh.unique().scalar_one_or_none() from app.api.v1.endpoints.users import _build_user_response response_data = await _build_user_response(updated_user, db=db) # KibƑvĂ­tjĂŒk a hiĂĄnyzĂł mezƑkkel, amik a User modellen lĂ©teznek, # de a _build_user_response dict-bƑl Ă©s a UserResponse sĂ©mĂĄbĂłl hiĂĄnyoznak response_data["is_vip"] = updated_user.is_vip response_data["preferred_currency"] = updated_user.preferred_currency response_data["custom_permissions"] = updated_user.custom_permissions response_data["subscription_expires_at"] = ( updated_user.subscription_expires_at.isoformat() if updated_user.subscription_expires_at else None ) response_data["scope_level"] = updated_user.scope_level or "individual" response_data["scope_id"] = str(updated_user.scope_id) if updated_user.scope_id else None return response_data # ============================================================================= # Memberships Endpoint # ============================================================================= class UserMembershipResponse(BaseModel): """Egy szervezeti tagsĂĄg adatai a felhasznĂĄlĂł nĂ©zetĂ©ben.""" id: int organization_id: int organization_name: str = Field(..., description="Szervezet neve") organization_display_name: Optional[str] = Field(default=None, description="Szervezet megjelenĂ­tĂ©si neve") role: str = Field(..., description="Szerepkör a szervezetben") status: str = Field(default="active", description="TagsĂĄg stĂĄtusza") is_verified: bool = Field(default=False, description="EllenƑrzött tagsĂĄg") is_permanent: bool = Field(default=False, description="ÁllandĂł tagsĂĄg") joined_at: Optional[str] = Field(default=None, description="CsatlakozĂĄs dĂĄtuma") @router.get( "/{user_id}/memberships", response_model=List[UserMembershipResponse], summary="FelhasznĂĄlĂł szervezeti tagsĂĄgai", description=( "Visszaadja egy felhasznĂĄlĂł összes szervezeti tagsĂĄgĂĄt. " "Csak admin/staff jogosultsĂĄggal Ă©rhetƑ el." ), ) async def get_user_memberships( user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("user:view")), ) -> List[UserMembershipResponse]: """ GET /admin/users/{user_id}/memberships LekĂ©rdezi a felhasznĂĄlĂł összes szervezeti tagsĂĄgĂĄt. """ stmt = ( select(OrganizationMember) .where(OrganizationMember.user_id == user_id) .options(joinedload(OrganizationMember.organization)) ) result = await db.execute(stmt) memberships = result.unique().scalars().all() membership_list = [] for m in memberships: membership_list.append(UserMembershipResponse( id=m.id, organization_id=m.organization_id, organization_name=m.organization.name if m.organization else "Ismeretlen", organization_display_name=m.organization.display_name if m.organization else None, role=m.role, status=m.status, is_verified=m.is_verified, is_permanent=m.is_permanent, joined_at=m.joined_at.isoformat() if m.joined_at else None, )) return membership_list # ============================================================================= # Hard Delete Endpoint (Danger Zone) # ============================================================================= @router.delete( "/{user_id}/hard", summary="[SUPERADMIN] FelhasznĂĄlĂł vĂ©gleges törlĂ©se", description=( "VĂ©glegesen eltĂĄvolĂ­t egy felhasznĂĄlĂłt az adatbĂĄzisbĂłl. " "KIZÁRÓLAG Superadmin szĂĄmĂĄra elĂ©rhetƑ. " "Gatekeeper: EllenƑrzi, hogy a felhasznĂĄlĂłnak nincs-e pĂ©nzĂŒgyi rekordja " "(FinancialLedger, Wallet) a törlĂ©s elƑtt. " "Audit trail: SecurityAuditLog-ba rögzĂ­ti a mƱveletet." ), ) async def hard_delete_user( user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("user:manage")), ) -> Dict[str, Any]: """ DELETE /admin/users/{user_id}/hard VĂ©glegesen töröl egy felhasznĂĄlĂłt az adatbĂĄzisbĂłl. Csak Superadmin jogosultsĂĄggal Ă©rhetƑ el. """ # ── 1. Superadmin ellenƑrzĂ©s ────────────────────────────────────────── if current_user.role != UserRole.SUPERADMIN: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only Superadmins can perform hard delete operations." ) # ── 2. FelhasznĂĄlĂł lekĂ©rĂ©se ─────────────────────────────────────────── stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {user_id} not found." ) # ── 3. Gatekeeper: PĂ©nzĂŒgyi rekordok ellenƑrzĂ©se ────────────────────── # FinancialLedger ellenƑrzĂ©s ledger_stmt = select(FinancialLedger).where(FinancialLedger.user_id == user_id).limit(1) ledger_result = await db.execute(ledger_stmt) has_ledger = ledger_result.scalar_one_or_none() is not None # Wallet ellenƑrzĂ©s wallet_stmt = select(Wallet).where(Wallet.user_id == user_id).limit(1) wallet_result = await db.execute(wallet_stmt) has_wallet = wallet_result.scalar_one_or_none() is not None if has_ledger or has_wallet: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=( "Cannot hard delete: User has financial or production records. " "Please ensure all financial data is cleared before deletion." ) ) # ── 4. FK referenciĂĄk nullĂĄzĂĄsa a törlĂ©s elƑtt ───────────────────────── # Az AuditLog (audit.audit_logs) Ă©s SecurityAuditLog (audit.security_audit_logs) # tĂĄblĂĄk FK constrainttel hivatkoznak identity.users.id-ra, de NINCS # ondelete="SET NULL" beĂĄllĂ­tva. EzĂ©rt manuĂĄlisan nullĂĄzzuk a referenciĂĄkat. user_data = { "user_id": user.id, "email": user.email, "role": str(user.role) if user.role else None, "is_active": user.is_active, "is_deleted": user.is_deleted, } # AuditLog.user_id nullĂĄzĂĄsa await db.execute( sa_update(AuditLog) .where(AuditLog.user_id == user_id) .values(user_id=None) ) # ── 5. VĂ©gleges törlĂ©s ───────────────────────────────────────────────── await db.delete(user) await db.flush() # ── 6. Audit Trail ───────────────────────────────────────────────────── # SecurityAuditLog.target_id FK-val hivatkozik identity.users.id-ra, # de nincs ondelete="SET NULL". Mivel a user mĂĄr törölve van, # a target_id=None kell legyen, kĂŒlönben ForeignKeyViolationError. # A tĂ©nyleges user_id a payload_before mezƑben kerĂŒl tĂĄrolĂĄsra. audit_log = SecurityAuditLog( actor_id=current_user.id, target_id=None, action="hard_delete_user", is_critical=True, payload_before=user_data, payload_after={ "details": f"User #{user_id} ({user.email}) permanently deleted by admin #{current_user.id}." }, ) db.add(audit_log) await db.commit() logger.warning( "HARD DELETE: User #%s (%s) permanently deleted by admin #%s", user_id, user.email, current_user.id ) return { "status": "success", "message": f"User #{user_id} has been permanently deleted.", "deleted_user_id": user_id, "deleted_email": user.email, }