Initial commit - Migrated to Dev environment

This commit is contained in:
2026-02-03 19:55:45 +00:00
commit a34e5b7976
3518 changed files with 481663 additions and 0 deletions

Binary file not shown.

132
backend/app/api/auth.py Executable file
View 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
View 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
View 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

Binary file not shown.

12
backend/app/api/v1/api.py Executable file
View 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"])

View 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."}

View 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!"}

View 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}

View 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."}

View 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)}

View 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()

View 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)

View 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()]

View 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]
}

View 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)

View 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

View 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}

View 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
View 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"])

Binary file not shown.

262
backend/app/api/v2/auth.py Executable file
View 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."}