chore: Archive legacy docs and backup files, prepare for codebase cleanup v2.0
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Cím beállítása
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def build_db():
|
||||
print(f"🔌 Kapcsolódás a rendszerhez...")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("🏗️ 'DATA' séma előkészítése...")
|
||||
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data;"))
|
||||
|
||||
# 1. FELHASZNÁLÓK TÁBLA (Céges/Magán logika)
|
||||
print("👤 Users tábla létrehozása...")
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS data.users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) DEFAULT 'PRIVATE', -- 'PRIVATE', 'COMPANY', 'ADMIN'
|
||||
company_name VARCHAR(255), -- Ha cég
|
||||
tax_number VARCHAR(50), -- Ha cég
|
||||
is_active BOOLEAN DEFAULT FALSE, -- Email megerősítésig
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
"""))
|
||||
|
||||
# 2. JÁRMŰ TÖRZS (A Fizikai Vas - Tulajdonos nélkül!)
|
||||
print("🚗 Vehicles tábla létrehozása (A Vas)...")
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS data.vehicles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
model_id INTEGER REFERENCES ref.vehicle_models(id),
|
||||
vin VARCHAR(50) UNIQUE NOT NULL, -- ALVÁZSZÁM (Az igazi kulcs)
|
||||
current_plate VARCHAR(20) NOT NULL, -- A mostani rendszám
|
||||
production_year INTEGER,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
"""))
|
||||
|
||||
# 3. ÉLETÚT ÉS JOGOSULTSÁG (A Történelem)
|
||||
print("📜 History tábla létrehozása (Kié mikor volt?)...")
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS data.vehicle_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vehicle_id INTEGER REFERENCES data.vehicles(id),
|
||||
user_id INTEGER REFERENCES data.users(id),
|
||||
|
||||
role VARCHAR(20) NOT NULL, -- 'OWNER' (Tulaj), 'DRIVER' (Sofőr), 'LEASE' (Lízingelő)
|
||||
|
||||
start_date DATE NOT NULL, -- Mikor vette át?
|
||||
end_date DATE, -- Mikor adta le? (Ha NULL, akkor nála van!)
|
||||
|
||||
start_mileage INTEGER, -- Óraállás átvételkor
|
||||
end_mileage INTEGER, -- Óraállás leadáskor
|
||||
|
||||
is_active BOOLEAN GENERATED ALWAYS AS (end_date IS NULL) STORED -- Segédmező a gyors kereséshez
|
||||
);
|
||||
"""))
|
||||
|
||||
# 4. KÖLTSÉGEK (Szeparált pénzügyek)
|
||||
print("💸 Costs tábla létrehozása...")
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS data.costs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vehicle_id INTEGER REFERENCES data.vehicles(id),
|
||||
user_id INTEGER REFERENCES data.users(id), -- Ki fizette? (Csak ő láthatja!)
|
||||
|
||||
cost_type VARCHAR(50) NOT NULL, -- FUEL, SERVICE, TAX, INSURANCE, LEASE...
|
||||
amount DECIMAL(10, 2) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
mileage_at_cost INTEGER,
|
||||
|
||||
description TEXT,
|
||||
document_url VARCHAR(255), -- Fotó linkje
|
||||
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
"""))
|
||||
|
||||
print("✅ KÉSZ! A Professzionális Adatmodell felépült.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(build_db())
|
||||
@@ -1,51 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
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"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def check_data():
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
async with engine.begin() as conn:
|
||||
print("\n🚗 A TE GARÁZSOD (User ID: 1):")
|
||||
print("-" * 80)
|
||||
|
||||
# Ez a lekérdezés összeköti a Történelmet (History), a Vasat (Vehicle) és a Katalógust (Model)
|
||||
query = text("""
|
||||
SELECT
|
||||
vh.role,
|
||||
vh.start_date,
|
||||
v.vin,
|
||||
v.current_plate,
|
||||
m.name as brand,
|
||||
mo.model_name
|
||||
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 = 1;
|
||||
""")
|
||||
|
||||
result = await conn.execute(query)
|
||||
rows = result.fetchall()
|
||||
|
||||
if not rows:
|
||||
print("📭 A garázs üres.")
|
||||
else:
|
||||
for r in rows:
|
||||
print(f"🔹 {r.brand} {r.model_name} | 🆔 {r.vin} | 🔢 {r.current_plate}")
|
||||
print(f" Jogviszony: {r.role} | Kezdete: {r.start_date}")
|
||||
print("-" * 80)
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(check_data())
|
||||
@@ -1,35 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Adatbázis elérés
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def create_user():
|
||||
print(f"🔌 Kapcsolódás...")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("👤 1-es számú felhasználó beszúrása...")
|
||||
# Kényszerítjük az ID=1-et, hogy passzoljon a main.py-hoz
|
||||
await conn.execute(text("""
|
||||
INSERT INTO data.users (id, email, password_hash, role, is_active)
|
||||
VALUES (1, 'demo@user.com', 'dummy_hash', 'PRIVATE', TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
"""))
|
||||
|
||||
# Frissítjük a számlálót, hogy a következő user ID 2 legyen (ne akadjon össze)
|
||||
await conn.execute(text("SELECT setval('data.users_id_seq', (SELECT MAX(id) FROM data.users));"))
|
||||
|
||||
print("✅ KÉSZ! A Demo User (ID: 1) létezik.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(create_user())
|
||||
@@ -1,42 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def hire_driver():
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("👤 Kovács János (User ID: 2) létrehozása...")
|
||||
# Létrehozzuk a User-t
|
||||
await conn.execute(text("""
|
||||
INSERT INTO data.users (id, email, password_hash, role, country, default_currency, is_active)
|
||||
VALUES (2, 'sofor@ceg.hu', 'hash123', 'PRIVATE', 'HU', 'HUF', TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
"""))
|
||||
|
||||
# Frissítjük a sorrendet
|
||||
await conn.execute(text("SELECT setval('data.users_id_seq', (SELECT MAX(id) FROM data.users));"))
|
||||
|
||||
print("🤝 Hozzárendelés a Te cégedhez (ID: 1)...")
|
||||
# Betesszük a fleet_members táblába
|
||||
await conn.execute(text("""
|
||||
INSERT INTO data.fleet_members (user_id, owner_id, role)
|
||||
VALUES (2, 1, 'DRIVER')
|
||||
ON CONFLICT (user_id, owner_id) DO NOTHING;
|
||||
"""))
|
||||
|
||||
print("✅ KÉSZ! Kovács János mostantól a csapatod tagja.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(hire_driver())
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
services:
|
||||
service_finder_api:
|
||||
build: .
|
||||
container_name: service_finder_api
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- existing_net
|
||||
ports:
|
||||
- "8000:8000"
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
existing_net:
|
||||
external: true
|
||||
name: docker-server_internal_net
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Cím összeállítása (Ugyanaz a logika, mint a main.py-ban)
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def init_db():
|
||||
print(f"🔌 Kapcsolódás ide: {DATABASE_URL}")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("🏗️ Séma és Táblák létrehozása...")
|
||||
|
||||
# 1. Séma létrehozása
|
||||
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS ref;"))
|
||||
|
||||
# 2. Márka tábla
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS ref.vehicle_makes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE
|
||||
);
|
||||
"""))
|
||||
|
||||
# 3. Modell tábla
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS ref.vehicle_models (
|
||||
id SERIAL PRIMARY KEY,
|
||||
make_id INTEGER REFERENCES ref.vehicle_makes(id),
|
||||
model_name VARCHAR(100) NOT NULL,
|
||||
category VARCHAR(50)
|
||||
);
|
||||
"""))
|
||||
|
||||
print("🧹 Meglévő adatok törlése (Tiszta lap)...")
|
||||
await conn.execute(text("TRUNCATE TABLE ref.vehicle_models, ref.vehicle_makes RESTART IDENTITY CASCADE;"))
|
||||
|
||||
print("🚗 Adatok beszúrása...")
|
||||
|
||||
# Adatok
|
||||
makes = ["BMW", "Audi", "Mercedes-Benz", "Toyota", "Honda", "Suzuki", "Volkswagen"]
|
||||
|
||||
models = [
|
||||
("BMW", "3 Series", "Autó"), ("BMW", "5 Series", "Autó"), ("BMW", "X5", "Autó"), ("BMW", "R 1250 GS", "Motor"),
|
||||
("Audi", "A3", "Autó"), ("Audi", "A4", "Autó"), ("Audi", "A6", "Autó"), ("Audi", "Q5", "Autó"),
|
||||
("Mercedes-Benz", "C-Class", "Autó"), ("Mercedes-Benz", "E-Class", "Autó"), ("Mercedes-Benz", "S-Class", "Autó"), ("Mercedes-Benz", "G-Class", "Autó"),
|
||||
("Toyota", "Corolla", "Autó"), ("Toyota", "Yaris", "Autó"), ("Toyota", "RAV4", "Autó"), ("Toyota", "Hilux", "Autó"),
|
||||
("Honda", "Civic", "Autó"), ("Honda", "CR-V", "Autó"), ("Honda", "Africa Twin", "Motor"), ("Honda", "CB500X", "Motor"),
|
||||
("Suzuki", "Swift", "Autó"), ("Suzuki", "Vitara", "Autó"), ("Suzuki", "S-Cross", "Autó"), ("Suzuki", "DL 650 V-Strom", "Motor"),
|
||||
("Volkswagen", "Golf", "Autó"), ("Volkswagen", "Passat", "Autó"), ("Volkswagen", "Polo", "Autó"), ("Volkswagen", "Tiguan", "Autó")
|
||||
]
|
||||
|
||||
# Márkák beszúrása
|
||||
for make in makes:
|
||||
await conn.execute(text(f"INSERT INTO ref.vehicle_makes (name) VALUES ('{make}') ON CONFLICT DO NOTHING;"))
|
||||
|
||||
# Modellek beszúrása (kicsit trükkös, mert kell a make_id)
|
||||
for brand, model, cat in models:
|
||||
# Megkeressük az ID-t
|
||||
result = await conn.execute(text(f"SELECT id FROM ref.vehicle_makes WHERE name = '{brand}'"))
|
||||
make_id = result.scalar()
|
||||
|
||||
# Beszúrjuk a modellt
|
||||
await conn.execute(text(f"""
|
||||
INSERT INTO ref.vehicle_models (make_id, model_name, category)
|
||||
VALUES ({make_id}, '{model}', '{cat}')
|
||||
"""))
|
||||
|
||||
print("✅ KÉSZ! Az adatbázis feltöltve.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(init_db())
|
||||
@@ -1,61 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Cím beállítása
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def inspect_schema():
|
||||
print(f"🔎 Kapcsolódás az adatbázishoz...")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
# SQL lekérdezés a rendszer katalógusból (information_schema)
|
||||
# Ez megmondja milyen táblák és oszlopok léteznek
|
||||
query = text("""
|
||||
SELECT
|
||||
table_schema,
|
||||
table_name,
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema IN ('public', 'ref', 'data')
|
||||
ORDER BY table_schema, table_name, ordinal_position;
|
||||
""")
|
||||
|
||||
result = await conn.execute(query)
|
||||
rows = result.fetchall()
|
||||
|
||||
if not rows:
|
||||
print("⚠️ Nem találtam táblákat a 'ref' vagy 'data' sémákban!")
|
||||
|
||||
current_table = ""
|
||||
for row in rows:
|
||||
schema = row.table_schema
|
||||
table = row.table_name
|
||||
full_table_name = f"{schema}.{table}"
|
||||
|
||||
# Ha új táblához érünk, kiírjuk a nevét
|
||||
if full_table_name != current_table:
|
||||
print(f"\n📦 TÁBLA: {full_table_name.upper()}")
|
||||
print("-" * 50)
|
||||
print(f"{'OSZLOP NÉV':<20} | {'TÍPUS':<15} | {'KÖTELEZŐ?'}")
|
||||
print("-" * 50)
|
||||
current_table = full_table_name
|
||||
|
||||
# Oszlop adatok
|
||||
req = "IGEN" if row.is_nullable == 'NO' else "nem"
|
||||
print(f"{row.column_name:<20} | {row.data_type:<15} | {req}")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(inspect_schema())
|
||||
@@ -1,69 +0,0 @@
|
||||
import asyncio
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
# CSERÉLD KI a saját adataidra, ha nem ezeket használod!
|
||||
DATABASE_URL = "postgresql+asyncpg://kincses:MiskociA74@postgres-db:5432/service_finder"
|
||||
|
||||
async def run_inspection():
|
||||
# Aszinkron motor létrehozása
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.connect() as conn:
|
||||
def do_inspect(sync_conn):
|
||||
inspector = inspect(sync_conn)
|
||||
|
||||
# 1. Sémák lekérése (amiket mi hoztunk létre + public)
|
||||
target_schemas = ['public', 'data', 'ref']
|
||||
all_schemas = inspector.get_schema_names()
|
||||
schemas_to_check = [s for s in target_schemas if s in all_schemas]
|
||||
|
||||
print(f"{'='*60}")
|
||||
print(f" JÁRMŰNYILVÁNTARTÓ RENDSZER - ADATBÁZIS AUDIT")
|
||||
print(f"{'='*60}")
|
||||
|
||||
for schema in schemas_to_check:
|
||||
print(f"\n--- SÉMA: {schema.upper()} ---")
|
||||
tables = inspector.get_table_names(schema=schema)
|
||||
|
||||
if not tables:
|
||||
print(" (Nincsenek táblák ebben a sémában)")
|
||||
continue
|
||||
|
||||
for table_name in tables:
|
||||
print(f"\n[Tábla: {schema}.{table_name}]")
|
||||
|
||||
# Oszlopok részletei
|
||||
columns = inspector.get_columns(table_name, schema=schema)
|
||||
for col in columns:
|
||||
pk = " [PK]" if col['primary_key'] else ""
|
||||
nullable = "NULL" if col['nullable'] else "NOT NULL"
|
||||
print(f" L {col['name']:<15} | {str(col['type']):<12} | {nullable}{pk}")
|
||||
|
||||
# Idegen kulcsok (Kapcsolatok)
|
||||
fks = inspector.get_foreign_keys(table_name, schema=schema)
|
||||
for fk in fks:
|
||||
constrained = fk['constrained_columns']
|
||||
referred_schema = fk['referred_schema']
|
||||
referred_table = fk['referred_table']
|
||||
referred_cols = fk['referred_columns']
|
||||
print(f" --> Kapcsolat: {constrained} -> {referred_schema}.{referred_table}({referred_cols})")
|
||||
|
||||
# Mivel az SQLAlchemy inspector alapvetően szinkron, run_sync-et használunk
|
||||
await conn.run_sync(do_inspect)
|
||||
|
||||
# 2. Extra: Kiterjesztések ellenőrzése
|
||||
print(f"\n{'='*60}")
|
||||
print(" AKTÍV POSTGRESQL KITERJESZTÉSEK:")
|
||||
result = await conn.execute(text("SELECT extname FROM pg_extension;"))
|
||||
for row in result:
|
||||
print(f" - {row[0]}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(run_inspection())
|
||||
except Exception as e:
|
||||
print(f"HIBA TÖRTÉNT: {e}")
|
||||
@@ -1,81 +0,0 @@
|
||||
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, traceback
|
||||
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 ---
|
||||
def get_password_hash(p): return bcrypt.hashpw(p.encode('utf-8')[:72], bcrypt.gensalt()).decode('utf-8')
|
||||
def verify_password(p, h): return bcrypt.checkpw(p.encode('utf-8')[:72], h.encode('utf-8'))
|
||||
|
||||
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)
|
||||
|
||||
@app.post("/api/auth/register")
|
||||
async def register(email: str = Form(...), password: str = Form(...)):
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
async with session.begin():
|
||||
await session.execute(text("INSERT INTO data.users (email, password_hash) VALUES (:e, :p)"),
|
||||
{"e": email, "p": get_password_hash(password)})
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=400, content={"detail": "Email már létezik vagy adatbázis hiba."})
|
||||
|
||||
@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 verify_password(f.password, u.password_hash): raise HTTPException(status_code=401, detail="Hibás adatok")
|
||||
t = jwt.encode({"sub": str(u.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return {"access_token": t, "token_type": "bearer"}
|
||||
|
||||
# --- DATA ---
|
||||
@app.get("/api/meta/vehicle-hierarchy")
|
||||
async def get_hierarchy():
|
||||
async with AsyncSessionLocal() as session:
|
||||
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")
|
||||
res = await session.execute(q)
|
||||
h = {}
|
||||
for r in res:
|
||||
if r.category not in h: h[r.category] = {}
|
||||
if r.brand not in h[r.category]: h[r.category][r.brand] = []
|
||||
h[r.category][r.brand].append({"id": r.model_id, "name": r.model_name})
|
||||
return h
|
||||
|
||||
@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()}
|
||||
|
||||
@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()]
|
||||
|
||||
@app.get("/")
|
||||
async def index(): return FileResponse("/app/frontend/index.html")
|
||||
@@ -1,120 +0,0 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, validator
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
import os
|
||||
from datetime import date
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# --- 1. ADATBÁZIS KAPCSOLAT ---
|
||||
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")
|
||||
|
||||
# --- 2. ADATMODELLEK (VALIDÁCIÓVAL) ---
|
||||
class VehicleRegister(BaseModel):
|
||||
model_id: int
|
||||
vin: str # Alvázszám (KÖTELEZŐ!)
|
||||
plate: str # Rendszám
|
||||
mileage: int # Km óra
|
||||
purchase_date: date
|
||||
role: str = "OWNER" # Alapértelmezett: Tulajdonos
|
||||
|
||||
# Adatőr: Automatikus nagybetűsítés és tisztítás
|
||||
@validator('vin')
|
||||
def clean_vin(cls, v):
|
||||
if len(v) < 5: raise ValueError('Az alvázszám túl rövid!')
|
||||
return v.upper().replace("-", "").replace(" ", "")
|
||||
|
||||
@validator('plate')
|
||||
def clean_plate(cls, v):
|
||||
return v.upper().replace("-", "").replace(" ", "")
|
||||
|
||||
@validator('mileage')
|
||||
def positive_mileage(cls, v):
|
||||
if v < 0: raise ValueError('A kilométer nem lehet negatív!')
|
||||
return v
|
||||
|
||||
# --- 3. VÉGPONTOK ---
|
||||
|
||||
# Lista lekérése (Katalógus)
|
||||
@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, vm.model_name
|
||||
"""))
|
||||
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.fetchall()]
|
||||
|
||||
# ÚJ: JÁRMŰ REGISZTRÁCIÓ (A Nagy Logika)
|
||||
@app.post("/api/register")
|
||||
async def register_vehicle(data: VehicleRegister):
|
||||
async with AsyncSessionLocal() as session:
|
||||
async with session.begin(): # Tranzakció indítása
|
||||
try:
|
||||
# 1. Megnézzük, létezik-e már ez a VAS (Alvázszám alapján)?
|
||||
check_query = text("SELECT id FROM data.vehicles WHERE vin = :vin")
|
||||
result = await session.execute(check_query, {"vin": data.vin})
|
||||
existing_car_id = result.scalar()
|
||||
|
||||
vehicle_id = existing_car_id
|
||||
|
||||
# 2. HA NEM LÉTEZIK -> Létrehozzuk a VASAT
|
||||
if not vehicle_id:
|
||||
print(f"🆕 Új autó az adatbázisban: {data.vin}")
|
||||
insert_car = text("""
|
||||
INSERT INTO data.vehicles (model_id, vin, current_plate)
|
||||
VALUES (:mid, :vin, :plt)
|
||||
RETURNING id
|
||||
""")
|
||||
res = await session.execute(insert_car, {
|
||||
"mid": data.model_id, "vin": data.vin, "plt": data.plate
|
||||
})
|
||||
vehicle_id = res.scalar()
|
||||
else:
|
||||
print(f"♻️ Létező autó átvétele: {data.vin} (ID: {vehicle_id})")
|
||||
# Itt később lezárhatjuk az előző tulajdonos history-ját!
|
||||
|
||||
# 3. BEJEGYZÉS A TÖRTÉNELEMBE (HISTORY)
|
||||
# Ez köti össze a User-t (most fixen ID=1) az Autóval
|
||||
insert_history = text("""
|
||||
INSERT INTO data.vehicle_history
|
||||
(vehicle_id, user_id, role, start_date, start_mileage)
|
||||
VALUES (:vid, 1, :role, :s_date, :s_mil)
|
||||
""")
|
||||
|
||||
await session.execute(insert_history, {
|
||||
"vid": vehicle_id,
|
||||
"role": data.role,
|
||||
"s_date": data.purchase_date,
|
||||
"s_mil": data.mileage
|
||||
})
|
||||
|
||||
return {"status": "success", "message": "Jármű sikeresen rögzítve a flottában!"}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Hiba: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Frontend kiszolgálása
|
||||
@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"}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(title="Service Finder API")
|
||||
|
||||
# --- A DUPLA JAVÍTÁS ---
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
|
||||
if not raw_url:
|
||||
# Vészhelyzeti fallback (ha nem lenne .env)
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
|
||||
# 1. Javítás: Driver csere (asyncpg)
|
||||
fixed_url = raw_url.replace("postgresql://", "postgresql+asyncpg://")
|
||||
|
||||
# 2. Javítás: Adatbázis név csere (Ha véletlenül _db a vége, levágjuk)
|
||||
fixed_url = fixed_url.replace("/service_finder_db", "/service_finder")
|
||||
|
||||
print(f"🔧 VÉGLEGES ADATBÁZIS CÍM: {fixed_url}")
|
||||
|
||||
# Kapcsolódás a javított címmel
|
||||
engine = create_async_engine(fixed_url)
|
||||
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Service Finder API működik! 🚀"}
|
||||
|
||||
@app.get("/vehicles")
|
||||
async def get_vehicles():
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
query = text("""
|
||||
SELECT 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, vm.model_name
|
||||
""")
|
||||
result = await session.execute(query)
|
||||
vehicles = result.fetchall()
|
||||
return [{"brand": r.brand, "model": r.model_name, "category": r.category} for r in vehicles]
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
@@ -1,52 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Betöltjük a környezeti változókat
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(title="Service Finder API")
|
||||
|
||||
# --- A FIX ---
|
||||
# Kiolvassuk a címet
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
|
||||
# Ha nincs beállítva, adunk egy vészhelyzeti alapértelmezettet
|
||||
if not raw_url:
|
||||
print("⚠️ FIGYELEM: Nincs DATABASE_URL, alapértelmezett használata!")
|
||||
# Itt a konténer belső nevét (postgres-db) használjuk
|
||||
raw_url = "postgresql://admin:MiskociA74@postgres-db:5432/service_finder"
|
||||
|
||||
# A LÉNYEG: Ha hiányzik az '+asyncpg', hozzáadjuk!
|
||||
if "asyncpg" not in raw_url:
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://")
|
||||
print(f"🔧 URL javítva erre: {DATABASE_URL}")
|
||||
else:
|
||||
DATABASE_URL = raw_url
|
||||
|
||||
# Motor indítása a javított URL-lel
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Service Finder API működik! 🚀"}
|
||||
|
||||
@app.get("/vehicles")
|
||||
async def get_vehicles():
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
query = text("""
|
||||
SELECT 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, vm.model_name
|
||||
""")
|
||||
result = await session.execute(query)
|
||||
vehicles = result.fetchall()
|
||||
return [{"brand": r.brand, "model": r.model_name, "category": r.category} for r in vehicles]
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
@@ -1,71 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def seed_cost_types():
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
async with engine.begin() as conn:
|
||||
print("🔨 Költség típusok tábla létrehozása...")
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS ref.cost_types (
|
||||
code VARCHAR(50) PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_code VARCHAR(50) REFERENCES ref.cost_types(code), -- Hierarchia!
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
"""))
|
||||
|
||||
print("📥 Adatok feltöltése...")
|
||||
# Először a szülők (Főkategóriák)
|
||||
parents = [
|
||||
('FUEL', '⛽ Tankolás', 10),
|
||||
('PURCHASE', '💰 Beszerzés / Pénzügy', 20),
|
||||
('INSURANCE', '📄 Biztosítás', 30),
|
||||
('TAX', '🏛️ Adó / Illeték', 40),
|
||||
('SERVICE', '🔧 Szerviz', 50),
|
||||
('OTHER', 'Egyéb', 99)
|
||||
]
|
||||
for p in parents:
|
||||
await conn.execute(text("INSERT INTO ref.cost_types (code, name, sort_order) VALUES (:c, :n, :s) ON CONFLICT (code) DO NOTHING"), {"c": p[0], "n": p[1], "s": p[2]})
|
||||
|
||||
# Aztán a gyerekek (Alkategóriák)
|
||||
children = [
|
||||
# Beszerzés
|
||||
('PURCHASE_PRICE', 'Vételár', 'PURCHASE'),
|
||||
('FINANCE_LEASE', 'Lízing díj', 'PURCHASE'),
|
||||
('FINANCE_LOAN', 'Hitel törlesztő', 'PURCHASE'),
|
||||
('IMPORT_FEE', 'Honosítás / Regadó', 'PURCHASE'),
|
||||
# Biztosítás
|
||||
('INSURANCE_KGFB', 'Kötelező (KGFB)', 'INSURANCE'),
|
||||
('INSURANCE_CASCO', 'Casco', 'INSURANCE'),
|
||||
('INSURANCE_GAP', 'GAP', 'INSURANCE'),
|
||||
('INSURANCE_ASSIST', 'Assistance', 'INSURANCE'),
|
||||
# Adó
|
||||
('TAX_WEIGHT', 'Gépjárműadó (Súlyadó)', 'TAX'),
|
||||
('TAX_COMPANY', 'Cégautóadó', 'TAX'),
|
||||
('TAX_TRANSFER', 'Vagyonszerzési illeték', 'TAX'),
|
||||
('TAX_OTHER', 'Egyéb adó', 'TAX'),
|
||||
# Szerviz
|
||||
('SERVICE_MAINTENANCE', 'Kötelező karbantartás', 'SERVICE'),
|
||||
('SERVICE_REPAIR', 'Javítás', 'SERVICE'),
|
||||
('SERVICE_TIRE', 'Gumicsere', 'SERVICE'),
|
||||
('SERVICE_MOT', 'Műszaki vizsga', 'SERVICE')
|
||||
]
|
||||
for c in children:
|
||||
await conn.execute(text("INSERT INTO ref.cost_types (code, name, parent_code) VALUES (:c, :n, :p) ON CONFLICT (code) DO NOTHING"), {"c": c[0], "n": c[1], "p": c[2]})
|
||||
|
||||
print("✅ KÉSZ! A tudás most már az adatbázisban van, nem a HTML-ben.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_cost_types())
|
||||
@@ -1,46 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def build_audit_system():
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("📜 Audit Log tábla létrehozása...")
|
||||
# Ez a tábla sosem töröl, csak ír! (Append-only)
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS data.audit_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES data.users(id), -- KI csinálta?
|
||||
event_type VARCHAR(50) NOT NULL, -- MIT? (pl. ISSUE_REPORT, MILEAGE_UPDATE)
|
||||
target_id INTEGER, -- MELYIK autón? (Vehicle ID)
|
||||
old_value TEXT, -- MI volt előtte? (A visszaállításhoz)
|
||||
new_value TEXT, -- MI lett utána?
|
||||
details TEXT, -- Egyéb megjegyzés (pl. hiba leírása)
|
||||
ip_address VARCHAR(45), -- Honnan? (Biztonság)
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
"""))
|
||||
|
||||
print("🚦 Jármű Státusz mezők hozzáadása...")
|
||||
# Bővítjük a járműveket, hogy tárolják a hibát
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.vehicles
|
||||
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'OK', -- OK, WARNING, CRITICAL
|
||||
ADD COLUMN IF NOT EXISTS current_issue TEXT; -- A hiba leírása
|
||||
"""))
|
||||
|
||||
print("✅ KÉSZ! A mindent látó szem (Audit Log) aktív.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(build_audit_system())
|
||||
@@ -1,56 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def expand_categories():
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
async with engine.begin() as conn:
|
||||
print("📥 Új Főkategóriák beszúrása...")
|
||||
parents = [
|
||||
('EQUIPMENT', '🛠️ Felszerelés / Extrák', 25), # Vételár után
|
||||
('OPERATIONAL', '🅿️ Üzemeltetés / Út', 15), # Tankolás után
|
||||
('FINE', '👮 Bírság / Büntetés', 90)
|
||||
]
|
||||
for p in parents:
|
||||
await conn.execute(text("INSERT INTO ref.cost_types (code, name, sort_order) VALUES (:c, :n, :s) ON CONFLICT (code) DO NOTHING"), {"c": p[0], "n": p[1], "s": p[2]})
|
||||
|
||||
print("📥 Új Alkategóriák beszúrása...")
|
||||
children = [
|
||||
# Felszerelés (EQUIPMENT)
|
||||
('EQUIP_SHELVING', 'Raktérburkolat / Polcrendszer', 'EQUIPMENT'),
|
||||
('EQUIP_BRANDING', 'Matricázás / Fóliázás', 'EQUIPMENT'),
|
||||
('EQUIP_TOWBAR', 'Vonóhorog / Tetőcsomagtartó', 'EQUIPMENT'),
|
||||
('EQUIP_SAFETY', 'Kötelező tartozékok (Eü. csomag)', 'EQUIPMENT'),
|
||||
('EQUIP_GPS', 'GPS / Nyomkövető hardver', 'EQUIPMENT'),
|
||||
('EQUIP_WINTER_TIRE', 'Téli gumi szett (Beszerzés)', 'EQUIPMENT'),
|
||||
|
||||
# Üzemeltetés (OPERATIONAL)
|
||||
('OP_PARKING', 'Parkolás', 'OPERATIONAL'),
|
||||
('OP_TOLL', 'Útdíj / Matrica / Behajtás', 'OPERATIONAL'),
|
||||
('OP_WASH', 'Mosás / Kozmetika', 'OPERATIONAL'),
|
||||
('OP_ADBLUE', 'AdBlue', 'OPERATIONAL'),
|
||||
('OP_EV_CHARGE', 'Elektromos töltés', 'OPERATIONAL'),
|
||||
('OP_STORAGE', 'Gumi hotel / Tárolás', 'OPERATIONAL'),
|
||||
|
||||
# Bírság (FINE)
|
||||
('FINE_SPEEDING', 'Gyorshajtás', 'FINE'),
|
||||
('FINE_PARKING', 'Parkolási bírság', 'FINE'),
|
||||
('FINE_ADMIN', 'Közigazgatási bírság', 'FINE')
|
||||
]
|
||||
for c in children:
|
||||
await conn.execute(text("INSERT INTO ref.cost_types (code, name, parent_code) VALUES (:c, :n, :p) ON CONFLICT (code) DO NOTHING"), {"c": c[0], "n": c[1], "p": c[2]})
|
||||
|
||||
print("✅ KÉSZ! A költséglista most már teljeskörű.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(expand_categories())
|
||||
@@ -1,46 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def upgrade_db():
|
||||
print(f"🔌 Kapcsolódás...")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("🌍 Felhasználói tábla bővítése (Ország + Alapértelmezett pénznem)...")
|
||||
# 1. Users tábla bővítése
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.users
|
||||
ADD COLUMN IF NOT EXISTS country VARCHAR(2) DEFAULT 'HU',
|
||||
ADD COLUMN IF NOT EXISTS default_currency VARCHAR(3) DEFAULT 'HUF';
|
||||
"""))
|
||||
|
||||
print("💶 Költség tábla bővítése (Tranzakciós pénznem)...")
|
||||
# 2. Costs tábla bővítése
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.costs
|
||||
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'HUF';
|
||||
"""))
|
||||
|
||||
# Frissítjük a meglévő Demo Usert (ID=1)
|
||||
print("👤 Demo User beállítása: Magyarország / HUF")
|
||||
await conn.execute(text("""
|
||||
UPDATE data.users
|
||||
SET country = 'HU', default_currency = 'HUF'
|
||||
WHERE id = 1;
|
||||
"""))
|
||||
|
||||
print("✅ KÉSZ! Az adatbázis mostantól támogatja a több pénznemet.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(upgrade_db())
|
||||
@@ -1,26 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def add_doc_column():
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
async with engine.begin() as conn:
|
||||
print("📄 Dokumentum oszlop hozzáadása a Costs táblához...")
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.costs
|
||||
ADD COLUMN IF NOT EXISTS document_url VARCHAR(255);
|
||||
"""))
|
||||
print("✅ KÉSZ! Mehetnek a fájlok.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(add_doc_column())
|
||||
@@ -1,58 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def upgrade_invites():
|
||||
print(f"🔌 Kapcsolódás...")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("📨 Invitations (Meghívók) tábla létrehozása...")
|
||||
# Ez tárolja a függőben lévő meghívásokat
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS data.invitations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
inviter_id INTEGER REFERENCES data.users(id), -- Ki hívta meg? (Cég)
|
||||
role VARCHAR(20) NOT NULL, -- Milyen szerepre? (MANAGER, DRIVER)
|
||||
access_level VARCHAR(20) DEFAULT 'FULL', -- A/B Sofőr szint
|
||||
token VARCHAR(100) UNIQUE NOT NULL, -- A titkos link kódja
|
||||
status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, ACCEPTED, EXPIRED
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
expires_at TIMESTAMP
|
||||
);
|
||||
"""))
|
||||
|
||||
print("🤝 Fleet Members (Többcéges tagság) tábla létrehozása...")
|
||||
# Ez teszi lehetővé, hogy valaki több céghez is tartozzon
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS data.fleet_members (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES data.users(id), -- A Dolgozó
|
||||
owner_id INTEGER REFERENCES data.users(id), -- A Cég / Tulajdonos
|
||||
role VARCHAR(20) NOT NULL, -- FLEET_MANAGER, DRIVER
|
||||
joined_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
-- Egy ember egy cégnél csak egyszer szerepelhet
|
||||
UNIQUE(user_id, owner_id)
|
||||
);
|
||||
"""))
|
||||
|
||||
# Takarítás: A régi 'parent_id' már nem kell, mert a fleet_members kiváltja
|
||||
# De biztonságból egyelőre csak NULL-ra állítjuk, nem töröljük az oszlopot
|
||||
# await conn.execute(text("UPDATE data.users SET parent_id = NULL;"))
|
||||
|
||||
print("✅ KÉSZ! A rendszer készen áll a biztonságos meghívókra.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(upgrade_invites())
|
||||
@@ -1,61 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
raw_url = os.getenv("DATABASE_URL")
|
||||
if not raw_url:
|
||||
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
||||
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
||||
|
||||
async def upgrade_permissions():
|
||||
print(f"🔌 Kapcsolódás...")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
print("👑 USERS tábla bővítése (Rendszer és Szervezeti szintek)...")
|
||||
|
||||
# 1. SYS_ROLE: Rendszergazda / Moderátor / User
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.users
|
||||
ADD COLUMN IF NOT EXISTS sys_role VARCHAR(20) DEFAULT 'USER';
|
||||
"""))
|
||||
|
||||
# 2. HIERARCHIA: Ki a főnököd? (Parent User ID)
|
||||
# Ha NULL, akkor ő a Cégtulajdonos (SuperUser). Ha van ID, akkor Alkalmazott.
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.users
|
||||
ADD COLUMN IF NOT EXISTS parent_id INTEGER REFERENCES data.users(id);
|
||||
"""))
|
||||
|
||||
# 3. ORG_ROLE: Cégen belüli szerep (Owner, Fleet Manager, Employee)
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.users
|
||||
ADD COLUMN IF NOT EXISTS org_role VARCHAR(20) DEFAULT 'OWNER';
|
||||
"""))
|
||||
|
||||
print("🚦 VEHICLE_HISTORY tábla bővítése (Sofőr jogosultságok)...")
|
||||
|
||||
# 4. ACCESS_LEVEL: Mit láthat a sofőr? (COST_MANAGER vs LOG_ONLY)
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE data.vehicle_history
|
||||
ADD COLUMN IF NOT EXISTS access_level VARCHAR(20) DEFAULT 'FULL';
|
||||
-- Lehetséges értékek: 'FULL', 'COST_MANAGER', 'LOG_ONLY'
|
||||
"""))
|
||||
|
||||
# --- DEMO ADATOK FRISSÍTÉSE ---
|
||||
print("👤 Demo User (ID:1) kinevezése Rendszergazdának és Cégtulajdonosnak...")
|
||||
await conn.execute(text("""
|
||||
UPDATE data.users
|
||||
SET sys_role = 'SYS_ADMIN', org_role = 'OWNER', parent_id = NULL
|
||||
WHERE id = 1;
|
||||
"""))
|
||||
|
||||
print("✅ KÉSZ! A jogosultsági mátrix beépítve az adatbázisba.")
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(upgrade_permissions())
|
||||
@@ -1,18 +0,0 @@
|
||||
import os
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail
|
||||
|
||||
def send_verification_email(to_email: str, token: str):
|
||||
message = Mail(
|
||||
from_email='noreply@servicefinder.pro', # Ezt majd igazítsd a SendGrid verified senderhez
|
||||
to_emails=to_email,
|
||||
subject='Service Finder - Regisztráció megerősítése',
|
||||
html_content=f'<h3>Üdvözöljük a Service Finderben!</h3><p>A regisztráció befejezéséhez kattintson az alábbi linkre:</p><p><a href="https://servicefinder.pro/verify?token={token}">Megerősítem a regisztrációmat</a></p>'
|
||||
)
|
||||
try:
|
||||
sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
|
||||
response = sg.send(message)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Email hiba: {e}")
|
||||
return False
|
||||
@@ -1,24 +0,0 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/core/security.py
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any
|
||||
import bcrypt
|
||||
from jose import jwt
|
||||
from app.core.config import settings
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
if not hashed_password:
|
||||
return False
|
||||
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
salt = bcrypt.gensalt()
|
||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
||||
|
||||
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
@@ -1,18 +0,0 @@
|
||||
from sqlalchemy import Column, String, JSON, Boolean, DateTime, Integer, text
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class SystemParameter(Base):
|
||||
"""
|
||||
Rendszerszintű dinamikus paraméterek tárolása.
|
||||
Szinkronban az admin.py és config.py elvárásaival.
|
||||
"""
|
||||
__tablename__ = "system_parameters"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
# Az admin.py 'key' mezőt vár, nem 'key_name'-et!
|
||||
key = Column(String(50), primary_key=True, index=True)
|
||||
value = Column(JSON, server_default=text("'{}'::jsonb"), nullable=False)
|
||||
description = Column(String(255), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
@@ -1,116 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import SystemParameter
|
||||
|
||||
logger = logging.getLogger("AI-Service")
|
||||
|
||||
class AIService:
|
||||
"""
|
||||
AI Service v1.2.4 - Production Ready
|
||||
- Robot 2 (Technical Enrichment) & Robot 3 (OCR)
|
||||
- Fix: JSON response cleaning and array-to-dict transformation.
|
||||
"""
|
||||
api_key = os.getenv("GEMINI_API_KEY")
|
||||
client = genai.Client(api_key=api_key) if api_key else None
|
||||
PRIMARY_MODEL = "gemini-2.0-flash"
|
||||
|
||||
@classmethod
|
||||
async def get_config_delay(cls) -> float:
|
||||
"""Lekéri az adminisztrálható késleltetést az adatbázisból."""
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
|
||||
res = await db.execute(stmt)
|
||||
param = res.scalar_one_or_none()
|
||||
return float(param.value) if param else 1.0
|
||||
except Exception:
|
||||
return 1.0
|
||||
|
||||
@classmethod
|
||||
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 2: Gépjármű technikai adatok dúsítása."""
|
||||
if not cls.client:
|
||||
return None
|
||||
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompt = f"""
|
||||
Jármű: {make} {raw_model} ({v_type}).
|
||||
Adj technikai adatokat JSON formátumban.
|
||||
FONTOS: A 'technical_code' mező NEM lehet üres. Ha nem tudod a gyári kódot, adj 'N/A' értéket!
|
||||
|
||||
Várt struktúra:
|
||||
{{
|
||||
"marketing_name": "tiszta marketing név",
|
||||
"technical_code": "gyári kód vagy N/A",
|
||||
"ccm": egész szám,
|
||||
"kw": egész szám,
|
||||
"maintenance": {{
|
||||
"oil_type": "viszkozitás",
|
||||
"oil_qty": tizedes tört literben,
|
||||
"spark_plug": "gyertya típus",
|
||||
"coolant": "hűtőfolyadék"
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi gépjárműtechnikus vagy. Kizárólag tiszta JSON-t válaszolsz.",
|
||||
response_mime_type="application/json",
|
||||
temperature=0.1
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
|
||||
res_json = json.loads(response.text)
|
||||
|
||||
if isinstance(res_json, list) and len(res_json) > 0:
|
||||
res_json = res_json[0]
|
||||
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 3: Multimodális OCR elemzés (Képbeolvasás)."""
|
||||
if not cls.client:
|
||||
return None
|
||||
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompts = {
|
||||
"identity": "Személyes okmány adatok.",
|
||||
"vehicle_reg": "Rendszám, alvázszám, technikai adatok.",
|
||||
"invoice": "Számla adatok, összegek, dátumok.",
|
||||
"odometer": "Csak a kilométeróra állása számként."
|
||||
}
|
||||
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
|
||||
response_mime_type="application/json"
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.client.models.generate_content(
|
||||
model=cls.PRIMARY_MODEL,
|
||||
contents=[
|
||||
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, '')}",
|
||||
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
|
||||
],
|
||||
config=config
|
||||
)
|
||||
res_json = json.loads(response.text)
|
||||
if isinstance(res_json, list) and len(res_json) > 0:
|
||||
res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI OCR hiba ({doc_type}): {e}")
|
||||
return None
|
||||
@@ -1,102 +0,0 @@
|
||||
import asyncio
|
||||
import httpx
|
||||
import logging
|
||||
import os # <--- EZ HIÁNYZOTT!
|
||||
import datetime
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.vehicle_definitions import VehicleModelDefinition
|
||||
from app.services.ai_service import AIService
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("Robot-v1.2.4-Fixed")
|
||||
|
||||
class TechEnricher:
|
||||
API_URL = "https://opendata.rdw.nl/resource/kyri-nuah.json"
|
||||
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
|
||||
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
|
||||
|
||||
@classmethod
|
||||
async def fetch_rdw_tech_data(cls, make, model):
|
||||
"""Hibatűrő RDW lekérdezés tisztított paraméterekkel."""
|
||||
clean_model = str(model).strip().upper()
|
||||
params = {"merk": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
|
||||
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
|
||||
try:
|
||||
resp = await client.get(cls.API_URL, params=params, timeout=15)
|
||||
if resp.status_code == 200 and resp.json():
|
||||
return resp.json()[0]
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def run(cls):
|
||||
logger.info("🚀 Master Enricher INDUL (Atomi mentés üzemmód)...")
|
||||
|
||||
# 1. Csak az ID-kat kérjük le, hogy ne tartsuk nyitva a tranzakciót feleslegesen
|
||||
async with SessionLocal() as main_db:
|
||||
stmt = select(VehicleModelDefinition.id).where(
|
||||
VehicleModelDefinition.status == "unverified"
|
||||
).limit(50)
|
||||
res = await main_db.execute(stmt)
|
||||
ids = res.scalars().all()
|
||||
|
||||
if not ids:
|
||||
logger.info("😴 Nincs dúsítandó adat.")
|
||||
return
|
||||
|
||||
# 2. Egyesével dolgozzuk fel a rekordokat saját session-ben
|
||||
for m_id in ids:
|
||||
async with SessionLocal() as db:
|
||||
try:
|
||||
master = await db.get(VehicleModelDefinition, m_id)
|
||||
if not master:
|
||||
continue
|
||||
|
||||
logger.info(f"🧪 Feldolgozás: {master.make} {master.marketing_name} (ID: {m_id})")
|
||||
data_found = False
|
||||
|
||||
# A: RDW fázis
|
||||
rdw_data = await cls.fetch_rdw_tech_data(master.make, master.marketing_name)
|
||||
if rdw_data:
|
||||
master.engine_capacity = int(float(rdw_data.get("cilinderinhoud", 0))) or None
|
||||
master.power_kw = int(float(rdw_data.get("netto_maximum_vermogen_kw", 0))) or None
|
||||
data_found = True
|
||||
|
||||
# B: AI fázis (ha hiányzik adat vagy pontosítani kell)
|
||||
if not data_found or master.engine_capacity is None:
|
||||
ai_data = await AIService.get_clean_vehicle_data(
|
||||
master.make, master.marketing_name, master.vehicle_type
|
||||
)
|
||||
if ai_data:
|
||||
master.marketing_name = ai_data.get("marketing_name", master.marketing_name)
|
||||
master.technical_code = ai_data.get("technical_code") or master.technical_code or "N/A"
|
||||
master.engine_capacity = ai_data.get("ccm") or master.engine_capacity
|
||||
master.power_kw = ai_data.get("kw") or master.power_kw
|
||||
master.specifications = ai_data.get("maintenance", {})
|
||||
data_found = True
|
||||
|
||||
# C: Mentés és véglegesítés
|
||||
if data_found:
|
||||
master.status = "ai_enriched"
|
||||
master.updated_at = datetime.datetime.now()
|
||||
await db.commit() # AZONNALI COMMIT A LEMEZRE
|
||||
logger.info(f"✅ Sikeresen mentve: {master.marketing_name} (CCM: {master.engine_capacity})")
|
||||
else:
|
||||
logger.warning(f"⚠️ Nem találtam adatot az ID {m_id} esetében.")
|
||||
|
||||
except IntegrityError:
|
||||
await db.rollback()
|
||||
logger.warning(f"🚫 Duplikáció vagy Constraint hiba (ID: {m_id}). Kihagyva.")
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Váratlan hiba az ID {m_id} esetében: {e}")
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
logger.info("🏁 50-es batch feldolgozva.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(TechEnricher.run())
|
||||
Reference in New Issue
Block a user