Initial commit - Migrated to Dev environment
This commit is contained in:
24
backend/Dockerfile
Executable file
24
backend/Dockerfile
Executable file
@@ -0,0 +1,24 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 1. Rendszerfüggőségek telepítése
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
python3-dev \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 2. PIP frissítése (Ez némítja el a panaszkodást)
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
# 3. Függőségek telepítése
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 4. A kód másolása
|
||||
COPY . .
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
95
backend/_legacy_backup/build_complex_db.py
Executable file
95
backend/_legacy_backup/build_complex_db.py
Executable file
@@ -0,0 +1,95 @@
|
||||
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())
|
||||
51
backend/_legacy_backup/check_garage.py
Executable file
51
backend/_legacy_backup/check_garage.py
Executable file
@@ -0,0 +1,51 @@
|
||||
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())
|
||||
35
backend/_legacy_backup/create_demo_user.py
Executable file
35
backend/_legacy_backup/create_demo_user.py
Executable file
@@ -0,0 +1,35 @@
|
||||
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())
|
||||
42
backend/_legacy_backup/create_dummy_employee.py
Executable file
42
backend/_legacy_backup/create_dummy_employee.py
Executable file
@@ -0,0 +1,42 @@
|
||||
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())
|
||||
|
||||
17
backend/_legacy_backup/docker-compose.backend.yml
Executable file
17
backend/_legacy_backup/docker-compose.backend.yml
Executable file
@@ -0,0 +1,17 @@
|
||||
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
|
||||
|
||||
81
backend/_legacy_backup/init_db.py
Executable file
81
backend/_legacy_backup/init_db.py
Executable file
@@ -0,0 +1,81 @@
|
||||
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())
|
||||
61
backend/_legacy_backup/inspect_db.py
Executable file
61
backend/_legacy_backup/inspect_db.py
Executable file
@@ -0,0 +1,61 @@
|
||||
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())
|
||||
69
backend/_legacy_backup/inspect_db_full.py
Executable file
69
backend/_legacy_backup/inspect_db_full.py
Executable file
@@ -0,0 +1,69 @@
|
||||
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}")
|
||||
81
backend/_legacy_backup/main.py
Executable file
81
backend/_legacy_backup/main.py
Executable file
@@ -0,0 +1,81 @@
|
||||
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")
|
||||
120
backend/_legacy_backup/main_2.py
Executable file
120
backend/_legacy_backup/main_2.py
Executable file
@@ -0,0 +1,120 @@
|
||||
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"}
|
||||
|
||||
49
backend/_legacy_backup/main_final.py
Executable file
49
backend/_legacy_backup/main_final.py
Executable file
@@ -0,0 +1,49 @@
|
||||
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)}
|
||||
52
backend/_legacy_backup/main_fixed.py
Executable file
52
backend/_legacy_backup/main_fixed.py
Executable file
@@ -0,0 +1,52 @@
|
||||
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)}
|
||||
71
backend/_legacy_backup/migrate_ref_data.py
Executable file
71
backend/_legacy_backup/migrate_ref_data.py
Executable file
@@ -0,0 +1,71 @@
|
||||
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())
|
||||
0
backend/_legacy_backup/teszt.txt
Executable file
0
backend/_legacy_backup/teszt.txt
Executable file
46
backend/_legacy_backup/update_audit_system.py
Executable file
46
backend/_legacy_backup/update_audit_system.py
Executable file
@@ -0,0 +1,46 @@
|
||||
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())
|
||||
56
backend/_legacy_backup/update_cost_categories.py
Executable file
56
backend/_legacy_backup/update_cost_categories.py
Executable file
@@ -0,0 +1,56 @@
|
||||
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())
|
||||
46
backend/_legacy_backup/update_db_i18n.py
Executable file
46
backend/_legacy_backup/update_db_i18n.py
Executable file
@@ -0,0 +1,46 @@
|
||||
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())
|
||||
26
backend/_legacy_backup/update_docs.py
Executable file
26
backend/_legacy_backup/update_docs.py
Executable file
@@ -0,0 +1,26 @@
|
||||
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())
|
||||
58
backend/_legacy_backup/update_invitations.py
Executable file
58
backend/_legacy_backup/update_invitations.py
Executable file
@@ -0,0 +1,58 @@
|
||||
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())
|
||||
61
backend/_legacy_backup/update_permissions.py
Executable file
61
backend/_legacy_backup/update_permissions.py
Executable file
@@ -0,0 +1,61 @@
|
||||
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())
|
||||
149
backend/alembic.ini
Executable file
149
backend/alembic.ini
Executable file
@@ -0,0 +1,149 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = %(here)s/migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = postgresql+asyncpg://user:pass@postgres-db:5432/service_finder
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
0
backend/app/__init__.py
Executable file
0
backend/app/__init__.py
Executable file
BIN
backend/app/__pycache__/__init__.cpython-312.pyc
Executable file
BIN
backend/app/__pycache__/__init__.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/__pycache__/main.cpython-312.pyc
Executable file
BIN
backend/app/__pycache__/main.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/api/__pycache__/deps.cpython-312.pyc
Executable file
BIN
backend/app/api/__pycache__/deps.cpython-312.pyc
Executable file
Binary file not shown.
132
backend/app/api/auth.py
Executable file
132
backend/app/api/auth.py
Executable file
@@ -0,0 +1,132 @@
|
||||
from datetime import timedelta
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.core.config import settings
|
||||
from app.core.security import create_token, decode_token
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
@router.post("/login")
|
||||
def login(payload: Dict[str, Any]):
|
||||
"""
|
||||
payload:
|
||||
{
|
||||
"org_id": "<uuid>",
|
||||
"login": "<username or email>",
|
||||
"password": "<plain>"
|
||||
}
|
||||
"""
|
||||
from app.db.session import get_conn
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("BEGIN;")
|
||||
|
||||
org_id = (payload.get("org_id") or "").strip()
|
||||
login_id = (payload.get("login") or "").strip()
|
||||
password = payload.get("password") or ""
|
||||
|
||||
if not org_id or not login_id or not password:
|
||||
raise HTTPException(status_code=400, detail="org_id, login, password required")
|
||||
|
||||
# RLS miatt kötelező: org kontextus beállítás
|
||||
cur.execute("SELECT set_config('app.tenant_org_id', %s, false);", (org_id,))
|
||||
|
||||
# account + credential
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
a.account_id::text,
|
||||
a.org_id::text,
|
||||
a.username::text,
|
||||
a.email::text,
|
||||
c.password_hash,
|
||||
c.is_active
|
||||
FROM app.account a
|
||||
JOIN app.account_credential c ON c.account_id = a.account_id
|
||||
WHERE a.org_id = %s::uuid
|
||||
AND (a.username = %s::citext OR a.email = %s::citext)
|
||||
AND c.is_active = true
|
||||
LIMIT 1;
|
||||
""",
|
||||
(org_id, login_id, login_id),
|
||||
)
|
||||
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
account_id, org_id_db, username, email, password_hash, cred_active = row
|
||||
|
||||
# Jelszó ellenőrzés pgcrypto-val: crypt(plain, stored_hash) = stored_hash
|
||||
cur.execute("SELECT crypt(%s, %s) = %s;", (password, password_hash, password_hash))
|
||||
ok = cur.fetchone()[0]
|
||||
if not ok:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
# MVP: role később membershipből; most fixen tenant_admin
|
||||
role_code = "tenant_admin"
|
||||
is_platform_admin = False
|
||||
|
||||
access = create_token(
|
||||
{
|
||||
"sub": account_id,
|
||||
"org_id": org_id_db,
|
||||
"role": role_code,
|
||||
"is_platform_admin": is_platform_admin,
|
||||
"type": "access",
|
||||
},
|
||||
settings.JWT_SECRET,
|
||||
timedelta(minutes=settings.JWT_ACCESS_MINUTES),
|
||||
)
|
||||
|
||||
refresh = create_token(
|
||||
{
|
||||
"sub": account_id,
|
||||
"org_id": org_id_db,
|
||||
"role": role_code,
|
||||
"is_platform_admin": is_platform_admin,
|
||||
"type": "refresh",
|
||||
},
|
||||
settings.JWT_SECRET,
|
||||
timedelta(days=settings.JWT_REFRESH_DAYS),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer"}
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
def refresh_token(payload: Dict[str, Any]):
|
||||
token = payload.get("refresh_token") or ""
|
||||
if not token:
|
||||
raise HTTPException(status_code=400, detail="refresh_token required")
|
||||
|
||||
try:
|
||||
claims = decode_token(token, settings.JWT_SECRET)
|
||||
if claims.get("type") != "refresh":
|
||||
raise HTTPException(status_code=401, detail="Invalid refresh token type")
|
||||
|
||||
access = create_token(
|
||||
{
|
||||
"sub": claims.get("sub"),
|
||||
"org_id": claims.get("org_id"),
|
||||
"role": claims.get("role"),
|
||||
"is_platform_admin": claims.get("is_platform_admin", False),
|
||||
"type": "access",
|
||||
},
|
||||
settings.JWT_SECRET,
|
||||
timedelta(minutes=settings.JWT_ACCESS_MINUTES),
|
||||
)
|
||||
return {"access_token": access, "token_type": "bearer"}
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired refresh token")
|
||||
39
backend/app/api/deps.py
Executable file
39
backend/app/api/deps.py
Executable file
@@ -0,0 +1,39 @@
|
||||
from typing import Generator
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.session import SessionLocal
|
||||
from app.core.security import decode_token
|
||||
from app.models.user import User
|
||||
|
||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v2/auth/login")
|
||||
|
||||
async def get_db() -> Generator:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
token: str = Depends(reusable_oauth2),
|
||||
) -> User:
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token error")
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
|
||||
res = await db.execute(select(User).where(User.id == int(user_id)))
|
||||
user = res.scalars().first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Fiók nem aktív.")
|
||||
|
||||
return user
|
||||
14
backend/app/api/recommend.py
Executable file
14
backend/app/api/recommend.py
Executable file
@@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/provider/inbox")
|
||||
def provider_inbox(request: Request, provider_id: str):
|
||||
cur = request.state.db.cursor()
|
||||
cur.execute("""
|
||||
SELECT * FROM app.v_provider_inbox
|
||||
WHERE provider_listing_id = %s
|
||||
ORDER BY created_at DESC
|
||||
""", (provider_id,))
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
BIN
backend/app/api/v1/__pycache__/api.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/__pycache__/api.cpython-312.pyc
Executable file
Binary file not shown.
12
backend/app/api/v1/api.py
Executable file
12
backend/app/api/v1/api.py
Executable file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1.endpoints import auth, users, vehicles, billing, fleet, expenses, reports
|
||||
|
||||
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"])
|
||||
BIN
backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/api/v1/endpoints/__pycache__/vehicles.cpython-312.pyc
Executable file
BIN
backend/app/api/v1/endpoints/__pycache__/vehicles.cpython-312.pyc
Executable file
Binary file not shown.
79
backend/app/api/v1/endpoints/admin.py
Executable file
79
backend/app/api/v1/endpoints/admin.py
Executable file
@@ -0,0 +1,79 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api import deps
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.system_settings import SystemSetting # ÚJ import
|
||||
from app.models.gamification import PointRule, LevelConfig, RegionalSetting
|
||||
from app.models.translation import Translation
|
||||
from app.services.translation_service import TranslationService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def check_admin_access(current_user: User, required_roles: List[UserRole]):
|
||||
if current_user.role not in required_roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Nincs jogosultságod ehhez a művelethez."
|
||||
)
|
||||
|
||||
# --- ⚙️ ÚJ: DINAMIKUS RENDSZERBEÁLLÍTÁSOK (Pl. Jármű limit) ---
|
||||
|
||||
@router.get("/settings", response_model=List[dict])
|
||||
async def get_all_system_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user)
|
||||
):
|
||||
"""Az összes globális rendszerbeállítás listázása."""
|
||||
check_admin_access(current_user, [UserRole.SUPERUSER])
|
||||
result = await db.execute(select(SystemSetting))
|
||||
settings = result.scalars().all()
|
||||
return [{"key": s.key, "value": s.value, "description": s.description} for s in settings]
|
||||
|
||||
@router.put("/settings/{key}")
|
||||
async def update_system_setting(
|
||||
key: str,
|
||||
new_value: int, # Később lehet JSON is, ha komplexebb a beállítás
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user)
|
||||
):
|
||||
"""Egy adott beállítás (pl. FREE_VEHICLE_LIMIT) módosítása."""
|
||||
check_admin_access(current_user, [UserRole.SUPERUSER])
|
||||
|
||||
result = await db.execute(select(SystemSetting).where(SystemSetting.key == key))
|
||||
setting = result.scalar_one_or_none()
|
||||
|
||||
if not setting:
|
||||
raise HTTPException(status_code=404, detail="Beállítás nem található")
|
||||
|
||||
setting.value = new_value
|
||||
await db.commit()
|
||||
return {"status": "success", "key": key, "new_value": new_value}
|
||||
|
||||
|
||||
# --- 🌍 FORDÍTÁSOK KEZELÉSE (Meglévő kódod) ---
|
||||
|
||||
@router.post("/translations", status_code=status.HTTP_201_CREATED)
|
||||
async def add_translation_draft(
|
||||
key: str, lang: str, value: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user)
|
||||
):
|
||||
check_admin_access(current_user, [UserRole.SUPERUSER, UserRole.REGIONAL_ADMIN])
|
||||
new_t = Translation(key=key, lang_code=lang, value=value, is_published=False)
|
||||
db.add(new_t)
|
||||
await db.commit()
|
||||
return {"message": "Fordítás piszkozatként mentve. Ne felejtsd el publikálni!"}
|
||||
|
||||
@router.post("/translations/publish")
|
||||
async def publish_translations(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user)
|
||||
):
|
||||
check_admin_access(current_user, [UserRole.SUPERUSER, UserRole.REGIONAL_ADMIN])
|
||||
await TranslationService.publish_all(db)
|
||||
return {"message": "Sikeres publikálás! A változások minden szerveren élesedtek."}
|
||||
|
||||
91
backend/app/api/v1/endpoints/auth.py
Executable file
91
backend/app/api/v1/endpoints/auth.py
Executable file
@@ -0,0 +1,91 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register")
|
||||
async def register(
|
||||
request: Request,
|
||||
email: str,
|
||||
password: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
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!")
|
||||
|
||||
# 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.")
|
||||
|
||||
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!"}
|
||||
125
backend/app/api/v1/endpoints/billing.py
Executable file
125
backend/app/api/v1/endpoints/billing.py
Executable file
@@ -0,0 +1,125 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from typing import List, Dict
|
||||
import secrets
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 1. EGYENLEG LEKÉRDEZÉSE (A felhasználó Széfjéhez kötve)
|
||||
@router.get("/balance")
|
||||
async def get_balance(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Visszaadja a felhasználó aktuális kreditegyenlegét és a Széfje (Cége) nevét.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
uc.balance,
|
||||
c.name as company_name
|
||||
FROM data.user_credits uc
|
||||
JOIN data.companies c ON uc.user_id = c.owner_id
|
||||
WHERE uc.user_id = :user_id
|
||||
LIMIT 1
|
||||
""")
|
||||
result = await db.execute(query, {"user_id": current_user.id})
|
||||
row = result.fetchone()
|
||||
|
||||
if not row:
|
||||
return {
|
||||
"company_name": "Privát Széf",
|
||||
"balance": 0.0,
|
||||
"currency": "Credit"
|
||||
}
|
||||
|
||||
return {
|
||||
"company_name": row.company_name,
|
||||
"balance": float(row.balance),
|
||||
"currency": "Credit"
|
||||
}
|
||||
|
||||
# 2. TRANZAKCIÓS ELŐZMÉNYEK
|
||||
@router.get("/history")
|
||||
async def get_history(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Kilistázza a kreditmozgásokat (bevételek, költések, voucherek).
|
||||
"""
|
||||
query = text("""
|
||||
SELECT amount, reason, created_at
|
||||
FROM data.credit_transactions
|
||||
WHERE user_id = :user_id
|
||||
ORDER BY created_at DESC
|
||||
""")
|
||||
result = await db.execute(query, {"user_id": current_user.id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
|
||||
# 3. VOUCHER BEVÁLTÁS (A rendszer gazdaságának motorja)
|
||||
@router.post("/vouchers/redeem")
|
||||
async def redeem_voucher(code: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Bevált egy kódot, és jóváírja az értékét a felhasználó egyenlegén.
|
||||
"""
|
||||
# 1. Voucher ellenőrzése
|
||||
check_query = text("""
|
||||
SELECT id, value, is_used, expires_at
|
||||
FROM data.vouchers
|
||||
WHERE code = :code AND is_used = False AND (expires_at > now() OR expires_at IS NULL)
|
||||
""")
|
||||
res = await db.execute(check_query, {"code": code.strip().upper()})
|
||||
voucher = res.fetchone()
|
||||
|
||||
if not voucher:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen, lejárt vagy már felhasznált kód.")
|
||||
|
||||
# 2. Egyenleg frissítése (vagy létrehozása, ha még nincs sor a user_credits-ben)
|
||||
update_balance = text("""
|
||||
INSERT INTO data.user_credits (user_id, balance)
|
||||
VALUES (:u, :v)
|
||||
ON CONFLICT (user_id) DO UPDATE SET balance = data.user_credits.balance + :v
|
||||
""")
|
||||
await db.execute(update_balance, {"u": current_user.id, "v": voucher.value})
|
||||
|
||||
# 3. Tranzakció naplózása
|
||||
log_transaction = text("""
|
||||
INSERT INTO data.credit_transactions (user_id, amount, reason)
|
||||
VALUES (:u, :v, :r)
|
||||
""")
|
||||
await db.execute(log_transaction, {
|
||||
"u": current_user.id,
|
||||
"v": voucher.value,
|
||||
"r": f"Voucher beváltva: {code}"
|
||||
})
|
||||
|
||||
# 4. Voucher megjelölése felhasználtként
|
||||
await db.execute(text("""
|
||||
UPDATE data.vouchers
|
||||
SET is_used = True, used_by = :u, used_at = now()
|
||||
WHERE id = :vid
|
||||
"""), {"u": current_user.id, "vid": voucher.id})
|
||||
|
||||
await db.commit()
|
||||
return {"status": "success", "added_value": float(voucher.value), "message": "Kredit jóváírva!"}
|
||||
|
||||
# 4. ADMIN: VOUCHER GENERÁLÁS (Csak Neked)
|
||||
@router.post("/vouchers/generate", include_in_schema=True)
|
||||
async def generate_vouchers(
|
||||
count: int = 1,
|
||||
value: float = 500.0,
|
||||
batch_name: str = "ADMIN_GEN",
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Tömeges voucher generálás az admin felületről.
|
||||
"""
|
||||
generated_codes = []
|
||||
for _ in range(count):
|
||||
# Generálunk egy SF-XXXX-XXXX formátumú kódot
|
||||
code = f"SF-{secrets.token_hex(3).upper()}-{secrets.token_hex(3).upper()}"
|
||||
await db.execute(text("""
|
||||
INSERT INTO data.vouchers (code, value, batch_id, expires_at)
|
||||
VALUES (:c, :v, :b, now() + interval '90 days')
|
||||
"""), {"c": code, "v": value, "b": batch_name})
|
||||
generated_codes.append(code)
|
||||
|
||||
await db.commit()
|
||||
return {"batch": batch_name, "count": count, "codes": generated_codes}
|
||||
51
backend/app/api/v1/endpoints/expenses.py
Executable file
51
backend/app/api/v1/endpoints/expenses.py
Executable file
@@ -0,0 +1,51 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from pydantic import BaseModel
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ExpenseCreate(BaseModel):
|
||||
vehicle_id: str
|
||||
category: str # Pl: REFUELING, SERVICE, INSURANCE
|
||||
amount: float
|
||||
date: date
|
||||
odometer_value: Optional[float] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
@router.post("/add")
|
||||
async def add_expense(
|
||||
expense: ExpenseCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Új költség rögzítése egy járműhöz.
|
||||
"""
|
||||
# 1. Ellenőrizzük, hogy a jármű létezik-e
|
||||
query = text("SELECT id FROM data.vehicles WHERE id = :v_id")
|
||||
res = await db.execute(query, {"v_id": expense.vehicle_id})
|
||||
if not res.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
||||
|
||||
# 2. Beszúrás a vehicle_expenses táblába
|
||||
insert_query = text("""
|
||||
INSERT INTO data.vehicle_expenses
|
||||
(vehicle_id, category, amount, date, odometer_value, description)
|
||||
VALUES (:v_id, :cat, :amt, :date, :odo, :desc)
|
||||
""")
|
||||
|
||||
await db.execute(insert_query, {
|
||||
"v_id": expense.vehicle_id,
|
||||
"cat": expense.category,
|
||||
"amt": expense.amount,
|
||||
"date": expense.date,
|
||||
"odo": expense.odometer_value,
|
||||
"desc": expense.description
|
||||
})
|
||||
|
||||
await db.commit()
|
||||
return {"status": "success", "message": "Költség rögzítve."}
|
||||
46
backend/app/api/v1/endpoints/fleet.py
Executable file
46
backend/app/api/v1/endpoints/fleet.py
Executable file
@@ -0,0 +1,46 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.vehicle import Vehicle
|
||||
from app.models.company import CompanyMember
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/vehicles")
|
||||
async def get_my_vehicles(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
# Megkeressük a cégeket (széfeket), amikhez a felhasználónak köze van
|
||||
company_query = select(CompanyMember.company_id).where(CompanyMember.user_id == current_user.id)
|
||||
company_res = await db.execute(company_query)
|
||||
company_ids = company_res.scalars().all()
|
||||
|
||||
if not company_ids:
|
||||
return []
|
||||
|
||||
# Lekérjük az összes járművet, ami ezekhez a cégekhez tartozik
|
||||
query = select(Vehicle).where(Vehicle.current_company_id.in_(company_ids))
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/vehicles")
|
||||
async def add_vehicle(vehicle_in: dict, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
# Itt a meglévő logika fut tovább, de a Vehicle-t a user alapértelmezett cégéhez kötjük
|
||||
# Először lekérjük a user "owner" típusú cégét
|
||||
org_query = text("SELECT company_id FROM data.company_members WHERE user_id = :uid AND role = 'owner' LIMIT 1")
|
||||
org_res = await db.execute(org_query, {"uid": current_user.id})
|
||||
company_id = org_res.scalar()
|
||||
|
||||
if not company_id:
|
||||
raise HTTPException(status_code=404, detail="Nem található saját széf a jármű rögzítéséhez.")
|
||||
|
||||
# Új jármű létrehozása az új modell alapján
|
||||
new_vehicle = Vehicle(
|
||||
current_company_id=company_id,
|
||||
brand_id=vehicle_in.get("brand_id"),
|
||||
model_name=vehicle_in.get("model_name"),
|
||||
identification_number=vehicle_in.get("vin"),
|
||||
license_plate=vehicle_in.get("license_plate")
|
||||
)
|
||||
db.add(new_vehicle)
|
||||
await db.commit()
|
||||
return {"status": "success", "vehicle_id": str(new_vehicle.id)}
|
||||
54
backend/app/api/v1/endpoints/gamification.py
Executable file
54
backend/app/api/v1/endpoints/gamification.py
Executable file
@@ -0,0 +1,54 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api import deps
|
||||
from app.models.user import User
|
||||
from app.models.gamification import UserStats, UserBadge, Badge
|
||||
from app.schemas.social import UserStatSchema, BadgeSchema # Itt feltételezzük, hogy a sémákat már létrehoztad
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/my-stats", response_model=UserStatSchema)
|
||||
async def get_my_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user)
|
||||
):
|
||||
"""
|
||||
A bejelentkezett felhasználó aktuális pontszámának és szintjének lekérése.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
)
|
||||
stats = result.scalar_one_or_none()
|
||||
|
||||
if not stats:
|
||||
# Ha még nincs statisztika (új user), visszaadunk egy alapértelmezett kezdő állapotot
|
||||
return {
|
||||
"total_points": 0,
|
||||
"current_level": 1,
|
||||
"last_updated": None
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
@router.get("/my-badges", response_model=List[BadgeSchema])
|
||||
async def get_my_badges(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user)
|
||||
):
|
||||
"""
|
||||
A felhasználó által eddig megszerzett összes jelvény listázása.
|
||||
"""
|
||||
# Összekapcsoljuk a Badge és UserBadge táblákat
|
||||
query = (
|
||||
select(Badge.name, Badge.description, UserBadge.earned_at)
|
||||
.join(UserBadge, Badge.id == UserBadge.badge_id)
|
||||
.where(UserBadge.user_id == current_user.id)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
# Az .all() itt Tuple-öket ad vissza, amiket a Pydantic automatikusan validál
|
||||
return result.all()
|
||||
12
backend/app/api/v1/endpoints/providers.py
Executable file
12
backend/app/api/v1/endpoints/providers.py
Executable file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.schemas.social import ServiceProviderCreate, ServiceProviderResponse
|
||||
from app.services.social_service import create_service_provider
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=ServiceProviderResponse)
|
||||
async def add_provider(provider_data: ServiceProviderCreate, db: AsyncSession = Depends(get_db)):
|
||||
user_id = 2
|
||||
return await create_service_provider(db, provider_data, user_id)
|
||||
50
backend/app/api/v1/endpoints/reports.py
Executable file
50
backend/app/api/v1/endpoints/reports.py
Executable file
@@ -0,0 +1,50 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
|
||||
router = APIRouter() # EZ HIÁNYZOTT!
|
||||
|
||||
@router.get("/summary/{vehicle_id}")
|
||||
async def get_vehicle_summary(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Összesített jelentés egy járműhöz: kategóriánkénti költségek.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
category,
|
||||
SUM(amount) as total_amount,
|
||||
COUNT(*) as transaction_count
|
||||
FROM data.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY category
|
||||
""")
|
||||
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
rows = result.fetchall()
|
||||
|
||||
total_cost = sum(row.total_amount for row in rows) if rows else 0
|
||||
|
||||
return {
|
||||
"vehicle_id": vehicle_id,
|
||||
"total_cost": float(total_cost),
|
||||
"breakdown": [dict(row._mapping) for row in rows]
|
||||
}
|
||||
|
||||
@router.get("/trends/{vehicle_id}")
|
||||
async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Visszaadja az utolsó 6 hónap költéseit havi bontásban.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
SUM(amount) as monthly_total
|
||||
FROM data.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
LIMIT 6
|
||||
""")
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
72
backend/app/api/v1/endpoints/search.py
Executable file
72
backend/app/api/v1/endpoints/search.py
Executable file
@@ -0,0 +1,72 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.services.matching_service import matching_service
|
||||
from app.services.config_service import config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/match")
|
||||
async def match_service(
|
||||
lat: float,
|
||||
lng: float,
|
||||
radius: int = 20,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
# 1. SQL lekérdezés: Haversine-formula a távolság számításhoz
|
||||
# 6371 a Föld sugara km-ben
|
||||
query = text("""
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
ol.latitude,
|
||||
ol.longitude,
|
||||
ol.label as location_name,
|
||||
(6371 * 2 * ASIN(SQRT(
|
||||
POWER(SIN((RADIANS(ol.latitude) - RADIANS(:lat)) / 2), 2) +
|
||||
COS(RADIANS(:lat)) * COS(RADIANS(ol.latitude)) *
|
||||
POWER(SIN((RADIANS(ol.longitude) - RADIANS(:lng)) / 2), 2)
|
||||
))) AS distance
|
||||
FROM data.organizations o
|
||||
JOIN data.organization_locations ol ON o.id = ol.organization_id
|
||||
WHERE o.org_type = 'SERVICE'
|
||||
AND o.is_active = True
|
||||
HAVING
|
||||
(6371 * 2 * ASIN(SQRT(
|
||||
POWER(SIN((RADIANS(ol.latitude) - RADIANS(:lat)) / 2), 2) +
|
||||
COS(RADIANS(:lat)) * COS(RADIANS(ol.latitude)) *
|
||||
POWER(SIN((RADIANS(ol.longitude) - RADIANS(:lng)) / 2), 2)
|
||||
))) <= :radius
|
||||
ORDER BY distance ASC
|
||||
""")
|
||||
|
||||
result = await db.execute(query, {"lat": lat, "lng": lng, "radius": radius})
|
||||
|
||||
# Adatok átalakítása a MatchingService számára (mock rating-et adunk hozzá, amíg nincs review tábla)
|
||||
services_to_rank = []
|
||||
for row in result.all():
|
||||
services_to_rank.append({
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"distance": row.distance,
|
||||
"rating": 4.5, # Alapértelmezett, amíg nincs kész az értékelési rendszer
|
||||
"tier": "gold" if row.id == 1 else "free" # Példa logika
|
||||
})
|
||||
|
||||
if not services_to_rank:
|
||||
return {"status": "no_results", "message": "Nem található szerviz a megadott körzetben."}
|
||||
|
||||
# 2. Limit lekérése a beállításokból
|
||||
limit = await config.get_setting('match_limit_default', default=5)
|
||||
|
||||
# 3. Okos rangsorolás (Admin súlyozás alapján)
|
||||
ranked_results = await matching_service.rank_services(services_to_rank)
|
||||
|
||||
return {
|
||||
"user_location": {"lat": lat, "lng": lng},
|
||||
"radius_km": radius,
|
||||
"results": ranked_results[:limit]
|
||||
}
|
||||
15
backend/app/api/v1/endpoints/social.py
Executable file
15
backend/app/api/v1/endpoints/social.py
Executable file
@@ -0,0 +1,15 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.services.social_service import vote_for_provider, get_leaderboard
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||
return await get_leaderboard(db, limit)
|
||||
|
||||
@router.post("/vote/{provider_id}")
|
||||
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
|
||||
user_id = 2
|
||||
return await vote_for_provider(db, user_id, provider_id, vote_value)
|
||||
16
backend/app/api/v1/endpoints/users.py
Executable file
16
backend/app/api/v1/endpoints/users.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def read_users_me(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Visszaadja a bejelentkezett felhasználó profilját"""
|
||||
return current_user
|
||||
23
backend/app/api/v1/endpoints/vehicle_search.py
Executable file
23
backend/app/api/v1/endpoints/vehicle_search.py
Executable file
@@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.session import get_db
|
||||
from app.models.vehicle import VehicleBrand # Feltételezve, hogy létezik a modell
|
||||
from typing import List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/search/brands")
|
||||
def search_brands(q: str = Query(..., min_length=2), db: Session = Depends(get_db)):
|
||||
# 1. KERESÉS A SAJÁT ADATBÁZISBAN
|
||||
results = db.query(VehicleBrand).filter(
|
||||
VehicleBrand.name.ilike(f"%{q}%"),
|
||||
VehicleBrand.is_active == True
|
||||
).limit(10).all()
|
||||
|
||||
# 2. HA NINCS TALÁLAT, INDÍTHATJUK A BOT-OT (Logikai váz)
|
||||
if not results:
|
||||
# Itt hívnánk meg a Discovery Bot-ot async módon
|
||||
# discovery_bot.find_brand_remotely(q)
|
||||
return {"status": "not_found", "message": "Nincs találat, a Bot elindult keresni...", "data": []}
|
||||
|
||||
return {"status": "success", "data": results}
|
||||
59
backend/app/api/v1/endpoints/vehicles.py
Executable file
59
backend/app/api/v1/endpoints/vehicles.py
Executable file
@@ -0,0 +1,59 @@
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from typing import List, Dict, Optional
|
||||
from app.models.vehicle import Vehicle
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/search/brands")
|
||||
async def search_brands(q: str = Query(..., min_length=2), db: AsyncSession = Depends(get_db)):
|
||||
query = text("""
|
||||
SELECT id, name, slug, country_of_origin
|
||||
FROM data.vehicle_brands
|
||||
WHERE (name ILIKE :q OR slug ILIKE :q) AND is_active = true
|
||||
ORDER BY name ASC LIMIT 10
|
||||
""")
|
||||
# 1. Megvárjuk az execute-ot
|
||||
result = await db.execute(query, {"q": f"%{q}%"})
|
||||
# 2. Külön hívjuk a fetchall-t az eredményen
|
||||
rows = result.fetchall()
|
||||
|
||||
brand_list = [dict(row._mapping) for row in rows]
|
||||
if not brand_list:
|
||||
return {"status": "discovery_mode", "data": []}
|
||||
return {"status": "success", "data": brand_list}
|
||||
|
||||
@router.get("/search/providers")
|
||||
async def search_providers(q: str = Query(..., min_length=2), db: AsyncSession = Depends(get_db)):
|
||||
query = text("""
|
||||
SELECT id, name, technical_rating_pct, location_city, service_type
|
||||
FROM data.service_providers
|
||||
WHERE (name ILIKE :q OR service_type ILIKE :q) AND is_active = true
|
||||
ORDER BY technical_rating_pct DESC LIMIT 15
|
||||
""")
|
||||
result = await db.execute(query, {"q": f"%{q}%"})
|
||||
rows = result.fetchall()
|
||||
return {"status": "success", "data": [dict(row._mapping) for row in rows]}
|
||||
|
||||
@router.post("/register")
|
||||
async def register_user_vehicle(data: dict, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
company_res = await db.execute(text("SELECT id FROM data.companies WHERE owner_id = :u LIMIT 1"), {"u": current_user.id})
|
||||
company = company_res.fetchone()
|
||||
if not company:
|
||||
raise HTTPException(status_code=404, detail="Széf nem található.")
|
||||
|
||||
new_vehicle = Vehicle(
|
||||
current_company_id=company.id,
|
||||
brand_id=data.get("brand_id"),
|
||||
model_name=data.get("model_name"),
|
||||
engine_spec_id=data.get("engine_spec_id"),
|
||||
identification_number=data.get("vin"),
|
||||
license_plate=data.get("plate"),
|
||||
tracking_mode=data.get("tracking_mode", "km"),
|
||||
total_real_usage=data.get("current_odo", 0)
|
||||
)
|
||||
db.add(new_vehicle)
|
||||
await db.commit()
|
||||
return {"status": "success", "vehicle_id": str(new_vehicle.id)}
|
||||
12
backend/app/api/v1/router.py
Executable file
12
backend/app/api/v1/router.py
Executable file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
import os
|
||||
import subprocess
|
||||
from app.api.v1 import social, users, fleet, auth
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
api_router.include_router(social.router, prefix="/social", tags=["Social & Providers"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
api_router.include_router(fleet.router, prefix="/fleet", tags=["Fleet & Logistics"])
|
||||
BIN
backend/app/api/v2/__pycache__/auth.cpython-312.pyc
Executable file
BIN
backend/app/api/v2/__pycache__/auth.cpython-312.pyc
Executable file
Binary file not shown.
262
backend/app/api/v2/auth.py
Executable file
262
backend/app/api/v2/auth.py
Executable file
@@ -0,0 +1,262 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request, status, Query
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text, select
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import get_password_hash, verify_password, create_access_token
|
||||
from app.api.deps import get_db
|
||||
from app.models.user import User
|
||||
from app.models.company import Company
|
||||
from app.services.email_manager import email_manager
|
||||
|
||||
router = APIRouter(prefix="", tags=["Authentication V2"])
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Pydantic request models
|
||||
# -----------------------
|
||||
class RegisterIn(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=1, max_length=200) # policy endpointben
|
||||
first_name: str = Field(min_length=1, max_length=80)
|
||||
last_name: str = Field(min_length=1, max_length=80)
|
||||
|
||||
|
||||
class ResetPasswordIn(BaseModel):
|
||||
token: str
|
||||
new_password: str = Field(min_length=1, max_length=200)
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Helpers
|
||||
# -----------------------
|
||||
def _hash_token(token: str) -> str:
|
||||
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _enforce_password_policy(password: str) -> None:
|
||||
# Most: teszt policy (min length), később bővítjük nagybetű/szám/special szabályokra
|
||||
min_len = int(getattr(settings, "PASSWORD_MIN_LENGTH", 4) or 4)
|
||||
if len(password) < min_len:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "password_policy_failed",
|
||||
"message": "A jelszó nem felel meg a biztonsági szabályoknak.",
|
||||
"rules": [f"Minimum hossz: {min_len} karakter"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _mark_token_used(db: AsyncSession, token_id: int) -> None:
|
||||
await db.execute(
|
||||
text(
|
||||
"UPDATE data.verification_tokens "
|
||||
"SET is_used = true, used_at = now() "
|
||||
"WHERE id = :id"
|
||||
),
|
||||
{"id": token_id},
|
||||
)
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Endpoints
|
||||
# -----------------------
|
||||
@router.post("/register")
|
||||
async def register(payload: RegisterIn, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
_enforce_password_policy(payload.password)
|
||||
|
||||
# email unique (később: soft-delete esetén engedjük az újra-reget a szabályaid szerint)
|
||||
res = await db.execute(select(User).where(User.email == payload.email))
|
||||
if res.scalars().first():
|
||||
raise HTTPException(status_code=400, detail="Ez az e-mail cím már foglalt.")
|
||||
|
||||
# create inactive user
|
||||
new_user = User(
|
||||
email=payload.email,
|
||||
hashed_password=get_password_hash(payload.password),
|
||||
first_name=payload.first_name,
|
||||
last_name=payload.last_name,
|
||||
is_active=False,
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.flush()
|
||||
|
||||
# create default private company
|
||||
new_company = Company(name=f"{payload.first_name} Privát Széfje", owner_id=new_user.id)
|
||||
db.add(new_company)
|
||||
await db.flush()
|
||||
|
||||
# membership (enum miatt raw SQL)
|
||||
await db.execute(
|
||||
text(
|
||||
"INSERT INTO data.company_members (company_id, user_id, role, is_active) "
|
||||
"VALUES (:c, :u, 'owner'::data.companyrole, true)"
|
||||
),
|
||||
{"c": new_company.id, "u": new_user.id},
|
||||
)
|
||||
|
||||
# verification token (store hash only)
|
||||
token = secrets.token_urlsafe(48)
|
||||
token_hash = _hash_token(token)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(hours=48)
|
||||
|
||||
await db.execute(
|
||||
text(
|
||||
"INSERT INTO data.verification_tokens (user_id, token_hash, token_type, expires_at, is_used) "
|
||||
"VALUES (:u, :t, 'email_verify'::data.tokentype, :e, false)"
|
||||
),
|
||||
{"u": new_user.id, "t": token_hash, "e": expires_at},
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Send email (best-effort)
|
||||
try:
|
||||
link = f"{settings.FRONTEND_BASE_URL}/verify?token={token}"
|
||||
await email_manager.send_email(
|
||||
payload.email,
|
||||
"registration",
|
||||
{"first_name": payload.first_name, "link": link},
|
||||
user_id=new_user.id,
|
||||
)
|
||||
except Exception:
|
||||
# tesztben nem állítjuk meg a regisztrációt email hiba miatt
|
||||
pass
|
||||
|
||||
return {"message": "Sikeres regisztráció! Kérlek aktiváld az emailedet."}
|
||||
|
||||
|
||||
@router.get("/verify")
|
||||
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
|
||||
token_hash = _hash_token(token)
|
||||
|
||||
res = await db.execute(
|
||||
text(
|
||||
"SELECT id, user_id, expires_at, is_used "
|
||||
"FROM data.verification_tokens "
|
||||
"WHERE token_hash = :h "
|
||||
" AND token_type = 'email_verify'::data.tokentype "
|
||||
" AND is_used = false "
|
||||
"LIMIT 1"
|
||||
),
|
||||
{"h": token_hash},
|
||||
)
|
||||
row = res.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen vagy már felhasznált token.")
|
||||
|
||||
# expired? -> mark used (audit) and reject
|
||||
if row.expires_at is not None and row.expires_at < datetime.now(timezone.utc):
|
||||
await _mark_token_used(db, row.id)
|
||||
await db.commit()
|
||||
raise HTTPException(status_code=400, detail="A token lejárt. Kérj újat.")
|
||||
|
||||
# activate user
|
||||
await db.execute(
|
||||
text("UPDATE data.users SET is_active = true WHERE id = :u"),
|
||||
{"u": row.user_id},
|
||||
)
|
||||
|
||||
# mark used (one-time)
|
||||
await _mark_token_used(db, row.id)
|
||||
|
||||
await db.commit()
|
||||
return {"message": "Fiók aktiválva. Most már be tudsz jelentkezni."}
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):
|
||||
res = await db.execute(
|
||||
text("SELECT id, hashed_password, is_active, is_superuser FROM data.users WHERE email = :e"),
|
||||
{"e": form_data.username},
|
||||
)
|
||||
u = res.fetchone()
|
||||
|
||||
if not u or not verify_password(form_data.password, u.hashed_password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Hibás hitelesítés.")
|
||||
|
||||
if not u.is_active:
|
||||
raise HTTPException(status_code=400, detail="Fiók nem aktív.")
|
||||
|
||||
token = create_access_token({"sub": str(u.id), "is_admin": bool(u.is_superuser)})
|
||||
return {"access_token": token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(email: EmailStr = Query(...), db: AsyncSession = Depends(get_db)):
|
||||
# Anti-enumeration: mindig ugyanazt válaszoljuk
|
||||
res = await db.execute(select(User).where(User.email == email))
|
||||
user = res.scalars().first()
|
||||
|
||||
if user:
|
||||
token = secrets.token_urlsafe(48)
|
||||
token_hash = _hash_token(token)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(hours=2)
|
||||
|
||||
await db.execute(
|
||||
text(
|
||||
"INSERT INTO data.verification_tokens (user_id, token_hash, token_type, expires_at, is_used) "
|
||||
"VALUES (:u, :t, 'password_reset'::data.tokentype, :e, false)"
|
||||
),
|
||||
{"u": user.id, "t": token_hash, "e": expires_at},
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token}"
|
||||
try:
|
||||
await email_manager.send_email(
|
||||
email,
|
||||
"password_reset",
|
||||
{"first_name": user.first_name or "", "link": link},
|
||||
user_id=user.id,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"message": "Ha a cím létezik, a helyreállító levelet elküldtük."}
|
||||
|
||||
|
||||
@router.post("/reset-password-confirm")
|
||||
async def reset_password_confirm(payload: ResetPasswordIn, db: AsyncSession = Depends(get_db)):
|
||||
_enforce_password_policy(payload.new_password)
|
||||
|
||||
token_hash = _hash_token(payload.token)
|
||||
|
||||
res = await db.execute(
|
||||
text(
|
||||
"SELECT id, user_id, expires_at, is_used "
|
||||
"FROM data.verification_tokens "
|
||||
"WHERE token_hash = :h "
|
||||
" AND token_type = 'password_reset'::data.tokentype "
|
||||
" AND is_used = false "
|
||||
"LIMIT 1"
|
||||
),
|
||||
{"h": token_hash},
|
||||
)
|
||||
row = res.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen vagy már felhasznált token.")
|
||||
|
||||
if row.expires_at is not None and row.expires_at < datetime.now(timezone.utc):
|
||||
await _mark_token_used(db, row.id)
|
||||
await db.commit()
|
||||
raise HTTPException(status_code=400, detail="A token lejárt. Kérj újat.")
|
||||
|
||||
new_hash = get_password_hash(payload.new_password)
|
||||
|
||||
await db.execute(
|
||||
text("UPDATE data.users SET hashed_password = :p WHERE id = :u"),
|
||||
{"p": new_hash, "u": row.user_id},
|
||||
)
|
||||
|
||||
await _mark_token_used(db, row.id)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Jelszó sikeresen megváltoztatva."}
|
||||
240
backend/app/auth/router.py
Executable file
240
backend/app/auth/router.py
Executable file
@@ -0,0 +1,240 @@
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Header
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from passlib.context import CryptContext
|
||||
from jose import JWTError, jwt
|
||||
import redis.asyncio as redis
|
||||
|
||||
# --- KONFIGURÁCIÓ ---
|
||||
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/service_finder_db"
|
||||
REDIS_URL = "redis://localhost:6379"
|
||||
SECRET_KEY = "szuper_titkos_jwt_kulcs_amit_env_bol_kellene_olvasni"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
REFRESH_TOKEN_EXPIRE_DAYS = 7
|
||||
|
||||
# --- ADATBÁZIS SETUP (SQLAlchemy 2.0) ---
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
|
||||
# --- REDIS SETUP ---
|
||||
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
|
||||
# --- SECURITY UTILS ---
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v2/auth/login")
|
||||
|
||||
class ClientType(str, Enum):
|
||||
WEB = "web"
|
||||
MOBILE = "mobile"
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_token(data: dict, expires_delta: timedelta):
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
# --- PYDANTIC SCHEMAS ---
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: int
|
||||
email: EmailStr
|
||||
is_active: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str # OAuth2 form compatibility miatt username, de emailt várunk
|
||||
password: str
|
||||
client_type: ClientType # 'web' vagy 'mobile'
|
||||
|
||||
# --- ÜZLETI LOGIKA & ROUTER ---
|
||||
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
# 1. Email ellenőrzése
|
||||
stmt = select(User).where(User.email == user.email)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalars().first():
|
||||
raise HTTPException(status_code=400, detail="Ez az email cím már regisztrálva van.")
|
||||
|
||||
# 2. User létrehozása (inaktív)
|
||||
hashed_pwd = get_password_hash(user.password)
|
||||
new_user = User(email=user.email, password_hash=hashed_pwd, is_active=False)
|
||||
|
||||
db.add(new_user)
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
|
||||
# Itt kellene elküldeni az emailt a verify linkkel (most szimuláljuk)
|
||||
return new_user
|
||||
|
||||
@router.get("/verify/{token}")
|
||||
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
|
||||
# Megjegyzés: A valóságban a token-t dekódolni kellene, hogy kinyerjük a user ID-t.
|
||||
# Most szimuláljuk, hogy a token valójában a user email-címe base64-ben vagy hasonló.
|
||||
# Egyszerűsítés a példa kedvéért: feltételezzük, hogy a token = user_id
|
||||
|
||||
try:
|
||||
user_id = int(token) # DEMO ONLY
|
||||
stmt = select(User).where(User.id == user_id)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Felhasználó nem található")
|
||||
|
||||
user.is_active = True
|
||||
await db.commit()
|
||||
return {"message": "Fiók sikeresen aktiválva!"}
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen token")
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
client_type: ClientType = ClientType.WEB, # Query param vagy form field
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Kritikus Redis Session Limitáció implementációja.
|
||||
"""
|
||||
# 1. User keresése
|
||||
stmt = select(User).where(User.email == form_data.username)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user or not verify_password(form_data.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Hibás email vagy jelszó")
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=403, detail="A fiók még nincs aktiválva.")
|
||||
|
||||
# 2. Token generálás
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
refresh_token_expires = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
|
||||
# A tokenbe beleégetjük a client_type-ot is, hogy validálásnál ellenőrizhessük
|
||||
token_data = {"sub": str(user.id), "client_type": client_type.value}
|
||||
|
||||
access_token = create_token(token_data, access_token_expires)
|
||||
refresh_token = create_token({"sub": str(user.id), "type": "refresh"}, refresh_token_expires)
|
||||
|
||||
# 3. REDIS SESSION KEZELÉS (A feladat kritikus része)
|
||||
# Kulcs formátum: session:{user_id}:{client_type} -> access_token
|
||||
session_key = f"session:{user.id}:{client_type.value}"
|
||||
|
||||
# A Redis 'SET' parancsa felülírja a kulcsot, ha az már létezik.
|
||||
# Ez megvalósítja a "Logout other devices" logikát az AZONOS típusú eszközökre.
|
||||
# Ezzel egy időben, mivel a kulcs tartalmazza a típust (web/mobile),
|
||||
# garantáljuk, hogy max 1 web és 1 mobile lehet (külön kulcsok).
|
||||
|
||||
await redis_client.set(
|
||||
name=session_key,
|
||||
value=access_token,
|
||||
ex=ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
|
||||
# --- MIDDLEWARE / DEPENDENCY TOKEN ELLENŐRZÉSHEZ ---
|
||||
async def get_current_user(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Nem sikerült hitelesíteni a felhasználót",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
client_type: str = payload.get("client_type")
|
||||
|
||||
if user_id is None or client_type is None:
|
||||
raise credentials_exception
|
||||
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
# KRITIKUS: Token validálása Redis ellenében (Stateful JWT)
|
||||
# Ha a Redisben lévő token nem egyezik a küldött tokennel,
|
||||
# akkor a felhasználót kijelentkeztették egy másik eszközről.
|
||||
session_key = f"session:{user_id}:{client_type}"
|
||||
stored_token = await redis_client.get(session_key)
|
||||
|
||||
if stored_token != token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="A munkamenet lejárt vagy egy másik eszközről beléptek."
|
||||
)
|
||||
|
||||
stmt = select(User).where(User.id == int(user_id))
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalars().first()
|
||||
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
# --- MAIN APP ---
|
||||
app = FastAPI(title="Service Finder API")
|
||||
app.include_router(router)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Service Finder API fut"}
|
||||
|
||||
@app.get("/protected-route")
|
||||
async def protected(user: User = Depends(get_current_user)):
|
||||
|
||||
return {"message": f"Szia {user.email}, érvényes a munkameneted!"}
|
||||
|
||||
0
backend/app/core/__init__.py
Executable file
0
backend/app/core/__init__.py
Executable file
BIN
backend/app/core/__pycache__/__init__.cpython-312.pyc
Executable file
BIN
backend/app/core/__pycache__/__init__.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Executable file
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Executable file
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Executable file
Binary file not shown.
53
backend/app/core/config.py
Executable file
53
backend/app/core/config.py
Executable file
@@ -0,0 +1,53 @@
|
||||
from typing import Optional
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic import computed_field
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# --- General ---
|
||||
PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
|
||||
VERSION: str = "2.0.0"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
DEBUG: bool = False
|
||||
|
||||
# --- Security / JWT ---
|
||||
SECRET_KEY: str
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
|
||||
|
||||
# --- Password policy (TEST -> laza, PROD -> szigorú) ---
|
||||
PASSWORD_MIN_LENGTH: int = 4 # TESZT: 4, ÉLES: 10-12
|
||||
|
||||
# --- 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"
|
||||
EMAILS_FROM_NAME: str = "Profibot"
|
||||
|
||||
# SendGrid API
|
||||
SENDGRID_API_KEY: Optional[str] = None
|
||||
|
||||
# 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"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=True,
|
||||
extra="ignore"
|
||||
)
|
||||
|
||||
settings = Settings()
|
||||
10
backend/app/core/email.py
Executable file
10
backend/app/core/email.py
Executable file
@@ -0,0 +1,10 @@
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def send_verification_email(email_to: str, token: str, first_name: str):
|
||||
logger.info(f"MOCK EMAIL -> Címzett: {email_to}, Token: {token}")
|
||||
return True
|
||||
|
||||
async def send_new_account_email(email_to: str, username: str, password: str):
|
||||
logger.info(f"MOCK EMAIL -> Új fiók: {username}")
|
||||
return True
|
||||
18
backend/app/core/email.py.bak
Executable file
18
backend/app/core/email.py.bak
Executable file
@@ -0,0 +1,18 @@
|
||||
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
|
||||
33
backend/app/core/security.py
Executable file
33
backend/app/core/security.py
Executable file
@@ -0,0 +1,33 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
import bcrypt
|
||||
from jose import jwt, JWTError
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# --- JELSZÓ ---
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
try:
|
||||
if not hashed_password:
|
||||
return False
|
||||
return bcrypt.checkpw(
|
||||
plain_password.encode("utf-8"),
|
||||
hashed_password.encode("utf-8"),
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
salt = bcrypt.gensalt()
|
||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
||||
|
||||
# --- JWT ---
|
||||
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
to_encode = dict(data)
|
||||
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])
|
||||
0
backend/app/crud/__init__.py
Executable file
0
backend/app/crud/__init__.py
Executable file
11
backend/app/database.py
Executable file
11
backend/app/database.py
Executable file
@@ -0,0 +1,11 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
# A .env fájlból olvassuk majd, de teszthez:
|
||||
DATABASE_URL = "postgresql+asyncpg://user:password@db_container_name:5432/db_name"
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
SessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
0
backend/app/db/__init__.py
Executable file
0
backend/app/db/__init__.py
Executable file
BIN
backend/app/db/__pycache__/__init__.cpython-312.pyc
Executable file
BIN
backend/app/db/__pycache__/__init__.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/db/__pycache__/base.cpython-312.pyc
Executable file
BIN
backend/app/db/__pycache__/base.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/db/__pycache__/session.cpython-312.pyc
Executable file
BIN
backend/app/db/__pycache__/session.cpython-312.pyc
Executable file
Binary file not shown.
10
backend/app/db/base.py
Executable file
10
backend/app/db/base.py
Executable file
@@ -0,0 +1,10 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
"""
|
||||
Base class for all SQLAlchemy models.
|
||||
Includes AsyncAttrs to support async attribute access (lazy loading).
|
||||
"""
|
||||
pass
|
||||
38
backend/app/db/context.py
Executable file
38
backend/app/db/context.py
Executable file
@@ -0,0 +1,38 @@
|
||||
from typing import Generator, Optional, Dict, Any
|
||||
from fastapi import Request
|
||||
from app.db.session import get_conn
|
||||
|
||||
def _set_config(cur, key: str, value: str) -> None:
|
||||
cur.execute("SELECT set_config(%s, %s, false);", (key, value))
|
||||
|
||||
def db_tx(request: Request) -> Generator[Dict[str, Any], None, None]:
|
||||
"""
|
||||
Egységes DB tranzakció + session context:
|
||||
BEGIN
|
||||
set_config(app.tenant_org_id, app.account_id, app.is_platform_admin)
|
||||
COMMIT/ROLLBACK
|
||||
"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("BEGIN;")
|
||||
|
||||
claims: Optional[dict] = getattr(request.state, "claims", None)
|
||||
if claims:
|
||||
org_id = claims.get("org_id") or ""
|
||||
account_id = claims.get("sub") or ""
|
||||
is_platform_admin = claims.get("is_platform_admin", False)
|
||||
|
||||
# Fontos: set_config stringeket vár
|
||||
_set_config(cur, "app.tenant_org_id", str(org_id))
|
||||
_set_config(cur, "app.account_id", str(account_id))
|
||||
_set_config(cur, "app.is_platform_admin", "true" if is_platform_admin else "false")
|
||||
|
||||
yield {"conn": conn, "cur": cur}
|
||||
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
31
backend/app/db/middleware.py
Executable file
31
backend/app/db/middleware.py
Executable file
@@ -0,0 +1,31 @@
|
||||
from fastapi import Request
|
||||
from app.db.session import SessionLocal
|
||||
from app.services.config_service import config
|
||||
from sqlalchemy import text
|
||||
import json
|
||||
|
||||
async def audit_log_middleware(request: Request, call_next):
|
||||
logging_enabled = await config.get_setting('audit_log_enabled', default=True)
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
if logging_enabled and request.method != 'GET': # GET-et általában nem naplózunk a zaj miatt, de állítható
|
||||
try:
|
||||
user_id = getattr(request.state, 'user_id', None) # Ha már be van lépve
|
||||
|
||||
async with SessionLocal() as db:
|
||||
await db.execute(text("""
|
||||
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address)
|
||||
VALUES (:u, :a, :e, :m, :ip)
|
||||
"""), {
|
||||
'u': user_id,
|
||||
'a': f'API_CALL_{request.method}',
|
||||
'e': str(request.url.path),
|
||||
'm': request.method,
|
||||
'ip': request.client.host
|
||||
})
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass # A naplózás hibája nem akaszthatja meg a kiszolgálást
|
||||
|
||||
return response
|
||||
31
backend/app/db/session.py
Executable file
31
backend/app/db/session.py
Executable file
@@ -0,0 +1,31 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from app.core.config import settings
|
||||
from typing import AsyncGenerator
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL, # A te eredeti kulcsod
|
||||
echo=getattr(settings, "DEBUG", False),
|
||||
future=True,
|
||||
pool_size=20,
|
||||
max_overflow=10
|
||||
)
|
||||
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autoflush=False
|
||||
)
|
||||
# Ez a sor kell, mert a main.py és a többiek ezen a néven keresik
|
||||
SessionLocal = AsyncSessionLocal
|
||||
|
||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
37
backend/app/final_admin_fix.py
Executable file
37
backend/app/final_admin_fix.py
Executable file
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
from sqlalchemy import text
|
||||
from app.db.session import SessionLocal, engine
|
||||
from app.models.user import User, UserRole
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
async def run_fix():
|
||||
async with SessionLocal() as db:
|
||||
# 1. Ellenőrizzük az oszlopokat (biztonsági játék)
|
||||
res = await db.execute(text("SELECT column_name FROM information_schema.columns WHERE table_schema = \u0027data\u0027 AND table_name = \u0027users\u0027"))
|
||||
cols = [r[0] for r in res.fetchall()]
|
||||
print(f"INFO: Meglévő oszlopok: {cols}")
|
||||
|
||||
if "hashed_password" not in cols:
|
||||
print("❌ HIBA: A hashed_password oszlop még mindig hiányzik! A migráció nem volt sikeres.")
|
||||
return
|
||||
|
||||
# 2. Admin létrehozása
|
||||
res = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": "admin@profibot.hu"})
|
||||
if res.fetchone():
|
||||
print("⚠ Az admin@profibot.hu már létezik.")
|
||||
else:
|
||||
admin = User(
|
||||
email="admin@profibot.hu",
|
||||
hashed_password=get_password_hash("Admin123!"),
|
||||
first_name="Admin",
|
||||
last_name="Profibot",
|
||||
role=UserRole.ADMIN,
|
||||
is_superuser=True,
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin)
|
||||
await db.commit()
|
||||
print("✅ SIKER: Admin felhasználó létrehozva!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_fix())
|
||||
13
backend/app/init_db_direct.py
Executable file
13
backend/app/init_db_direct.py
Executable file
@@ -0,0 +1,13 @@
|
||||
import asyncio
|
||||
from app.db.base import Base
|
||||
from app.db.session import engine
|
||||
from app.models import * # Minden modellt beimportálunk
|
||||
|
||||
async def init_db():
|
||||
async with engine.begin() as conn:
|
||||
# Ez a parancs hozza létre a táblákat a modellek alapján
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
print("✅ Minden tábla sikeresen létrejött a 'data' sémában!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(init_db())
|
||||
45
backend/app/main.py
Executable file
45
backend/app/main.py
Executable file
@@ -0,0 +1,45 @@
|
||||
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
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
from app.db.session import engine
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data"))
|
||||
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",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
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_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ÚTVONALAK INTEGRÁCIÓJA
|
||||
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"}
|
||||
|
||||
24
backend/app/models/__init__.py
Executable file
24
backend/app/models/__init__.py
Executable file
@@ -0,0 +1,24 @@
|
||||
from app.db.base import Base
|
||||
from .user import User, UserRole
|
||||
from .company import Company, CompanyMember, VehicleAssignment
|
||||
from .vehicle import (
|
||||
Vehicle,
|
||||
VehicleOwnership,
|
||||
VehicleBrand,
|
||||
EngineSpec,
|
||||
ServiceProvider,
|
||||
ServiceRecord,
|
||||
VehicleCategory,
|
||||
VehicleModel,
|
||||
VehicleVariant
|
||||
)
|
||||
|
||||
# Alias a kompatibilitás kedvéért
|
||||
UserVehicle = Vehicle
|
||||
|
||||
__all__ = [
|
||||
"Base", "User", "UserRole", "Vehicle", "VehicleOwnership", "VehicleBrand",
|
||||
"EngineSpec", "ServiceProvider", "ServiceRecord", "Company",
|
||||
"CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory",
|
||||
"VehicleModel", "VehicleVariant"
|
||||
]
|
||||
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Executable file
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/models/__pycache__/company.cpython-312.pyc
Executable file
BIN
backend/app/models/__pycache__/company.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/models/__pycache__/user.cpython-312.pyc
Executable file
BIN
backend/app/models/__pycache__/user.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/models/__pycache__/vehicle.cpython-312.pyc
Executable file
BIN
backend/app/models/__pycache__/vehicle.cpython-312.pyc
Executable file
Binary file not shown.
63
backend/app/models/company.py
Executable file
63
backend/app/models/company.py
Executable file
@@ -0,0 +1,63 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
# A Python enum marad, de a Column definíciónál pontosítunk
|
||||
class CompanyRole(str, enum.Enum):
|
||||
OWNER = "owner"
|
||||
MANAGER = "manager"
|
||||
DRIVER = "driver"
|
||||
|
||||
class Company(Base):
|
||||
__tablename__ = "companies"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
tax_number = Column(String, nullable=True)
|
||||
subscription_tier = Column(String, default="free")
|
||||
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
members = relationship("CompanyMember", back_populates="company", cascade="all, delete-orphan")
|
||||
assignments = relationship("VehicleAssignment", back_populates="company")
|
||||
|
||||
class CompanyMember(Base):
|
||||
__tablename__ = "company_members"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
company_id = Column(Integer, ForeignKey("data.companies.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
# JAVÍTÁS: Kifejezetten megadjuk a natív Postgres típust
|
||||
role = Column(
|
||||
PG_ENUM('owner', 'manager', 'driver', name='companyrole', schema='data', create_type=False),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
can_edit_service = Column(Boolean, default=False)
|
||||
can_see_costs = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
company = relationship("Company", back_populates="members")
|
||||
user = relationship("User")
|
||||
|
||||
class VehicleAssignment(Base):
|
||||
__tablename__ = "vehicle_assignments"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
company_id = Column(Integer, ForeignKey("data.companies.id"), nullable=False)
|
||||
vehicle_id = Column(UUID(as_uuid=True), ForeignKey("data.vehicles.id"), nullable=False)
|
||||
driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
start_date = Column(DateTime(timezone=True), server_default=func.now())
|
||||
end_date = Column(DateTime(timezone=True), nullable=True)
|
||||
notes = Column(String, nullable=True)
|
||||
|
||||
company = relationship("Company", back_populates="assignments")
|
||||
vehicle = relationship("Vehicle") # Itt már a Vehicle-re hivatkozunk
|
||||
driver = relationship("User", foreign_keys=[driver_id])
|
||||
42
backend/app/models/core_logic.py
Executable file
42
backend/app/models/core_logic.py
Executable file
@@ -0,0 +1,42 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, JSON, Numeric
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class SubscriptionTier(Base):
|
||||
__tablename__ = "subscription_tiers"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, unique=True) # Free, Premium, VIP, Custom
|
||||
rules = Column(JSON) # {"max_vehicles": 5, "allow_api": true}
|
||||
is_custom = Column(Boolean, default=False)
|
||||
|
||||
class OrganizationSubscription(Base):
|
||||
__tablename__ = "org_subscriptions"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
org_id = Column(Integer, ForeignKey("data.organizations.id"))
|
||||
tier_id = Column(Integer, ForeignKey("data.subscription_tiers.id"))
|
||||
valid_from = Column(DateTime, server_default=func.now())
|
||||
valid_until = Column(DateTime)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
class CreditTransaction(Base):
|
||||
__tablename__ = "credit_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
org_id = Column(Integer, ForeignKey("data.organizations.id"))
|
||||
amount = Column(Numeric(10, 2))
|
||||
description = Column(String)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
class ServiceSpecialty(Base):
|
||||
"""Fa struktúra a szerviz szolgáltatásokhoz"""
|
||||
__tablename__ = "service_specialties"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
parent_id = Column(Integer, ForeignKey("data.service_specialties.id"), nullable=True)
|
||||
name = Column(String, nullable=False)
|
||||
slug = Column(String, unique=True)
|
||||
|
||||
parent = relationship("ServiceSpecialty", remote_side=[id], backref="children")
|
||||
17
backend/app/models/email_log.py
Executable file
17
backend/app/models/email_log.py
Executable file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailLog(Base):
|
||||
__tablename__ = "email_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, nullable=True) # Hozzáadva
|
||||
recipient = Column(String, index=True) # Hozzáadva
|
||||
email = Column(String, index=True)
|
||||
email_type = Column(String) # Frissítve a kódhoz
|
||||
type = Column(String) # Megtartva a kompatibilitás miatt
|
||||
provider_id = Column(Integer) # Hozzáadva
|
||||
status = Column(String) # Hozzáadva
|
||||
sent_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
21
backend/app/models/email_provider.py
Executable file
21
backend/app/models/email_provider.py
Executable file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, JSON, Float
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailProviderConfig(Base):
|
||||
__tablename__ = "email_provider_configs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(50), unique=True) # Pl: SendGrid_Main, Office365_Backup
|
||||
provider_type = Column(String(20)) # SENDGRID, SMTP, MAILGUN
|
||||
priority = Column(Integer, default=1) # 1 = legfontosabb
|
||||
|
||||
# JSON-ban tároljuk a paramétereket (host, port, api_key, user, stb.)
|
||||
settings = Column(JSON, nullable=False)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Failover figyelés
|
||||
fail_count = Column(Integer, default=0)
|
||||
max_fail_threshold = Column(Integer, default=3) # Hány hiba után kapcsoljon le?
|
||||
success_rate = Column(Float, default=100.0) # Statisztika az adminnak
|
||||
30
backend/app/models/email_system.py
Executable file
30
backend/app/models/email_system.py
Executable file
@@ -0,0 +1,30 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailProvider(Base):
|
||||
__tablename__ = 'email_providers'
|
||||
__table_args__ = {'schema': 'data'}
|
||||
id = Column(Integer, PRIMARY KEY=True)
|
||||
name = Column(String(50), nullable=False)
|
||||
priority = Column(Integer, default=1)
|
||||
provider_type = Column(String(10), default='SMTP')
|
||||
host = Column(String(255))
|
||||
port = Column(Integer)
|
||||
username = Column(String(255))
|
||||
password_hash = Column(String(255))
|
||||
is_active = Column(Boolean, default=True)
|
||||
daily_limit = Column(Integer, default=300)
|
||||
current_daily_usage = Column(Integer, default=0)
|
||||
|
||||
class EmailLog(Base):
|
||||
__tablename__ = 'email_logs'
|
||||
__table_args__ = {'schema': 'data'}
|
||||
id = Column(Integer, PRIMARY KEY=True)
|
||||
user_id = Column(Integer, ForeignKey('data.users.id'), nullable=True)
|
||||
email_type = Column(String(50))
|
||||
recipient = Column(String(255))
|
||||
provider_id = Column(Integer, ForeignKey('data.email_providers.id'))
|
||||
status = Column(String(20))
|
||||
sent_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
error_message = Column(Text)
|
||||
17
backend/app/models/email_template.py
Executable file
17
backend/app/models/email_template.py
Executable file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, Enum
|
||||
import enum
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailType(str, enum.Enum):
|
||||
REGISTRATION = "REGISTRATION"
|
||||
PASSWORD_RESET = "PASSWORD_RESET"
|
||||
GDPR_NOTICE = "GDPR_NOTICE"
|
||||
|
||||
class EmailTemplate(Base):
|
||||
__tablename__ = "email_templates"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
type = Column(Enum(EmailType), unique=True, index=True)
|
||||
subject = Column(String(255), nullable=False)
|
||||
body_html = Column(Text, nullable=False) # Adminról szerkeszthető HTML tartalom
|
||||
50
backend/app/models/expense.py
Executable file
50
backend/app/models/expense.py
Executable file
@@ -0,0 +1,50 @@
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Enum, DateTime, Boolean, Date, JSON
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
# Költség Kategóriák
|
||||
class ExpenseCategory(str, enum.Enum):
|
||||
PURCHASE_PRICE = "PURCHASE_PRICE" # Vételár
|
||||
TRANSFER_TAX = "TRANSFER_TAX" # Vagyonszerzési illeték
|
||||
ADMIN_FEE = "ADMIN_FEE" # Eredetiség, forgalmi, törzskönyv
|
||||
VEHICLE_TAX = "VEHICLE_TAX" # Gépjárműadó
|
||||
INSURANCE = "INSURANCE" # Biztosítás
|
||||
REFUELING = "REFUELING" # Tankolás
|
||||
SERVICE = "SERVICE" # Szerviz / Javítás
|
||||
PARKING = "PARKING" # Parkolás
|
||||
TOLL = "TOLL" # Autópálya matrica
|
||||
FINE = "FINE" # Bírság
|
||||
TUNING_ACCESSORIES = "TUNING_ACCESSORIES" # Extrák
|
||||
OTHER = "OTHER" # Egyéb
|
||||
|
||||
class VehicleEvent(Base):
|
||||
__tablename__ = "vehicle_events"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vehicle_id = Column(Integer, ForeignKey("data.user_vehicles.id"), nullable=False)
|
||||
|
||||
# Esemény típusa
|
||||
event_type = Column(Enum(ExpenseCategory, schema="data", name="expense_category_enum"), nullable=False)
|
||||
|
||||
date = Column(Date, nullable=False)
|
||||
|
||||
# Kilométeróra (KÖTELEZŐ!)
|
||||
odometer_value = Column(Integer, nullable=False)
|
||||
odometer_anomaly = Column(Boolean, default=False) # Ha csökkenést észlelünk, True lesz
|
||||
|
||||
# Pénzügyek
|
||||
cost_amount = Column(Integer, nullable=False, default=0) # HUF
|
||||
|
||||
# Leírás és Képek
|
||||
description = Column(String, nullable=True)
|
||||
image_paths = Column(JSON, nullable=True) # Lista a feltöltött képek (számla, fotó) útvonalairól
|
||||
|
||||
# Kapcsolat a szolgáltatóval
|
||||
# Ha is_diy=True, akkor a user maga csinálta.
|
||||
# Ha is_diy=False és service_provider_id=None, akkor ismeretlen helyen készült.
|
||||
is_diy = Column(Boolean, default=False)
|
||||
service_provider_id = Column(Integer, ForeignKey("data.service_providers.id"), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
70
backend/app/models/gamification.py
Executable file
70
backend/app/models/gamification.py
Executable file
@@ -0,0 +1,70 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.db.base import Base
|
||||
|
||||
# Közös beállítás az összes táblához ebben a fájlban
|
||||
SCHEMA_ARGS = {"schema": "data"}
|
||||
|
||||
class PointRule(Base):
|
||||
__tablename__ = "point_rules"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
|
||||
points: Mapped[int] = mapped_column(Integer, default=0)
|
||||
description: Mapped[Optional[str]] = mapped_column(String)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
class LevelConfig(Base):
|
||||
__tablename__ = "level_configs"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
level_number: Mapped[int] = mapped_column(Integer, unique=True)
|
||||
min_points: Mapped[int] = mapped_column(Integer)
|
||||
rank_name: Mapped[str] = mapped_column(String)
|
||||
|
||||
class RegionalSetting(Base):
|
||||
__tablename__ = "regional_settings"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
country_code: Mapped[str] = mapped_column(String, unique=True)
|
||||
currency: Mapped[str] = mapped_column(String, default="HUF")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
class PointsLedger(Base):
|
||||
__tablename__ = "points_ledger"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
# JAVÍTVA: data.users.id hivatkozás
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
||||
points: Mapped[int] = mapped_column(Integer)
|
||||
reason: Mapped[str] = mapped_column(String)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
||||
|
||||
class UserStats(Base):
|
||||
__tablename__ = "user_stats"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
# JAVÍTVA: data.users.id hivatkozás
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), unique=True)
|
||||
total_points: Mapped[int] = mapped_column(Integer, default=0)
|
||||
current_level: Mapped[int] = mapped_column(Integer, default=1)
|
||||
last_activity: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
||||
|
||||
class Badge(Base):
|
||||
__tablename__ = "badges"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String, unique=True)
|
||||
description: Mapped[str] = mapped_column(String)
|
||||
icon_url: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
class UserBadge(Base):
|
||||
__tablename__ = "user_badges"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
# JAVÍTVA: data.users.id hivatkozás
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
||||
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
|
||||
earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
||||
63
backend/app/models/history.py
Executable file
63
backend/app/models/history.py
Executable file
@@ -0,0 +1,63 @@
|
||||
# /opt/service_finder/backend/app/models/history.py
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
# --- 1. Jármű Birtoklási Előzmények (Ownership History) ---
|
||||
# Ez a tábla mondja meg, kié volt az autó egy adott időpillanatban.
|
||||
# Így biztosítjuk, hogy a régi tulajdonos adatai védve legyenek az újtól.
|
||||
class VehicleOwnership(Base):
|
||||
__tablename__ = "vehicle_ownerships"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Kapcsolatok
|
||||
vehicle_id = Column(Integer, ForeignKey("data.user_vehicles.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
# Időszak
|
||||
start_date = Column(Date, nullable=False, default=func.current_date()) # Mikor került hozzá
|
||||
end_date = Column(Date, nullable=True) # Ha NULL, akkor ő a jelenlegi tulajdonos!
|
||||
|
||||
# Jegyzet (pl. adásvételi szerződés száma)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
# SQLAlchemy kapcsolatok (visszahivatkozások a fő modellekben kellenek majd)
|
||||
vehicle = relationship("UserVehicle", back_populates="ownership_history")
|
||||
user = relationship("User", back_populates="owned_vehicles")
|
||||
|
||||
|
||||
# --- 2. Audit Log (A "Fekete Doboz") ---
|
||||
# Minden kritikus módosítást itt tárolunk. Ez a rendszer "igazságügyi naplója".
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# KI? (A felhasználó, aki a műveletet végezte)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
|
||||
# MIT? (Milyen objektumot érintett?)
|
||||
target_type = Column(String, index=True) # pl. "vehicle", "cost", "user_profile"
|
||||
target_id = Column(Integer, index=True) # pl. az autó ID-ja
|
||||
|
||||
# HOGYAN?
|
||||
action = Column(String, nullable=False) # CREATE, UPDATE, DELETE, LOGIN_FAILED, EXPORT_DATA
|
||||
|
||||
# RÉSZLETEK (Mi változott?)
|
||||
# Pl: {"field": "odometer", "old_value": 150000, "new_value": 120000} <- Visszatekerés gyanú!
|
||||
changes = Column(JSON, nullable=True)
|
||||
|
||||
# BIZTONSÁG
|
||||
ip_address = Column(String, nullable=True) # Honnan jött a kérés?
|
||||
user_agent = Column(String, nullable=True) # Milyen böngészőből?
|
||||
|
||||
# MIKOR?
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Kapcsolat (Opcionális, csak ha le akarjuk kérdezni a user adatait a logból)
|
||||
user = relationship("User")
|
||||
29
backend/app/models/legal.py
Executable file
29
backend/app/models/legal.py
Executable file
@@ -0,0 +1,29 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class LegalDocument(Base):
|
||||
__tablename__ = "legal_documents"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(255))
|
||||
content = Column(Text, nullable=False)
|
||||
version = Column(String(20), nullable=False)
|
||||
|
||||
region_code = Column(String(5), default="HU")
|
||||
language = Column(String(5), default="hu")
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class LegalAcceptance(Base):
|
||||
__tablename__ = "legal_acceptances"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
||||
document_id = Column(Integer, ForeignKey("data.legal_documents.id"))
|
||||
accepted_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
ip_address = Column(String(45))
|
||||
user_agent = Column(Text)
|
||||
25
backend/app/models/logistics.py
Executable file
25
backend/app/models/logistics.py
Executable file
@@ -0,0 +1,25 @@
|
||||
from sqlalchemy import Column, Integer, String, Enum
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
# Enum definiálása
|
||||
class LocationType(str, enum.Enum):
|
||||
stop = "stop" # Megálló / Parkoló
|
||||
warehouse = "warehouse" # Raktár
|
||||
client = "client" # Ügyfél címe
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
|
||||
# FONTOS: Itt is megadjuk a schema="data"-t, hogy ne a public sémába akarja írni!
|
||||
type = Column(Enum(LocationType, schema="data", name="location_type_enum"), nullable=False)
|
||||
|
||||
# Koordináták (egyelőre String, később PostGIS)
|
||||
coordinates = Column(String, nullable=True)
|
||||
address_full = Column(String, nullable=True)
|
||||
|
||||
capacity = Column(Integer, nullable=True)
|
||||
36
backend/app/models/organization.py
Executable file
36
backend/app/models/organization.py
Executable file
@@ -0,0 +1,36 @@
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class OrgType(str, enum.Enum):
|
||||
INDIVIDUAL = "individual"
|
||||
SERVICE = "service"
|
||||
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"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
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)
|
||||
|
||||
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")
|
||||
26
backend/app/models/organization_member.py
Executable file
26
backend/app/models/organization_member.py
Executable file
@@ -0,0 +1,26 @@
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Enum, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
# Átnevezve OrgUserRole-ra, hogy ne ütközzön a globális UserRole-al
|
||||
class OrgUserRole(str, enum.Enum):
|
||||
OWNER = "OWNER"
|
||||
ADMIN = "ADMIN"
|
||||
FLEET_MANAGER = "FLEET_MANAGER"
|
||||
DRIVER = "DRIVER"
|
||||
|
||||
class OrganizationMember(Base):
|
||||
__tablename__ = "organization_members"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
org_id = Column(Integer, ForeignKey("data.organizations.id", ondelete="CASCADE"))
|
||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"))
|
||||
# Itt is frissítjük a hivatkozást
|
||||
role = Column(Enum(OrgUserRole), default=OrgUserRole.DRIVER)
|
||||
|
||||
is_permanent = Column(Boolean, default=False)
|
||||
|
||||
organization = relationship("Organization", back_populates="members")
|
||||
# # # user = relationship("User", back_populates="memberships")
|
||||
71
backend/app/models/social.py
Executable file
71
backend/app/models/social.py
Executable file
@@ -0,0 +1,71 @@
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Enum, DateTime, Boolean, Text, UniqueConstraint
|
||||
from app.db.base import Base
|
||||
from datetime import datetime
|
||||
|
||||
# Enums (már schema="data" beállítással a biztonságért)
|
||||
class ModerationStatus(str, enum.Enum):
|
||||
pending = "pending"
|
||||
approved = "approved"
|
||||
rejected = "rejected"
|
||||
|
||||
class SourceType(str, enum.Enum):
|
||||
manual = "manual"
|
||||
ocr = "ocr"
|
||||
api_import = "import"
|
||||
|
||||
class ServiceProvider(Base):
|
||||
__tablename__ = "service_providers"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
address = Column(String, nullable=False)
|
||||
category = Column(String)
|
||||
|
||||
status = Column(Enum(ModerationStatus, schema="data", name="moderation_status_enum"), default=ModerationStatus.pending, nullable=False)
|
||||
source = Column(Enum(SourceType, schema="data", name="source_type_enum"), default=SourceType.manual, nullable=False)
|
||||
|
||||
# --- ÚJ MEZŐ ---
|
||||
validation_score = Column(Integer, default=0) # A közösségi szavazatok összege
|
||||
# ---------------
|
||||
|
||||
evidence_image_path = Column(String, nullable=True)
|
||||
added_by_user_id = Column(Integer, ForeignKey("data.users.id"))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class Vote(Base):
|
||||
__tablename__ = "votes"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
provider_id = Column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
|
||||
vote_value = Column(Integer, nullable=False) # +1 vagy -1
|
||||
|
||||
class Competition(Base):
|
||||
__tablename__ = "competitions"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False) # Pl: "Januári Feltöltő Verseny"
|
||||
description = Column(Text)
|
||||
start_date = Column(DateTime, nullable=False)
|
||||
end_date = Column(DateTime, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
class UserScore(Base):
|
||||
__tablename__ = "user_scores"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
||||
competition_id = Column(Integer, ForeignKey("data.competitions.id"))
|
||||
points = Column(Integer, default=0)
|
||||
last_updated = Column(DateTime, default=datetime.utcnow)
|
||||
17
backend/app/models/staged_data.py
Executable file
17
backend/app/models/staged_data.py
Executable file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, JSON, DateTime, func
|
||||
from app.db.base import Base
|
||||
|
||||
class StagedVehicleData(Base):
|
||||
"""Ide érkeznek a nyers, validálatlan adatok a külső forrásokból"""
|
||||
__tablename__ = "staged_vehicle_data"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
source_url = Column(String) # Honnan jött az adat?
|
||||
raw_data = Column(JSON) # A teljes leszedett JSON struktúra
|
||||
|
||||
# Feldolgozási állapot
|
||||
status = Column(String, default="PENDING") # PENDING, PROCESSED, ERROR
|
||||
error_log = Column(String, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
15
backend/app/models/system_settings.py
Executable file
15
backend/app/models/system_settings.py
Executable file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, String, JSON
|
||||
from app.db.base import Base
|
||||
|
||||
class SystemSetting(Base):
|
||||
"""
|
||||
Globális rendszerbeállítások tárolása.
|
||||
Kulcs-Érték párok (JSON támogatással a komplex szabályokhoz).
|
||||
Példa: key='FREE_VEHICLE_LIMIT', value='2'
|
||||
"""
|
||||
__tablename__ = "system_settings"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
key = Column(String, primary_key=True, index=True)
|
||||
value = Column(JSON, nullable=False)
|
||||
description = Column(String, nullable=True)
|
||||
15
backend/app/models/translation.py
Executable file
15
backend/app/models/translation.py
Executable file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, UniqueConstraint
|
||||
from app.db.base import Base
|
||||
|
||||
class Translation(Base):
|
||||
__tablename__ = "translations"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("key", "lang_code", name="uq_translation_key_lang"),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(100), nullable=False, index=True)
|
||||
lang_code = Column(String(5), nullable=False, index=True)
|
||||
value = Column(Text, nullable=False)
|
||||
is_published = Column(Boolean, default=False) # Publikálási állapot
|
||||
35
backend/app/models/user.py
Executable file
35
backend/app/models/user.py
Executable file
@@ -0,0 +1,35 @@
|
||||
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())
|
||||
|
||||
# Kapcsolatok
|
||||
# memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan")
|
||||
# vehicles = relationship("VehicleOwnership", back_populates="user", cascade="all, delete-orphan")
|
||||
77
backend/app/models/vehicle.py
Executable file
77
backend/app/models/vehicle.py
Executable file
@@ -0,0 +1,77 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Numeric, DateTime, JSON, Date
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
from app.db.base import Base
|
||||
|
||||
class VehicleBrand(Base):
|
||||
__tablename__ = "vehicle_brands"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False, unique=True)
|
||||
slug = Column(String(100), unique=True)
|
||||
country_of_origin = Column(String(50))
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
class ServiceProvider(Base):
|
||||
__tablename__ = "service_providers"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
official_brand_partner = Column(Boolean, default=False)
|
||||
technical_rating_pct = Column(Integer, default=80)
|
||||
social_rating_pct = Column(Integer, default=80)
|
||||
location_city = Column(String(100))
|
||||
service_type = Column(String(50))
|
||||
search_tags = Column(String)
|
||||
latitude = Column(Numeric(10, 8))
|
||||
longitude = Column(Numeric(11, 8))
|
||||
is_active = Column(Boolean, default=True)
|
||||
records = relationship("ServiceRecord", back_populates="provider")
|
||||
|
||||
class EngineSpec(Base):
|
||||
__tablename__ = "engine_specs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
engine_code = Column(String(50), unique=True)
|
||||
fuel_type = Column(String(20))
|
||||
power_kw = Column(Integer)
|
||||
default_service_interval_km = Column(Integer, default=15000)
|
||||
vehicles = relationship("Vehicle", back_populates="engine_spec")
|
||||
|
||||
class Vehicle(Base):
|
||||
__tablename__ = "vehicles"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
current_company_id = Column(Integer, ForeignKey("data.companies.id"))
|
||||
brand_id = Column(Integer, ForeignKey("data.vehicle_brands.id"))
|
||||
model_name = Column(String(100))
|
||||
engine_spec_id = Column(Integer, ForeignKey("data.engine_specs.id"))
|
||||
identification_number = Column(String(50), unique=True)
|
||||
license_plate = Column(String(20))
|
||||
tracking_mode = Column(String(10), default="km")
|
||||
current_rating_pct = Column(Integer, default=100)
|
||||
total_real_usage = Column(Numeric(15, 2), default=0)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
engine_spec = relationship("EngineSpec", back_populates="vehicles")
|
||||
service_records = relationship("ServiceRecord", back_populates="vehicle", cascade="all, delete-orphan")
|
||||
|
||||
# --- KOMPATIBILITÁSI RÉTEG A RÉGI KÓDOKHOZ ---
|
||||
VehicleOwnership = Vehicle
|
||||
VehicleModel = Vehicle
|
||||
VehicleVariant = Vehicle
|
||||
VehicleCategory = VehicleBrand # JAVÍTVA: Nagy "B" betűvel
|
||||
|
||||
class ServiceRecord(Base):
|
||||
__tablename__ = "service_records"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
vehicle_id = Column(UUID(as_uuid=True), ForeignKey("data.vehicles.id"))
|
||||
provider_id = Column(Integer, ForeignKey("data.service_providers.id"))
|
||||
service_date = Column(Date, nullable=False)
|
||||
usage_value = Column(Numeric(15, 2))
|
||||
repair_quality_pct = Column(Integer, default=100)
|
||||
|
||||
vehicle = relationship("Vehicle", back_populates="service_records")
|
||||
provider = relationship("ServiceProvider", back_populates="records") # JAVÍTVA
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user