Spaces:
Sleeping
Sleeping
feat: Add project team management endpoints - Fix team stats to count actual team members - Add filtering to GET /projects/{id}/team (role, region, active status, search) - Add PATCH /projects/{id}/team/{member_id} for updating team members - Add DELETE /projects/{id}/team/{member_id} for removing team members
9dd2bfc | """ | |
| API Route Dependencies - Supabase Auth Integration | |
| """ | |
| from typing import Generator | |
| from fastapi import Depends, HTTPException, status | |
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | |
| from sqlalchemy.orm import Session | |
| from app.core.database import SessionLocal | |
| from app.core.supabase_auth import supabase_auth | |
| from app.models.user import User | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| security = HTTPBearer() | |
| def get_db() -> Generator[Session, None, None]: | |
| """ | |
| Database session dependency with automatic transaction management | |
| CRITICAL: Sessions MUST be committed or rolled back explicitly. | |
| If you don't commit, all changes are lost when the session closes. | |
| """ | |
| db = SessionLocal() | |
| try: | |
| yield db | |
| # AUTO-COMMIT: If endpoint doesn't raise exception, commit the transaction | |
| db.commit() | |
| except Exception: | |
| # AUTO-ROLLBACK: If endpoint raises exception, rollback the transaction | |
| db.rollback() | |
| raise | |
| finally: | |
| db.close() | |
| async def get_current_user( | |
| credentials: HTTPAuthorizationCredentials = Depends(security), | |
| db: Session = Depends(get_db) | |
| ) -> User: | |
| """ | |
| Get current authenticated user from Supabase Auth token | |
| Args: | |
| credentials: HTTP Bearer token (Supabase JWT) | |
| db: Database session | |
| Returns: | |
| Current user | |
| Raises: | |
| HTTPException: If token is invalid or user not found | |
| """ | |
| credentials_exception = HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Could not validate credentials", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| try: | |
| token = credentials.credentials | |
| # Verify token with Supabase and get user | |
| auth_user = await supabase_auth.get_user(token) | |
| if not auth_user: | |
| logger.warning("Invalid or expired token") | |
| raise credentials_exception | |
| user_id = auth_user.id | |
| # Get user from our database | |
| user = db.query(User).filter( | |
| User.id == user_id, | |
| User.deleted_at == None | |
| ).first() | |
| if user is None: | |
| logger.warning(f"User not found in database: {user_id}") | |
| raise credentials_exception | |
| return user | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Authentication error: {str(e)}") | |
| raise credentials_exception | |
| async def get_current_active_user( | |
| current_user: User = Depends(get_current_user) | |
| ) -> User: | |
| """ | |
| Get current active user | |
| Args: | |
| current_user: Current authenticated user | |
| Returns: | |
| Current active user | |
| Raises: | |
| HTTPException: If user is inactive | |
| """ | |
| logger.info(f"Checking active user: {current_user.id}, is_active: {current_user.is_active}, type: {type(current_user.is_active)}") | |
| # Handle None case explicitly | |
| if current_user.is_active is None: | |
| logger.warning(f"User {current_user.id} has NULL is_active field - defaulting to False") | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="User account status is undefined" | |
| ) | |
| if not current_user.is_active: | |
| logger.warning(f"User {current_user.id} is inactive") | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="Inactive user" | |
| ) | |
| logger.info(f"User {current_user.id} is active - proceeding") | |
| return current_user | |
| def get_org_scope(user: User) -> dict: | |
| """ | |
| Get organization scope for org admin users. | |
| Returns dict with org filtering info: | |
| - is_org_scoped: bool - Whether user is org admin (needs scoping) | |
| - client_id: UUID or None - Client org to filter by | |
| - contractor_id: UUID or None - Contractor org to filter by | |
| Platform admins and non-org-admins return is_org_scoped=False | |
| """ | |
| if user.role in ['client_admin', 'contractor_admin']: | |
| return { | |
| 'is_org_scoped': True, | |
| 'client_id': user.client_id, | |
| 'contractor_id': user.contractor_id, | |
| 'org_type': 'client' if user.client_id else 'contractor' | |
| } | |
| return { | |
| 'is_org_scoped': False, | |
| 'client_id': None, | |
| 'contractor_id': None, | |
| 'org_type': None | |
| } | |