2026.03.30 front és garázs logika
This commit is contained in:
@@ -5,6 +5,7 @@ from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload, joinedload
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
||||
@@ -51,7 +52,7 @@ async def get_current_token_payload(
|
||||
return payload
|
||||
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
payload: Dict = Depends(get_current_token_payload)
|
||||
) -> User:
|
||||
"""
|
||||
@@ -60,17 +61,19 @@ async def get_current_user(
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
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()
|
||||
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés with eager loading
|
||||
# We need to load the person relationship to avoid lazy loading issues
|
||||
stmt = select(User).where(User.id == int(user_id)).options(joinedload(User.person))
|
||||
result = await db.execute(stmt)
|
||||
user = result.unique().scalar_one_or_none()
|
||||
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A felhasználó nem található."
|
||||
)
|
||||
return user
|
||||
|
||||
@@ -30,34 +30,69 @@ async def get_user_vehicles(
|
||||
|
||||
This endpoint returns a paginated list of vehicles that the authenticated user
|
||||
has access to (either as owner or through organization membership).
|
||||
|
||||
Garage-centric logic: In corporate mode, only returns vehicles physically
|
||||
parked in the active organization's garages (branches).
|
||||
"""
|
||||
# Query assets where user is owner or organization member
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import or_, select
|
||||
|
||||
# First, get user's organization memberships
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
)
|
||||
org_result = await db.execute(org_stmt)
|
||||
user_org_ids = [row[0] for row in org_result.all()]
|
||||
|
||||
# Build query: assets owned by user OR assets in user's organizations
|
||||
stmt = (
|
||||
select(Asset)
|
||||
.where(
|
||||
or_(
|
||||
Asset.owner_person_id == current_user.id,
|
||||
Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False,
|
||||
Asset.operator_person_id == current_user.id,
|
||||
Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False
|
||||
if current_user.scope_id is None:
|
||||
# Personal mode: only show vehicles owned/operated personally (no organization)
|
||||
stmt = (
|
||||
select(Asset)
|
||||
.where(
|
||||
or_(
|
||||
Asset.owner_org_id.is_(None),
|
||||
Asset.operator_org_id.is_(None)
|
||||
),
|
||||
or_(
|
||||
Asset.owner_person_id == current_user.person_id,
|
||||
Asset.operator_person_id == current_user.person_id
|
||||
)
|
||||
)
|
||||
.order_by(Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
)
|
||||
else:
|
||||
# Corporate mode: only show vehicles belonging to the active organization's garages
|
||||
try:
|
||||
scope_org_id = int(current_user.scope_id)
|
||||
except (ValueError, TypeError):
|
||||
# If scope_id is not a valid integer, treat as no organization
|
||||
scope_org_id = None
|
||||
|
||||
if scope_org_id is None:
|
||||
# Fallback: no valid organization, return empty list
|
||||
return []
|
||||
|
||||
# First, get all branch IDs (garages) for this organization
|
||||
from app.models.marketplace.organization import Branch
|
||||
branch_stmt = select(Branch.id).where(
|
||||
Branch.organization_id == scope_org_id,
|
||||
Branch.is_deleted == False,
|
||||
Branch.status == "active"
|
||||
)
|
||||
branch_result = await db.execute(branch_stmt)
|
||||
branch_ids = [row[0] for row in branch_result.all()]
|
||||
|
||||
if not branch_ids:
|
||||
# Organization has no active garages, return empty list
|
||||
return []
|
||||
|
||||
# Query assets that are in any of the organization's garages
|
||||
stmt = (
|
||||
select(Asset)
|
||||
.where(
|
||||
Asset.branch_id.in_(branch_ids),
|
||||
Asset.status == "active"
|
||||
)
|
||||
.order_by(Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
)
|
||||
.order_by(Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
assets = result.scalars().all()
|
||||
@@ -170,25 +205,16 @@ async def create_or_claim_vehicle(
|
||||
- XP jutalom adása a felhasználónak
|
||||
"""
|
||||
try:
|
||||
# Determine organization ID: use provided or default to user's first organization
|
||||
org_id = payload.organization_id
|
||||
if org_id is None:
|
||||
# Get user's organization memberships
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
).limit(1)
|
||||
org_result = await db.execute(org_stmt)
|
||||
user_org = org_result.scalar_one_or_none()
|
||||
|
||||
if user_org is None:
|
||||
# User has no organization - create a personal organization or use default
|
||||
# For now, raise an error (in future, we could create a personal org)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No organization found for user. Please specify an organization_id or join/create an organization first."
|
||||
)
|
||||
org_id = user_org
|
||||
# Determine organization ID based on user's active scope (garage isolation)
|
||||
# The owner_org_id MUST be set to the current_user.scope_id.
|
||||
# If the scope is null, it stays null (personal).
|
||||
org_id = None
|
||||
if current_user.scope_id is not None:
|
||||
try:
|
||||
org_id = int(current_user.scope_id)
|
||||
except (ValueError, TypeError):
|
||||
# If scope_id is not a valid integer, treat as personal (no organization)
|
||||
pass
|
||||
|
||||
asset = await AssetService.create_or_claim_vehicle(
|
||||
db=db,
|
||||
|
||||
@@ -106,7 +106,7 @@ async def onboard_organization(
|
||||
|
||||
return {"organization_id": new_org.id, "status": new_org.status}
|
||||
|
||||
@router.get("/my", response_model=List[CorpOnboardResponse])
|
||||
@router.get("/my", response_model=List[dict])
|
||||
async def get_my_organizations(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
@@ -120,4 +120,19 @@ async def get_my_organizations(
|
||||
result = await db.execute(stmt)
|
||||
orgs = result.scalars().all()
|
||||
|
||||
return [{"organization_id": o.id, "status": o.status} for o in orgs]
|
||||
# Return full organization details
|
||||
return [
|
||||
{
|
||||
"organization_id": o.id,
|
||||
"status": o.status,
|
||||
"name": o.name,
|
||||
"full_name": o.full_name,
|
||||
"display_name": o.display_name,
|
||||
"tax_number": o.tax_number,
|
||||
"country_code": o.country_code,
|
||||
"is_active": o.is_active,
|
||||
"is_deleted": o.is_deleted,
|
||||
"subscription_plan": o.subscription_plan
|
||||
}
|
||||
for o in orgs
|
||||
]
|
||||
@@ -1,10 +1,11 @@
|
||||
#/opt/docker/dev/service_finder/backend/app/api/v1/endpoints/users.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse, UserUpdate
|
||||
from app.schemas.user import UserResponse, UserUpdate, ActiveOrganizationUpdate
|
||||
from app.models.identity import User
|
||||
from app.services.trust_engine import TrustEngine
|
||||
|
||||
@@ -70,8 +71,12 @@ async def read_users_me(
|
||||
# Create a response dictionary with the active_organization_id
|
||||
# Get first_name and last_name from person relation if available
|
||||
person = current_user.person
|
||||
first_name = person.first_name if person else ""
|
||||
last_name = person.last_name if person else ""
|
||||
# Safe extraction with fallback to empty string
|
||||
first_name = ""
|
||||
last_name = ""
|
||||
if person:
|
||||
first_name = getattr(person, 'first_name', '')
|
||||
last_name = getattr(person, 'last_name', '')
|
||||
|
||||
response_data = {
|
||||
"id": current_user.id,
|
||||
@@ -141,6 +146,59 @@ async def update_user_preferences(
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid field: {field}")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(current_user)
|
||||
return current_user
|
||||
try:
|
||||
await db.commit()
|
||||
await db.refresh(current_user)
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||
|
||||
# Return the Pydantic model instead of raw SQLAlchemy object
|
||||
return UserResponse.model_validate(current_user)
|
||||
|
||||
|
||||
@router.patch("/me/active-organization", response_model=UserResponse)
|
||||
async def update_active_organization(
|
||||
update_data: ActiveOrganizationUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update the user's active organization (scope_id).
|
||||
|
||||
Accepts an organization_id (UUID/string) or None to revert to personal mode.
|
||||
"""
|
||||
# Extract organization_id from request
|
||||
org_id = update_data.organization_id
|
||||
|
||||
# Validate that the user has access to this organization if org_id is provided
|
||||
if org_id is not None:
|
||||
from sqlalchemy import select
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
|
||||
# Check if user is a member of the organization
|
||||
stmt = select(OrganizationMember).where(
|
||||
OrganizationMember.organization_id == org_id,
|
||||
OrganizationMember.user_id == current_user.id
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
member = result.scalar_one_or_none()
|
||||
|
||||
if not member:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You are not a member of this organization"
|
||||
)
|
||||
|
||||
# Update user's scope_id
|
||||
current_user.scope_id = org_id
|
||||
|
||||
try:
|
||||
await db.commit()
|
||||
await db.refresh(current_user)
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||
|
||||
# Return updated user data
|
||||
return UserResponse.model_validate(current_user)
|
||||
|
||||
Reference in New Issue
Block a user