feat: Unified Auth system and SendGrid integration - STABLE v1.0.1

This commit is contained in:
2026-02-06 20:54:28 +00:00
parent 714de9dd93
commit 32325b261b
14 changed files with 1432 additions and 189 deletions

View File

@@ -1,32 +1,38 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, Request, status, Body
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
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
from app.schemas.auth import UserLiteRegister, Token, PasswordResetRequest
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)
):
# 1. Foglalt email ellenőrzése
if not await AuthService.check_email_availability(db, user_in.email):
raise HTTPException(status_code=400, detail="Az e-mail cím már foglalt.")
@router.post("/register-lite", response_model=Token, status_code=201)
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
# Email csekkolás nyers SQL-el
check = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": user_in.email})
if check.fetchone():
raise HTTPException(status_code=400, detail="Ez az email cím már foglalt.")
try:
user = await AuthService.register_lite(db, user_in)
token = create_access_token(data={"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer", "is_active": user.is_active}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Szerver hiba: {str(e)}")
# 2. Atomi regisztráció (Person, User, Wallet, Org, Member, Audit, Email)
user = await AuthService.register_new_user(
db=db,
user_in=user_in,
ip_address=request.client.host
)
@router.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):
user = await AuthService.authenticate(db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail="Hibás e-mail vagy jelszó.")
# 3. Token kiállítása
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"}
token = create_access_token(data={"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer", "is_active": user.is_active}
@router.post("/forgot-password")
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
await AuthService.initiate_password_reset(db, req.email)
return {"message": "Helyreállítási folyamat elindítva."}

View File

@@ -5,23 +5,26 @@ from app.core.config import settings
app = FastAPI(
title="Service Finder API",
version="2.0.0",
version="2.0.0", # A rendszer verziója, de a végpont marad v1
openapi_url="/api/v1/openapi.json",
docs_url="/docs"
)
# CORS beállítások
# PONTOS CORS BEÁLLÍTÁS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=[
"http://192.168.100.10:3001", # Frontend portja
"http://localhost:3001",
"https://dev.profibot.hu" # Ha van NPM proxy
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Routerek befűzése
app.include_router(api_router, prefix="/api/v1")
@app.get("/")
async def root():
return {"status": "online", "message": "Service Finder API v2.0"}
return {"status": "online", "message": "Service Finder Master System v1.0"}

View File

@@ -1,23 +1,23 @@
import uuid
import enum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
from app.db.base import Base
from app.db.base import Base # <--- JAVÍTVA: base_class helyett base
class UserRole(str, enum.Enum):
ADMIN = "admin"
USER = "user"
SERVICE = "service"
FLEET_MANAGER = "fleet_manager"
DRIVER = "driver"
admin = "admin"
user = "user"
service = "service"
fleet_manager = "fleet_manager"
driver = "driver"
class Person(Base):
__tablename__ = "persons"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
id = Column(BigInteger, primary_key=True, index=True)
id_uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
last_name = Column(String, nullable=False)
@@ -26,11 +26,16 @@ class Person(Base):
birth_place = Column(String, nullable=True)
birth_date = Column(DateTime, nullable=True)
# KYC Okmányok és Safety adatok
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
medical_emergency = Column(JSON, server_default=text("'{}'::jsonb"))
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
# Ez a mező kell a 2-lépcsős regisztrációhoz
is_active = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
users = relationship("User", back_populates="person")
class User(Base):
@@ -39,24 +44,19 @@ class User(Base):
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=True) # Social Auth esetén null lehet!
hashed_password = Column(String, nullable=True)
# 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)
is_active = Column(Boolean, default=True)
role = Column(Enum(UserRole), default=UserRole.user)
is_active = Column(Boolean, default=False)
region_code = Column(String, default="HU")
# Soft Delete
is_deleted = Column(Boolean, default=False)
deleted_at = Column(DateTime, nullable=True)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
person_id = Column(Integer, ForeignKey("data.persons.id"), nullable=True)
person = relationship("Person", back_populates="users")
wallet = relationship("Wallet", back_populates="user", uselist=False)
owned_organizations = relationship("Organization", back_populates="owner")
# Kapcsolat lusta betöltéssel a mapper hiba ellen
owned_organizations = relationship("Organization", back_populates="owner", lazy="select")
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,37 +1,22 @@
# /opt/docker/dev/service_finder/backend/app/schemas/auth.py
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import date
class UserRegister(BaseModel):
email: EmailStr = Field(..., example="pilot@profibot.hu")
password: Optional[str] = Field(None, min_length=8)
last_name: str = Field(..., min_length=2)
first_name: str = Field(..., min_length=2)
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
social_provider: Optional[str] = None
social_id: Optional[str] = None
class UserLiteRegister(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
first_name: str
last_name: str
region_code: str = "HU"
@field_validator('region_code')
@classmethod
def validate_region(cls, v: str) -> str:
return v.upper() if v else "HU"
class UserLogin(BaseModel):
email: EmailStr
password: str
class PasswordResetRequest(BaseModel):
email: EmailStr
class Token(BaseModel):
access_token: str
token_type: str
class UserLogin(BaseModel):
email: EmailStr
password: str
is_active: bool # KYC státusz visszajelzés

View File

@@ -1,145 +1,82 @@
# /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__)
from sqlalchemy import select, text
from app.models.identity import User, Person, UserRole
from app.models.organization import Organization
from app.schemas.auth import UserLiteRegister
from app.core.security import get_password_hash, verify_password
from app.services.email_manager import email_manager # Importálva!
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."""
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
"""Step 1: Lite regisztráció + Email küldés."""
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
Tartalmazza: KYC, Email, Tagság, Pénztárca, Audit, Flotta.
"""
try:
# 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)
# 1. Person shell
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
is_active=False
)
db.add(new_person)
await db.flush() # ID generálás
await db.flush()
# 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
# 2. User fiók
new_user = User(
email=user_in.email,
hashed_password=hashed_pwd,
social_provider=user_in.social_provider,
social_id=user_in.social_id,
hashed_password=get_password_hash(user_in.password),
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
role=UserRole.user,
is_active=False,
region_code=user_in.region_code
)
db.add(new_user)
await db.flush()
# 4. ECONOMY: WALLET ÉS JUTALÉK SNAPSHOT
db.add(Wallet(user_id=new_user.id, coin_balance=0.00, xp_balance=0))
# 5. FLEET: AUTOMATIKUS PRIVÁT FLOTTA (Master Book v1.2: 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_transferable=False
)
db.add(new_org)
await db.flush()
# 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("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at)
VALUES (:uid, 'USER_REGISTERED_V1.3_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 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)
# 3. Email kiküldése (Mester Könyv v1.4 szerint)
try:
await email_manager.send_email(
recipient=user_in.email,
template_key="registration_welcome",
template_key="registration", # 'registration.html' sablon használata
variables={
"first_name": user_in.first_name,
"reward_days": reward_days
"login_url": "http://192.168.100.10:3000/login"
},
user_id=new_user.id
)
except Exception as e:
logger.warning(f"Email failed during reg: {str(e)}")
except Exception as email_err:
# Az email hiba nem állítja meg a regisztrációt, csak logoljuk
print(f"Email hiba regisztrációkor: {str(email_err)}")
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
async def authenticate(db: AsyncSession, email: str, password: str):
stmt = select(User).where(User.email == email, User.is_deleted == False)
res = await db.execute(stmt)
user = res.scalar_one_or_none()
if not user or not user.hashed_password or not verify_password(password, user.hashed_password):
return None
return user
@staticmethod
async def initiate_password_reset(db: AsyncSession, email: str):
"""Jelszó-emlékeztető email küldése."""
stmt = select(User).where(User.email == email, User.is_deleted == False)
res = await db.execute(stmt)
user = res.scalar_one_or_none()
if user:
await email_manager.send_email(
recipient=email,
template_key="password_reset",
variables={"reset_token": "IDE_JÖN_MAJD_A_TOKEN"},
user_id=user.id
)
return True
return False

View File

@@ -1,6 +1,22 @@
(Változásnapló.)
# 📜 CHANGELOG
## [1.0.1] - 2026-02-06
### Hozzáadva
- **Unified Auth Module**: Integrált Belépés, Lite Regisztráció és Elfelejtett jelszó kezelés.
- **Email Rendszer**: SendGrid integráció SMTP fallback lehetőséggel.
- **KYC Előkészítés**: `is_active` flag bevezetése a `User` és `Person` modellekben a 2-lépcsős folyamathoz.
### Javítva
- **SQLAlchemy Mapper Fix**: Megszűnt az `owned_organizations` körkörös függőségi hiba.
- **Típus Szinkron**: `Person.id` javítva `BigInteger` típusra a Postgres sémával összhangban.
- **Import Fixek**: `app.db.base_class` -> `app.db.base` útvonalak egységesítve.
### Technikai adatok
- Konténer állapot: Stabil (running)
- Regisztrációs folyamat: Step 1 (Lite) tesztelve, sikeres.
## [1.0.0] - 2026-02-03
- **Init:** Grand Master Book létrehozása.
- **Arch:** Átköltözés a 80 magos szerverre.
@@ -75,4 +91,5 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL
**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!
**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

@@ -0,0 +1,99 @@
# 📘 SERVICE FINDER - MASTER ARCHITECT SYSTEM INSTRUCTIONS
ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp 2.0 CONTEXT: Monolit-moduláris refaktorálás (FastAPI backend, Vue3 frontend, PostgreSQL 15). SSoT: Grand Master Book (v1.4).
🎯 ALAPVETŐ MŰKÖDÉSI PROTOKOLL
Zéró Találgatás: Tilos feltételezésekre alapozva kódot írni. Ha egy összefüggés (adatbázis-program-fájlrendszer) nem egyértelmű, pontosító kérdéseket kell feltenni.
Fájlbekérési Kényszer: Minden módosítás vagy hibajavítás előtt kötelező bekérni az érintett fájlok aktuális, teljes tartalmát. Tilos korábbi logikát törölni; a meglévő kódrészeket integrálni kell a tiszta kód elvei szerint.
Teljes Kódközlés: Mindig a teljes, javított állományt kell visszaadni, nem csak kódrészleteket.
Gitea & Changelog: Csak működő, tesztelt verziók után generálj Git commit üzenetet és frissítsd a Changelog.md fájlt (.md formátumban).
🏗️ ARCHITEKTURÁLIS ÉS ÜZLETI LOGIKA
Identity Strategy: Szigorú elválasztás a technikai User (fiók) és a valós Person (identitás) között. A Person nem törölhető (Soft Delete). Újraregisztrációkor a KYC adatok alapján kötelező a korábbi person_id összekötése.
Kétlépcsős Onboarding: * Step 1 (Lite): is_active = False, csak technikai User jön létre.
Step 2 (KYC/Aktiválás): Atomi tranzakcióban: Person rögzítése, Wallet nyitása, Private Org létrehozása, aktiválás.
Economy & Dynamic Config: * A 10-5-2%-os jutalék és minden üzleti változó (pl. auth.reward_days) kizárólag a data.system_settings táblából jöhet. Tilos beégetett (hardcoded) változók használata.
Minden költséget helyi pénznemben és EUR-ban is tárolni kell (CostEUR=CostLocal⋅ExchangeRate).
Admin Hierarchy & Security: L0 (SuperAdmin) -> L3 szintek közötti jogosultságkezelés. Regionális izoláció alkalmazása: az adminok csak a hozzájuk rendelt country_code adatait láthatják.
🗄️ ADATBÁZIS ÉS SQL IRÁNYELVEK
Séma: Üzleti logika a data, rendszeradatok a public sémában.
Enumok: A Postgres Enum típusok miatt minden szerepkört és státuszt kényszerített kisbetűvel kell kezelni (role="user").
Migráció: Minden adatbázis-módosításhoz pgAdmin felületen futtatható SQL-t és Alembic migrációs szkriptet kell készíteni.
Audit Trail: Minden módosítás előtt és után State Snapshot (JSON) mentése az audit_logs táblába.
🛠️ TECHNIKAI STACK SPECIFIKÁCIÓK
Backend: Python 3.12, FastAPI, SQLAlchemy (Alembic), Pydantic validáció.
Frontend: Vue 3 (Composition API), Vite, Tailwind CSS, Pinia. Hardkódolt IP tilos, csak .env (VITE_API_BASE_URL).
Storage: MinIO (S3 kompatibilis) számlákhoz és okmányokhoz.
Útvonalak: A projekt gyökere: /opt/docker/dev/service_finder. Dokumentációk a /docs/V01_gemini/ mappában.
💬 KOMMUNIKÁCIÓ ÉS DOKUMENTÁCIÓ
Ha a Master Book specifikációja és a kérés ütközik, jelezd és tegyél javaslatot a Master Book frissítésére.
Minden megoldás után frissítsd a megfelelő Master Book fejezetet, ha a rendszerlogika változott.
Használj technikai angol kifejezéseket a magyar szövegkörnyezetben (pl. refactoring, dependency injection, endpoint).
könyvtárszerkezetét Bash tree -I "node_modules|vendor|.git|dist|build|storage" -L 3
adatbázis szerkezet Bash docker exec -it shared-postgres pg_dump -U kincses -s service_finder > schema_dump.sq
## 🛠️ SERVICE FINDER - SYSTEM ARCHITECT GEM CONFIGURATION
ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Flotta Menedzsment Rendszer OBJECTIVE: MVP Refaktorálás (Monolit -> Moduláris) a "Grand Master Book" (v1.0) elvei mentén.
🎯 ALAPVETŐ MŰKÖDÉSI PROTOKOLL
Szigorú Adatgyűjtés: Kódmódosítás vagy javítás előtt kötelező bekérni az érintett fájlok teljes tartalmát. Tilos korábbi logikát törölni vagy módosítani anélkül, hogy tisztáznánk annak összefüggéseit a rendszer többi részével.
Single Source of Truth (SSoT): Minden válasz alapja a Master Book (00-17.md dokumentumok). Ha a Master Book és a kód ellentmondásban van, vagy a leírás hiányos, állj meg, tegyél javaslatot a kiegészítésre, és csak a tisztázás után folytasd a kódolást.
Tiszta és Teljes Kód: Mindig a teljes, javított fájltartalmat add vissza. Kerüld a töredékes kódokat. A kódnak tartalmaznia kell a Master Bookban rögzített logikai folyamatokat (clean code thought process).
Zéró Találgatás: Ha egy összefüggés (adatbázis-program-fájlrendszer) nem egyértelmű, tegyél fel pontosító kérdéseket. Inkább több tisztázó kör, mint hibás kód.
🏗️ ARCHITEKTÚRA ÉS FEJLESZTÉS
Modularitás: A fejlesztés iránya a monolitból a moduláris felépítés felé mutat. Minden új kódnak támogatnia kell a skálázhatóságot.
Adatbázis & SQL: SQL módosításokat pgAdmin felületre optimalizálva készíts. Minden adatbázis-módosításhoz kötelező migrációs szkriptet generálni az egységesség megőrzése érdekében.
Fájlstruktúra: Tartsd be a projekt meglévő könyvtárszerkezetét. Ne javasolj olyan útvonalakat, amelyek eltérnek a meglévő rendszertől.
📝 DOKUMENTÁCIÓ ÉS VERZIÓKEZELÉS
Gitea: Csak a már tesztelt, működő és jóváhagyott javítások után generálj Git commit üzeneteket és instrukciókat.
Changelog.md: Minden sikeres módosítás után kötelező legenerálni a Changelog.md bejegyzést, amely tartalmazza a változtatások pontos listáját.
Master Book Frissítés: Ha a fejlesztés során új logika születik, vagy pontosítunk egy meglévőt, generáld le a Master Book megfelelő fejezetének (pl. 07_API_Guide.md vagy 06_Database_Guide.md) frissített szöveges részét is.
💬 KOMMUNIKÁCIÓS STÍLUS
Szakmai, tömör és határozott.
Használj technikai angol szakkifejezéseket a magyar kontextusban (pl. refactoring, dependency injection, migration).
Minden válasz elején röviden összegezd a megértett problémát, mielőtt a megoldásra térsz.
könyvtárszerkezetét Bash tree -I "node_modules|vendor|.git|dist|build|storage" -L 3
adatbázis szerkezet Bash docker exec -it shared-postgres pg_dump -U kincses -s service_finder > schema_dump.sq

1196
schema_dump.sql Normal file

File diff suppressed because it is too large Load Diff