admin frontend elkezdése, járművek tisztázása, frontend fejleszts

This commit is contained in:
Roo
2026-06-15 18:52:38 +00:00
parent ef8df9608c
commit 213ba3b0f1
80 changed files with 11615 additions and 2304 deletions

View File

@@ -3,9 +3,10 @@ import os
import re
import uuid
import hashlib
import random
import logging
from typing import List
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@@ -13,9 +14,11 @@ from sqlalchemy import select
from app.db.session import get_db
from app.api.deps import get_current_user
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse, OrganizationUpdate, OrganizationResponse
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember, Branch
from app.models.identity import User # JAVÍTVA: Központi Identity modell
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember, Branch, OrgUserRole
from app.models.identity import User, OneTimePassword # JAVÍTVA: Központi Identity modell
from app.core.config import settings
from app.services.security_service import security_service
from app.models import LogSeverity
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -275,6 +278,79 @@ async def update_organization_visual_settings(
)
return OrganizationResponse.model_validate(org)
@router.patch("/{org_id}", response_model=OrganizationResponse)
async def update_organization(
org_id: int,
update_data: OrganizationUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Szervezet adatainak részleges frissítése (általános PATCH).
Támogatja a cégadatok (name, full_name, tax_number, display_name),
cím adatok (address_zip, address_city, address_street_name, stb.),
valamint a visual_settings, language, default_currency mezők frissítését.
Csak OWNER vagy ADMIN jogosultságú felhasználó módosíthatja.
"""
# 1. Jogosultság ellenőrzése
stmt_member = select(OrganizationMember).where(
(OrganizationMember.organization_id == org_id) &
(OrganizationMember.user_id == current_user.id) &
(OrganizationMember.role.in_(["OWNER", "ADMIN"]))
)
member = (await db.execute(stmt_member)).scalar_one_or_none()
if not member:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Nincs jogosultságod a szervezet adatainak módosításához (csak OWNER/ADMIN)."
)
# 2. Szervezet lekérése
stmt_org = select(Organization).where(
Organization.id == org_id,
Organization.is_deleted == False
)
result = await db.execute(stmt_org)
org = result.scalar_one_or_none()
if not org:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Szervezet nem található."
)
# 3. JSONB merge visual_settings esetén
update_dict = update_data.model_dump(exclude_unset=True)
if "visual_settings" in update_dict and update_dict["visual_settings"] is not None:
incoming_vs = update_dict["visual_settings"]
current_vs = dict(org.visual_settings) if org.visual_settings else {}
current_vs.update(incoming_vs)
org.visual_settings = current_vs
del update_dict["visual_settings"]
# 4. Többi mező frissítése
for field, value in update_dict.items():
if hasattr(org, field) and value is not None:
setattr(org, field, value)
try:
await db.commit()
await db.refresh(org)
except Exception as e:
await db.rollback()
logger.error(f"Hiba a szervezet frissítésekor (org_id={org_id}): {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Adatbázis hiba: {str(e)}"
)
return OrganizationResponse.model_validate(org)
# --- B2B MEGHÍVÓ LOGIKA ---
from pydantic import BaseModel, EmailStr
@@ -393,9 +469,274 @@ async def accept_invitation(
status="active"
)
db.add(new_member)
token_rec.is_used = True
await db.commit()
token_rec.is_used = True
await db.commit()
return {"status": "success", "message": "Meghívó sikeresen elfogadva."}
return {"status": "success", "message": "Meghívó sikeresen elfogadva."}
# ── CÉG CSATLAKOZÁS ÉS ÁRVA CÉG ÁTVÉTEL ──
from app.models.identity import OneTimePassword
import random
class JoinRequestIn(BaseModel):
"""Csatlakozási kérelem egy céghez, amelynek van aktív adminja."""
message: Optional[str] = Field(None, description="Üzenet az adminnak")
class ClaimRequestIn(BaseModel):
"""Árva cég átvételi kérelem - email ellenőrzés."""
email: EmailStr = Field(..., description="A cég eredeti email címe")
class ClaimVerifyIn(BaseModel):
"""Árva cég átvételi kérelem - OTP kód ellenőrzés."""
email: EmailStr = Field(..., description="A cég eredeti email címe")
code: str = Field(..., min_length=6, max_length=6, description="6-jegyű megerősítő kód")
@router.post("/{org_id}/join-request")
async def request_join_organization(
org_id: int,
request: JoinRequestIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Csatlakozási kérelem egy szervezethez, amelynek van aktív adminja.
A kérelem 7 napig él, amíg az admin jóvá nem hagyja.
"""
# Ellenőrizzük, hogy létezik-e a szervezet
stmt = select(Organization).where(
Organization.id == org_id,
Organization.is_deleted == False
)
result = await db.execute(stmt)
org = result.scalar_one_or_none()
if not org:
raise HTTPException(status_code=404, detail="Szervezet nem található.")
# Ellenőrizzük, hogy van-e aktív admin
stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.role.in_([OrgUserRole.OWNER, OrgUserRole.ADMIN])
).join(User, OrganizationMember.user_id == User.id).where(User.is_active == True, User.is_deleted == False)
result = await db.execute(stmt)
active_admins = result.scalars().all()
if not active_admins:
raise HTTPException(
status_code=400,
detail="A szervezetnek nincs aktív adminja. Használja a /claim/request végpontot."
)
# Ellenőrizzük, hogy a felhasználó már nem tag-e
stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.user_id == current_user.id
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
raise HTTPException(status_code=400, detail="Már tagja vagy ennek a szervezetnek.")
# Létrehozzuk a pending tagot
new_member = OrganizationMember(
organization_id=org_id,
user_id=current_user.id,
role=OrgUserRole.DRIVER,
status="pending"
)
db.add(new_member)
# Naplózás
await security_service.log_event(
db, user_id=current_user.id, action="ORG_JOIN_REQUEST",
severity=LogSeverity.info, target_type="Organization", target_id=str(org_id),
new_data={"message": request.message or ""}
)
await db.commit()
return {
"status": "pending",
"message": "Csatlakozási kérelmed rögzítettük. Az admin jóváhagyására vársz."
}
@router.post("/{org_id}/claim/request")
async def claim_orphaned_organization_request(
org_id: int,
request: ClaimRequestIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Árva cég átvételi kérelem indítása.
Ellenőrzi, hogy a megadott email cím egyezik-e a cég eredeti email címével,
majd 6-jegyű OTP kódot küld ki.
"""
# Ellenőrizzük a szervezetet
stmt = select(Organization).where(
Organization.id == org_id,
Organization.is_deleted == False
)
result = await db.execute(stmt)
org = result.scalar_one_or_none()
if not org:
raise HTTPException(status_code=404, detail="Szervezet nem található.")
# Ellenőrizzük, hogy tényleg árva-e (nincs aktív admin)
stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.role.in_([OrgUserRole.OWNER, OrgUserRole.ADMIN])
).join(User, OrganizationMember.user_id == User.id).where(User.is_active == True, User.is_deleted == False)
result = await db.execute(stmt)
active_admins = result.scalars().all()
if active_admins:
raise HTTPException(
status_code=400,
detail="A szervezetnek van aktív adminja. Használja a /join-request végpontot."
)
# Ellenőrizzük az email címet - keressük a tulajdonos usert
# A tulajdonos email-jét kell ellenőrizni
owner_stmt = select(User).where(
User.id == org.owner_id,
User.email == request.email
)
owner_result = await db.execute(owner_stmt)
owner_user = owner_result.scalar_one_or_none()
if not owner_user:
# Lehet, hogy a tulajdonos törölve van, és az email át lett írva
# Keressük a deleted_ prefix-es email-ben
owner_stmt = select(User).where(
User.id == org.owner_id,
User.email.like(f"%{request.email}")
)
owner_result = await db.execute(owner_stmt)
owner_user = owner_result.scalar_one_or_none()
if not owner_user:
raise HTTPException(
status_code=400,
detail="Az email cím nem egyezik a cég tulajdonosának email címével."
)
# Generáljunk 6-jegyű OTP kódot
code = f"{random.randint(0, 999999):06d}"
expires_at = datetime.now(timezone.utc) + timedelta(minutes=15)
otp = OneTimePassword(
email=request.email,
code=code,
otp_type="company_claim",
extra_data={"org_id": org_id, "user_id": current_user.id},
expires_at=expires_at
)
db.add(otp)
await db.commit()
# Szimulált email küldés
logger.info(f"=== CÉG ÁTVÉTELI OTP ===")
logger.info(f"Email: {request.email}")
logger.info(f"Kód: {code}")
logger.info(f"Lejárat: {expires_at}")
logger.info(f"=== A kódot email-ben kell elküldeni a cég tulajdonosának ===")
return {
"status": "otp_sent",
"message": "Egy 6-jegyű megerősítő kódot küldtünk a cég tulajdonosának email címére.",
"expires_at": expires_at.isoformat()
}
@router.post("/{org_id}/claim/verify")
async def claim_orphaned_organization_verify(
org_id: int,
request: ClaimVerifyIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Árva cég átvételének véglegesítése OTP kód alapján.
Sikeres ellenőrzés után a próbálkozó lesz az új admin.
"""
now = datetime.now(timezone.utc)
# Ellenőrizzük a szervezetet
stmt = select(Organization).where(
Organization.id == org_id,
Organization.is_deleted == False
)
result = await db.execute(stmt)
org = result.scalar_one_or_none()
if not org:
raise HTTPException(status_code=404, detail="Szervezet nem található.")
# Ellenőrizzük az OTP kódot
otp_stmt = select(OneTimePassword).where(
OneTimePassword.email == request.email,
OneTimePassword.code == request.code,
OneTimePassword.otp_type == "company_claim",
OneTimePassword.is_used == False,
OneTimePassword.expires_at > now
).order_by(OneTimePassword.created_at.desc()).limit(1)
result = await db.execute(otp_stmt)
otp = result.scalar_one_or_none()
if not otp:
raise HTTPException(
status_code=400,
detail="Érvénytelen vagy lejárt kód."
)
# Ellenőrizzük, hogy az OTP ehhez a szervezethez tartozik-e
extra = otp.extra_data or {}
if extra.get("org_id") != org_id:
raise HTTPException(status_code=400, detail="A kód nem ehhez a szervezethez tartozik.")
# Mark OTP as used
otp.is_used = True
# Ellenőrizzük, hogy a felhasználó már nem tag-e
member_stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.user_id == current_user.id
)
result = await db.execute(member_stmt)
existing_member = result.scalar_one_or_none()
if existing_member:
# Frissítjük a szerepkört
existing_member.role = OrgUserRole.ADMIN
else:
# Új tag létrehozása ADMIN szerepkörrel
new_member = OrganizationMember(
organization_id=org_id,
user_id=current_user.id,
role=OrgUserRole.ADMIN,
status="active"
)
db.add(new_member)
# Átírjuk a tulajdonost (owner_id)
org.owner_id = current_user.id
# Naplózás
await security_service.log_event(
db, user_id=current_user.id, action="ORG_CLAIMED",
severity=LogSeverity.warning, target_type="Organization", target_id=str(org_id),
new_data={"previous_owner": str(extra.get("user_id")), "claim_type": "orphaned"}
)
await db.commit()
return {
"status": "success",
"message": "Sikeresen átvetted a szervezet irányítását. Mostantól te vagy az admin."
}