180 lines
7.9 KiB
Python
Executable File
180 lines
7.9 KiB
Python
Executable File
from fastapi import FastAPI, HTTPException, Request
|
|
from fastapi.responses import FileResponse
|
|
from pydantic import BaseModel, validator
|
|
from typing import Optional, List
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy import text
|
|
import os
|
|
from datetime import date
|
|
import uuid
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
# DB Config
|
|
raw_url = os.getenv("DATABASE_URL")
|
|
if not raw_url:
|
|
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
|
fixed_url = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
|
|
|
engine = create_async_engine(fixed_url)
|
|
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
|
|
app = FastAPI(title="Service Finder API")
|
|
|
|
# --- MODELLEK ---
|
|
class VehicleRegister(BaseModel):
|
|
model_id: int
|
|
vin: str
|
|
plate: str
|
|
mileage: int
|
|
purchase_date: date
|
|
role: str = "OWNER"
|
|
|
|
@validator('vin')
|
|
def clean_vin(cls, v): return v.upper().replace("-", "").replace(" ", "")
|
|
@validator('plate')
|
|
def clean_plate(cls, v): return v.upper().replace("-", "").replace(" ", "")
|
|
@validator('mileage')
|
|
def positive_mileage(cls, v): return v if v >= 0 else 0
|
|
|
|
class InviteRequest(BaseModel):
|
|
email: str
|
|
role: str
|
|
access_level: str
|
|
|
|
class IssueReport(BaseModel):
|
|
vehicle_id: int
|
|
description: str
|
|
is_critical: bool
|
|
|
|
# --- SEGÉDFÜGGVÉNY: AUDIT LOG ÍRÁSA ---
|
|
async def create_audit_log(session, user_id, event, target_id, details, old_val=None, new_val=None):
|
|
await session.execute(text("""
|
|
INSERT INTO data.audit_logs (user_id, event_type, target_id, details, old_value, new_value)
|
|
VALUES (:uid, :evt, :tid, :det, :old, :new)
|
|
"""), {
|
|
"uid": user_id, "evt": event, "tid": target_id,
|
|
"det": details, "old": old_val, "new": new_val
|
|
})
|
|
|
|
# --- VÉGPONTOK ---
|
|
|
|
@app.get("/api/my_vehicles")
|
|
async def get_my_garage():
|
|
user_id = 1
|
|
async with AsyncSessionLocal() as session:
|
|
# Most már lekérjük a státuszt is!
|
|
query = text("""
|
|
SELECT v.id as vehicle_id, v.vin, v.current_plate, m.name as brand,
|
|
mo.model_name, mo.category, vh.start_mileage, vh.role,
|
|
v.status, v.current_issue
|
|
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
|
|
ORDER BY vh.start_date DESC
|
|
""")
|
|
result = await session.execute(query, {"uid": user_id})
|
|
return [{
|
|
"vehicle_id": r.vehicle_id, "brand": r.brand, "model": r.model_name,
|
|
"plate": r.current_plate, "category": r.category, "role": r.role,
|
|
"status": r.status, "current_issue": r.current_issue # <--- ÚJ MEZŐK
|
|
} for r in result.fetchall()]
|
|
|
|
@app.get("/api/vehicle/{vehicle_id}")
|
|
async def get_vehicle_details(vehicle_id: int):
|
|
user_id = 1
|
|
async with AsyncSessionLocal() as session:
|
|
q = text("""
|
|
SELECT v.id, v.vin, v.current_plate, v.production_year,
|
|
m.name as brand, mo.model_name, mo.category,
|
|
vh.role, vh.start_date, vh.start_mileage, u.default_currency,
|
|
v.status, v.current_issue -- <--- ÚJ
|
|
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
|
|
JOIN data.users u ON vh.user_id = u.id
|
|
WHERE v.id = :vid AND vh.user_id = :uid AND vh.end_date IS NULL
|
|
""")
|
|
res = await session.execute(q, {"vid": vehicle_id, "uid": user_id})
|
|
car = res.fetchone()
|
|
if not car: raise HTTPException(status_code=404, detail="Nincs adat")
|
|
|
|
return {
|
|
"id": car.id, "brand": car.brand, "model": car.model_name,
|
|
"plate": car.current_plate, "vin": car.vin, "role": car.role,
|
|
"start_date": car.start_date, "mileage": car.start_mileage,
|
|
"currency": car.default_currency,
|
|
"status": car.status, "current_issue": car.current_issue
|
|
}
|
|
|
|
# --- ÚJ: HIBA BEJELENTÉS (LOGOLÁSSAL) ---
|
|
@app.post("/api/report_issue")
|
|
async def report_issue(data: IssueReport):
|
|
user_id = 1 # Demo User
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
# 1. Frissítjük a járművet
|
|
new_status = 'CRITICAL' if data.is_critical else 'WARNING'
|
|
await session.execute(text("""
|
|
UPDATE data.vehicles
|
|
SET status = :st, current_issue = :desc
|
|
WHERE id = :vid
|
|
"""), {"st": new_status, "desc": data.description, "vid": data.vehicle_id})
|
|
|
|
# 2. ÍRUNK A NAPLÓBA (AUDIT LOG)
|
|
await create_audit_log(
|
|
session, user_id, "ISSUE_REPORT", data.vehicle_id,
|
|
details=f"Hiba: {data.description}",
|
|
old_val="OK", new_val=new_status
|
|
)
|
|
|
|
return {"status": "success", "message": "Hiba naplózva és rögzítve."}
|
|
|
|
# --- MARADÉK (REGISZTER, FLOTTA, MEGHÍVÁS) ---
|
|
# (Ezeket most rövidítve hagyom, de a fájlban benne kell lenniük a korábbi verzió szerint)
|
|
@app.get("/api/vehicles")
|
|
async def get_vehicles():
|
|
async with AsyncSessionLocal() as session:
|
|
result = await session.execute(text("SELECT vm.id, m.name as brand, vm.model_name, vm.category FROM ref.vehicle_models vm JOIN ref.vehicle_makes m ON vm.make_id = m.id ORDER BY m.name"))
|
|
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.fetchall()]
|
|
|
|
@app.post("/api/register")
|
|
async def register_vehicle(data: VehicleRegister):
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
res = await session.execute(text("SELECT id FROM data.vehicles WHERE vin = :vin"), {"vin": data.vin})
|
|
vid = res.scalar()
|
|
if not vid:
|
|
ins = text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id")
|
|
r = await session.execute(ins, {"mid": data.model_id, "vin": data.vin, "plt": data.plate})
|
|
vid = r.scalar()
|
|
hist = text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, 1, :role, :sd, :sm)")
|
|
await session.execute(hist, {"vid": vid, "role": data.role, "sd": data.purchase_date, "sm": data.mileage})
|
|
# LOGOLÁS ITT IS!
|
|
await create_audit_log(session, 1, "REGISTER_VEHICLE", vid, "Jármű regisztrálva")
|
|
return {"status": "success"}
|
|
|
|
@app.get("/api/fleet/members")
|
|
async def get_team_members():
|
|
async with AsyncSessionLocal() as session:
|
|
res = await session.execute(text("SELECT u.email, fm.role, fm.joined_at, u.country FROM data.fleet_members fm JOIN data.users u ON fm.user_id = u.id WHERE fm.owner_id = 1"))
|
|
return [{"email": r.email, "role": r.role, "joined_at": r.joined_at, "country": r.country} for r in res.fetchall()]
|
|
|
|
@app.post("/api/fleet/invite")
|
|
async def invite_member(data: InviteRequest):
|
|
token = str(uuid.uuid4())
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
await session.execute(text("INSERT INTO data.invitations (email, inviter_id, role, access_level, token, expires_at) VALUES (:e, 1, :r, :l, :t, NOW() + INTERVAL '7 days')"), {"e": data.email, "r": data.role, "l": data.access_level, "t": token})
|
|
return {"status": "success", "message": "Meghívó elküldve!", "debug_token": token}
|
|
|
|
@app.get("/")
|
|
async def serve_frontend():
|
|
if os.path.exists("/app/frontend/index.html"):
|
|
return FileResponse("/app/frontend/index.html")
|
|
return {"error": "Frontend hiányzik"} |