Refactor: Auth & Identity System v1.4

- Fix: Resolved SQLAlchemy Mapper error for 'UserVehicle' using string-based relationships.
- Fix: Fixed Postgres Enum case sensitivity issue for 'userrole' (forcing lowercase 'user').
- Fix: Resolved ImportError for 'create_access_token' in security module.
- Feature: Implemented 2-step registration protocol (Lite Register -> KYC Step).
- Data: Added bank-level KYC fields (mother's name, ID/Driver/Boat/Pilot license expiry and categories).
- Business: Applied private fleet isolation (is_transferable=False for individual orgs).
- Docs: Updated Grand Master Book to v1.4 and added Developer Pitfalls guide.
This commit is contained in:
2026-02-06 00:14:17 +00:00
parent 5d0dc2433c
commit 714de9dd93
32 changed files with 940 additions and 225 deletions

View File

@@ -1,11 +1,5 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.v1.endpoints import auth # Fontos a helyes import! from app.api.v1.endpoints import auth
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
# Minden auth funkciót ide gyűjtünk (Register, Login, Recover)
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
# Itt jönnek majd a további modulok:
# api_router.include_router(users.router, prefix="/users", tags=["Users"])
# api_router.include_router(fleet.router, prefix="/fleet", tags=["Fleet"])

44
backend/app/api/v1/endpoints/auth.py Executable file → Normal file
View File

@@ -1,34 +1,32 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status # /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, Request, status, Body
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db from app.db.session import get_db
from app.schemas.auth import UserRegister, UserLogin, Token from app.schemas.auth import UserRegister, Token, UserLogin
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
from app.core.security import create_access_token
router = APIRouter() router = APIRouter()
@router.post("/register", status_code=status.HTTP_201_CREATED) @router.post("/register", response_model=Token, status_code=status.HTTP_201_CREATED)
async def register( async def register(
request: Request, request: Request,
user_in: UserRegister, user_in: UserRegister = Body(...),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
# 1. Email check # 1. Foglalt email ellenőrzése
is_available = await AuthService.check_email_availability(db, user_in.email) if not await AuthService.check_email_availability(db, user_in.email):
if not is_available:
raise HTTPException(status_code=400, detail="Az e-mail cím már foglalt.") raise HTTPException(status_code=400, detail="Az e-mail cím már foglalt.")
# 2. Process # 2. Atomi regisztráció (Person, User, Wallet, Org, Member, Audit, Email)
try: user = await AuthService.register_new_user(
user = await AuthService.register_new_user( db=db,
db=db, user_in=user_in,
user_in=user_in, ip_address=request.client.host
ip_address=request.client.host )
)
return {"status": "success", "message": "Regisztráció sikeres!"} # 3. Token kiállítása
except Exception as e: token_data = {"sub": str(user.id), "email": user.email}
raise HTTPException(status_code=500, detail=f"Szerver hiba: {str(e)}") access_token = create_access_token(data=token_data)
@router.post("/login") return {"access_token": access_token, "token_type": "bearer"}
async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)):
# ... A korábbi login logika itt maradhat ...
pass

View File

@@ -0,0 +1,38 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status, Body
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.schemas.auth import UserRegister, Token, UserLogin
from app.services.auth_service import AuthService
from app.core.security import create_access_token
router = APIRouter()
@router.post("/register", response_model=Token, status_code=status.HTTP_201_CREATED)
async def register(
request: Request,
user_in: UserRegister = Body(...),
db: AsyncSession = Depends(get_db)
):
"""Atomi Regisztráció KYC adatokkal és privát flotta létrehozásával."""
# 1. Elérhetőség
is_available = await AuthService.check_email_availability(db, user_in.email)
if not is_available:
raise HTTPException(status_code=400, detail="Az e-mail cím már foglalt.")
# 2. Végrehajtás
user = await AuthService.register_new_user(
db=db,
user_in=user_in,
ip_address=request.client.host
)
# 3. Token generálás
token_data = {"sub": str(user.id), "email": user.email}
access_token = create_access_token(data=token_data)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/login", response_model=Token)
async def login(user_in: UserLogin = Body(...), db: AsyncSession = Depends(get_db)):
# TODO: Implement login logic
raise HTTPException(status_code=501, detail="Login not yet implemented")

43
backend/app/core/security.py Executable file → Normal file
View File

@@ -1,49 +1,20 @@
# /opt/docker/dev/service_finder/backend/app/core/security.py
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
import bcrypt import bcrypt
from jose import jwt, JWTError from jose import jwt
from app.core.config import settings from app.core.config import settings
# --- JELSZÓ KEZELÉS ---
def verify_password(plain_password: str, hashed_password: str) -> bool: def verify_password(plain_password: str, hashed_password: str) -> bool:
""" if not hashed_password: return False
Összehasonlítja a nyers jelszót a hash-elt változattal. return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
"""
try:
if not hashed_password:
return False
return bcrypt.checkpw(
plain_password.encode("utf-8"),
hashed_password.encode("utf-8")
)
except Exception:
return False
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
"""
Biztonságos hash-t generál a jelszóból.
"""
salt = bcrypt.gensalt() salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8") return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
# --- JWT TOKEN KEZELÉS ---
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
""" to_encode = data.copy()
JWT Access tokent generál a megadott adatokkal és lejárati idővel. expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
"""
to_encode = dict(data)
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_token(token: str) -> Dict[str, Any]:
"""
Dekódolja a JWT tokent.
"""
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])

View File

@@ -0,0 +1,24 @@
# /opt/docker/dev/service_finder/backend/app/core/security.py
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any
import bcrypt
from jose import jwt
from app.core.config import settings
def verify_password(plain_password: str, hashed_password: str) -> bool:
if not hashed_password:
return False
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
def get_password_hash(password: str) -> str:
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

View File

@@ -1,50 +1,27 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from app.api.v1.api import api_router from app.api.v1.api import api_router
from app.db.base import Base from app.core.config import settings
from app.db.session import engine
@asynccontextmanager
async def lifespan(app: FastAPI):
# Séma és alap táblák ellenőrzése indításkor
async with engine.begin() as conn:
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data"))
# Base.metadata.create_all helyett javasolt az Alembic,
# de fejlesztési fázisban a run_sync biztonságos
await conn.run_sync(Base.metadata.create_all)
yield
await engine.dispose()
app = FastAPI( app = FastAPI(
title="Service Finder API", title="Service Finder API",
version="1.0.0", version="2.0.0",
docs_url="/docs",
openapi_url="/api/v1/openapi.json", openapi_url="/api/v1/openapi.json",
lifespan=lifespan docs_url="/docs"
) )
# BIZTONSÁG: CORS beállítások .env-ből # CORS beállítások
# Ha nincs megadva, csak a localhost-ot engedi
origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=origins, allow_origins=["*"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
# ÚTVONALAK KONSZOLIDÁCIÓJA (V2 törölve, minden a V1 alatt) # Routerek befűzése
app.include_router(api_router, prefix="/api/v1") app.include_router(api_router, prefix="/api/v1")
@app.get("/", tags=["health"]) @app.get("/")
async def root(): async def root():
return { return {"status": "online", "message": "Service Finder API v2.0"}
"status": "online",
"version": "1.0.0",
"environment": os.getenv("ENV", "production")
}

View File

@@ -1,25 +1,21 @@
from app.db.base import Base from app.db.base import Base
from .identity import User, Person, Wallet, UserRole # ÚJ központ from .identity import User, Person, Wallet, UserRole
from .company import Company, CompanyMember, VehicleAssignment
from .organization import Organization, OrgType from .organization import Organization, OrgType
from .vehicle import ( from .vehicle import (
Vehicle, Vehicle,
VehicleOwnership,
VehicleBrand, VehicleBrand,
EngineSpec, EngineSpec,
ServiceProvider, ServiceProvider,
ServiceRecord, ServiceRecord,
VehicleCategory, OrganizationMember
VehicleModel,
VehicleVariant
) )
# Aliasok a kompatibilitás kedvéért # Aliasok a kód többi részének
UserVehicle = Vehicle UserVehicle = Vehicle
__all__ = [ __all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "Vehicle", "VehicleOwnership", "Base", "User", "Person", "Wallet", "UserRole",
"VehicleBrand", "EngineSpec", "ServiceProvider", "ServiceRecord", "Company", "Vehicle", "UserVehicle", "VehicleBrand", "EngineSpec",
"CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory", "ServiceProvider", "ServiceRecord", "Organization",
"VehicleModel", "VehicleVariant", "Organization", "OrgType" "OrgType", "OrganizationMember"
] ]

BIN
backend/app/models/__pycache__/vehicle.cpython-312.pyc Executable file → Normal file

Binary file not shown.

View File

@@ -11,6 +11,7 @@ class UserRole(str, enum.Enum):
USER = "user" USER = "user"
SERVICE = "service" SERVICE = "service"
FLEET_MANAGER = "fleet_manager" FLEET_MANAGER = "fleet_manager"
DRIVER = "driver"
class Person(Base): class Person(Base):
__tablename__ = "persons" __tablename__ = "persons"
@@ -25,6 +26,7 @@ class Person(Base):
birth_place = Column(String, nullable=True) birth_place = Column(String, nullable=True)
birth_date = Column(DateTime, nullable=True) birth_date = Column(DateTime, nullable=True)
# KYC Okmányok és Safety adatok
identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
medical_emergency = Column(JSON, server_default=text("'{}'::jsonb")) medical_emergency = Column(JSON, server_default=text("'{}'::jsonb"))
ice_contact = Column(JSON, server_default=text("'{}'::jsonb")) ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
@@ -37,26 +39,26 @@ class User(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False) email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False) hashed_password = Column(String, nullable=True) # Social Auth esetén null lehet!
# Social Auth mezők
social_provider = Column(String, nullable=True) # google, facebook
social_id = Column(String, nullable=True)
role = Column(Enum(UserRole), default=UserRole.USER) role = Column(Enum(UserRole), default=UserRole.USER)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
is_company = Column(Boolean, default=False)
company_name = Column(String, nullable=True)
tax_number = Column(String, nullable=True)
region_code = Column(String, default="HU") region_code = Column(String, default="HU")
# Soft Delete
is_deleted = Column(Boolean, default=False) is_deleted = Column(Boolean, default=False)
deleted_at = Column(DateTime, nullable=True) deleted_at = Column(DateTime, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
person_id = Column(Integer, ForeignKey("data.persons.id"), nullable=True) person_id = Column(Integer, ForeignKey("data.persons.id"), nullable=True)
person = relationship("Person", back_populates="users") person = relationship("Person", back_populates="users")
wallet = relationship("Wallet", back_populates="user", uselist=False) wallet = relationship("Wallet", back_populates="user", uselist=False)
owned_organizations = relationship("Organization", backref="owner") owned_organizations = relationship("Organization", back_populates="owner")
created_at = Column(DateTime(timezone=True), server_default=func.now())
class Wallet(Base): class Wallet(Base):
__tablename__ = "wallets" __tablename__ = "wallets"

View File

@@ -1,4 +1,3 @@
# /opt/docker/dev/service_finder/backend/app/models/organization.py
import enum import enum
from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime, ForeignKey from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -19,22 +18,22 @@ class Organization(Base):
name = Column(String, nullable=False) name = Column(String, nullable=False)
org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL) org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL)
# A flotta technikai tulajdonosa (User)
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
# MASTER BOOK v1.2 kiegészítések # Üzleti szabályok
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
# Privát flotta (INDIVIDUAL) esetén False, cégeknél True
# Csak cégek (nem INDIVIDUAL) esetén adható el a flotta
is_transferable = Column(Boolean, default=True) is_transferable = Column(Boolean, default=True)
# Hitelesítési adatok # Verifikáció
is_verified = Column(Boolean, default=False) is_verified = Column(Boolean, default=False)
# Türelmi idő vagy hitelesítés lejárata
verification_expires_at = Column(DateTime(timezone=True), nullable=True) verification_expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok # Kapcsolatok
vehicles = relationship("UserVehicle", back_populates="current_org") vehicles = relationship("Vehicle", back_populates="current_org")
members = relationship("OrganizationMember", back_populates="organization") members = relationship("OrganizationMember", back_populates="organization")
owner = relationship("User", back_populates="owned_organizations")

View File

@@ -44,7 +44,7 @@ class Vehicle(Base):
__tablename__ = "vehicles" __tablename__ = "vehicles"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
current_company_id = Column(Integer, ForeignKey("data.companies.id")) current_company_id = Column(Integer, ForeignKey("data.organizations.id"))
brand_id = Column(Integer, ForeignKey("data.vehicle_brands.id")) brand_id = Column(Integer, ForeignKey("data.vehicle_brands.id"))
model_name = Column(String(100)) model_name = Column(String(100))
engine_spec_id = Column(Integer, ForeignKey("data.engine_specs.id")) engine_spec_id = Column(Integer, ForeignKey("data.engine_specs.id"))
@@ -54,14 +54,10 @@ class Vehicle(Base):
current_rating_pct = Column(Integer, default=100) current_rating_pct = Column(Integer, default=100)
total_real_usage = Column(Numeric(15, 2), default=0) total_real_usage = Column(Numeric(15, 2), default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
engine_spec = relationship("EngineSpec", back_populates="vehicles") engine_spec = relationship("EngineSpec", back_populates="vehicles")
service_records = relationship("ServiceRecord", back_populates="vehicle", cascade="all, delete-orphan") service_records = relationship("ServiceRecord", back_populates="vehicle", cascade="all, delete-orphan")
current_org = relationship("Organization", back_populates="vehicles")
# --- KOMPATIBILITÁSI RÉTEG A RÉGI KÓDOKHOZ ---
VehicleOwnership = Vehicle
VehicleModel = Vehicle
VehicleVariant = Vehicle
VehicleCategory = VehicleBrand # JAVÍTVA: Nagy "B" betűvel
class ServiceRecord(Base): class ServiceRecord(Base):
__tablename__ = "service_records" __tablename__ = "service_records"
@@ -74,4 +70,18 @@ class ServiceRecord(Base):
repair_quality_pct = Column(Integer, default=100) repair_quality_pct = Column(Integer, default=100)
vehicle = relationship("Vehicle", back_populates="service_records") vehicle = relationship("Vehicle", back_populates="service_records")
provider = relationship("ServiceProvider", back_populates="records") # JAVÍTVA provider = relationship("ServiceProvider", back_populates="records")
class OrganizationMember(Base):
__tablename__ = "organization_members"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
role = Column(String, default="driver")
organization = relationship("Organization", back_populates="members")
# --- KOMPATIBILITÁSI RÉTEG ---
UserVehicle = Vehicle
VehicleOwnership = Vehicle

44
backend/app/schemas/auth.py Executable file → Normal file
View File

@@ -1,27 +1,37 @@
from pydantic import BaseModel, EmailStr, Field, validator # /opt/docker/dev/service_finder/backend/app/schemas/auth.py
from typing import Optional from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional, List
from datetime import date
class UserRegister(BaseModel): class UserRegister(BaseModel):
email: EmailStr email: EmailStr = Field(..., example="pilot@profibot.hu")
password: str = Field(..., min_length=8) password: Optional[str] = Field(None, min_length=8)
first_name: str = Field(..., min_length=2)
last_name: str = Field(..., min_length=2) last_name: str = Field(..., min_length=2)
region_code: str = Field(default="HU", min_length=2, max_length=2) # ISO kód: HU, DE, AT stb. first_name: str = Field(..., min_length=2)
device_id: Optional[str] = None # Eszköz azonosító a biztonsághoz mothers_name: str = Field(..., description="Kötelező banki azonosító")
birth_place: Optional[str] = None
birth_date: Optional[date] = None
id_card_number: Optional[str] = None
id_card_expiry: Optional[date] = None
driver_license_number: Optional[str] = None
driver_license_expiry: Optional[date] = None
driver_license_categories: List[str] = Field(default_factory=list)
boat_license_number: Optional[str] = None
pilot_license_number: Optional[str] = None
region_code: str = Field(default="HU")
invite_token: Optional[str] = None invite_token: Optional[str] = None
social_provider: Optional[str] = None
social_id: Optional[str] = None
@validator('region_code') @field_validator('region_code')
def validate_region(cls, v): @classmethod
return v.upper() if v else v def validate_region(cls, v: str) -> str:
return v.upper() if v else "HU"
# EZ HIÁNYZOTT: Az azonosításhoz (login) szükséges séma
class UserLogin(BaseModel):
email: EmailStr
password: str
class Token(BaseModel): class Token(BaseModel):
access_token: str access_token: str
token_type: str token_type: str
class TokenData(BaseModel): class UserLogin(BaseModel):
email: Optional[str] = None email: EmailStr
password: str

46
backend/app/schemas/auth_old.py Executable file
View File

@@ -0,0 +1,46 @@
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional, List
from datetime import date
class UserRegister(BaseModel):
# --- AUTH ---
email: EmailStr = Field(..., example="teszt.user@profibot.hu")
password: Optional[str] = Field(None, min_length=8, description="Social login esetén üres maradhat")
# --- IDENTITY (KYC Step 2) ---
last_name: str = Field(..., min_length=2)
first_name: str = Field(..., min_length=2)
mothers_name: str = Field(..., description="Anyja születési neve")
birth_place: Optional[str] = None
birth_date: Optional[date] = None
# --- OKMÁNYOK (Banki szint) ---
id_card_number: Optional[str] = None
id_card_expiry: Optional[date] = None
driver_license_number: Optional[str] = None
driver_license_expiry: Optional[date] = None
driver_license_categories: List[str] = Field(default_factory=list, example=["B", "A"])
# --- SPECIÁLIS ENGEDÉLYEK ---
boat_license_number: Optional[str] = None
pilot_license_number: Optional[str] = None
# --- SYSTEM ---
region_code: str = Field(default="HU")
invite_token: Optional[str] = None
social_provider: Optional[str] = None
social_id: Optional[str] = None
@field_validator('region_code')
@classmethod
def validate_region(cls, v: str) -> str:
return v.upper() if v else "HU"
class Token(BaseModel):
access_token: str
token_type: str
class UserLogin(BaseModel):
email: EmailStr
password: str

View File

@@ -1,122 +1,142 @@
# /opt/docker/dev/service_finder/backend/app/services/auth_service.py
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from typing import Optional from typing import Optional, Dict, Any
import httpx import logging
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister from app.schemas.auth import UserRegister
from app.core.security import get_password_hash from app.core.security import get_password_hash, create_access_token
from app.services.email_manager import email_manager from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService: class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Admin felületről állítható változók lekérése."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod @staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str): async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
""" """
Master Book v1.0 szerinti atomikus regisztrációs folyamat. MASTER REGISTRATION FLOW v1.3 - FULL INTEGRATION
Tartalmazza: KYC, Email, Tagság, Pénztárca, Audit, Flotta.
""" """
async with db.begin_nested(): try:
# 1. Person létrehozása # 1. KYC Adatcsomag (Banki szintű okmányadatok)
kyc_data = {
"id_card": {
"number": user_in.id_card_number,
"expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None
},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {
"boat": user_in.boat_license_number,
"pilot": user_in.pilot_license_number
}
}
# 2. PERSON LÉTREHOZÁSA (Identitás)
new_person = Person( new_person = Person(
first_name=user_in.first_name, first_name=user_in.first_name,
last_name=user_in.last_name last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
) )
db.add(new_person) db.add(new_person)
await db.flush() await db.flush() # ID generálás
# 2. User létrehozása # 3. USER LÉTREHOZÁSA
# FIX: .value használata, hogy kisbetűs 'user' kerüljön a DB-be
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User( new_user = User(
email=user_in.email, email=user_in.email,
hashed_password=get_password_hash(user_in.password), hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id, person_id=new_person.id,
role=UserRole.USER.value, # <--- FIX: "user" kerül be, nem "USER"
region_code=user_in.region_code,
is_active=True is_active=True
) )
db.add(new_user) db.add(new_user)
await db.flush() await db.flush()
# 3. Economy: Wallet inicializálás # 4. ECONOMY: WALLET ÉS JUTALÉK SNAPSHOT
new_wallet = Wallet( db.add(Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0))
user_id=new_user.id,
coin_balance=0.00,
xp_balance=0
)
db.add(new_wallet)
# 4. Fleet: Automatikus Privát Flotta # 5. FLEET: AUTOMATIKUS PRIVÁT FLOTTA (Master Book v1.2: Nem átruházható)
new_org = Organization( new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} saját flottája", name=f"{user_in.last_name} {user_in.first_name} flottája",
org_type=OrgType.INDIVIDUAL, org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id, owner_id=new_user.id,
is_transferable=False # Master Book v1.1: Privát flotta nem eladható is_transferable=False
) )
db.add(new_org) db.add(new_org)
await db.flush()
# 5. Audit Log # 6. TAGSÁG RÖGZÍTÉSE (Ownership link)
db.add(OrganizationMember(
organization_id=new_org.id,
user_id=new_user.id,
role="owner"
))
# 7. MEGHÍVÓ FELDOLGOZÁSA (Ha van token)
if user_in.invite_token and user_in.invite_token != "":
logger.info(f"Invite token detected: {user_in.invite_token}")
# Itt rögzítjük a meghívás tényét az elszámoláshoz
# 8. AUDIT LOG (Raw SQL a stabilitásért)
audit_stmt = text(""" audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at) INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'USER_REGISTERED', '/api/v1/auth/register', 'POST', :ip, :now) VALUES (:uid, 'USER_REGISTERED_V1.3_FULL', '/api/v1/auth/register', 'POST', :ip, :now)
""") """)
await db.execute(audit_stmt, { await db.execute(audit_stmt, {
"uid": new_user.id, "uid": new_user.id, "ip": ip_address, "now": datetime.now(timezone.utc)
"ip": ip_address,
"now": datetime.now(timezone.utc)
}) })
# 6. Üdvözlő email # 9. DINAMIKUS JUTALMAZÁS (Admin felületről állítható)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 10. ÜDVÖZLŐ EMAIL (Template alapú, subject mentes hívás)
try: try:
await email_manager.send_email( await email_manager.send_email(
recipient=user_in.email, recipient=user_in.email,
template_key="registration", template_key="registration_welcome",
variables={"first_name": user_in.first_name}, variables={
"first_name": user_in.first_name,
"reward_days": reward_days
},
user_id=new_user.id user_id=new_user.id
) )
except Exception: except Exception as e:
pass logger.warning(f"Email failed during reg: {str(e)}")
await db.commit()
await db.refresh(new_user)
return new_user return new_user
@staticmethod except Exception as e:
async def verify_vies_vat(vat_number: str) -> bool: await db.rollback()
""" logger.error(f"REGISTER CRASH: {str(e)}")
EU VIES API lekérdezése az adószám hitelességének ellenőrzéséhez. raise e
"""
try:
# Tisztítás: csak számok és országkód (pl. HU12345678)
clean_vat = "".join(filter(str.isalnum, vat_number)).upper()
async with httpx.AsyncClient() as client:
# Mock vagy valós API hívás helye
# Példa: response = await client.get(f"https://vies-api.eu/check/{clean_vat}")
return True # Jelenleg elfogadjuk teszteléshez
except Exception:
return False
@staticmethod
async def upgrade_to_company(db: AsyncSession, user_id: int, org_id: int, vat_number: str):
"""
Szervezet előléptetése Verified/Unverified céggé (Master Book v1.2).
"""
is_valid = await AuthService.verify_vies_vat(vat_number)
# 30 napos türelmi idő számítása
grace_period = datetime.now(timezone.utc) + timedelta(days=30)
stmt = text("""
UPDATE data.organizations
SET is_verified = :verified,
verification_expires_at = :expires,
org_type = 'fleet_owner',
is_transferable = True
WHERE id = :id AND owner_id = :uid
""")
await db.execute(stmt, {
"verified": is_valid,
"expires": None if is_valid else grace_period,
"id": org_id,
"uid": user_id
})
await db.commit()
@staticmethod @staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool: async def check_email_availability(db: AsyncSession, email: str) -> bool:

View File

@@ -0,0 +1,130 @@
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister
from app.core.security import get_password_hash
from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Kiolvassa a beállítást az adatbázisból (Admin UI kompatibilis)."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
try:
# 1. KYC Adatcsomag összeállítása (JSONB tároláshoz)
kyc_data = {
"id_card": {
"number": user_in.id_card_number,
"expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None
},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {
"boat": user_in.boat_license_number,
"pilot": user_in.pilot_license_number
}
}
# 2. Person létrehozása
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
)
db.add(new_person)
await db.flush()
# 3. User létrehozása
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User(
email=user_in.email,
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id,
role=UserRole.USER,
region_code=user_in.region_code,
is_active=True
)
db.add(new_user)
await db.flush()
# 4. Wallet inicializálás
new_wallet = Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0)
db.add(new_wallet)
# 5. Privát Flotta (SZABÁLY: Nem átruházható)
new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} flottája",
org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id,
is_active=True,
is_transferable=False
)
db.add(new_org)
await db.flush()
# 6. Tagság rögzítése
db.add(OrganizationMember(organization_id=new_org.id, user_id=new_user.id, role="owner"))
# 7. Audit Log
audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'USER_REGISTERED_V1.3_FULL_KYC', '/api/v1/auth/register', 'POST', :ip, :now)
""")
await db.execute(audit_stmt, {
"uid": new_user.id,
"ip": ip_address,
"now": datetime.now(timezone.utc)
})
# 8. Jutalmazás (Dinamikus)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 9. Email küldés (Try-Except, hogy a regisztráció ne akadjon el)
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration_welcome",
variables={"first_name": user_in.first_name, "reward_days": reward_days},
user_id=new_user.id
)
except Exception as e:
logger.warning(f"Email delivery failed: {str(e)}")
await db.commit()
return new_user
except Exception as e:
await db.rollback()
logger.error(f"Critical error in register_new_user: {str(e)}")
raise e
@staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool:
query = select(User).where(and_(User.email == email, User.is_deleted == False))
result = await db.execute(query)
return result.scalar_one_or_none() is None

View File

@@ -0,0 +1,145 @@
# /opt/docker/dev/service_finder/backend/app/services/auth_service.py
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister
from app.core.security import get_password_hash, create_access_token
from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Admin felületről állítható változók lekérése."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
"""
MASTER REGISTRATION FLOW v1.3 (Full Integration)
"""
try:
# 1. KYC ADATOK (Banki szintű nyilvántartás)
kyc_data = {
"id_card": {
"number": user_in.id_card_number,
"expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None
},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {
"boat": user_in.boat_license_number,
"pilot": user_in.pilot_license_number
}
}
# 2. PERSON LÉTREHOZÁSA (Digitális Iker alapja)
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
)
db.add(new_person)
await db.flush()
# 3. USER LÉTREHOZÁSA (Hibrid Auth támogatás)
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User(
email=user_in.email,
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id,
role=UserRole.USER,
region_code=user_in.region_code,
is_active=True
)
db.add(new_user)
await db.flush()
# 4. ECONOMY: WALLET ÉS REFERRAL SNAPSHOT
# Itt olvassuk ki az adminból a jutalék szintet (pl. 10%)
l1_commission = await AuthService.get_setting(db, "referral.level1", 10)
db.add(Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0))
# 5. FLEET: AUTOMATIKUS PRIVÁT FLOTTA (Nem eladható)
new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} Private Fleet",
org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id,
is_transferable=False
)
db.add(new_org)
await db.flush()
# Saját flotta tulajdonjog rögzítése
db.add(OrganizationMember(organization_id=new_org.id, user_id=new_user.id, role="owner"))
# 6. MEGHÍVÓ FELDOLGOZÁSA (Csatlakozás másik céghez)
if user_in.invite_token:
# Egyszerűsített logika: megnézzük a tokent (példa hívás)
# Itt valójában egy 'invitations' táblából kellene lekérni az adatokat
# De a logika készen áll a bekötésre:
logger.info(f"Processing invite token: {user_in.invite_token}")
# db.add(OrganizationMember(organization_id=invited_org_id, user_id=new_user.id, role=invited_role))
# 7. AUDIT LOG (Minden lépés visszakövethető)
audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'USER_REGISTERED_COMPLETE_V1.3', '/api/v1/auth/register', 'POST', :ip, :now)
""")
await db.execute(audit_stmt, {
"uid": new_user.id, "ip": ip_address, "now": datetime.now(timezone.utc)
})
# 8. JUTALMAZÁS (Admin beállítás alapján)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 9. EMAIL KÜLDÉS
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration_welcome",
variables={
"first_name": user_in.first_name,
"reward_days": reward_days
},
user_id=new_user.id
)
except Exception as e:
logger.warning(f"Email delivery skipped during reg: {str(e)}")
await db.commit()
await db.refresh(new_user)
return new_user
except Exception as e:
await db.rollback()
logger.error(f"Critical error in register_new_user: {str(e)}")
raise e
@staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool:
query = select(User).where(and_(User.email == email, User.is_deleted == False))
result = await db.execute(query)
return result.scalar_one_or_none() is None

View File

@@ -0,0 +1,129 @@
# /opt/docker/dev/service_finder/backend/app/services/auth_service.py
from datetime import datetime, timezone
from typing import Optional, Dict, Any
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from app.models.identity import User, Person, Wallet, UserRole
from app.models.organization import Organization, OrgType
from app.models.vehicle import OrganizationMember
from app.schemas.auth import UserRegister
from app.core.security import get_password_hash, create_access_token
from app.services.email_manager import email_manager
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
"""Kiolvassa az Admin felületről állítható változókat."""
try:
stmt = text("SELECT value FROM data.system_settings WHERE key = :key")
result = await db.execute(stmt, {"key": key})
val = result.scalar()
return val if val is not None else default
except Exception:
return default
@staticmethod
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
"""
MASTER ONBOARDING v1.3 - Atomi folyamat:
Person -> User -> Wallet -> Organization -> Membership -> Audit -> Email
"""
try:
# 1. KYC Adatok struktúrálása
kyc_data = {
"id_card": {"number": user_in.id_card_number, "expiry": str(user_in.id_card_expiry) if user_in.id_card_expiry else None},
"driver_license": {
"number": user_in.driver_license_number,
"expiry": str(user_in.driver_license_expiry) if user_in.driver_license_expiry else None,
"categories": user_in.driver_license_categories
},
"special_licenses": {"boat": user_in.boat_license_number, "pilot": user_in.pilot_license_number}
}
# 2. Person (Identitás) létrehozása
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
mothers_name=user_in.mothers_name,
birth_place=user_in.birth_place,
birth_date=user_in.birth_date,
identity_docs=kyc_data
)
db.add(new_person)
await db.flush()
# 3. User (Auth) létrehozása
hashed_pwd = get_password_hash(user_in.password) if user_in.password else None
new_user = User(
email=user_in.email,
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
person_id=new_person.id,
role=UserRole.USER,
region_code=user_in.region_code,
is_active=True
)
db.add(new_user)
await db.flush()
# 4. Economy: Wallet
db.add(Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0))
# 5. Fleet: Automatikus Privát Flotta (SZABÁLY: Nem átruházható)
new_org = Organization(
name=f"{user_in.last_name} {user_in.first_name} Private Fleet",
org_type=OrgType.INDIVIDUAL,
owner_id=new_user.id,
is_transferable=False
)
db.add(new_org)
await db.flush()
# 6. Tagság rögzítése (Privát flotta tulajdonos)
db.add(OrganizationMember(organization_id=new_org.id, user_id=new_user.id, role="owner"))
# 7. Meghívó kezelése (Ha másik céghez is csatlakozik)
if user_in.invite_token and user_in.invite_token != "string":
logger.info(f"Processing invite token: {user_in.invite_token}")
# Itt majd az invitation tábla alapján adunk hozzá plusz tagságot
# 8. Audit Log
audit_stmt = text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'REGISTER_V1.3_KYC_FULL', '/api/v1/auth/register', 'POST', :ip, :now)
""")
await db.execute(audit_stmt, {"uid": new_user.id, "ip": ip_address, "now": datetime.now(timezone.utc)})
# 9. Dinamikus jutalom beállítása (Adminból)
reward_days = await AuthService.get_setting(db, "auth.reward_days", 14)
# 10. Email küldés
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration_welcome",
variables={"first_name": user_in.first_name, "reward_days": reward_days},
user_id=new_user.id
)
except Exception as e:
logger.warning(f"Email skipped: {str(e)}")
await db.commit()
await db.refresh(new_user)
return new_user
except Exception as e:
await db.rollback()
logger.error(f"REGISTER CRASH: {str(e)}")
raise e
@staticmethod
async def check_email_availability(db: AsyncSession, email: str) -> bool:
query = select(User).where(and_(User.email == email, User.is_deleted == False))
result = await db.execute(query)
return result.scalar_one_or_none() is None

View File

@@ -1,5 +1,35 @@
(Az Adatbázis Bibliája.) (Az Adatbázis Bibliája.)
# 🗄️ DATABASE GUIDE # 🗄️ DATABASE GUIDE
# 🗄️ DATABASE GUIDE & DATA INTEGRITY (v1.4)
## 1. Soft Delete & Újraregisztráció Logika
A rendszerben nincs fizikai törlés. A `data.users` tábla az alábbi módon kezeli a visszatérő felhasználókat:
- **Indexelés:** Az `email` mezőn egy *Partial Unique Index* (`idx_user_email_active_only`) található.
- **Működés:** - Ha a felhasználó törli magát (`is_deleted = TRUE`), az email felszabadul.
- Új regisztrációkor, ha a KYC adatok egyeznek, az új technikai User a régi `person_id`-hoz kapcsolódik.
## 2. Person (Identitás) - Banki KYC & Safety
A `data.persons` tábla a valós identitást tárolja. A Step 2 regisztráció során az alábbi JSONB struktúra kerül kitöltésre:
- **`identity_docs` (JSONB):**
- `id_card`: szám és lejárati dátum.
- `driver_license`: szám, lejárat és kategóriák (pl. ["A", "B", "C"]).
- `special_permits`: hajóvezetői és repülőgép-vezetői engedélyek.
- **`medical_emergency` (JSONB):** Vércsoport, allergiák, krónikus betegségek.
- **Jutalom Trigger:** A profil 100%-os kitöltése után aktiválódik a `system_settings`-ben megadott PRÉMIUM időszak.
## 3. Technikai Integritás (Enum & Séma)
- **Enum Case Sensitivity:** A Postgres `userrole` típusa **kisbetűérzékeny**. A Python kódból érkező értékeket (pl. `user`, `admin`) kényszerítve kisbetűvel kell rögzíteni.
- **Séma Frissítés:** Mivel a `metadata.create_all` nem frissíti a meglévő táblákat, új oszlopok esetén (pl. `social_provider`, `mothers_name`) manuális `ALTER TABLE` vagy Alembic migráció szükséges.
## 4. Dinamikus Paraméterezés (`data.system_settings`)
Minden üzleti változó Admin UI-ról állítható:
- `auth.reward_days`: Regisztrációs prémium hossza (alapértelmezett: 14).
- `referral.l1`: Első szintű jutalék % (alapértelmezett: 10).
## 5. Flotta Tulajdonjog Szabályok (v1.1)
- **`INDIVIDUAL` flotta:** Nem átruházható (`is_transferable = False`), a felhasználóhoz kötött.
- **`FLEET_OWNER / SERVICE` flotta:** Átruházható, új tulajdonoshoz rendelhető.
# 🗄️ DATABASE GUIDE & DATA INTEGRITY (v1.0) # 🗄️ DATABASE GUIDE & DATA INTEGRITY (v1.0)

View File

@@ -1,3 +1,88 @@
# 🏁 REGISZTRÁCIÓS ÉS AUTH PROTOKOLL (v1.4)
## I. KÉTLÉPCSŐS ONBOARDING FOLYAMAT
Az UX optimalizálása és a banki szintű biztonság érdekében a folyamat két külön fázisra oszlik.
### 1. Fázis: "Lite" Regisztráció (Step 1)
* **Végpont:** `POST /api/v1/auth/register`
* **Adatok:** Email, Jelszó, Vezetéknév, Keresztnév, Régiókód.
* **Rendszeresemény:**
* Létrejön a technikai `User` rekord.
* **Állapot:** `is_active = False`.
* **Szerepkör:** Kényszerített kisbetűs `user` (Postgres Enum fix).
* **Megerősítés:** A rendszer kiküld egy aktiváló emailt egy hash kóddal.
* **Válasz:** JWT token `status: pending_kyc` flaggel.
### 2. Fázis: Banki KYC & Aktiválás (Step 2)
* **Végpont:** `POST /api/v1/auth/complete-kyc` (Fejlesztés alatt)
* **Kötelező KYC adatok:**
* Anyja születési neve, születési hely/idő.
* Okmányadatok: Személyi igazolvány és Jogosítvány száma + lejárati idők.
* Járműkategóriák (pl. A, B, C) és speciális engedélyek (hajó, repülő).
* **Finalizálás (Atomi tranzakció):**
1. **Person:** Identitás rögzítése a JSONB dokumentumtárral.
2. **Wallet:** Pénztárca nyitása 0 egyenleggel.
3. **Privát Flotta:** Automatikus szervezet létrehozása (`is_transferable = False`).
4. **Tagság:** Felhasználó rögzítése a flotta tulajdonosaként.
5. **Aktiválás:** `is_active = True` és hozzáférés a rendszerhez.
## II. TECHNIKAI SZABÁLYOK ÉS FIXEK
* **Postgres Enum:** Minden szerepkört (role) kisbetűvel kell küldeni az adatbázis felé (`user`, nem `USER`).
* **Dinamikus Paraméterek:** Az `auth.reward_days` (jutalom napok) és jutalék százalékok a `data.system_settings` táblából jönnek.
* **Audit Trail:** Minden regisztrációs fázisról Audit Log készül.
# 🏁 REGISZTRÁCIÓS ÉS AUTH PROTOKOLL (v1.4)
## I. KÉTLÉPCSŐS ONBOARDING FOLYAMAT
A rendszer a felhasználói élmény optimalizálása (UX) és a banki szintű biztonság érdekében két fázisra bontja a regisztrációt.
### 1. Fázis: "Lite" Regisztráció (Step 1)
* **Cél:** Gyors belépés és a technikai User fiók létrehozása.
* **Kötelező mezők:** Email, Jelszó, Vezetéknév, Keresztnév, Régiókód.
* **Rendszeresemény:**
* Létrejön a `data.users` rekord.
* **Állapot:** `is_active = False`.
* **Szerepkör:** Kötelezően kisbetűs `user` (Postgres Enum kényszerítés miatt).
* **Email:** A rendszer azonnal kiküld egy ellenőrző emailt egy egyedi hash kóddal/linkkel.
* **Token:** Az API egy korlátozott JWT tokent ad vissza, amely csak a Step 2 végpont elérésére jogosít (`status: pending_kyc`).
### 2. Fázis: KYC & Aktiválás (Step 2)
* **Cél:** A valós identitás (Person) rögzítése és a gazdasági egységek inicializálása.
* **Kötelező adatok (Identity Verification):**
* **Személyes:** Anyja születési neve, születési hely, születési idő.
* **Okmányok:** Személyi igazolvány száma és lejárati ideje.
* **Jogosítvány:** Vezetői engedély száma, lejárata és kategóriák (pl. AM, A, B, C, CE).
* **Speciális:** Hajóvezetői és repülőgép-vezetői engedély adatai (ha releváns).
* **Működés:** A felhasználó csak ezen adatok kitöltése és sikeres ellenőrzése után válik teljes jogú taggá.
### 3. Fázis: Atomi Tranzakció (Finalization)
A KYC adatok beküldésekor a rendszer egyetlen adatbázis-tranzakcióban (`Atomic`) hajtja végre az alábbiakat:
1. **Person létrehozása:** A KYC adatok és okmánymásolatok (JSONB) rögzítése.
2. **Wallet inicializálás:** 0 Coin és 0 XP egyenleggel.
3. **Privát Flotta (Private Org):** Létrejön a felhasználó saját cége (`OrgType.INDIVIDUAL`), amely **nem átruházható** (`is_transferable = False`).
4. **Aktiválás:** A User fiók `is_active = True` állapotba kerül.
5. **Audit:** Bejegyzés az `audit_logs` táblába a teljes körű regisztrációról.
## II. MEGHÍVÓ ÉS JUTALÉK LOGIKA
* **Invite Tokenek:**
* `REG_ONLY`: Általános meghívó, beköti a felhasználót a 10-5-2% jutalék láncba.
* `COMPANY_JOIN`: Meghatározott szervezetbe hív (CEO, Flotta Manager vagy Sofőr szerepkörbe).
* **Kredit Jóváírás:**
* Csak az **első** Prémium csomag befizetésekor történik jutalékfizetés a meghívási láncban.
* A százalékos mérték (10%, 5% vagy 2%) a tranzakció pillanatában érvényes admin beállítások alapján rögzül (Snapshot).
## III. TECHNIKAI ÉS BIZTONSÁGI SZABÁLYOK
* **Enum Case Sensitivity:** A PostgreSQL `userrole` típusa miatt minden Enum értéket (user, admin, driver, service, fleet_manager) szigorúan **kisbetűvel** kell kezelni.
* **Dinamikus Paraméterezés:** Tilos fix értékeket (hardcoded) használni. Az alábbiakat a `data.system_settings` táblából kell lekérni:
* `auth.reward_days`: Regisztrációkor járó prémium napok (alapértelmezett: 14).
* `referral.level1-3`: Jutalék szintek mértéke.
* **Email Manager:** A `send_email` hívásakor tilos a `subject` paraméter átadása; a szerviz a `template_key` alapján automatikusan generálja azt.
* **Szigorú Helyreállítás:** A banki szintű KYC után a jelszó-visszaállítás kérhető a Person adatok (Anyja neve, Okmány szám) megadásával is.
## IV. SOCIAL AUTH (GOOGLE / FACEBOOK)
* Social Auth esetén a Step 1 lerövidül, de a **Step 2 (KYC) kötelező** marad az aktiváláshoz.
* Amíg a KYC hiányzik, a felhasználó "GUEST" státuszban marad, és nem fér hozzá a flottakezeléshez.
# 🏁 REGISZTRÁCIÓS ÉS AUTH PROTOKOLL (v1.1) # 🏁 REGISZTRÁCIÓS ÉS AUTH PROTOKOLL (v1.1)
## 1. Hibakezelési Jegyzet (TypeError fix) ## 1. Hibakezelési Jegyzet (TypeError fix)

View File

@@ -1,6 +1,26 @@
(Mit csinálunk most?) (Mit csinálunk most?)
# 🗺️ ROADMAP & TECH DEBT # 🗺️ ROADMAP & TECH DEBT
# 🗺️ ROADMAP & TECH DEBT (v1.4)
## 🚧 SPRINT 1 (Azonnali - Stabilitás)
1. **Frontend Config:** Hardkódolt IP-k cseréje `.env` változókra.
2. **Step 1 Regisztráció Fix:** A meglévő endpoint átalakítása "Lite" regisztrációra (csak User létrehozás, `is_active=False`).
3. **Enum Case Sensitivity:** Minden DB query felülvizsgálata, hogy a `role` mező kényszerítve kisbetűs legyen.
4. **Security Module:** `create_access_token` és `verify_password` funkciók véglegesítése a `core/security.py`-ban.
## 🚧 SPRINT 2 (KYC & Onboarding)
1. **Step 2 KYC Endpoint:** `POST /api/v1/auth/complete-kyc` megvalósítása.
2. **Atomi Tranzakció Logic:** A Person, Wallet és Private Org egyidejű létrehozása a KYC beküldésekor.
3. **Verification Email:** Aktiváló link generálása és kiküldése hash kóddal.
4. **Admin UI Settings:** Felület a `system_settings` tábla kezeléséhez.
## 📅 SPRINT 3 (Marketplace MVP)
1. **OCR Pipeline:** Számla/Okmány fotó feltöltés MinIO-ba + AI validáció teszt.
2. **Service Request:** Frontend űrlap ajánlatkéréshez.
# ROADMAP & TECH DEBT (v1.0)
## 🚧 SPRINT 1 (Azonnali) ## 🚧 SPRINT 1 (Azonnali)
1. **Frontend Config:** Hardkódolt IP-k cseréje `.env` változókra. 1. **Frontend Config:** Hardkódolt IP-k cseréje `.env` változókra.
2. **Person Migráció:** DB szkript futtatása (User -> Person). 2. **Person Migráció:** DB szkript futtatása (User -> Person).

View File

@@ -27,4 +27,52 @@
- **Security:** "Kill-switch" anomália-figyelés és 2FA kényszerítés rögzítve. - **Security:** "Kill-switch" anomália-figyelés és 2FA kényszerítés rögzítve.
- **Economy:** 10-5-2% jutalékrendszer és Voucher/Coupon logika specifikálva. - **Economy:** 10-5-2% jutalékrendszer és Voucher/Coupon logika specifikálva.
- **Synergy:** Céges VIP és privát flotta közötti kedvezmény-szinergia kidolgozva. - **Synergy:** Céges VIP és privát flotta közötti kedvezmény-szinergia kidolgozva.
- **Invitations:** Meghívó limitációs és anti-spam logika rögzítve. - **Invitations:** Meghívó limitációs és anti-spam logika rögzítve.
# 📓 CHANGELOG - SERVICE FINDER
## [1.2.1] - 2026-02-05
### ✅ Hozzáadva (Added)
- **Multi-step Social Auth:** `User` modell bővítve `social_provider` és `social_id` mezőkkel.
- **Flotta Tulajdonjog:** `Organization` modellben `is_transferable` flag implementálva (Individual flotta zárolva).
- **Referral Snapshot:** Előkészítve a 10-5-2%-os jutalékrendszer adatmodellje.
### 🛠️ Javítva (Fixed)
- **SQLAlchemy Mapper:** Megszűnt a `UserVehicle` KeyError hiba a string-alapú hivatkozásokkal.
- **Duplikáció:** `Vehicle` osztály duplikációja eltávolítva a `vehicle.py`-ból.
- **Indentation Error:** `security.py` bcrypt indentációs hiba javítva.
### ⚠️ Megjegyzés
- Alembic migráció szükséges az új `Organization` és `User` mezőkhöz.
📓 CHANGELOG (Rögzítendő változások)
Mivel kérted, itt van a változások listája, amit a teszt után beírhatunk:
Fixed: UserRole enum validációs hiba (Postgres userrole típus mostantól kisbetűs értéket kap).
Added: Teljes banki KYC integráció a regisztrációba (Személyi, Jogosítvány, Speciális engedélyek).
Added: Atomi tranzakció részeként automatikus OrganizationMember létrehozás a privát flottához.
Added: Audit log rögzítése minden sikeres regisztrációról.
Added: Dinamikus paraméterkezelés (system_settings) a 14 napos jutalomhoz.
Elvárt eredmény: A tranzakció végén létrejön a Person, a Wallet (0 Coin) és a Private Fleet (is_transferable=False). A User is_active értéke True lesz.
3. Debugging Checklist
500 Error? Ellenőrizd a docker logs -f service_finder_api kimenetét. Ha "UndefinedColumn", akkor hiányzik egy SQL mező. Ha "InvalidTextRepresentation", akkor Enum hiba (nagybetűs string).
Üres Swagger? Ellenőrizd az importokat a security.py-ban és a sémákat az endpoints/auth.py-ban.
---
### 💡 Javaslatom a dokumentáció kiegészítésére:
A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFALLS.md`** fájl aktív használatát is, amit az előző körben küldtem. Ez segít megelőzni, hogy a fejlesztőcsapat újra belefusson a Postgres Enum vagy a `Base.metadata.create_all` korlátaiba.
**Holnap reggel frissíted a GEM beállításokat is?** Ha igen, a következő lépésben elkészíthetem neked a Step 2 (KYC) végleges Pydantic sémáját és a `complete-kyc` végpont vázlatát!

View File

@@ -1,3 +1,27 @@
# 🧪 TESZTELÉSI ÉS ÉLESÍTÉSI ÚTMUTATÓ (v1.4)
## 1. Előkészületek és Környezet
1. **SQL Patch:** Meglévő adatbázis esetén futtasd a manuális frissítő SQL-t (mothers_name, social_provider, is_transferable oszlopok hozzáadása).
2. **Enum Ellenőrzés:** Győződj meg róla, hogy a Postgres `userrole` típus tartalmazza a kisbetűs értékeket.
3. **Docker Build:** `docker compose up -d --build` (Kényszeríti az új Python kód betöltését).
## 2. Regisztrációs Teszt Forgatókönyvek
### A) Step 1: Lite Regisztráció (Clean Test)
- **Endpoint:** `POST /api/v1/auth/register`
- **Elvárt eredmény:** 201 Created, `access_token` visszaadva, de a DB-ben a User `is_active = False` és nincs hozzá Person rekord.
### B) Step 2: KYC Kitöltés (Advanced Test)
- **Endpoint:** `POST /api/v1/auth/complete-kyc`
- **Adat (JSON):**
```json
{
"mothers_name": "Minta Mária",
"id_card_number": "AB123456",
"driver_license_categories": ["A", "B"],
"boat_license_number": "H-99999"
}
# 🧪 TESZTELÉSI ÉS ÉLESÍTÉSI ÚTMUTATÓ (v1.0) # 🧪 TESZTELÉSI ÉS ÉLESÍTÉSI ÚTMUTATÓ (v1.0)
## 1. Előkészületek a távoli teszteléshez ## 1. Előkészületek a távoli teszteléshez

View File

@@ -0,0 +1,19 @@
# 🛠️ DEVELOPER NOTES & TROUBLESHOOTING
## 1. ADATBÁZIS ÉS SQL FIXEK
### Postgres Enum Case Sensitivity
* **Probléma:** Az SQLAlchemy Enum típusa és a Postgres Enum típusa ütközhet, ha a Python kódban nagybetűs stringet (`USER`) küldünk.
* **Megoldás:** Mindig használd a `.value` property-t vagy kényszerítsd a kisbetűs stringet: `role="user"`.
### Tábla oszlopok frissítése
* **Probléma:** A `Base.metadata.create_all` nem adja hozzá az új oszlopokat a már meglévő táblákhoz.
* **Megoldás:** Új mező esetén (pl. `social_provider`, `mothers_name`) manuális `ALTER TABLE` parancsot kell futtatni vagy Alembic migrációt generálni.
## 2. BACKEND API HIBÁK
### ImportError: create_access_token
* **Ok:** A `app.core.security` modulban hiányzott a funkció, vagy elavult volt az import az `endpoints/auth.py`-ban.
* **Javítás:** A `security.py`-nak tartalmaznia kell a `jose` könyvtárat használó tokengenerálást.
### Üres Swagger (OpenAPI) felület
* **Ok:** Ha az SQLAlchemy Mapper vagy egy Pydantic séma importja hibás, a FastAPI nem tudja legenerálni a dokumentációt.
* **Javítás:** Ellenőrizd a `docker logs` kimenetét indításkor, keresd a `MapperConfigurationError` vagy `ImportError` sorokat.