98 lines
4.4 KiB
Python
Executable File
98 lines
4.4 KiB
Python
Executable File
from fastapi import FastAPI, HTTPException, Form, Depends
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy import text
|
|
from datetime import datetime, timedelta, date
|
|
from jose import jwt
|
|
import bcrypt, os
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
|
|
ALGORITHM = "HS256"
|
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
|
|
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
|
|
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
|
|
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
|
|
app = FastAPI()
|
|
|
|
# --- AUTH HELPER ---
|
|
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
try:
|
|
p = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
return int(p.get("sub"))
|
|
except: raise HTTPException(status_code=401)
|
|
|
|
# --- DINAMIKUS METAADATOK (Ez az újdonság!) ---
|
|
@app.get("/api/meta/vehicle-hierarchy")
|
|
async def get_hierarchy():
|
|
async with AsyncSessionLocal() as session:
|
|
# Lekérjük az összes kategóriát, márkát és modellt egyben
|
|
q = text("""
|
|
SELECT vm.category, m.name as brand, vm.id as model_id, vm.model_name
|
|
FROM ref.vehicle_models vm
|
|
JOIN ref.vehicle_makes m ON vm.make_id = m.id
|
|
ORDER BY vm.category, m.name, vm.model_name
|
|
""")
|
|
res = await session.execute(q)
|
|
rows = res.fetchall()
|
|
|
|
hierarchy = {}
|
|
for r in rows:
|
|
cat = r.category
|
|
brand = r.brand
|
|
if cat not in hierarchy: hierarchy[cat] = {}
|
|
if brand not in hierarchy[cat]: hierarchy[cat][brand] = []
|
|
hierarchy[cat][brand].append({"id": r.model_id, "name": r.model_name})
|
|
return hierarchy
|
|
|
|
@app.get("/api/meta/cost-types")
|
|
async def get_cost_types():
|
|
async with AsyncSessionLocal() as session:
|
|
res = await session.execute(text("SELECT code, name FROM ref.cost_types ORDER BY name"))
|
|
return {r.code: r.name for r in res.fetchall()}
|
|
|
|
# --- CORE API ---
|
|
@app.post("/api/auth/login")
|
|
async def login(f: OAuth2PasswordRequestForm = Depends()):
|
|
async with AsyncSessionLocal() as session:
|
|
res = await session.execute(text("SELECT id, password_hash FROM data.users WHERE email = :e"), {"e": f.username})
|
|
u = res.fetchone()
|
|
if not u or not bcrypt.checkpw(f.password.encode('utf-8')[:72], u.password_hash.encode('utf-8')):
|
|
raise HTTPException(status_code=401)
|
|
t = jwt.encode({"sub": str(u.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
|
|
return {"access_token": t, "token_type": "bearer"}
|
|
|
|
@app.get("/api/my_vehicles")
|
|
async def my_vehicles(uid: int = Depends(get_current_user)):
|
|
async with AsyncSessionLocal() as session:
|
|
q = text("""
|
|
SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status
|
|
FROM data.vehicle_history vh
|
|
JOIN data.vehicles v ON vh.vehicle_id = v.id
|
|
JOIN ref.vehicle_models mo ON v.model_id = mo.id
|
|
JOIN ref.vehicle_makes m ON mo.make_id = m.id
|
|
WHERE vh.user_id = :uid AND vh.end_date IS NULL
|
|
""")
|
|
res = await session.execute(q, {"uid": uid})
|
|
return [dict(r._mapping) for r in res.fetchall()]
|
|
|
|
class VehicleReg(BaseModel): model_id: int; vin: str; plate: str; mileage: int; purchase_date: date
|
|
|
|
@app.post("/api/register")
|
|
async def register_vehicle(d: VehicleReg, uid: int = Depends(get_current_user)):
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
res = await session.execute(text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id"), {"mid": d.model_id, "vin": d.vin, "plt": d.plate})
|
|
vid = res.scalar()
|
|
await session.execute(text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, :uid, 'OWNER', :sd, :sm)"), {"vid": vid, "uid": uid, "sd": d.purchase_date, "sm": d.mileage})
|
|
return {"status": "success"}
|
|
|
|
@app.get("/")
|
|
async def index(): return FileResponse("/app/frontend/index.html")
|