Initial commit: Robot ökoszisztéma v2.0 - Stabilizált jármű és szerviz robotok
This commit is contained in:
132
backend/app/api/auth.py.old
Executable file
132
backend/app/api/auth.py.old
Executable file
@@ -0,0 +1,132 @@
|
||||
from datetime import timedelta
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.core.config import settings
|
||||
from app.core.security import create_token, decode_token
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
@router.post("/login")
|
||||
def login(payload: Dict[str, Any]):
|
||||
"""
|
||||
payload:
|
||||
{
|
||||
"org_id": "<uuid>",
|
||||
"login": "<username or email>",
|
||||
"password": "<plain>"
|
||||
}
|
||||
"""
|
||||
from app.db.session import get_conn
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("BEGIN;")
|
||||
|
||||
org_id = (payload.get("org_id") or "").strip()
|
||||
login_id = (payload.get("login") or "").strip()
|
||||
password = payload.get("password") or ""
|
||||
|
||||
if not org_id or not login_id or not password:
|
||||
raise HTTPException(status_code=400, detail="org_id, login, password required")
|
||||
|
||||
# RLS miatt kötelező: org kontextus beállítás
|
||||
cur.execute("SELECT set_config('app.tenant_org_id', %s, false);", (org_id,))
|
||||
|
||||
# account + credential
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
a.account_id::text,
|
||||
a.org_id::text,
|
||||
a.username::text,
|
||||
a.email::text,
|
||||
c.password_hash,
|
||||
c.is_active
|
||||
FROM app.account a
|
||||
JOIN app.account_credential c ON c.account_id = a.account_id
|
||||
WHERE a.org_id = %s::uuid
|
||||
AND (a.username = %s::citext OR a.email = %s::citext)
|
||||
AND c.is_active = true
|
||||
LIMIT 1;
|
||||
""",
|
||||
(org_id, login_id, login_id),
|
||||
)
|
||||
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
account_id, org_id_db, username, email, password_hash, cred_active = row
|
||||
|
||||
# Jelszó ellenőrzés pgcrypto-val: crypt(plain, stored_hash) = stored_hash
|
||||
cur.execute("SELECT crypt(%s, %s) = %s;", (password, password_hash, password_hash))
|
||||
ok = cur.fetchone()[0]
|
||||
if not ok:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
# MVP: role később membershipből; most fixen tenant_admin
|
||||
role_code = "tenant_admin"
|
||||
is_platform_admin = False
|
||||
|
||||
access = create_token(
|
||||
{
|
||||
"sub": account_id,
|
||||
"org_id": org_id_db,
|
||||
"role": role_code,
|
||||
"is_platform_admin": is_platform_admin,
|
||||
"type": "access",
|
||||
},
|
||||
settings.JWT_SECRET,
|
||||
timedelta(minutes=settings.JWT_ACCESS_MINUTES),
|
||||
)
|
||||
|
||||
refresh = create_token(
|
||||
{
|
||||
"sub": account_id,
|
||||
"org_id": org_id_db,
|
||||
"role": role_code,
|
||||
"is_platform_admin": is_platform_admin,
|
||||
"type": "refresh",
|
||||
},
|
||||
settings.JWT_SECRET,
|
||||
timedelta(days=settings.JWT_REFRESH_DAYS),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer"}
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
def refresh_token(payload: Dict[str, Any]):
|
||||
token = payload.get("refresh_token") or ""
|
||||
if not token:
|
||||
raise HTTPException(status_code=400, detail="refresh_token required")
|
||||
|
||||
try:
|
||||
claims = decode_token(token, settings.JWT_SECRET)
|
||||
if claims.get("type") != "refresh":
|
||||
raise HTTPException(status_code=401, detail="Invalid refresh token type")
|
||||
|
||||
access = create_token(
|
||||
{
|
||||
"sub": claims.get("sub"),
|
||||
"org_id": claims.get("org_id"),
|
||||
"role": claims.get("role"),
|
||||
"is_platform_admin": claims.get("is_platform_admin", False),
|
||||
"type": "access",
|
||||
},
|
||||
settings.JWT_SECRET,
|
||||
timedelta(minutes=settings.JWT_ACCESS_MINUTES),
|
||||
)
|
||||
return {"access_token": access, "token_type": "bearer"}
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired refresh token")
|
||||
139
backend/app/api/deps.py
Executable file
139
backend/app/api/deps.py
Executable file
@@ -0,0 +1,139 @@
|
||||
# /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
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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 # 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"
|
||||
)
|
||||
|
||||
async def get_current_token_payload(
|
||||
token: str = Depends(reusable_oauth2)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
JWT token visszafejtése és a típus (access) ellenőrzése.
|
||||
"""
|
||||
# Fejlesztői bypass (opcionális, csak DEBUG módban)
|
||||
if settings.DEBUG and token == "dev_bypass_active":
|
||||
return {
|
||||
"sub": "1",
|
||||
"role": "superadmin",
|
||||
"rank": 100,
|
||||
"scope_level": "global",
|
||||
"scope_id": "all",
|
||||
"type": "access"
|
||||
}
|
||||
|
||||
payload = decode_token(token)
|
||||
if not payload or payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Érvénytelen vagy lejárt munkamenet."
|
||||
)
|
||||
return payload
|
||||
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
payload: Dict = Depends(get_current_token_payload)
|
||||
) -> User:
|
||||
"""
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
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()
|
||||
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A felhasználó nem található."
|
||||
)
|
||||
return user
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
"""
|
||||
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 szükséges."
|
||||
)
|
||||
return current_user
|
||||
|
||||
async def check_resource_access(
|
||||
resource_scope_id: Union[str, int],
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Scoped RBAC: Megakadályozza a jogosulatlan hozzáférést mások adataihoz.
|
||||
"""
|
||||
if current_user.role == UserRole.superadmin:
|
||||
return True
|
||||
|
||||
user_scope = str(current_user.scope_id) if current_user.scope_id else None
|
||||
requested_scope = str(resource_scope_id)
|
||||
|
||||
# 1. Saját ID ellenőrzése
|
||||
if str(current_user.id) == requested_scope:
|
||||
return True
|
||||
|
||||
# 2. Szervezeti/Flotta scope ellenőrzése
|
||||
if user_scope and user_scope == requested_scope:
|
||||
return True
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Nincs jogosultsága ehhez az erőforráshoz."
|
||||
)
|
||||
|
||||
def check_min_rank(role_key: str):
|
||||
"""
|
||||
Dinamikus Rank ellenőrzés a system_parameters tábla alapján.
|
||||
"""
|
||||
async def rank_checker(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
payload: Dict = Depends(get_current_token_payload)
|
||||
):
|
||||
# A settings.get_db_setting-et használjuk a dinamikus lekéréshez
|
||||
ranks = await settings.get_db_setting(
|
||||
db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP
|
||||
)
|
||||
|
||||
required_rank = ranks.get(role_key, 0)
|
||||
user_rank = payload.get("rank", 0)
|
||||
|
||||
if user_rank < required_rank:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Alacsony jogosultsági szint. (Elvárt: {required_rank})"
|
||||
)
|
||||
return True
|
||||
return rank_checker
|
||||
17
backend/app/api/recommend.py
Executable file
17
backend/app/api/recommend.py
Executable file
@@ -0,0 +1,17 @@
|
||||
# /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")
|
||||
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()]
|
||||
20
backend/app/api/v1/api.py
Executable file
20
backend/app/api/v1/api.py
Executable file
@@ -0,0 +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, social
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Minden modul az új, refaktorált végpontokra mutat
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"])
|
||||
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
|
||||
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
|
||||
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
||||
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
|
||||
api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"])
|
||||
api_router.include_router(evidence.router, prefix="/evidence", tags=["Evidence & OCR (Robot 3)"])
|
||||
api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"])
|
||||
api_router.include_router(social.router, prefix="/social", tags=["Social & Leaderboard"])
|
||||
131
backend/app/api/v1/endpoints/admin.py
Executable file
131
backend/app/api/v1/endpoints/admin.py
Executable file
@@ -0,0 +1,131 @@
|
||||
# /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
|
||||
from typing import List, Any, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.api import deps
|
||||
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.services.security_service import security_service
|
||||
from app.services.translation_service import TranslationService
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
key: str
|
||||
value: Any
|
||||
scope_level: str = "global"
|
||||
scope_id: Optional[str] = None
|
||||
category: str = "general"
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
|
||||
""" 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!"
|
||||
)
|
||||
return current_user
|
||||
|
||||
@router.get("/health-monitor", tags=["Sentinel Monitoring"])
|
||||
async def get_system_health(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
stats = {}
|
||||
|
||||
# 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}
|
||||
|
||||
asset_count = await db.execute(text("SELECT count(*) FROM data.assets"))
|
||||
stats["total_assets"] = asset_count.scalar()
|
||||
|
||||
org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
|
||||
stats["total_organizations"] = org_count.scalar()
|
||||
|
||||
# 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(SecurityAuditLog.id))
|
||||
.where(
|
||||
SecurityAuditLog.is_critical == True,
|
||||
SecurityAuditLog.created_at >= day_ago
|
||||
)
|
||||
)
|
||||
stats["critical_alerts_24h"] = crit_logs.scalar() or 0
|
||||
|
||||
return stats
|
||||
|
||||
@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)
|
||||
):
|
||||
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/approve/{action_id}", tags=["Sentinel Security"])
|
||||
async def approve_action(
|
||||
action_id: int,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
try:
|
||||
await security_service.approve_action(db, admin.id, action_id)
|
||||
return {"status": "success", "message": "Művelet végrehajtva."}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.get("/parameters", tags=["Dynamic Configuration"])
|
||||
async def list_all_parameters(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
result = await db.execute(select(SystemParameter))
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/parameters", tags=["Dynamic Configuration"])
|
||||
async def set_parameter(
|
||||
config: ConfigUpdate,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
query = text("""
|
||||
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
|
||||
VALUES (:key, :val, :sl, :sid, :cat, :user)
|
||||
ON CONFLICT (key, scope_level, scope_id)
|
||||
DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
category = EXCLUDED.category,
|
||||
last_modified_by = EXCLUDED.last_modified_by,
|
||||
updated_at = now()
|
||||
""")
|
||||
|
||||
await db.execute(query, {
|
||||
"key": config.key,
|
||||
"val": config.value,
|
||||
"sl": config.scope_level,
|
||||
"sid": config.scope_id,
|
||||
"cat": config.category,
|
||||
"user": admin.email
|
||||
})
|
||||
await db.commit()
|
||||
return {"status": "success", "message": f"'{config.key}' frissítve."}
|
||||
|
||||
@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)
|
||||
):
|
||||
await TranslationService.export_to_json(db)
|
||||
return {"message": "JSON fájlok frissítve."}
|
||||
54
backend/app/api/v1/endpoints/assets.py
Executable file
54
backend/app/api/v1/endpoints/assets.py
Executable file
@@ -0,0 +1,54 @@
|
||||
# /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
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
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
|
||||
from app.models.identity import User
|
||||
from app.services.cost_service import cost_service
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||
from app.schemas.asset import AssetResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/{asset_id}/financial-summary", response_model=Dict[str, Any])
|
||||
async def get_asset_financial_report(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
MB 2.0 Dinamikus Pénzügyi Riport.
|
||||
Visszaadja a kategóriákra bontott és az összesített költségeket (Local/EUR).
|
||||
"""
|
||||
# 1. Jogosultság ellenőrzése (Csak a tulajdonos vagy admin láthatja)
|
||||
# (Itt egy gyors check, hogy az asset az övé-e)
|
||||
|
||||
try:
|
||||
return await cost_service.get_asset_financial_summary(db, asset_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="Hiba a riport generálásakor")
|
||||
|
||||
@router.get("/{asset_id}/costs", response_model=List[AssetCostResponse])
|
||||
async def list_asset_costs(
|
||||
asset_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Tételes költséglista lapozással (Pagination)."""
|
||||
stmt = (
|
||||
select(AssetCost)
|
||||
.where(AssetCost.asset_id == asset_id)
|
||||
.order_by(desc(AssetCost.date))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
res = await db.execute(stmt)
|
||||
return res.scalars().all()
|
||||
41
backend/app/api/v1/endpoints/auth.py
Executable file
41
backend/app/api/v1/endpoints/auth.py
Executable file
@@ -0,0 +1,41 @@
|
||||
# backend/app/api/v1/endpoints/auth.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.db.session import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
||||
from app.core.config import settings
|
||||
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User # JAVÍTVA: Új központi modell
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
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)
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": role_name,
|
||||
"rank": ranks.get(role_name, 10),
|
||||
"scope_level": user.scope_level or "individual",
|
||||
"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.post("/complete-kyc")
|
||||
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."}
|
||||
63
backend/app/api/v1/endpoints/billing.py
Executable file
63
backend/app/api/v1/endpoints/billing.py
Executable file
@@ -0,0 +1,63 @@
|
||||
# backend/app/api/v1/endpoints/billing.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity import User, Wallet, UserRole
|
||||
from app.models.audit import FinancialLedger
|
||||
from app.services.config_service import config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/upgrade")
|
||||
async def upgrade_account(target_package: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Univerzális csomagváltó.
|
||||
Kezeli az 5+ csomagot, a Rank-ugrást és a különleges 'Service Coin' bónuszokat.
|
||||
"""
|
||||
# 1. Lekérjük a teljes csomagmátrixot az adminból
|
||||
# Példa JSON: {"premium": {"price": 2000, "rank": 5, "type": "credit"}, "service_pro": {"price": 10000, "rank": 30, "type": "coin"}}
|
||||
package_matrix = await config.get_setting(db, "subscription_packages_matrix")
|
||||
|
||||
if target_package not in package_matrix:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen csomagválasztás.")
|
||||
|
||||
pkg_info = package_matrix[target_package]
|
||||
price = pkg_info["price"]
|
||||
|
||||
# 2. Pénztárca ellenőrzése
|
||||
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
|
||||
wallet = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
total_balance = wallet.purchased_credits + wallet.earned_credits
|
||||
|
||||
if total_balance < price:
|
||||
raise HTTPException(status_code=402, detail="Nincs elég kredited a csomagváltáshoz.")
|
||||
|
||||
# 3. Levonási logika (Purchased -> Earned sorrend)
|
||||
if wallet.purchased_credits >= price:
|
||||
wallet.purchased_credits -= price
|
||||
else:
|
||||
remaining = price - wallet.purchased_credits
|
||||
wallet.purchased_credits = 0
|
||||
wallet.earned_credits -= remaining
|
||||
|
||||
# 4. Speciális Szerviz Logika (Service Coins)
|
||||
# Ha a csomag típusa 'coin', akkor a szerviz kap egy kezdő Coin csomagot is
|
||||
if pkg_info.get("type") == "coin":
|
||||
initial_coins = pkg_info.get("initial_coin_bonus", 100)
|
||||
wallet.service_coins += initial_coins
|
||||
logger.info(f"User {current_user.id} upgraded to Service Pro, awarded {initial_coins} coins.")
|
||||
|
||||
# 5. Rang frissítése és naplózás
|
||||
current_user.role = target_package # Pl. 'service_pro' vagy 'vip'
|
||||
|
||||
db.add(FinancialLedger(
|
||||
user_id=current_user.id,
|
||||
amount=-price,
|
||||
transaction_type=f"UPGRADE_{target_package.upper()}",
|
||||
details=pkg_info
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
return {"status": "success", "package": target_package, "rank_granted": pkg_info["rank"]}
|
||||
46
backend/app/api/v1/endpoints/catalog.py
Executable file
46
backend/app/api/v1/endpoints/catalog.py
Executable file
@@ -0,0 +1,46 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.services.asset_service import AssetService
|
||||
from typing import List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/makes", response_model=List[str])
|
||||
async def list_makes(db: AsyncSession = Depends(get_db)):
|
||||
"""1. Szint: Márkák listázása."""
|
||||
return await AssetService.get_makes(db)
|
||||
|
||||
@router.get("/models", response_model=List[str])
|
||||
async def list_models(make: str, db: AsyncSession = Depends(get_db)):
|
||||
"""2. Szint: Típusok listázása egy adott márkához."""
|
||||
models = await AssetService.get_models(db, make)
|
||||
if not models:
|
||||
raise HTTPException(status_code=404, detail="Márka nem található vagy nincsenek típusok.")
|
||||
return models
|
||||
|
||||
@router.get("/generations", response_model=List[str])
|
||||
async def list_generations(make: str, model: str, db: AsyncSession = Depends(get_db)):
|
||||
"""3. Szint: Generációk/Évjáratok listázása."""
|
||||
generations = await AssetService.get_generations(db, make, model)
|
||||
if not generations:
|
||||
raise HTTPException(status_code=404, detail="Nincs generációs adat ehhez a típushoz.")
|
||||
return generations
|
||||
|
||||
@router.get("/engines")
|
||||
async def list_engines(make: str, model: str, gen: str, db: AsyncSession = Depends(get_db)):
|
||||
"""4. Szint: Motorváltozatok és technikai specifikációk."""
|
||||
engines = await AssetService.get_engines(db, make, model, gen)
|
||||
if not engines:
|
||||
raise HTTPException(status_code=404, detail="Nincs motorváltozat adat.")
|
||||
|
||||
# Itt visszaküldjük a teljes katalógus objektumokat (ID, motorváltozat, specifikációk)
|
||||
return [
|
||||
{
|
||||
"id": e.id,
|
||||
"variant": e.engine_variant,
|
||||
"engine_code": e.engine_code,
|
||||
"fuel_type": e.fuel_type,
|
||||
"factory_data": e.factory_data
|
||||
} for e in engines
|
||||
]
|
||||
87
backend/app/api/v1/endpoints/documents.py
Executable file
87
backend/app/api/v1/endpoints/documents.py
Executable file
@@ -0,0 +1,87 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/documents.py
|
||||
import uuid
|
||||
from typing import Any, Dict
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, Form, BackgroundTasks, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.services.document_service import DocumentService
|
||||
from app.models.identity import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/upload/{parent_type}/{parent_id}")
|
||||
async def upload_document(
|
||||
parent_type: str,
|
||||
parent_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
doc_type: str = Form(..., description="A dokumentum típusa: 'invoice', 'registration_card', 'sale_contract'"),
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
MB 2.0 Dokumentum Pipeline.
|
||||
1. Ellenőrzi a felhasználó havi OCR kvótáját (Admin 2.0).
|
||||
2. Optimalizálja és NAS-ra menti a képet (WebP konverzió).
|
||||
3. Automatikusan elindítja a Robot 1-et (OCR), ha a típus engedélyezett.
|
||||
"""
|
||||
|
||||
# 1. Bemeneti validáció
|
||||
valid_parents = ["organizations", "assets", "transfers"]
|
||||
if parent_type not in valid_parents:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Érvénytelen cél-típus! Megengedett: {', '.join(valid_parents)}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 2. Feldolgozás a szolgáltatás rétegben
|
||||
# Itt történik a kvóta-ellenőrzés és a Robot 1 triggerelése is
|
||||
doc = await DocumentService.process_upload(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
file=file,
|
||||
parent_type=parent_type,
|
||||
parent_id=parent_id,
|
||||
doc_type=doc_type,
|
||||
background_tasks=background_tasks
|
||||
)
|
||||
|
||||
# 3. Válasz összeállítása az állapot alapján
|
||||
response_data = {
|
||||
"document_id": doc.id,
|
||||
"original_name": doc.original_name,
|
||||
"status": doc.status,
|
||||
"thumbnail": doc.thumbnail_path,
|
||||
}
|
||||
|
||||
if doc.status == "processing":
|
||||
response_data["message"] = "🤖 Robot 1 megkezdte a dokumentum elemzését. Értesítjük, ha kész!"
|
||||
else:
|
||||
response_data["message"] = "Dokumentum sikeresen archiválva a széfben."
|
||||
|
||||
return response_data
|
||||
|
||||
except HTTPException as he:
|
||||
# Közvetlenül átengedjük a service-ből jövő (pl. kvóta) hibákat
|
||||
raise he
|
||||
except ValueError as ve:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(ve))
|
||||
except Exception as e:
|
||||
# Sentinel naplózás és általános hiba
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Kritikus hiba a dokumentum feldolgozása során."
|
||||
)
|
||||
|
||||
@router.get("/{document_id}/status")
|
||||
async def get_document_status(
|
||||
document_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Lekérdezhető, hogy a robot végzett-e már a feldolgozással."""
|
||||
# (Itt egy egyszerű lekérdezés a Document táblából a státuszra)
|
||||
pass
|
||||
24
backend/app/api/v1/endpoints/evidence.py
Executable file
24
backend/app/api/v1/endpoints/evidence.py
Executable file
@@ -0,0 +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 select, func, text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.asset import Asset # JAVÍTVA: Asset modell
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@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
|
||||
|
||||
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.")
|
||||
|
||||
# OCR hívás helye...
|
||||
return {"success": True, "message": "Feldolgozás megkezdődött."}
|
||||
33
backend/app/api/v1/endpoints/expenses.py
Executable file
33
backend/app/api/v1/endpoints/expenses.py
Executable file
@@ -0,0 +1,33 @@
|
||||
# backend/app/api/v1/endpoints/expenses.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ExpenseCreate(BaseModel):
|
||||
asset_id: str
|
||||
category: str
|
||||
amount: float
|
||||
date: date
|
||||
|
||||
@router.post("/add")
|
||||
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ó.")
|
||||
|
||||
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"}
|
||||
40
backend/app/api/v1/endpoints/gamification.py
Executable file
40
backend/app/api/v1/endpoints/gamification.py
Executable file
@@ -0,0 +1,40 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from typing import List
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.gamification import UserStats, PointsLedger
|
||||
from app.services.config_service import config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/my-stats")
|
||||
async def get_my_stats(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""A bejelentkezett felhasználó aktuális XP-je, szintje és büntetőpontjai."""
|
||||
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not stats:
|
||||
return {"total_xp": 0, "current_level": 1, "penalty_points": 0}
|
||||
|
||||
return stats
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def get_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||
"""A 10 legtöbb XP-vel rendelkező felhasználó listája."""
|
||||
stmt = (
|
||||
select(User.email, UserStats.total_xp, UserStats.current_level)
|
||||
.join(UserStats, User.id == UserStats.user_id)
|
||||
.order_by(desc(UserStats.total_xp))
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
# Az email-eket maszkoljuk a GDPR miatt (pl. k***s@p***.hu)
|
||||
return [
|
||||
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
|
||||
for r in result.all()
|
||||
]
|
||||
100
backend/app/api/v1/endpoints/notifications.py
Executable file
100
backend/app/api/v1/endpoints/notifications.py
Executable file
@@ -0,0 +1,100 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/notifications.py
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, desc, func
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.system import InternalNotification
|
||||
from app.schemas.social import NotificationResponse, NotificationUpdate # Feltételezett sémák
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/my", response_model=List[NotificationResponse])
|
||||
async def get_my_notifications(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
unread_only: bool = False,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
offset: int = 0
|
||||
):
|
||||
"""
|
||||
Lekéri a bejelentkezett felhasználó értesítéseit.
|
||||
Támogatja a szűrést az olvasatlanokra és a lapozást.
|
||||
"""
|
||||
stmt = (
|
||||
select(InternalNotification)
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
)
|
||||
|
||||
if unread_only:
|
||||
stmt = stmt.where(InternalNotification.is_read == False)
|
||||
|
||||
stmt = stmt.order_by(desc(InternalNotification.created_at)).offset(offset).limit(limit)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/mark-read")
|
||||
async def mark_as_read(
|
||||
notification_ids: List[uuid.UUID],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Egy vagy több értesítés olvasottnak jelölése.
|
||||
"""
|
||||
if not notification_ids:
|
||||
raise HTTPException(status_code=400, detail="Nincs megadva azonosító.")
|
||||
|
||||
stmt = (
|
||||
update(InternalNotification)
|
||||
.where(InternalNotification.id.in_(notification_ids))
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
.values(is_read=True, read_at=func.now())
|
||||
)
|
||||
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "marked_count": len(notification_ids)}
|
||||
|
||||
@router.post("/mark-all-read")
|
||||
async def mark_all_read(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
A felhasználó összes értesítésének olvasottnak jelölése.
|
||||
"""
|
||||
stmt = (
|
||||
update(InternalNotification)
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
.where(InternalNotification.is_read == False)
|
||||
.values(is_read=True, read_at=func.now())
|
||||
)
|
||||
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "Minden értesítés olvasottnak jelölve."}
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_notification_summary(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Gyors összesítő a Dashboard-hoz (pl. hány olvasatlan van).
|
||||
"""
|
||||
stmt = (
|
||||
select(func.count(InternalNotification.id))
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
.where(InternalNotification.is_read == False)
|
||||
)
|
||||
|
||||
unread_count = (await db.execute(stmt)).scalar() or 0
|
||||
return {"unread_count": unread_count}
|
||||
115
backend/app/api/v1/endpoints/organizations.py
Executable file
115
backend/app/api/v1/endpoints/organizations.py
Executable file
@@ -0,0 +1,115 @@
|
||||
# /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 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
|
||||
from app.models.identity import User # JAVÍTVA: Központi Identity modell
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
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),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Ú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)
|
||||
if org_in.country_code == "HU":
|
||||
if not re.match(r"^\d{8}-\d-\d{2}$", org_in.tax_number):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Érvénytelen magyar adószám formátum!"
|
||||
)
|
||||
|
||||
# 2. Duplikáció ellenőrzés
|
||||
stmt_exist = select(Organization).where(Organization.tax_number == org_in.tax_number)
|
||||
result_exist = await db.execute(stmt_exist)
|
||||
if result_exist.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Ezzel az adószámmal már regisztráltak céget!"
|
||||
)
|
||||
|
||||
# 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
|
||||
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,
|
||||
address_street_type=org_in.address_street_type,
|
||||
address_house_number=org_in.address_house_number,
|
||||
address_hrsz=org_in.address_hrsz,
|
||||
address_stairwell=org_in.address_stairwell,
|
||||
address_floor=org_in.address_floor,
|
||||
address_door=org_in.address_door,
|
||||
country_code=org_in.country_code,
|
||||
org_type=OrgType.business,
|
||||
status="pending_verification"
|
||||
)
|
||||
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
|
||||
# 5. TULAJDONOS RÖGZÍTÉSE
|
||||
owner_member = OrganizationMember(
|
||||
organization_id=new_org.id,
|
||||
user_id=current_user.id,
|
||||
role="OWNER" # JAVÍTVA: Enum kompatibilis nagybetűs forma
|
||||
)
|
||||
db.add(owner_member)
|
||||
|
||||
# 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 kész: {org_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"NAS hiba: {e}")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_org)
|
||||
|
||||
return {"organization_id": new_org.id, "status": new_org.status}
|
||||
|
||||
@router.get("/my", response_model=List[CorpOnboardResponse])
|
||||
async def get_my_organizations(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
""" 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()
|
||||
|
||||
return [{"organization_id": o.id, "status": o.status} for o in orgs]
|
||||
12
backend/app/api/v1/endpoints/providers.py
Executable file
12
backend/app/api/v1/endpoints/providers.py
Executable file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.schemas.social import ServiceProviderCreate, ServiceProviderResponse
|
||||
from app.services.social_service import create_service_provider
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=ServiceProviderResponse)
|
||||
async def add_provider(provider_data: ServiceProviderCreate, db: AsyncSession = Depends(get_db)):
|
||||
user_id = 2
|
||||
return await create_service_provider(db, provider_data, user_id)
|
||||
50
backend/app/api/v1/endpoints/reports.py
Executable file
50
backend/app/api/v1/endpoints/reports.py
Executable file
@@ -0,0 +1,50 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
|
||||
router = APIRouter() # EZ HIÁNYZOTT!
|
||||
|
||||
@router.get("/summary/{vehicle_id}")
|
||||
async def get_vehicle_summary(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Összesített jelentés egy járműhöz: kategóriánkénti költségek.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
category,
|
||||
SUM(amount) as total_amount,
|
||||
COUNT(*) as transaction_count
|
||||
FROM data.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY category
|
||||
""")
|
||||
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
rows = result.fetchall()
|
||||
|
||||
total_cost = sum(row.total_amount for row in rows) if rows else 0
|
||||
|
||||
return {
|
||||
"vehicle_id": vehicle_id,
|
||||
"total_cost": float(total_cost),
|
||||
"breakdown": [dict(row._mapping) for row in rows]
|
||||
}
|
||||
|
||||
@router.get("/trends/{vehicle_id}")
|
||||
async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Visszaadja az utolsó 6 hónap költéseit havi bontásban.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
SUM(amount) as monthly_total
|
||||
FROM data.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
LIMIT 6
|
||||
""")
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
24
backend/app/api/v1/endpoints/search.py
Executable file
24
backend/app/api/v1/endpoints/search.py
Executable file
@@ -0,0 +1,24 @@
|
||||
# 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.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)):
|
||||
# PostGIS alapú keresés a data.branches táblában (a régi locations helyett)
|
||||
query = text("""
|
||||
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.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, "r": radius})
|
||||
return {"results": [dict(row._mapping) for row in result.fetchall()]}
|
||||
58
backend/app/api/v1/endpoints/services.py
Executable file
58
backend/app/api/v1/endpoints/services.py
Executable file
@@ -0,0 +1,58 @@
|
||||
from fastapi import APIRouter, Depends, Form, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, text
|
||||
from typing import List, Optional
|
||||
from app.db.session import get_db
|
||||
from app.services.gamification_service import GamificationService
|
||||
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- 🎯 SZERVIZ VADÁSZAT (Service Hunt) ---
|
||||
@router.post("/hunt")
|
||||
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 jutalompontért. """
|
||||
# Új szerviz-jelölt rögzítése
|
||||
await db.execute(text("""
|
||||
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})
|
||||
|
||||
# MB 2.0 Gamification: 50 pont a felfedezésért
|
||||
# TODO: A 1-es ID helyett a bejelentkezett felhasználót kell használni (current_user.id)
|
||||
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
|
||||
await db.commit()
|
||||
return {"status": "success", "message": "Discovery registered and points awarded."}
|
||||
|
||||
# --- 🔍 SZERVIZ KERESŐ (Service Search) ---
|
||||
@router.get("/search")
|
||||
async def search_services(
|
||||
expertise_key: Optional[str] = Query(None, description="Szakmai címke (pl. brake_service)"),
|
||||
city: Optional[str] = Query(None, description="Város szűrés"),
|
||||
min_confidence: int = Query(0, description="Minimum hitelességi szint (0-2)"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
""" Szakmai szempontú keresőmotor, ami a validált címkék alapján szűr. """
|
||||
# Alap lekérdezés: Szervizek, akiknek van szakértelmük
|
||||
query = select(ServiceProfile).join(ServiceProfile.expertises).join(ServiceExpertise.tag)
|
||||
|
||||
filters = []
|
||||
if expertise_key:
|
||||
filters.append(ExpertiseTag.key == expertise_key)
|
||||
if city:
|
||||
filters.append(ServiceProfile.city.ilike(f"%{city}%"))
|
||||
if min_confidence > 0:
|
||||
filters.append(ServiceExpertise.confidence_level >= min_confidence)
|
||||
|
||||
if filters:
|
||||
query = query.where(and_(*filters))
|
||||
|
||||
result = await db.execute(query.distinct())
|
||||
services = result.scalars().all()
|
||||
|
||||
return services
|
||||
16
backend/app/api/v1/endpoints/social.py
Executable file
16
backend/app/api/v1/endpoints/social.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
# 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 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 social_service.vote_for_provider(db, user_id, provider_id, vote_value)
|
||||
16
backend/app/api/v1/endpoints/users.py
Executable file
16
backend/app/api/v1/endpoints/users.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def read_users_me(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Visszaadja a bejelentkezett felhasználó profilját"""
|
||||
return current_user
|
||||
Reference in New Issue
Block a user