308 lines
12 KiB
Python
Executable File
308 lines
12 KiB
Python
Executable File
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/auth.py
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Request, Form, Response
|
|
from fastapi.security import OAuth2PasswordRequestForm
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from app.db.session import get_db
|
|
from app.services.auth_service import AuthService
|
|
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
|
from app.core.config import settings
|
|
from app.services.config_service import config
|
|
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
|
|
from app.api.deps import get_current_user
|
|
from app.models.identity import User, Device, UserDeviceLink # JAVÍTVA: Device-Hub modellek
|
|
from app.core.translation_helper import t # Translation helper
|
|
from pydantic import BaseModel, Field, EmailStr
|
|
from datetime import datetime, timezone
|
|
|
|
router = APIRouter()
|
|
|
|
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
|
async def register(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
Regisztráció (Lite fázis) - új felhasználó létrehozása.
|
|
"""
|
|
user = await AuthService.register_lite(db, user_in)
|
|
return {
|
|
"status": "success",
|
|
"message": t("AUTH.REGISTRATION_SUCCESS"),
|
|
"user_id": user.id,
|
|
"email": user.email
|
|
}
|
|
|
|
@router.post("/login", response_model=Token)
|
|
async def login(
|
|
response: Response,
|
|
db: AsyncSession = Depends(get_db),
|
|
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
remember_me: bool = Form(False),
|
|
device_fingerprint: Optional[str] = Form(None)
|
|
):
|
|
"""
|
|
Bejelentkezés remember_me opcióval és Device Fingerprinting támogatással.
|
|
|
|
A remember_me paramétert explicit Form mezőként kell elküldeni, mivel az
|
|
OAuth2PasswordRequestForm nem támogatja alapból.
|
|
A device_fingerprint opcionális, a frontend FingerprintJS által generált hash.
|
|
"""
|
|
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail=t("AUTH.INVALID_CREDENTIALS"))
|
|
|
|
# DEBUG: Inaktív fiók explicit ellenőrzése
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="AUTH.USER_INACTIVE"
|
|
)
|
|
|
|
# ── Device Fingerprint Tracking ──
|
|
if device_fingerprint:
|
|
# 1. Ellenőrizzük, létezik-e már a Device a hash alapján
|
|
existing_device = await db.execute(
|
|
select(Device).where(Device.fingerprint_hash == device_fingerprint)
|
|
)
|
|
device = existing_device.scalar_one_or_none()
|
|
|
|
if not device:
|
|
# Új eszköz létrehozása
|
|
device = Device(
|
|
fingerprint_hash=device_fingerprint,
|
|
risk_score=100,
|
|
is_banned=False
|
|
)
|
|
db.add(device)
|
|
|
|
# 2. Ellenőrizzük a UserDeviceLink-et
|
|
existing_link = await db.execute(
|
|
select(UserDeviceLink).where(
|
|
UserDeviceLink.user_id == user.id,
|
|
UserDeviceLink.device_hash == device_fingerprint
|
|
)
|
|
)
|
|
link = existing_link.scalar_one_or_none()
|
|
|
|
now = datetime.now(timezone.utc)
|
|
if link:
|
|
# Létező kapcsolat frissítése
|
|
link.last_seen_at = now
|
|
link.login_count = link.login_count + 1
|
|
else:
|
|
# Új kapcsolat létrehozása
|
|
link = UserDeviceLink(
|
|
user_id=user.id,
|
|
device_hash=device_fingerprint,
|
|
first_seen_at=now,
|
|
last_seen_at=now,
|
|
login_count=1
|
|
)
|
|
db.add(link)
|
|
|
|
await db.commit()
|
|
|
|
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
|
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
|
role_key = role_name.upper() # A DEFAULT_RANK_MAP nagybetűs kulcsokat vár
|
|
|
|
token_data = {
|
|
"sub": str(user.id),
|
|
"role": role_name,
|
|
"rank": ranks.get(role_key, 10),
|
|
"scope_level": user.scope_level or "individual",
|
|
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
|
}
|
|
|
|
access, refresh = create_tokens(data=token_data, remember_me=remember_me)
|
|
|
|
# Kinyerjük a beállításokból, hogy meddig él a refresh token (itt is, vagy a create_tokens is ezt csinálja)
|
|
# A SSoT szerint auth_remember_me_days = 30
|
|
if remember_me:
|
|
max_age_days = await config.get_setting(db, "auth_remember_me_days", default=30)
|
|
else:
|
|
max_age_days = await config.get_setting(db, "auth_refresh_default_days", default=1)
|
|
|
|
max_age_sec = int(max_age_days) * 24 * 60 * 60
|
|
|
|
response.set_cookie(
|
|
key="refresh_token",
|
|
value=refresh,
|
|
httponly=True,
|
|
secure=True, # Use Secure
|
|
samesite="lax",
|
|
max_age=max_age_sec
|
|
)
|
|
|
|
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
|
|
|
|
class VerifyEmailRequest(BaseModel):
|
|
token: str = Field(..., description="Email verification token (UUID)")
|
|
device_fingerprint: Optional[str] = Field(None, description="Device fingerprint hash from FingerprintJS")
|
|
|
|
@router.post("/verify-email", response_model=Token)
|
|
async def verify_email(
|
|
request: VerifyEmailRequest,
|
|
response: Response,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Email megerősítés token alapján + automatikus bejelentkezés (Magic Link).
|
|
Sikeres verifikáció után azonnal JWT tokent ad vissza, így a felhasználó
|
|
bejelentkezik anélkül, hogy külön be kellene írnia a jelszavát.
|
|
"""
|
|
user = await AuthService.verify_email(db, request.token)
|
|
if not user:
|
|
raise HTTPException(status_code=400, detail=t("AUTH.INVALID_OR_EXPIRED_TOKEN"))
|
|
|
|
# ── Device Fingerprint Tracking (Magic Link) ──
|
|
if request.device_fingerprint:
|
|
# 1. Ellenőrizzük, létezik-e már a Device a hash alapján
|
|
existing_device = await db.execute(
|
|
select(Device).where(Device.fingerprint_hash == request.device_fingerprint)
|
|
)
|
|
device = existing_device.scalar_one_or_none()
|
|
|
|
if not device:
|
|
# Új eszköz létrehozása
|
|
device = Device(
|
|
fingerprint_hash=request.device_fingerprint,
|
|
risk_score=100,
|
|
is_banned=False
|
|
)
|
|
db.add(device)
|
|
|
|
# 2. Ellenőrizzük a UserDeviceLink-et
|
|
existing_link = await db.execute(
|
|
select(UserDeviceLink).where(
|
|
UserDeviceLink.user_id == user.id,
|
|
UserDeviceLink.device_hash == request.device_fingerprint
|
|
)
|
|
)
|
|
link = existing_link.scalar_one_or_none()
|
|
|
|
now = datetime.now(timezone.utc)
|
|
if link:
|
|
# Létező kapcsolat frissítése
|
|
link.last_seen_at = now
|
|
link.login_count = link.login_count + 1
|
|
else:
|
|
# Új kapcsolat létrehozása
|
|
link = UserDeviceLink(
|
|
user_id=user.id,
|
|
device_hash=request.device_fingerprint,
|
|
first_seen_at=now,
|
|
last_seen_at=now,
|
|
login_count=1
|
|
)
|
|
db.add(link)
|
|
|
|
await db.commit()
|
|
|
|
# Generálj JWT tokent a most aktivált felhasználóhoz (ugyanaz a logika, mint login-nál)
|
|
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
|
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
|
role_key = role_name.upper()
|
|
|
|
token_data = {
|
|
"sub": str(user.id),
|
|
"role": role_name,
|
|
"rank": ranks.get(role_key, 10),
|
|
"scope_level": user.scope_level or "individual",
|
|
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
|
}
|
|
|
|
# Magic Link esetén ne használjunk remember_me-t (alapértelmezett 1 napos token)
|
|
access, refresh = create_tokens(data=token_data, remember_me=False)
|
|
|
|
# Refresh token lejárat
|
|
max_age_days = await config.get_setting(db, "auth_refresh_default_days", default=1)
|
|
max_age_sec = int(max_age_days) * 24 * 60 * 60
|
|
|
|
response.set_cookie(
|
|
key="refresh_token",
|
|
value=refresh,
|
|
httponly=True,
|
|
secure=True,
|
|
samesite="lax",
|
|
max_age=max_age_sec
|
|
)
|
|
|
|
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
|
|
|
|
class ResendVerificationRequest(BaseModel):
|
|
email: EmailStr = Field(..., description="Email cím az aktiváló levél újraküldéséhez")
|
|
|
|
@router.post("/resend-verification")
|
|
async def resend_verification(request: ResendVerificationRequest, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
Aktiváló e-mail újraküldése.
|
|
Mindig sikeres választ ad, hogy megelőzzük az email enumerációt.
|
|
"""
|
|
result = await AuthService.resend_verification(db, request.email)
|
|
return {
|
|
"status": "success",
|
|
"message": "Ha ez az email cím regisztrálva van és még nem aktív, akkor elküldtük az aktiváló e-mailt."
|
|
}
|
|
|
|
class ForgotPasswordRequest(BaseModel):
|
|
email: EmailStr = Field(..., description="Email cím a jelszó visszaállításhoz")
|
|
|
|
@router.post("/forgot-password")
|
|
async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
Elfelejtett jelszó folyamat indítása.
|
|
DEBUG MODE: Explicit hibákat dob az email küldési problémák diagnosztizálásához.
|
|
"""
|
|
result = await AuthService.initiate_password_reset(db, request.email)
|
|
|
|
# DEBUG: Ha nem található a user, dobjunk explicit hibát
|
|
if result == "not_found":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="A felhasználó nem található ezzel az email címmel."
|
|
)
|
|
|
|
# Ha success, adjuk vissza a sikeres választ
|
|
return {
|
|
"status": "success",
|
|
"message": "Ha ez az email cím regisztrálva van, akkor elküldtünk egy jelszó-visszaállítási linket."
|
|
}
|
|
|
|
class ResetPasswordRequest(BaseModel):
|
|
email: EmailStr = Field(..., description="Email cím")
|
|
token: str = Field(..., description="Jelszó visszaállítási token")
|
|
new_password: str = Field(..., description="Új jelszó")
|
|
|
|
@router.post("/reset-password")
|
|
async def reset_password(request: ResetPasswordRequest, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
Jelszó visszaállítása token alapján.
|
|
"""
|
|
success = await AuthService.reset_password(db, request.email, request.token, request.new_password)
|
|
if not success:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Érvénytelen token, lejárt token vagy nem létező felhasználó."
|
|
)
|
|
return {"status": "success", "message": "Jelszó sikeresen megváltoztatva."}
|
|
|
|
@router.post("/complete-kyc")
|
|
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User nem található.")
|
|
return {"status": "success", "message": "Fiók aktiválva."}
|
|
|
|
@router.get("/me")
|
|
async def get_current_user_profile(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Return current user profile (alias for /users/me).
|
|
Uses _build_user_response() to ensure proper address field mapping.
|
|
"""
|
|
from app.schemas.user import UserResponse
|
|
from app.api.v1.endpoints.users import _build_user_response
|
|
response_data = _build_user_response(current_user)
|
|
return UserResponse.model_validate(response_data) |