admin_users_personels
This commit is contained in:
646
backend/app/api/v1/endpoints/admin_users.py
Normal file
646
backend/app/api/v1/endpoints/admin_users.py
Normal file
@@ -0,0 +1,646 @@
|
||||
"""
|
||||
👤 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
|
||||
|
||||
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.marketplace.organization import Organization, OrganizationMember
|
||||
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
|
||||
Reference in New Issue
Block a user