2026.06.05 frontend javítgatás és új belépési logika megvalósítva
This commit is contained in:
@@ -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. """
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user