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:
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||||
|
|
||||||
# Minden auth funkciót ide gyűjtünk (Register, Login, Recover)
|
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
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"])
|
|
||||||
Binary file not shown.
40
backend/app/api/v1/endpoints/auth.py
Executable file → Normal file
40
backend/app/api/v1/endpoints/auth.py
Executable file → Normal 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!"}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Szerver hiba: {str(e)}")
|
|
||||||
|
|
||||||
@router.post("/login")
|
# 3. Token kiállítása
|
||||||
async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)):
|
token_data = {"sub": str(user.id), "email": user.email}
|
||||||
# ... A korábbi login logika itt maradhat ...
|
access_token = create_access_token(data=token_data)
|
||||||
pass
|
|
||||||
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
38
backend/app/api/v1/endpoints/auth_old.py
Executable file
38
backend/app/api/v1/endpoints/auth_old.py
Executable 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")
|
||||||
Binary file not shown.
41
backend/app/core/security.py
Executable file → Normal file
41
backend/app/core/security.py
Executable file → Normal 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])
|
|
||||||
24
backend/app/core/security_old.py
Executable file
24
backend/app/core/security_old.py
Executable 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)
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
]
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/app/models/__pycache__/vehicle.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/models/__pycache__/vehicle.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -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"
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
Binary file not shown.
44
backend/app/schemas/auth.py
Executable file → Normal file
44
backend/app/schemas/auth.py
Executable file → Normal 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
46
backend/app/schemas/auth_old.py
Executable 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
|
||||||
Binary file not shown.
@@ -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:
|
||||||
|
|||||||
130
backend/app/services/auth_service_old.py
Normal file
130
backend/app/services/auth_service_old.py
Normal 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
|
||||||
145
backend/app/services/auth_service_old2.py
Normal file
145
backend/app/services/auth_service_old2.py
Normal 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
|
||||||
129
backend/app/services/auth_service_old3.py
Normal file
129
backend/app/services/auth_service_old3.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -28,3 +28,51 @@
|
|||||||
- **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!
|
||||||
@@ -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
|
||||||
|
|||||||
19
docs/V01_gemini/17_DEVELOPER_NOTES_AND_PITFALLS.md
Normal file
19
docs/V01_gemini/17_DEVELOPER_NOTES_AND_PITFALLS.md
Normal 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.
|
||||||
Reference in New Issue
Block a user