admin frontend elkezdése, járművek tisztázása, frontend fejleszts
This commit is contained in:
@@ -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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user