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