STABLE: Final schema sync, optimized gitignore
This commit is contained in:
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/deps.py
|
||||
from typing import Optional, Dict, Any, Union
|
||||
import logging
|
||||
from fastapi import Depends, HTTPException, status
|
||||
@@ -7,11 +8,18 @@ from sqlalchemy import select
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
||||
from app.models.identity import User, UserRole
|
||||
from app.models.identity import User, UserRole # JAVÍTVA: Új Identity modell használata
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- GONDOLATMENET / THOUGHT PROCESS ---
|
||||
# 1. Az OAuth2 folyamat a központosított bejelentkezési végponton keresztül fut.
|
||||
# 2. A token visszafejtésekor ellenőrizni kell a 'type' mezőt, hogy ne lehessen refresh tokennel belépni.
|
||||
# 3. A felhasználó lekérésekor a SQLAlchemy 2.0 aszinkron 'execute' és 'scalar_one_or_none' metódusait használjuk.
|
||||
# 4. A Scoped RBAC (Role-Based Access Control) biztosítja, hogy a felhasználók ne férjenek hozzá egymás flottáihoz.
|
||||
# ---------------------------------------
|
||||
|
||||
# Az OAuth2 folyamat a bejelentkezési végponton keresztül
|
||||
reusable_oauth2 = OAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/auth/login"
|
||||
@@ -23,8 +31,7 @@ async def get_current_token_payload(
|
||||
"""
|
||||
JWT token visszafejtése és a típus (access) ellenőrzése.
|
||||
"""
|
||||
# Dev bypass (ha esetleg fejlesztéshez használtad korábban, itt a helye,
|
||||
# de élesben a token validáció fut le)
|
||||
# Fejlesztői bypass (opcionális, csak DEBUG módban)
|
||||
if settings.DEBUG and token == "dev_bypass_active":
|
||||
return {
|
||||
"sub": "1",
|
||||
@@ -48,7 +55,7 @@ async def get_current_user(
|
||||
payload: Dict = Depends(get_current_token_payload)
|
||||
) -> User:
|
||||
"""
|
||||
Lekéri a felhasználót a token 'sub' mezője alapján.
|
||||
Lekéri a felhasználót a token 'sub' mezője alapján (SQLAlchemy 2.0 aszinkron módon).
|
||||
"""
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
@@ -57,6 +64,7 @@ async def get_current_user(
|
||||
detail="Token azonosítási hiba."
|
||||
)
|
||||
|
||||
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés
|
||||
result = await db.execute(select(User).where(User.id == int(user_id)))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
@@ -71,13 +79,12 @@ async def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
"""
|
||||
Ellenőrzi, hogy a felhasználó aktív-e.
|
||||
Ez elengedhetetlen az Admin felület és a védett végpontok számára.
|
||||
Ellenőrzi, hogy a felhasználó aktív-e (KYC Step 2 kész).
|
||||
"""
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="A művelethez aktív profil és KYC azonosítás (Step 2) szükséges."
|
||||
detail="A művelethez aktív profil és KYC azonosítás szükséges."
|
||||
)
|
||||
return current_user
|
||||
|
||||
@@ -86,22 +93,19 @@ async def check_resource_access(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Scoped RBAC: Megakadályozza, hogy egy felhasználó más valaki erőforrásaihoz nyúljon.
|
||||
Kezeli az ID-t (int) és a Scope ID-t / Slug-ot (str) is.
|
||||
Scoped RBAC: Megakadályozza a jogosulatlan hozzáférést mások adataihoz.
|
||||
"""
|
||||
if current_user.role == UserRole.superadmin:
|
||||
return True
|
||||
|
||||
# Ha a usernek van beállított scope_id-ja (pl. egy flottához tartozik),
|
||||
# akkor ellenőrizzük, hogy a kért erőforrás abba a scope-ba tartozik-e.
|
||||
user_scope = current_user.scope_id
|
||||
user_scope = str(current_user.scope_id) if current_user.scope_id else None
|
||||
requested_scope = str(resource_scope_id)
|
||||
|
||||
# 1. Saját erőforrás (saját ID)
|
||||
# 1. Saját ID ellenőrzése
|
||||
if str(current_user.id) == requested_scope:
|
||||
return True
|
||||
|
||||
# 2. Scope alapú hozzáférés (pl. flotta tagja)
|
||||
# 2. Szervezeti/Flotta scope ellenőrzése
|
||||
if user_scope and user_scope == requested_scope:
|
||||
return True
|
||||
|
||||
@@ -112,8 +116,7 @@ async def check_resource_access(
|
||||
|
||||
def check_min_rank(role_key: str):
|
||||
"""
|
||||
Dinamikus Rank ellenőrzés.
|
||||
Az adatbázisból (system_parameters) kéri le az elvárt szintet.
|
||||
Dinamikus Rank ellenőrzés a system_parameters tábla alapján.
|
||||
"""
|
||||
async def rank_checker(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -130,7 +133,7 @@ def check_min_rank(role_key: str):
|
||||
if user_rank < required_rank:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Alacsony jogosultsági szint. (Szükséges: {required_rank})"
|
||||
detail=f"Alacsony jogosultsági szint. (Elvárt: {required_rank})"
|
||||
)
|
||||
return True
|
||||
return rank_checker
|
||||
@@ -1,14 +1,17 @@
|
||||
from fastapi import APIRouter, Request
|
||||
# /opt/docker/dev/service_finder/backend/app/api/recommend.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.db.session import get_db
|
||||
|
||||
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
|
||||
async def provider_inbox(provider_id: str, db: AsyncSession = Depends(get_db)):
|
||||
""" Aszinkron szerviz-postaláda lekérdezés. """
|
||||
query = text("""
|
||||
SELECT * FROM data.service_profiles
|
||||
WHERE id = :p_id
|
||||
""")
|
||||
result = await db.execute(query, {"p_id": provider_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
Binary file not shown.
@@ -1,32 +1,20 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/api.py
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1.endpoints import auth, catalog, assets, organizations, documents, services, admin, expenses, evidence
|
||||
from app.api.v1.endpoints import (
|
||||
auth, catalog, assets, organizations, documents,
|
||||
services, admin, expenses, evidence, social
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Hitelesítés (Authentication)
|
||||
# Minden modul az új, refaktorált végpontokra mutat
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
|
||||
# Szolgáltatások és Vadászat (Service Hunt & Discovery)
|
||||
api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"])
|
||||
|
||||
# Katalógus (Vehicle Catalog)
|
||||
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
|
||||
|
||||
# Eszközök / Járművek (Assets)
|
||||
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
|
||||
|
||||
# Szervezetek (Organizations)
|
||||
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
||||
|
||||
# Dokumentumok (Documents)
|
||||
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
|
||||
|
||||
# --- 🛡️ SENTINEL ADMIN KONTROLL PANEL ---
|
||||
# Ez a rész tette láthatóvá az Admin API-t a felületen
|
||||
api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"])
|
||||
|
||||
# Evidence & OCR Robot 3
|
||||
api_router.include_router(evidence.router, prefix="/evidence", tags=["Evidence & OCR (Robot 3)"])
|
||||
|
||||
# Fleet Expenses TCO
|
||||
api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"])
|
||||
api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"])
|
||||
api_router.include_router(social.router, prefix="/social", tags=["Social & Leaderboard"])
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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."}
|
||||
@@ -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(
|
||||
|
||||
@@ -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."}
|
||||
@@ -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)}
|
||||
@@ -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."}
|
||||
@@ -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"}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()]}
|
||||
@@ -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"}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user