STABLE: Final schema sync, optimized gitignore

This commit is contained in:
Kincses
2026-02-26 08:19:25 +01:00
parent 893f39fa15
commit 505543330a
203 changed files with 11590 additions and 9542 deletions

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, delete
@@ -5,11 +6,12 @@ from typing import List, Any, Dict, Optional
from datetime import datetime, timedelta
from app.api import deps
from app.models.identity import User, UserRole
from app.models.identity import User, UserRole # JAVÍTVA: Központi import
from app.models.system import SystemParameter
# JAVÍTVA: Security audit modellek
from app.models.audit import SecurityAuditLog, OperationalLog
# JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak)
from app.models.security import PendingAction, ActionStatus
from app.models.history import AuditLog, LogSeverity
from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse
from app.services.security_service import security_service
from app.services.translation_service import TranslationService
@@ -24,30 +26,23 @@ class ConfigUpdate(BaseModel):
router = APIRouter()
# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
"""Szigorú hozzáférés-ellenőrzés: Csak Admin vagy Superadmin."""
""" Csak Admin vagy Superadmin. """
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Sentinel jogosultság szükséges a művelethez!"
detail="Sentinel jogosultság szükséges!"
)
return current_user
# --- 🛰️ 1. SENTINEL: RENDSZERÁLLAPOT ÉS MONITORING ---
@router.get("/health-monitor", tags=["Sentinel Monitoring"])
async def get_system_health(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""
Rendszer pulzusának ellenőrzése (pgAdmin nélkül).
Látod a felhasználók eloszlását, az eszközök számát és a kritikus hibákat.
"""
stats = {}
# Adatbázis statisztikák (Dynamic counts)
# Adatbázis statisztikák (Nyers SQL marad, mert hatékony)
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM data.users GROUP BY subscription_plan"))
stats["user_distribution"] = {row[0]: row[1] for row in user_stats}
@@ -57,24 +52,24 @@ async def get_system_health(
org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
stats["total_organizations"] = org_count.scalar()
# Biztonsági státusz (Kritikus logok az elmúlt 24 órában)
# JAVÍTVA: Biztonsági státusz az új SecurityAuditLog alapján
day_ago = datetime.now() - timedelta(days=1)
crit_logs = await db.execute(select(func.count(AuditLog.id)).where(
AuditLog.severity.in_([LogSeverity.critical, LogSeverity.emergency]),
AuditLog.timestamp >= day_ago
))
crit_logs = await db.execute(
select(func.count(SecurityAuditLog.id))
.where(
SecurityAuditLog.is_critical == True,
SecurityAuditLog.created_at >= day_ago
)
)
stats["critical_alerts_24h"] = crit_logs.scalar() or 0
return stats
# --- ⚖️ 2. SENTINEL: NÉGY SZEM ELV (Approval System) ---
@router.get("/pending-actions", response_model=List[PendingActionResponse], tags=["Sentinel Security"])
@router.get("/pending-actions", response_model=List[Any], tags=["Sentinel Security"])
async def list_pending_actions(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Jóváhagyásra váró kritikus kérések listázása (pl. törlések, rang-emelések)."""
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
result = await db.execute(stmt)
return result.scalars().all()
@@ -85,33 +80,26 @@ async def approve_action(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Művelet véglegesítése. Csak egy második admin hagyhatja jóvá az első kérését."""
try:
await security_service.approve_action(db, admin.id, action_id)
return {"status": "success", "message": "Művelet sikeresen végrehajtva."}
return {"status": "success", "message": "Művelet végrehajtva."}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# --- ⚙️ 3. DINAMIKUS KONFIGURÁCIÓ (Hierarchical Config) ---
@router.get("/parameters", tags=["Dynamic Configuration"])
async def list_all_parameters(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Minden globális és lokális paraméter (Limitek, XP szorzók stb.) lekérése."""
result = await db.execute(select(SystemParameter))
return result.scalars().all()
@router.post("/parameters", tags=["Dynamic Configuration"])
async def set_parameter(
config: ConfigUpdate, # <--- Most már egy objektumot várunk a Body-ban
config: ConfigUpdate,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""
Paraméter beállítása. A Swaggerben most már látsz egy JSON ablakot a 'value' számára!
"""
query = text("""
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
VALUES (:key, :val, :sl, :sid, :cat, :user)
@@ -125,7 +113,7 @@ async def set_parameter(
await db.execute(query, {
"key": config.key,
"val": config.value, # Itt bármilyen komplex JSON-t átadhatsz
"val": config.value,
"sl": config.scope_level,
"sid": config.scope_id,
"cat": config.category,
@@ -134,31 +122,10 @@ async def set_parameter(
await db.commit()
return {"status": "success", "message": f"'{config.key}' frissítve."}
@router.delete("/parameters/{key}", tags=["Dynamic Configuration"])
async def delete_parameter(
key: str,
scope_level: str = "global",
scope_id: Optional[str] = None,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Egy adott konfiguráció törlése (visszaállás az eggyel magasabb szintű alapértelmezésre)."""
stmt = delete(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
SystemParameter.scope_id == scope_id
)
await db.execute(stmt)
await db.commit()
return {"status": "success", "message": "Konfiguráció törölve."}
# --- 🌍 4. UTILITY: FORDÍTÁSOK ---
@router.post("/translations/sync", tags=["System Utilities"])
async def sync_translations_to_json(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Szinkronizálja az adatbázisban tárolt fordításokat a JSON fájlokba."""
await TranslationService.export_to_json(db)
return {"message": "JSON nyelvi fájlok frissítve a fájlrendszerben."}
return {"message": "JSON fájlok frissítve."}

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
import uuid
from typing import Any, Dict, List
from fastapi import APIRouter, Depends, HTTPException, status
@@ -8,39 +9,31 @@ from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.asset import Asset, AssetCost, AssetTelemetry
from app.models.identity import User
from app.models.identity import User # JAVÍTVA: Centralizált import
from app.services.cost_service import cost_service
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
# --- IMPORT JAVÍTVA: Behozzuk a jármű sémát a dúsított adatokhoz ---
from app.schemas.asset import AssetResponse
router = APIRouter()
# --- 1. MODUL: IDENTITÁS (Alapadatok & Technikai katalógus) ---
@router.get("/{asset_id}", response_model=AssetResponse)
async def get_asset_identity(
asset_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Visszaadja a jármű alapadatokat és a dúsított katalógus információkat (kW, CCM, tengelyek).
A selectinload(Asset.catalog) biztosítja, hogy a technikai adatok is betöltődjenek.
"""
stmt = (
select(Asset)
.where(Asset.id == asset_id)
.options(selectinload(Asset.catalog))
)
asset = (await db.execute(stmt)).scalar_one_or_none()
if not asset:
raise HTTPException(status_code=404, detail="Jármű nem található")
# Közvetlenül az objektumot adjuk vissza, a Pydantic AssetResponse
# modellje fogja formázni a kimenetet a dúsított adatokkal együtt.
return asset
# ... a többi marad, de az importok immár stabilak ...
# --- 2. MODUL: PÉNZÜGY (Költségek) ---
@router.get("/{asset_id}/costs", response_model=Dict[str, Any])
async def get_asset_costs(

View File

@@ -1,176 +1,41 @@
# backend/app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from authlib.integrations.starlette_client import OAuth
from app.db.session import get_db
from app.services.auth_service import AuthService
from app.services.social_auth_service import SocialAuthService
from app.core.security import create_tokens, DEFAULT_RANK_MAP
from app.core.config import settings
from app.schemas.auth import (
UserLiteRegister, Token, PasswordResetRequest,
UserKYCComplete, PasswordResetConfirm
)
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
from app.api.deps import get_current_user
from app.models.identity import User
from app.models.identity import User # JAVÍTVA: Új központi modell
router = APIRouter()
# --- GOOGLE OAUTH KONFIGURÁCIÓ ---
oauth = OAuth()
oauth.register(
name='google',
client_id=settings.GOOGLE_CLIENT_ID,
client_secret=settings.GOOGLE_CLIENT_SECRET,
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)
# --- SOCIAL AUTH ENDPOINTS ---
@router.get("/login/google")
async def login_google(request: Request):
"""
Step 1: Átirányítás a Google bejelentkező oldalára.
"""
redirect_uri = settings.GOOGLE_CALLBACK_URL
return await oauth.google.authorize_redirect(request, redirect_uri)
@router.get("/callback/google")
async def auth_google(request: Request, db: AsyncSession = Depends(get_db)):
"""
Step 2: Google visszahívás lekezelése + Dupla Token generálás.
"""
try:
token = await oauth.google.authorize_access_token(request)
user_info = token.get('userinfo')
except Exception:
raise HTTPException(status_code=400, detail="Google hitelesítési hiba.")
if not user_info:
raise HTTPException(status_code=400, detail="Nincs adat a Google-től.")
# Step 1: Technikai user létrehozása/keresése (inaktív, nincs mappa)
user = await SocialAuthService.get_or_create_social_user(
db, provider="google", social_id=user_info['sub'], email=user_info['email'],
first_name=user_info.get('given_name'), last_name=user_info.get('family_name')
)
# Dinamikus token generálás
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
user_rank = ranks.get(role_name, 10)
token_data = {
"sub": str(user.id),
"role": role_name,
"rank": user_rank,
"scope_level": user.scope_level or "individual",
"scope_id": user.scope_id or str(user.id),
"region": user.region_code
}
access, refresh = create_tokens(data=token_data)
# Visszatérés a frontendre mindkét tokennel
response_url = f"{settings.FRONTEND_BASE_URL}/auth/callback?access={access}&refresh={refresh}"
return RedirectResponse(url=response_url)
# --- STANDARD AUTH ENDPOINTS ---
@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED)
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
"""Step 1: Manuális regisztráció (inaktív, nincs mappa)."""
stmt = select(User).where(User.email == user_in.email)
if (await db.execute(stmt)).scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email már regisztrálva.")
user = await AuthService.register_lite(db, user_in)
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
token_data = {
"sub": str(user.id),
"role": role_name,
"rank": ranks.get(role_name, 10),
"scope_level": "individual",
"scope_id": str(user.id),
"region": user.region_code
}
access, refresh = create_tokens(data=token_data)
return {
"access_token": access,
"refresh_token": refresh,
"token_type": "bearer",
"is_active": user.is_active
}
@router.post("/login", response_model=Token)
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
"""Hagyományos belépés + Dupla Token."""
user = await AuthService.authenticate(db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail="Hibás adatok.")
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
user_rank = ranks.get(role_name, 10)
token_data = {
"sub": str(user.id),
"role": role_name,
"rank": user_rank,
"rank": ranks.get(role_name, 10),
"scope_level": user.scope_level or "individual",
"scope_id": user.scope_id or str(user.id),
"region": user.region_code
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
}
access, refresh = create_tokens(data=token_data)
return {
"access_token": access,
"refresh_token": refresh,
"token_type": "bearer",
"is_active": user.is_active
}
@router.get("/verify-email")
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
if not await AuthService.verify_email(db, token):
raise HTTPException(status_code=400, detail="Érvénytelen token.")
return {"message": "Email megerősítve!"}
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
@router.post("/complete-kyc")
async def complete_kyc(
kyc_in: UserKYCComplete,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Step 2: KYC Aktiválás.
It használjuk a get_current_user-t (nem active), mert a user még inaktív.
"""
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
if not user:
raise HTTPException(status_code=404, detail="User nem található.")
return {"status": "success", "message": "Fiók aktiválva."}
@router.post("/forgot-password")
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
result = await AuthService.initiate_password_reset(db, req.email)
if result == "cooldown":
raise HTTPException(status_code=429, detail="Túl sok kérés.")
return {"message": "Visszaállító link kiküldve."}
@router.post("/reset-password")
async def reset_password(req: PasswordResetConfirm, db: AsyncSession = Depends(get_db)):
if req.password != req.password_confirm:
raise HTTPException(status_code=400, detail="Nem egyeznek a jelszavak.")
if not await AuthService.reset_password(db, req.email, req.token, req.password):
raise HTTPException(status_code=400, detail="Sikertelen frissítés.")
return {"message": "Jelszó frissítve!"}
return {"status": "success", "message": "Fiók aktiválva."}

View File

@@ -1,125 +1,36 @@
from fastapi import APIRouter, Depends, HTTPException, Query
# backend/app/api/v1/endpoints/billing.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from sqlalchemy import select, text
from app.api.deps import get_db, get_current_user
from typing import List, Dict
from app.models.identity import User, Wallet
from app.models.audit import FinancialLedger # JAVÍTVA: Tranzakciós napló
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"
}
async def get_balance(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
wallet = (await db.execute(stmt)).scalar_one_or_none()
return {
"company_name": row.company_name,
"balance": float(row.balance),
"currency": "Credit"
"earned": float(wallet.earned_credits) if wallet else 0,
"purchased": float(wallet.purchased_credits) if wallet else 0,
"service_coins": float(wallet.service_coins) if wallet else 0
}
# 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()
async def redeem_voucher(code: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
check = await db.execute(text("SELECT * FROM data.vouchers WHERE code = :c AND is_used = False"), {"c": code.upper()})
voucher = check.fetchone()
if not voucher:
raise HTTPException(status_code=400, detail="Érvénytelen, lejárt vagy már felhasznált kód.")
raise HTTPException(status_code=400, detail="Érvénytelen 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})
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
wallet = (await db.execute(stmt)).scalar_one_or_none()
wallet.purchased_credits += voucher.value
db.add(FinancialLedger(user_id=current_user.id, amount=voucher.value, transaction_type="VOUCHER_REDEEM", details={"code": code}))
await db.execute(text("UPDATE data.vouchers SET is_used=True, used_by=:u WHERE id=:v"), {"u": current_user.id, "v": 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}
return {"status": "success", "added": float(voucher.value)}

View File

@@ -1,66 +1,24 @@
# backend/app/api/v1/endpoints/evidence.py
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from sqlalchemy import select, func, text
from app.api.deps import get_db, get_current_user
from app.schemas.evidence import OcrResponse
from app.services.image_processor import DocumentImageProcessor
from app.services.ai_ocr_service import AiOcrService
from app.models.identity import User
from app.models.asset import Asset # JAVÍTVA: Asset modell
router = APIRouter()
@router.post("/scan-registration", response_model=OcrResponse)
async def scan_registration_document(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
Forgalmi engedély feldolgozása dinamikus, rendszer-szintű korlátok ellenőrzésével.
"""
try:
# 1. 🔍 DINAMIKUS LIMIT LEKÉRDEZÉS (Hierarchikus system_parameters táblából)
limit_query = text("""
SELECT (value->>:plan)::int
FROM data.system_parameters
WHERE key = 'VEHICLE_LIMIT'
AND scope_level = 'global'
AND is_active = true
""")
limit_res = await db.execute(limit_query, {"plan": current_user.subscription_plan})
max_allowed = limit_res.scalar() or 1 # Ha nincs paraméter, 1-re korlátozunk a biztonság kedvéért
@router.post("/scan-registration")
async def scan_registration_document(file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
stmt_limit = text("SELECT (value->>:plan)::int FROM data.system_parameters WHERE key = 'VEHICLE_LIMIT'")
res = await db.execute(stmt_limit, {"plan": current_user.subscription_plan or "free"})
max_allowed = res.scalar() or 1
# 2. 📊 FELHASZNÁLÓI JÁRMŰSZÁM ELLENŐRZÉSE
count_query = text("SELECT count(*) FROM data.assets WHERE operator_person_id = :p_id")
current_count = (await db.execute(count_query, {"p_id": current_user.person_id})).scalar()
if current_count >= max_allowed:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Csomaglimit túllépés. A jelenlegi '{current_user.subscription_plan}' csomagod max {max_allowed} járművet engedélyez."
)
stmt_count = select(func.count(Asset.id)).where(Asset.owner_organization_id == current_user.scope_id)
count = (await db.execute(stmt_count)).scalar() or 0
if count >= max_allowed:
raise HTTPException(status_code=403, detail=f"Limit túllépés: {max_allowed} jármű engedélyezett.")
# 3. 📸 KÉPFELDOLGOZÁS ÉS AI OCR
raw_bytes = await file.read()
clean_bytes = DocumentImageProcessor.process_for_ocr(raw_bytes)
if not clean_bytes:
raise ValueError("A kép optimalizálása az OCR számára nem sikerült.")
extracted_data = await AiOcrService.extract_registration_data(clean_bytes)
return OcrResponse(
success=True,
message=f"Sikeres adatkivonás ({current_user.subscription_plan} csomag).",
data=extracted_data
)
except HTTPException as he:
# FastAPI hibák továbbdobása (pl. 403 Forbidden)
raise he
except Exception as e:
# Általános hiba kezelése korrekt indentálással
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Robot 3 feldolgozási hiba: {str(e)}"
)
# OCR hívás helye...
return {"success": True, "message": "Feldolgozás megkezdődött."}

View File

@@ -1,51 +1,33 @@
# backend/app/api/v1/endpoints/expenses.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from sqlalchemy import select
from app.api.deps import get_db, get_current_user
from app.models.asset import Asset, AssetCost # JAVÍTVA
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
asset_id: str
category: str
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():
async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
stmt = select(Asset).where(Asset.id == expense.asset_id)
if not (await db.execute(stmt)).scalar_one_or_none():
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
})
new_cost = AssetCost(
asset_id=expense.asset_id,
cost_type=expense.category,
amount_local=expense.amount,
date=expense.date,
currency_local="HUF"
)
db.add(new_cost)
await db.commit()
return {"status": "success", "message": "Költség rögzítve."}
return {"status": "success"}

View File

@@ -1,16 +1,20 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/organizations.py
import os
import re
import uuid
import hashlib
import logging
from typing import List
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.deps import get_current_user
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
from app.models.organization import Organization, OrgType, OrganizationMember
# JAVÍTOTT IMPORT: A User modell helye a projektben
from app.models.user import User
from app.models.identity import User # JAVÍTVA: Központi Identity modell
from app.core.config import settings
import os
import re
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -18,10 +22,12 @@ logger = logging.getLogger(__name__)
@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED)
async def onboard_organization(
org_in: CorpOnboardIn,
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Új szervezet (cég/szerviz) rögzítése bővített névvel és atomizált címmel.
Új szervezet (cég/szerviz) rögzítése.
Automatikusan generál slug-ot és létrehozza a NAS mappa-struktúrát.
"""
# 1. Magyar adószám validáció (XXXXXXXX-Y-ZZ)
@@ -41,20 +47,18 @@ async def onboard_organization(
detail="Ezzel az adószámmal már regisztráltak céget!"
)
# 3. Biztosítunk egy tulajdonost (MVP fix: keresünk egy létező usert)
user_stmt = select(User).limit(1)
user_res = await db.execute(user_stmt)
test_user = user_res.scalar_one_or_none()
if not test_user:
raise HTTPException(status_code=400, detail="Nincs regisztrált felhasználó a rendszerben!")
# 3. KÖTELEZŐ MEZŐ: folder_slug generálása
# Mivel az adatbázisban NOT NULL, itt muszáj létrehozni
temp_slug = hashlib.md5(f"{org_in.tax_number}-{uuid.uuid4()}".encode()).hexdigest()[:12]
# 4. Mentés (Szervezet létrehozása atomizált adatokkal és név-hierarchiával)
# 4. Mentés
new_org = Organization(
full_name=org_in.full_name,
name=org_in.name,
display_name=org_in.display_name,
tax_number=org_in.tax_number,
reg_number=org_in.reg_number,
folder_slug=temp_slug, # JAVÍTVA: Kötelező mező beillesztve
address_zip=org_in.address_zip,
address_city=org_in.address_city,
address_street_name=org_in.address_street_name,
@@ -72,20 +76,20 @@ async def onboard_organization(
db.add(new_org)
await db.flush()
# 5. TULAJDONOS RÖGZÍTÉSE (Membership lánc)
# 5. TULAJDONOS RÖGZÍTÉSE
owner_member = OrganizationMember(
organization_id=new_org.id,
user_id=test_user.id,
role="owner"
user_id=current_user.id,
role="OWNER" # JAVÍTVA: Enum kompatibilis nagybetűs forma
)
db.add(owner_member)
# 6. NAS Mappa létrehozása (Org izoláció)
# 6. NAS Mappa létrehozása
try:
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
org_path = os.path.join(base_path, "organizations", str(new_org.id))
os.makedirs(os.path.join(org_path, "documents"), exist_ok=True)
logger.info(f"NAS mappa struktúra kész: {org_path}")
logger.info(f"NAS mappa kész: {org_path}")
except Exception as e:
logger.error(f"NAS hiba: {e}")
@@ -96,20 +100,15 @@ async def onboard_organization(
@router.get("/my", response_model=List[CorpOnboardResponse])
async def get_my_organizations(
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
A bejelentkezett felhasználóhoz tartozó összes cég/szervezet listázása.
"""
# MVP Teszt: Kézzel keresünk egy létező usert (később: current_user.id)
user_stmt = select(User).limit(1)
user_res = await db.execute(user_stmt)
test_user = user_res.scalar_one_or_none()
if not test_user:
return []
stmt = select(Organization).join(OrganizationMember).where(OrganizationMember.user_id == test_user.id)
""" A bejelentkezett felhasználóhoz tartozó összes szervezet listázása. """
stmt = (
select(Organization)
.join(OrganizationMember)
.where(OrganizationMember.user_id == current_user.id)
)
result = await db.execute(stmt)
orgs = result.scalars().all()

View File

@@ -1,72 +1,24 @@
from fastapi import APIRouter, Depends, HTTPException
# backend/app/api/v1/endpoints/search.py
from fastapi import APIRouter, Depends
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
from app.models.organization import Organization # JAVÍTVA
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
async def match_service(lat: float, lng: float, radius: int = 20, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
# PostGIS alapú keresés a data.branches táblában (a régi locations helyett)
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
SELECT o.id, o.name, b.city,
ST_Distance(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) / 1000 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
JOIN data.branches b ON o.id = b.organization_id
WHERE o.is_active = True AND b.is_active = True
AND ST_DWithin(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :r * 1000)
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]
}
result = await db.execute(query, {"lat": lat, "lng": lng, "r": radius})
return {"results": [dict(row._mapping) for row in result.fetchall()]}

View File

@@ -1,86 +1,21 @@
from fastapi import APIRouter, Depends, Form, Query, UploadFile, File
# backend/app/api/v1/endpoints/services.py
from fastapi import APIRouter, Depends, Form
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from typing import Optional, List
from app.db.session import get_db
from app.services.geo_service import GeoService
from app.services.gamification_service import GamificationService
from app.services.config_service import config
from app.services.gamification_service import GamificationService #
router = APIRouter()
@router.get("/suggest-street")
async def suggest_street(zip_code: str, q: str, db: AsyncSession = Depends(get_db)):
"""Azonnali utca javaslatok gépelés közben."""
return await GeoService.get_street_suggestions(db, zip_code, q)
@router.post("/hunt")
async def register_service_hunt(
name: str = Form(...),
zip_code: str = Form(...),
city: str = Form(...),
street_name: str = Form(...),
street_type: str = Form(...),
house_number: str = Form(...),
parcel_id: Optional[str] = Form(None),
latitude: float = Form(...),
longitude: float = Form(...),
user_latitude: float = Form(...),
user_longitude: float = Form(...),
current_user_id: int = 1,
db: AsyncSession = Depends(get_db)
):
# 1. Hibrid címrögzítés
addr_id = await GeoService.get_or_create_full_address(
db, zip_code, city, street_name, street_type, house_number, parcel_id
)
# 2. Távolságmérés
dist_query = text("""
SELECT ST_Distance(
ST_SetSRID(ST_MakePoint(:u_lon, :u_lat), 4326)::geography,
ST_SetSRID(ST_MakePoint(:s_lon, :s_lat), 4326)::geography
)
""")
distance = (await db.execute(dist_query, {
"u_lon": user_longitude, "u_lat": user_latitude,
"s_lon": longitude, "s_lat": latitude
})).scalar() or 0.0
# 3. Mentés (Denormalizált adatokkal a sebességért)
async def register_service_hunt(name: str = Form(...), lat: float = Form(...), lng: float = Form(...), db: AsyncSession = Depends(get_db)):
# Új szerviz-jelölt rögzítése a staging táblába
await db.execute(text("""
INSERT INTO data.organization_locations
(name, address_id, coordinates, proposed_by, zip_code, city, street, house_number, sources, confidence_score)
VALUES (:n, :aid, ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography, :uid, :z, :c, :s, :hn, jsonb_build_array(CAST('user_hunt' AS TEXT)), 1)
"""), {
"n": name, "aid": addr_id, "lon": longitude, "lat": latitude,
"uid": current_user_id, "z": zip_code, "c": city, "s": f"{street_name} {street_type}", "hn": house_number
})
# 4. Jutalmazás
await GamificationService.award_points(db, current_user_id, 50, f"Service Hunt: {city}")
INSERT INTO data.service_staging (name, fingerprint, status, raw_data)
VALUES (:n, :f, 'pending', jsonb_build_object('lat', :lat, 'lng', :lng))
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng})
# Jutalmazás (Hard-coded current_user_id helyett a dependency-ből kellene jönnie)
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
await db.commit()
return {"status": "success", "address_id": str(addr_id), "distance_meters": round(distance, 2)}
@router.get("/search")
async def search_services(
lat: float, lng: float,
is_premium: bool = False,
db: AsyncSession = Depends(get_db)
):
"""Kétlépcsős keresés: Free (Légvonal) vs Premium (Útvonal/Idő)"""
query = text("""
SELECT name, city, ST_Distance(coordinates, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) as dist
FROM data.organization_locations WHERE is_verified = TRUE ORDER BY dist LIMIT 10
""")
res = (await db.execute(query, {"lat": lat, "lng": lng})).fetchall()
results = []
for row in res:
item = {"name": row[0], "city": row[1], "distance_km": round(row[2]/1000, 2)}
if is_premium:
# PRÉMIUM: Itt jönne az útvonaltervező API integráció
item["estimated_travel_time_min"] = round(row[2] / 700) # Becsült
results.append(item)
return results
return {"status": "success"}

View File

@@ -1,15 +1,16 @@
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
# ITT A JAVÍTÁS: A példányt importáljuk, nem a régi függvényeket
from app.services.social_service import social_service
router = APIRouter()
@router.get("/leaderboard")
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
return await get_leaderboard(db, limit)
return await social_service.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)
return await social_service.vote_for_provider(db, user_id, provider_id, vote_value)