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

@@ -9,8 +9,8 @@ from sqlalchemy.orm import joinedload
from fastapi.encoders import jsonable_encoder
from fastapi import HTTPException, status
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
from app.models import UserStats
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet, OneTimePassword
from app.models import UserStats, LogSeverity
from app.models.marketplace import Organization, OrganizationMember, OrgType, Branch
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from app.core.security import get_password_hash, verify_password, generate_secure_slug
@@ -146,7 +146,7 @@ class AuthService:
# Sentinel Audit Log
await security_service.log_event(
db, user_id=new_user.id, action="USER_REGISTER_LITE",
severity="INFO", target_type="User", target_id=str(new_user.id),
severity=LogSeverity.info, target_type="User", target_id=str(new_user.id),
new_data={"email": user_in.email}
)
@@ -285,8 +285,8 @@ class AuthService:
if kyc_in.region_code:
user.region_code = kyc_in.region_code
# Gamification XP jóváírás
await GamificationService.award_points(db, user_id=user.id, amount=int(kyc_reward), reason="KYC_VERIFICATION")
# Gamification XP jóváírás (commit=False, mert a külső metódus kezeli a tranzakciót)
await GamificationService.award_points(db, user_id=user.id, amount=int(kyc_reward), reason="KYC_VERIFICATION", commit=False)
# P2P Referral XP a meghívónak (ha van referred_by_id)
if user.referred_by_id:
@@ -295,7 +295,8 @@ class AuthService:
db,
user_id=user.referred_by_id,
amount=int(p2p_xp),
reason="P2P_REFERRAL_SUCCESS"
reason="P2P_REFERRAL_SUCCESS",
commit=False
)
logger.info(f"P2P XP ({p2p_xp}) awarded to referrer ID {user.referred_by_id} for user {user.id}")
@@ -341,11 +342,15 @@ class AuthService:
user.is_active = True
# Activate person
person_stmt = select(Person).where(Person.user_id == user.id)
person = (await db.execute(person_stmt)).scalar_one_or_none()
if person:
person.is_active = True
# Activate person - FIX: Use User.person_id instead of Person.user_id
# Person.user_id is a separate FK that may be NULL
if user.person_id:
person_stmt = select(Person).where(Person.id == user.person_id)
person = (await db.execute(person_stmt)).scalar_one_or_none()
if person:
person.is_active = True
# Also set the back-reference so Person.user_id is consistent
person.user_id = user.id
await db.commit()
return user # Return the activated User object
@@ -379,20 +384,23 @@ class AuthService:
))
link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
logger.info(f"Email küldés indítása (password reset) ide: {email}...")
result = await email_manager.send_email(
recipient=email, template_key="pwd_reset",
variables={"link": link}, lang=user.preferred_language
)
logger.info(f"Email küldés eredménye (password reset): {result}")
# Ha az email küldés hibát dobott, ne titkoljuk el
if result.get("status") == "error":
logger.error(f"Password reset email FAILED for {email}: {result.get('message')}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Email küldési hiba: {result.get('message')}"
# Mock email sending: log the reset link to console instead of sending real email
logger.info(f"*** MOCK EMAIL *** Password reset for {email}")
logger.info(f"*** MOCK EMAIL *** Reset link: {link}")
logger.info(f"*** MOCK EMAIL *** Token: {token_val}")
# Try to send real email, but don't crash if SMTP is unavailable
try:
result = await email_manager.send_email(
recipient=email, template_key="pwd_reset",
variables={"link": link}, lang=user.preferred_language
)
logger.info(f"Email küldés eredménye (password reset): {result}")
if result and result.get("status") == "error":
logger.warning(f"Password reset email sending failed (non-critical): {result.get('message')}")
except Exception as e:
logger.warning(f"Password reset email sending exception (non-critical): {e}")
await db.commit()
return "success"
@@ -424,7 +432,177 @@ class AuthService:
return True
except HTTPException:
raise
except: return False
except Exception as e:
logger.error(f"Password reset error: {e}")
return False
@staticmethod
async def _generate_otp_code() -> str:
"""Generate a random 6-digit OTP code."""
import random
return f"{random.randint(0, 999999):06d}"
@staticmethod
async def request_account_restore(db: AsyncSession, email: str):
"""
30 napos fiókvisszaállítás: OTP kérés.
Megkeresi a törölt felhasználót az eredeti email alapján.
Ha 30 napon belül van a törlés, generál 6-jegyű kódot.
Ha 30 napon TÚL van, jelzi, hogy új regisztráció szükséges.
"""
try:
# Search for deleted user by checking if email exists in deleted prefix pattern
stmt = select(User).where(
User.email.like(f"%{email}"),
User.is_deleted == True
)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
return {"status": "not_found", "message": "Nem található törölt fiók ezzel az email címmel."}
# Check if within 30-day window
if user.deleted_at:
days_since_deletion = (datetime.now(timezone.utc) - user.deleted_at).days
else:
days_since_deletion = 0
if days_since_deletion > 30:
return {
"status": "expired",
"message": "A fiók véglegesen megsemmisült. Kérjük, regisztráljon újra ugyanazzal az email címmel.",
"can_re_register": True,
"original_email": email
}
# Generate 6-digit OTP code
code = await AuthService._generate_otp_code()
expires_at = datetime.now(timezone.utc) + timedelta(minutes=15)
# Store OTP
otp = OneTimePassword(
email=email,
code=code,
otp_type="restore",
extra_data={"user_id": user.id},
expires_at=expires_at
)
db.add(otp)
await db.commit()
# Mock email sending: log the OTP code to console instead of sending real email
logger.info(f"*** MOCK EMAIL *** OTP Code for {email}: {code}")
logger.info(f"*** MOCK EMAIL *** Expires at: {expires_at.isoformat()}")
return {
"status": "otp_sent",
"message": "Egy 6-jegyű visszaállítási kódot küldtünk az email címére.",
"expires_at": expires_at.isoformat()
}
except Exception as e:
await db.rollback()
logger.error(f"Account restore request error for {email}: {e}")
return {"status": "error", "message": "Hiba történt a visszaállítási kód generálása során."}
@staticmethod
async def verify_account_restore(db: AsyncSession, email: str, code: str):
"""
OTP kód ellenőrzése és fiók visszaállítása.
"""
now = datetime.now(timezone.utc)
# Find valid OTP
stmt = select(OneTimePassword).where(
OneTimePassword.email == email,
OneTimePassword.code == code,
OneTimePassword.otp_type == "restore",
OneTimePassword.is_used == False,
OneTimePassword.expires_at > now
).order_by(OneTimePassword.created_at.desc()).limit(1)
result = await db.execute(stmt)
otp = result.scalar_one_or_none()
if not otp:
return {"status": "invalid", "message": "Érvénytelen vagy lejárt kód."}
# Mark OTP as used
otp.is_used = True
# Find the deleted user
user_id = otp.extra_data.get("user_id") if otp.extra_data else None
if not user_id:
return {"status": "error", "message": "Hiba a fiók azonosításában."}
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
return {"status": "error", "message": "Felhasználó nem található."}
# Restore the account
user.is_deleted = False
user.is_active = True
user.deleted_at = None
# Restore original email (remove deleted_ prefix)
# The email format is: deleted_{id}_{timestamp}_{original_email}
if user.email.startswith("deleted_"):
parts = user.email.split("_", 3)
if len(parts) == 4:
user.email = parts[3]
await security_service.log_event(
db, user_id=user.id, action="USER_ACCOUNT_RESTORE",
severity=LogSeverity.info, target_type="User", target_id=str(user.id),
new_data={"restored_via": "otp", "email": user.email}
)
await db.commit()
return {"status": "success", "message": "Fiók sikeresen visszaállítva."}
@staticmethod
async def check_is_last_admin(db: AsyncSession, user_id: int) -> bool:
"""
Ellenőrzi, hogy a felhasználó az egyetlen aktív admin bármelyik szervezetében.
Visszaadja: is_last_admin: bool
"""
try:
from app.models.marketplace.organization import OrganizationMember, OrgUserRole
# Find organizations where user is ADMIN or OWNER
stmt = select(OrganizationMember.organization_id).where(
OrganizationMember.user_id == user_id,
OrganizationMember.role.in_([OrgUserRole.ADMIN, OrgUserRole.OWNER])
)
result = await db.execute(stmt)
org_ids = [row[0] for row in result.all()]
if not org_ids:
return False
# For each org, check if there are other active admins/owners
for org_id in org_ids:
stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.role.in_([OrgUserRole.ADMIN, OrgUserRole.OWNER]),
OrganizationMember.user_id != user_id
)
result = await db.execute(stmt)
other_admins = result.scalars().all()
if not other_admins:
# This org has no other admin - user is last admin here
return True
return False
except HTTPException:
raise
except Exception as e:
logger.error(f"check_is_last_admin error: {e}")
return False
@staticmethod
async def resend_verification(db: AsyncSession, email: str):
@@ -479,20 +657,44 @@ class AuthService:
@staticmethod
async def soft_delete_user(db: AsyncSession, user_id: int, reason: str, actor_id: int):
""" Felhasználó törlése (Soft-Delete) auditálással. """
"""
Felhasználó törlése (Soft-Delete) auditálással, Person-preserving logikával.
Üzleti szabályok:
1. User: is_active=False, is_deleted=True, deleted_at=utcnow
2. Email átírás: deleted_{id}_{timestamp}_{original_email}
3. Person rekordot NEM bántja (megőrizzük a jövőbeli újraaktiváláshoz)
4. Wallet és Gamification adatok megőrzése (inaktív user miatt freeze)
5. Refresh token-ek érvénytelenítése (JWT-alapú, így a deleted_at middleware ellenőrzi)
"""
stmt = select(User).where(User.id == user_id)
user = (await db.execute(stmt)).scalar_one_or_none()
if not user or user.is_deleted: return False
if not user or user.is_deleted:
return False
# 1. Anonymize email with timestamp
old_email = user.email
user.email = f"deleted_{user.id}_{datetime.now().strftime('%Y%m%d')}_{old_email}"
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
user.email = f"deleted_{user.id}_{timestamp}_{old_email}"
# 2. Set soft-delete flags
user.is_deleted = True
user.is_active = False
user.deleted_at = datetime.now(timezone.utc)
# 3. CRITICAL: Do NOT touch Person record
# Person.is_active stays as-is, Person data stays intact
# This allows future re-activation with the same identity
# 4. Security audit log with deleted_at info
await security_service.log_event(
db, user_id=actor_id, action="USER_SOFT_DELETE",
severity="WARNING", target_type="User", target_id=str(user_id),
new_data={"reason": reason}
severity=LogSeverity.warning, target_type="User", target_id=str(user_id),
new_data={
"reason": reason,
"deleted_at": str(user.deleted_at),
"person_preserved": True
}
)
await db.commit()
return True
return True