Save test environment changes
This commit is contained in:
BIN
backend/app/__pycache__/main.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/__pycache__/main.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
backend/app/api/v1/__pycache__/api.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/api/v1/__pycache__/api.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -1,12 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1.endpoints import auth, users, vehicles, billing, fleet, expenses, reports
|
||||
from app.api.v1.endpoints import auth # Fontos a helyes import!
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(billing.router, prefix="/billing", tags=["billing"])
|
||||
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["vehicles"])
|
||||
api_router.include_router(fleet.router, prefix="/fleet", tags=["fleet"])
|
||||
api_router.include_router(expenses.router, prefix="/expenses", tags=["expenses"])
|
||||
api_router.include_router(reports.router, prefix="/reports", tags=["reports"])
|
||||
# Minden auth funkciót ide gyűjtünk (Register, Login, Recover)
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
|
||||
# Itt jönnek majd a további modulok:
|
||||
# api_router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
# api_router.include_router(fleet.router, prefix="/fleet", tags=["Fleet"])
|
||||
BIN
backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -1,91 +1,34 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib, secrets
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.core.security import get_password_hash
|
||||
from app.services.email_manager import email_manager
|
||||
from app.services.config_service import config
|
||||
from app.schemas.auth import UserRegister, UserLogin, Token
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register")
|
||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||
async def register(
|
||||
request: Request,
|
||||
email: str,
|
||||
password: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
request: Request,
|
||||
user_in: UserRegister,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
ip = request.client.host
|
||||
|
||||
# 1. BOT-VÉDELEM
|
||||
throttle_min = await config.get_setting('registration_throttle_minutes', default=10)
|
||||
check_throttle = await db.execute(text("""
|
||||
SELECT count(*) FROM data.audit_logs
|
||||
WHERE ip_address = :ip AND action = 'USER_REGISTERED' AND created_at > :t
|
||||
"""), {'ip': ip, 't': datetime.utcnow() - timedelta(minutes=int(throttle_min))})
|
||||
|
||||
if check_throttle.scalar() > 0:
|
||||
raise HTTPException(status_code=429, detail="Túl sok próbálkozás. Várj pár percet!")
|
||||
# 1. Email check
|
||||
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. REGISZTRÁCIÓ
|
||||
res = await db.execute(select(User).where(User.email == email))
|
||||
if res.scalars().first():
|
||||
raise HTTPException(status_code=400, detail="Ez az email már foglalt.")
|
||||
# 2. Process
|
||||
try:
|
||||
user = await AuthService.register_new_user(
|
||||
db=db,
|
||||
user_in=user_in,
|
||||
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)}")
|
||||
|
||||
new_user = User(
|
||||
email=email,
|
||||
hashed_password=get_password_hash(password),
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
is_active=False
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.flush()
|
||||
|
||||
# 3. TOKEN & LOG
|
||||
raw_token = secrets.token_urlsafe(48)
|
||||
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
|
||||
await db.execute(text("""
|
||||
INSERT INTO data.verification_tokens (user_id, token_hash, token_type, expires_at)
|
||||
VALUES (:u, :t, 'email_verify', :e)
|
||||
"""), {'u': new_user.id, 't': token_hash, 'e': datetime.utcnow() + timedelta(days=2)})
|
||||
|
||||
await db.execute(text("""
|
||||
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address)
|
||||
VALUES (:u, 'USER_REGISTERED', '/register', 'POST', :ip)
|
||||
"""), {'u': new_user.id, 'ip': ip})
|
||||
|
||||
# 4. EMAIL KÜLDÉS
|
||||
verify_link = f"http://{request.headers.get('host')}/api/v1/auth/verify?token={raw_token}"
|
||||
email_body = f"<h1>Szia {first_name}!</h1><p>Aktiváld a fiókod: <a href='{verify_link}'>{verify_link}</a></p>"
|
||||
|
||||
await email_manager.send_email(
|
||||
recipient=email,
|
||||
subject="Regisztráció megerősítése",
|
||||
body=email_body,
|
||||
email_type="registration",
|
||||
user_id=new_user.id
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
return {"message": "Sikeres regisztráció! Ellenőrizd az email fiókodat."}
|
||||
|
||||
@router.get("/verify")
|
||||
async def verify_account(token: str, db: AsyncSession = Depends(get_db)):
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
query = text("SELECT user_id FROM data.verification_tokens WHERE token_hash = :t AND is_used = False")
|
||||
res = await db.execute(query, {'t': token_hash})
|
||||
row = res.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen aktiváló link")
|
||||
|
||||
await db.execute(text("UPDATE data.users SET is_active = True WHERE id = :id"), {'id': row[0]})
|
||||
await db.execute(text("UPDATE data.verification_tokens SET is_used = True WHERE token_hash = :t"), {'t': token_hash})
|
||||
await db.commit()
|
||||
return {"message": "Fiók aktiválva!"}
|
||||
@router.post("/login")
|
||||
async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)):
|
||||
# ... A korábbi login logika itt maradhat ...
|
||||
pass
|
||||
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -1,48 +1,61 @@
|
||||
from typing import Optional
|
||||
import os
|
||||
import json
|
||||
from typing import Any, Optional, List
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic import computed_field
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# --- General ---
|
||||
PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
|
||||
VERSION: str = "2.0.0"
|
||||
VERSION: str = "1.0.0"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
DEBUG: bool = False
|
||||
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||
|
||||
# --- Security / JWT ---
|
||||
SECRET_KEY: str
|
||||
# Szigorúan .env-ből!
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "NOT_SET_DANGER")
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap
|
||||
|
||||
# --- Password policy (TEST -> laza, PROD -> szigorú) ---
|
||||
PASSWORD_MIN_LENGTH: int = 4 # TESZT: 4, ÉLES: 10-12
|
||||
# --- Database & Cache ---
|
||||
DATABASE_URL: str = os.getenv("DATABASE_URL")
|
||||
REDIS_URL: str = os.getenv("REDIS_URL", "redis://service_finder_redis:6379/0")
|
||||
|
||||
# --- Database ---
|
||||
DATABASE_URL: str # már nálad compose-ban meg van adva
|
||||
|
||||
# --- Redis ---
|
||||
REDIS_URL: str = "redis://service_finder_redis:6379/0"
|
||||
|
||||
# --- Email sending ---
|
||||
# auto = ha van SENDGRID_API_KEY -> sendgrid api, különben smtp
|
||||
EMAIL_PROVIDER: str = "auto" # auto | sendgrid | smtp | disabled
|
||||
|
||||
EMAILS_FROM_EMAIL: str = "info@profibot.hu"
|
||||
# --- Email (Auto Provider) ---
|
||||
EMAIL_PROVIDER: str = os.getenv("EMAIL_PROVIDER", "auto")
|
||||
EMAILS_FROM_EMAIL: str = os.getenv("EMAILS_FROM_EMAIL", "info@profibot.hu")
|
||||
EMAILS_FROM_NAME: str = "Profibot"
|
||||
|
||||
# SMTP & SendGrid (Szigorúan .env-ből)
|
||||
SENDGRID_API_KEY: Optional[str] = os.getenv("SENDGRID_API_KEY")
|
||||
SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST")
|
||||
SMTP_PORT: int = int(os.getenv("SMTP_PORT", 587))
|
||||
SMTP_USER: Optional[str] = os.getenv("SMTP_USER")
|
||||
SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD")
|
||||
|
||||
# SendGrid API
|
||||
SENDGRID_API_KEY: Optional[str] = None
|
||||
# --- External URLs ---
|
||||
# .env-ben legyen átírva a .10-es IP-re!
|
||||
FRONTEND_BASE_URL: str = os.getenv("FRONTEND_BASE_URL", "http://localhost:3000")
|
||||
|
||||
# SMTP fallback (pl. Gmail App Password vagy más szolgáltató)
|
||||
SMTP_HOST: Optional[str] = None
|
||||
SMTP_PORT: int = 587
|
||||
SMTP_USER: Optional[str] = None
|
||||
SMTP_PASSWORD: Optional[str] = None
|
||||
SMTP_USE_TLS: bool = True
|
||||
|
||||
# Frontend base URL a linkekhez (később NPM/domain)
|
||||
FRONTEND_BASE_URL: str = "http://192.168.100.43:3000"
|
||||
# --- Dinamikus Admin Motor ---
|
||||
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Lekéri a paramétert a data.system_settings táblából.
|
||||
Ezzel érjük el, hogy a kód újraírása nélkül, adminból lehessen
|
||||
állítani a jutalom napokat, százalékokat, stb.
|
||||
"""
|
||||
try:
|
||||
query = text("SELECT value_json FROM data.system_settings WHERE key_name = :key")
|
||||
result = await db.execute(query, {"key": key_name})
|
||||
row = result.fetchone()
|
||||
if row and row[0] is not None:
|
||||
return row[0]
|
||||
return default
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
# .env fájl konfigurációja
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
@@ -50,4 +63,4 @@ class Settings(BaseSettings):
|
||||
extra="ignore"
|
||||
)
|
||||
|
||||
settings = Settings()
|
||||
settings = Settings()
|
||||
@@ -6,28 +6,44 @@ from jose import jwt, JWTError
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# --- JELSZÓ ---
|
||||
# --- JELSZÓ KEZELÉS ---
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
Összehasonlítja a nyers jelszót a hash-elt változattal.
|
||||
"""
|
||||
try:
|
||||
if not hashed_password:
|
||||
return False
|
||||
return bcrypt.checkpw(
|
||||
plain_password.encode("utf-8"),
|
||||
hashed_password.encode("utf-8"),
|
||||
hashed_password.encode("utf-8")
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""
|
||||
Biztonságos hash-t generál a jelszóból.
|
||||
"""
|
||||
salt = bcrypt.gensalt()
|
||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
||||
|
||||
# --- JWT ---
|
||||
# --- JWT TOKEN KEZELÉS ---
|
||||
|
||||
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""
|
||||
JWT Access tokent generál a megadott adatokkal és lejárati idővel.
|
||||
"""
|
||||
to_encode = dict(data)
|
||||
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
|
||||
expire = datetime.now(timezone.utc) + (
|
||||
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
)
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
|
||||
def decode_token(token: str) -> Dict[str, Any]:
|
||||
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
"""
|
||||
Dekódolja a JWT tokent.
|
||||
"""
|
||||
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
@@ -1,45 +1,50 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import text
|
||||
import os
|
||||
from app.api.v1.api import api_router
|
||||
from app.api.v2.auth import router as auth_v2_router
|
||||
from app.models import Base
|
||||
from app.db.base import Base
|
||||
from app.db.session import engine
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
from app.db.session import engine
|
||||
# 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(
|
||||
title="Traffic Ecosystem SuperApp 2.0",
|
||||
version="2.0.0",
|
||||
openapi_url="/api/v2/openapi.json",
|
||||
title="Service Finder API",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
openapi_url="/api/v1/openapi.json",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# BIZTONSÁG: CORS beállítások .env-ből
|
||||
# Ha nincs megadva, csak a localhost-ot engedi
|
||||
origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://192.168.100.43:3000", # A szerver címe a böngészőben
|
||||
"http://localhost:3000", # Helyi teszteléshez
|
||||
],
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ÚTVONALAK INTEGRÁCIÓJA
|
||||
# ÚTVONALAK KONSZOLIDÁCIÓJA (V2 törölve, minden a V1 alatt)
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
app.include_router(auth_v2_router, prefix="/api/v2/auth")
|
||||
|
||||
@app.get("/", tags=["health"])
|
||||
async def root():
|
||||
return {"status": "online", "version": "2.0.0", "docs": "/docs"}
|
||||
|
||||
return {
|
||||
"status": "online",
|
||||
"version": "1.0.0",
|
||||
"environment": os.getenv("ENV", "production")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
from app.db.base import Base
|
||||
from .user import User, UserRole
|
||||
from .identity import User, Person, Wallet, UserRole # ÚJ központ
|
||||
from .company import Company, CompanyMember, VehicleAssignment
|
||||
from .organization import Organization, OrgType
|
||||
from .vehicle import (
|
||||
Vehicle,
|
||||
VehicleOwnership,
|
||||
@@ -13,12 +14,12 @@ from .vehicle import (
|
||||
VehicleVariant
|
||||
)
|
||||
|
||||
# Alias a kompatibilitás kedvéért
|
||||
# Aliasok a kompatibilitás kedvéért
|
||||
UserVehicle = Vehicle
|
||||
|
||||
__all__ = [
|
||||
"Base", "User", "UserRole", "Vehicle", "VehicleOwnership", "VehicleBrand",
|
||||
"EngineSpec", "ServiceProvider", "ServiceRecord", "Company",
|
||||
"Base", "User", "Person", "Wallet", "UserRole", "Vehicle", "VehicleOwnership",
|
||||
"VehicleBrand", "EngineSpec", "ServiceProvider", "ServiceRecord", "Company",
|
||||
"CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory",
|
||||
"VehicleModel", "VehicleVariant"
|
||||
"VehicleModel", "VehicleVariant", "Organization", "OrgType"
|
||||
]
|
||||
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/identity.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/identity.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/organization.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/organization.cpython-312.pyc
Normal file
Binary file not shown.
73
backend/app/models/identity.py
Normal file
73
backend/app/models/identity.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# backend/app/models/identity.py
|
||||
import uuid
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "admin"
|
||||
USER = "user"
|
||||
SERVICE = "service"
|
||||
FLEET_MANAGER = "fleet_manager"
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = "persons"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, 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)
|
||||
first_name = Column(String, nullable=False)
|
||||
mothers_name = Column(String, nullable=True)
|
||||
birth_place = Column(String, nullable=True)
|
||||
birth_date = Column(DateTime, nullable=True)
|
||||
|
||||
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
medical_emergency = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
users = relationship("User", back_populates="person")
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
|
||||
# Technikai mezők átmentése a régi user.py-ból
|
||||
role = Column(Enum(UserRole), default=UserRole.USER)
|
||||
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")
|
||||
|
||||
is_deleted = Column(Boolean, default=False)
|
||||
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 = relationship("Person", back_populates="users")
|
||||
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
||||
# Az Organization kapcsolathoz (ha szükséges az import miatt)
|
||||
owned_organizations = relationship("Organization", backref="owner")
|
||||
|
||||
class Wallet(Base):
|
||||
__tablename__ = "wallets"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
|
||||
coin_balance = Column(Numeric(18, 2), default=0.00)
|
||||
xp_balance = Column(Integer, default=0)
|
||||
|
||||
user = relationship("User", back_populates="wallet")
|
||||
@@ -1,5 +1,5 @@
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
@@ -10,11 +10,6 @@ class OrgType(str, enum.Enum):
|
||||
FLEET_OWNER = "fleet_owner"
|
||||
CLUB = "club"
|
||||
|
||||
class UITheme(str, enum.Enum):
|
||||
LIGHT = "light"
|
||||
DARK = "dark"
|
||||
SYSTEM = "system"
|
||||
|
||||
class Organization(Base):
|
||||
__tablename__ = "organizations"
|
||||
__table_args__ = {"schema": "data"}
|
||||
@@ -23,14 +18,11 @@ class Organization(Base):
|
||||
name = Column(String, nullable=False)
|
||||
org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL)
|
||||
|
||||
# Új UI beállítások a V2-höz
|
||||
theme = Column(Enum(UITheme), default=UITheme.SYSTEM)
|
||||
logo_url = Column(String, nullable=True)
|
||||
# Spec 2.2: Az owner_id a magánszemély flottájának tulajdonosát jelöli
|
||||
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Kapcsolatok
|
||||
# members = relationship("OrganizationMember", back_populates="organization")
|
||||
vehicles = relationship("UserVehicle", back_populates="current_org")
|
||||
|
||||
# Kapcsolatok (UserVehicle modell megléte esetén)
|
||||
vehicles = relationship("UserVehicle", back_populates="current_org", cascade="all, delete-orphan")
|
||||
@@ -1,34 +1,6 @@
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Date, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "admin"
|
||||
USER = "user"
|
||||
SERVICE = "service"
|
||||
FLEET_MANAGER = "fleet_manager"
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
first_name = Column(String)
|
||||
last_name = Column(String)
|
||||
birthday = Column(Date, nullable=True)
|
||||
role = Column(String, default=UserRole.USER)
|
||||
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")
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
# DEPRECATED: Minden funkció átkerült az app.models.identity modulba.
|
||||
# Ez a fájl csak a kompatibilitás miatt maradt meg, de táblát nem definiál.
|
||||
from .identity import User, UserRole
|
||||
|
||||
# Kapcsolatok
|
||||
# memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
@@ -1,16 +1,27 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from pydantic import BaseModel, EmailStr, Field, validator
|
||||
from typing import Optional
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(..., min_length=8)
|
||||
first_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.
|
||||
device_id: Optional[str] = None # Eszköz azonosító a biztonsághoz
|
||||
invite_token: Optional[str] = None
|
||||
|
||||
@validator('region_code')
|
||||
def validate_region(cls, v):
|
||||
return v.upper() if v else v
|
||||
|
||||
# EZ HIÁNYZOTT: Az azonosításhoz (login) szükséges séma
|
||||
class UserLogin(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
email: Optional[str] = None
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(..., min_length=8)
|
||||
full_name: str
|
||||
region_code: str = "HU"
|
||||
device_id: str # Az eszköz egyedi azonosítója a védelemhez
|
||||
email: Optional[str] = None
|
||||
BIN
backend/app/services/__pycache__/auth_service.cpython-312.pyc
Normal file
BIN
backend/app/services/__pycache__/auth_service.cpython-312.pyc
Normal file
Binary file not shown.
81
backend/app/services/auth_service.py
Normal file
81
backend/app/services/auth_service.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, text
|
||||
|
||||
from app.models.identity import User, Person, Wallet
|
||||
from app.models.organization import Organization, OrgType
|
||||
from app.schemas.auth import UserRegister
|
||||
from app.core.security import get_password_hash
|
||||
from app.services.email_manager import email_manager
|
||||
|
||||
class AuthService:
|
||||
@staticmethod
|
||||
async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str):
|
||||
"""
|
||||
Master Book v1.0 szerinti atomikus regisztrációs folyamat.
|
||||
"""
|
||||
# Az AsyncSession.begin() biztosítja az ATOMICitást
|
||||
async with db.begin_nested(): # beágyazott tranzakció a biztonságért
|
||||
# 1. Person létrehozása (Identity Level)
|
||||
new_person = Person(
|
||||
first_name=user_in.first_name,
|
||||
last_name=user_in.last_name
|
||||
)
|
||||
db.add(new_person)
|
||||
await db.flush() # ID generáláshoz
|
||||
|
||||
# 2. User létrehozása (Technical Access)
|
||||
new_user = User(
|
||||
email=user_in.email,
|
||||
hashed_password=get_password_hash(user_in.password),
|
||||
person_id=new_person.id,
|
||||
is_active=True
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.flush()
|
||||
|
||||
# 3. Economy: Wallet inicializálás (0 Coin, 0 XP)
|
||||
new_wallet = Wallet(
|
||||
user_id=new_user.id,
|
||||
coin_balance=0.00,
|
||||
xp_balance=0
|
||||
)
|
||||
db.add(new_wallet)
|
||||
|
||||
# 4. Fleet: Automatikus Privát Flotta létrehozása
|
||||
new_org = Organization(
|
||||
name=f"{user_in.last_name} {user_in.first_name} saját flottája",
|
||||
org_type=OrgType.INDIVIDUAL,
|
||||
owner_id=new_user.id
|
||||
)
|
||||
db.add(new_org)
|
||||
|
||||
# 5. Audit Log (SQLAlchemy Core hívással a sebességért)
|
||||
audit_stmt = text("""
|
||||
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)
|
||||
""")
|
||||
await db.execute(audit_stmt, {
|
||||
"uid": new_user.id,
|
||||
"ip": ip_address,
|
||||
"now": datetime.now(timezone.utc)
|
||||
})
|
||||
|
||||
# 6. Üdvözlő email (Subject paraméter nélkül - Spec v1.1)
|
||||
try:
|
||||
await email_manager.send_email(
|
||||
recipient=user_in.email,
|
||||
template_key="registration",
|
||||
variables={"first_name": user_in.first_name},
|
||||
user_id=new_user.id
|
||||
)
|
||||
except Exception:
|
||||
pass # Email hiba ne állítsa meg a tranzakciót
|
||||
|
||||
return new_user
|
||||
|
||||
@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
|
||||
Reference in New Issue
Block a user