2026.06.05 frontend javítgatás és új belépési logika megvalósítva

This commit is contained in:
Roo
2026-06-05 05:46:04 +00:00
parent 59a30ac428
commit 18524a08f2
38 changed files with 3967 additions and 1028 deletions

View File

@@ -93,7 +93,7 @@ class AuthService:
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
is_active=False,
is_active=False, # Email verification required before activation
identity_docs={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
ice_contact={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
lifetime_xp=0, # default -1, de explicit 0
@@ -117,7 +117,7 @@ class AuthService:
hashed_password=get_password_hash(user_in.password),
person_id=new_person.id,
role=assigned_role,
is_active=False,
is_active=False, # Email verification required before activation
is_deleted=False,
region_code=user_in.region_code,
preferred_language=user_in.lang,
@@ -217,6 +217,8 @@ class AuthService:
user.person_id = shadow_person.id
# Frissítjük a megtalált Person rekordot a KYC adatokkal
if kyc_in.first_name: shadow_person.first_name = kyc_in.first_name
if kyc_in.last_name: shadow_person.last_name = kyc_in.last_name
shadow_person.mothers_last_name = kyc_in.mothers_last_name
shadow_person.mothers_first_name = kyc_in.mothers_first_name
shadow_person.birth_place = kyc_in.birth_place
@@ -231,6 +233,8 @@ class AuthService:
p = shadow_person
else:
if kyc_in.first_name: p.first_name = kyc_in.first_name
if kyc_in.last_name: p.last_name = kyc_in.last_name
p.mothers_last_name = kyc_in.mothers_last_name
p.mothers_first_name = kyc_in.mothers_first_name
p.birth_place = kyc_in.birth_place
@@ -314,7 +318,10 @@ class AuthService:
@staticmethod
async def verify_email(db: AsyncSession, token_str: str):
""" Email megerősítés. """
"""
Email megerősítés.
Visszaadja az aktivált User objektumot sikeres verifikáció után (Magic Link támogatás).
"""
try:
token_uuid = uuid.UUID(token_str)
stmt = select(VerificationToken).where(and_(
@@ -323,25 +330,28 @@ class AuthService:
VerificationToken.expires_at > datetime.now(timezone.utc)
))
token = (await db.execute(stmt)).scalar_one_or_none()
if not token: return False
if not token: return None
token.is_used = True
# Activate user
user_stmt = select(User).where(User.id == token.user_id)
user = (await db.execute(user_stmt)).scalar_one_or_none()
if user:
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
if not user: return None
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
await db.commit()
return True
except: return False
return user # Return the activated User object
except Exception as e:
logger.error(f"Email verification error: {e}")
return None
@staticmethod
async def initiate_password_reset(db: AsyncSession, email: str):
@@ -349,23 +359,43 @@ class AuthService:
stmt = select(User).where(and_(User.email == email, User.is_deleted == False))
user = (await db.execute(stmt)).scalar_one_or_none()
if user:
# Dinamikus lejárat az adminból
reset_h = await config.get_setting(db, "auth_password_reset_hours", region_code=user.region_code, default=2)
token_val = uuid.uuid4()
db.add(VerificationToken(
token=token_val, user_id=user.id, token_type="password_reset",
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_h))
))
link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
await email_manager.send_email(
recipient=email, template_key="pwd_reset",
variables={"link": link}, lang=user.preferred_language
if not user:
return "not_found"
# Ha a felhasználó létezik, de inaktív, dobjunk hibát
if not user.is_active:
logger.warning(f"Password reset requested for inactive user: {email}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="AUTH.USER_INACTIVE"
)
await db.commit()
return "success"
return "not_found"
# Dinamikus lejárat az adminból
reset_h = await config.get_setting(db, "auth_password_reset_hours", region_code=user.region_code, default=2)
token_val = uuid.uuid4()
db.add(VerificationToken(
token=token_val, user_id=user.id, token_type="password_reset",
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_h))
))
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')}"
)
await db.commit()
return "success"
@staticmethod
async def reset_password(db: AsyncSession, email: str, token_str: str, new_password: str):
@@ -396,6 +426,57 @@ class AuthService:
raise
except: return False
@staticmethod
async def resend_verification(db: AsyncSession, email: str):
"""
Aktiváló e-mail újraküldése.
Ellenőrzi, hogy a felhasználó létezik, de még nem aktív (is_active == False),
generál egy új verifikációs tokent, és kiküldi az e-mailt.
"""
# Eager load person relationship to avoid async lazy-load issue
stmt = select(User).options(joinedload(User.person)).where(
and_(User.email == email, User.is_deleted == False)
)
user = (await db.execute(stmt)).unique().scalar_one_or_none()
if not user:
# Ne áruljuk el, hogy létezik-e az email — biztonsági okokból mindig sikert adunk
logger.info(f"Resend verification requested for non-existent email: {email}")
return "not_found"
if user.is_active:
logger.info(f"Resend verification requested for already active user: {email}")
return "already_active"
# Dinamikus lejárat az adminból
reg_token_hours = await config.get_setting(db, "auth_registration_hours", region_code=user.region_code, default=48)
token_val = uuid.uuid4()
db.add(VerificationToken(
token=token_val,
user_id=user.id,
token_type="registration",
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_token_hours))
))
# Email küldés — user.person már eager-loaded, így nem okoz MissingGreenlet hibát
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
person = user.person
first_name = person.first_name if person else user.email
logger.info(f"Email küldés indítása (resend verification) ide: {email}...")
result = await email_manager.send_email(
recipient=email,
template_key="reg",
variables={"first_name": first_name, "link": verification_link},
lang=user.preferred_language
)
logger.info(f"Email küldés eredménye (resend verification): {result}")
await db.commit()
logger.info(f"Resend verification email sent to {email}")
return "success"
@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. """

View File

@@ -81,7 +81,7 @@ class ConfigService:
Returns:
A talált érték (a megfelelő típusban) vagy a default.
"""
from sqlalchemy import select, and_, cast, String
from sqlalchemy import select, and_, cast, String, Text
try:
# Convert scope_level to string for comparison - handle both Enum and string
@@ -91,12 +91,12 @@ class ConfigService:
scope_str = str(scope_level)
# Build query with case-insensitive comparison for scope_level
# Use ilike or lower() for case-insensitive comparison since enum values might have inconsistent casing
# Use cast to Text (not String) to avoid asyncpg enum codec cache issues
from sqlalchemy import func
query = select(SystemParameter).where(
and_(
SystemParameter.key == key,
func.lower(cast(SystemParameter.scope_level, String)) == scope_str.lower(),
func.lower(cast(SystemParameter.scope_level, Text)) == scope_str.lower(),
SystemParameter.is_active == True
)
)

View File

@@ -16,27 +16,12 @@ from app.db.session import AsyncSessionLocal
logger = logging.getLogger("Email-Manager-2.0")
class EmailManager:
@staticmethod
def _get_base_url() -> str:
"""Return the appropriate base URL for email links."""
# Check environment variable first
base_url = os.getenv("EMAIL_BASE_URL")
if base_url:
return base_url.rstrip('/')
# Fallback to dev domain for external access
return "https://dev.servicefinder.hu"
@staticmethod
def _build_verification_link(token: str) -> str:
"""Build verification link with proper path."""
base = EmailManager._get_base_url()
# Use frontend verification route (adjust if needed)
return f"{base}/verify?token={token}"
@staticmethod
def _get_html_template(template_key: str, variables: dict, lang: str = "hu") -> str:
"""HTML sablon generálása a fordítási fájlok alapján."""
"""HTML sablon generálása a fordítási fájlok alapján.
The caller MUST provide a 'link' key in variables. No fallback guessing.
"""
greeting = locale_manager.get(f"email.{template_key}_greeting", lang=lang, **variables)
body = locale_manager.get(f"email.{template_key}_body", lang=lang, **variables)
button_text = locale_manager.get(f"email.{template_key}_button", lang=lang)
@@ -44,10 +29,10 @@ class EmailManager:
link_fallback_text = locale_manager.get("email.link_fallback", lang=lang)
# If link is not provided but token is, build verification link
# Strict: only use variables.get('link'), no guessing
link = variables.get('link')
if not link and 'token' in variables:
link = EmailManager._build_verification_link(variables['token'])
if not link:
logger.error(f"No 'link' variable provided for template '{template_key}' (lang={lang})")
return f"""
<html>
@@ -83,8 +68,14 @@ class EmailManager:
session_internal = True
try:
# Check if emails are disabled via DB config
provider = await config.get_setting(db, "email_provider", default="smtp")
# --- FIX: Wrap DB config lookup in try-except with env fallback ---
# If the DB query fails (e.g. enum mismatch), fall back to environment variable
try:
provider = await config.get_setting(db, "email_provider", default=None)
except Exception as cfg_err:
logger.warning(f"DB config error reading email_provider: {cfg_err}. Falling back to ENV variables...")
provider = None
if provider == "disabled":
logger.info(f"Email küldés letiltva (Admin config). Cél: {recipient}")
return {"status": "success", "provider": "disabled", "message": "Email disabled by admin config"}
@@ -92,8 +83,9 @@ class EmailManager:
html = EmailManager._get_html_template(template_key, variables, lang)
subject = locale_manager.get(f"email.{template_key}_subject", lang=lang)
# Get email provider from environment
email_provider = os.getenv("EMAIL_PROVIDER", "smtp").lower()
# Determine email provider: DB config > ENV > fallback to 'smtp'
email_provider_raw = provider if provider else os.getenv("EMAIL_PROVIDER", "smtp")
email_provider = str(email_provider_raw).lower().strip()
if email_provider == "brevo_api":
result = await EmailManager._send_via_brevo_api(recipient, subject, html, variables)
@@ -243,7 +235,14 @@ class EmailManager:
else:
logger.info(f"Connecting via SMTP to {smtp_host}:{smtp_port}")
with smtplib.SMTP(smtp_host, smtp_port, timeout=15) as server:
server.starttls()
# Try STARTTLS, but fall back to plain if server doesn't support it
# (e.g. local Mailpit for testing)
try:
server.starttls()
except smtplib.SMTPNotSupportedError:
logger.warning(f"STARTTLS not supported by {smtp_host}:{smtp_port}, sending in plain text")
except Exception:
logger.warning(f"STARTTLS failed on {smtp_host}:{smtp_port}, sending in plain text")
if smtp_user and smtp_pass:
server.login(smtp_user, smtp_pass)
server.send_message(msg)