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