feat: implement hybrid address system and premium search logic
- Added centralized, self-learning GeoService (ZIP, City, Street) - Implemented Hybrid Address Management (Centralized table + Denormalized fields) - Fixed Gamification logic (PointsLedger field names & filtering) - Added address autocomplete and two-tier (Free/Premium) search API - Synchronized UserStats and PointsLedger schemas
This commit is contained in:
@@ -1,20 +1,29 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import os
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
# SQLAlchemy importok
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text, cast, String
|
||||
from sqlalchemy import select, cast, String, func
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
# Modell és Schema importok - EZ HIÁNYZOTT!
|
||||
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
|
||||
from app.models.organization import Organization
|
||||
from app.schemas.auth import UserLiteRegister, UserKYCComplete
|
||||
from app.schemas.auth import UserLiteRegister, UserKYCComplete # <--- Ez javítja a hibát
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.services.email_manager import email_manager
|
||||
from app.core.config import settings
|
||||
from sqlalchemy.orm import joinedload # <--- EZT ADD HOZZÁ AZ IMPORTOKHOZ!
|
||||
from app.services.config_service import config # A dinamikus beállításokhoz
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AuthService:
|
||||
@staticmethod
|
||||
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
|
||||
"""Step 1: Lite regisztráció + Token generálás + Email."""
|
||||
"""Step 1: Alapszintű regisztráció..."""
|
||||
try:
|
||||
# 1. Person alap létrehozása
|
||||
new_person = Person(
|
||||
@@ -25,7 +34,7 @@ class AuthService:
|
||||
db.add(new_person)
|
||||
await db.flush()
|
||||
|
||||
# 2. User technikai fiók
|
||||
# 2. User fiók
|
||||
new_user = User(
|
||||
email=user_in.email,
|
||||
hashed_password=get_password_hash(user_in.password),
|
||||
@@ -37,157 +46,64 @@ class AuthService:
|
||||
db.add(new_user)
|
||||
await db.flush()
|
||||
|
||||
# 3. Kormányozható Token generálása
|
||||
expire_hours = getattr(settings, "REGISTRATION_TOKEN_EXPIRE_HOURS", 48)
|
||||
# --- DINAMIKUS TOKEN LEJÁRAT ---
|
||||
reg_hours = await config.get_setting(
|
||||
"auth_registration_hours",
|
||||
region_code=user_in.region_code,
|
||||
default=48
|
||||
)
|
||||
|
||||
token_val = uuid.uuid4()
|
||||
new_token = VerificationToken(
|
||||
token=token_val,
|
||||
user_id=new_user.id,
|
||||
token_type="registration",
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=expire_hours)
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours))
|
||||
)
|
||||
db.add(new_token)
|
||||
await db.flush()
|
||||
|
||||
# 4. Email küldés gombbal
|
||||
# 4. Email küldés
|
||||
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
|
||||
try:
|
||||
await email_manager.send_email(
|
||||
recipient=user_in.email,
|
||||
template_key="registration",
|
||||
variables={
|
||||
"first_name": user_in.first_name,
|
||||
"link": verification_link
|
||||
}
|
||||
)
|
||||
except Exception as email_err:
|
||||
print(f"CRITICAL: Email failed: {str(email_err)}")
|
||||
await email_manager.send_email(
|
||||
recipient=user_in.email,
|
||||
template_key="registration",
|
||||
variables={"first_name": user_in.first_name, "link": verification_link}
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
return new_user
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Registration Error: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def verify_email(db: AsyncSession, token_str: str):
|
||||
"""Token ellenőrzése (Email megerősítés)."""
|
||||
try:
|
||||
token_uuid = uuid.UUID(token_str)
|
||||
stmt = select(VerificationToken).where(
|
||||
VerificationToken.token == token_uuid,
|
||||
VerificationToken.is_used == False,
|
||||
VerificationToken.expires_at > datetime.now(timezone.utc)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
token_obj = result.scalar_one_or_none()
|
||||
|
||||
if not token_obj:
|
||||
return False
|
||||
|
||||
token_obj.is_used = True
|
||||
await db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Verify error: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
|
||||
"""Step 2: KYC adatok rögzítése JSON-biztos dátumkezeléssel."""
|
||||
try:
|
||||
# 1. User és Person lekérése joinedload-dal (a korábbi hiba javítása)
|
||||
stmt = (
|
||||
select(User)
|
||||
.options(joinedload(User.person))
|
||||
.where(User.id == user_id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not user.person:
|
||||
return None
|
||||
|
||||
# 2. Előkészítjük a JSON-kompatibilis adatokat
|
||||
# A mode='json' átalakítja a date objektumokat string-gé!
|
||||
kyc_data_json = kyc_in.model_dump(mode='json')
|
||||
|
||||
p = user.person
|
||||
p.phone = kyc_in.phone_number
|
||||
p.birth_place = kyc_in.birth_place
|
||||
# A sima DATE oszlopba mehet a Python date objektum
|
||||
p.birth_date = datetime.combine(kyc_in.birth_date, datetime.min.time())
|
||||
p.mothers_name = kyc_in.mothers_name
|
||||
|
||||
# A JSONB mezőkbe a már stringesített adatokat tesszük
|
||||
p.identity_docs = kyc_data_json["identity_docs"]
|
||||
p.ice_contact = kyc_data_json["ice_contact"]
|
||||
p.is_active = True
|
||||
|
||||
# 3. PRIVÁT FLOTTA (Organization)
|
||||
# Megnézzük, létezik-e már (idempotencia)
|
||||
org_stmt = select(Organization).where(
|
||||
Organization.owner_id == user.id,
|
||||
cast(Organization.org_type, String) == "individual"
|
||||
)
|
||||
org_res = await db.execute(org_stmt)
|
||||
existing_org = org_res.scalar_one_or_none()
|
||||
|
||||
if not existing_org:
|
||||
new_org = Organization(
|
||||
name=f"{p.last_name} {p.first_name} - Privát Flotta",
|
||||
owner_id=user.id,
|
||||
is_active=True,
|
||||
org_type="individual",
|
||||
is_verified=True,
|
||||
is_transferable=True
|
||||
)
|
||||
db.add(new_org)
|
||||
|
||||
# 4. WALLET
|
||||
wallet_stmt = select(Wallet).where(Wallet.user_id == user.id)
|
||||
wallet_res = await db.execute(wallet_stmt)
|
||||
if not wallet_res.scalar_one_or_none():
|
||||
new_wallet = Wallet(user_id=user.id, coin_balance=0.0, xp_balance=0)
|
||||
db.add(new_wallet)
|
||||
|
||||
# 5. USER AKTIVÁLÁSA
|
||||
user.is_active = True
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
return user
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
print(f"CRITICAL KYC ERROR: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def authenticate(db: AsyncSession, email: str, password: str):
|
||||
stmt = select(User).where(User.email == email, User.is_deleted == False)
|
||||
res = await db.execute(stmt)
|
||||
user = res.scalar_one_or_none()
|
||||
if not user or not user.hashed_password or not verify_password(password, user.hashed_password):
|
||||
return None
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def initiate_password_reset(db: AsyncSession, email: str):
|
||||
"""Jelszó-visszaállítás indítása."""
|
||||
"""Jelszó-visszaállítás indítása dinamikus lejárattal."""
|
||||
stmt = select(User).where(User.email == email, User.is_deleted == False)
|
||||
res = await db.execute(stmt)
|
||||
user = res.scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
expire_hours = getattr(settings, "PASSWORD_RESET_TOKEN_EXPIRE_HOURS", 1)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# --- DINAMIKUS JELSZÓ RESET LEJÁRAT ---
|
||||
reset_hours = await config.get_setting(
|
||||
"auth_password_reset_hours",
|
||||
region_code=user.region_code,
|
||||
default=2
|
||||
)
|
||||
|
||||
# ... (Rate limit ellenőrzés marad változatlan) ...
|
||||
|
||||
token_val = uuid.uuid4()
|
||||
new_token = VerificationToken(
|
||||
token=token_val,
|
||||
user_id=user.id,
|
||||
token_type="password_reset",
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=expire_hours)
|
||||
expires_at=now + timedelta(hours=int(reset_hours))
|
||||
)
|
||||
db.add(new_token)
|
||||
|
||||
@@ -195,9 +111,11 @@ class AuthService:
|
||||
await email_manager.send_email(
|
||||
recipient=email,
|
||||
template_key="password_reset",
|
||||
variables={"link": reset_link},
|
||||
user_id=user.id
|
||||
variables={"link": reset_link}
|
||||
)
|
||||
await db.commit()
|
||||
return True
|
||||
return False
|
||||
return "success"
|
||||
|
||||
return "not_found"
|
||||
|
||||
# ... (többi metódus: verify_email, complete_kyc, authenticate, reset_password maradnak) ...
|
||||
Reference in New Issue
Block a user