Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
759 lines
24 KiB
Python
759 lines
24 KiB
Python
"""
|
|
Teams API endpoints for GT 2.0 Tenant Backend
|
|
|
|
Provides team collaboration management with two-tier permissions:
|
|
- Tier 1 (Team-level): 'read' or 'share' set by team owner
|
|
- Tier 2 (Resource-level): 'read' or 'edit' set by resource sharer
|
|
|
|
Follows GT 2.0 principles:
|
|
- Perfect tenant isolation
|
|
- Admin bypass for tenant admins
|
|
- Fail-fast error handling
|
|
- PostgreSQL-first storage
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from typing import Dict, Any, List
|
|
import logging
|
|
|
|
from app.core.security import get_current_user
|
|
from app.services.team_service import TeamService
|
|
from app.api.auth import get_tenant_user_uuid_by_email
|
|
from app.models.collaboration_team import (
|
|
TeamCreate,
|
|
TeamUpdate,
|
|
Team,
|
|
TeamListResponse,
|
|
TeamResponse,
|
|
TeamWithMembers,
|
|
TeamWithMembersResponse,
|
|
AddMemberRequest,
|
|
UpdateMemberPermissionRequest,
|
|
MemberListResponse,
|
|
MemberResponse,
|
|
ShareResourceRequest,
|
|
SharedResourcesResponse,
|
|
SharedResource,
|
|
TeamInvitation,
|
|
InvitationListResponse,
|
|
ObservableRequest,
|
|
ObservableRequestListResponse,
|
|
TeamActivityMetrics,
|
|
TeamActivityResponse,
|
|
ErrorResponse
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/teams", tags=["teams"])
|
|
|
|
|
|
async def get_team_service_for_user(current_user: Dict[str, Any]) -> TeamService:
|
|
"""Helper function to create TeamService with proper tenant UUID mapping"""
|
|
user_email = current_user.get('email')
|
|
if not user_email:
|
|
raise HTTPException(status_code=401, detail="User email not found in token")
|
|
|
|
tenant_user_uuid = await get_tenant_user_uuid_by_email(user_email)
|
|
if not tenant_user_uuid:
|
|
raise HTTPException(status_code=404, detail=f"User {user_email} not found in tenant system")
|
|
|
|
return TeamService(
|
|
tenant_domain=current_user.get('tenant_domain', 'test-company'),
|
|
user_id=tenant_user_uuid,
|
|
user_email=user_email
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# TEAM CRUD ENDPOINTS
|
|
# ============================================================================
|
|
|
|
@router.get("", response_model=TeamListResponse)
|
|
async def list_teams(
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
List all teams where the current user is owner or member.
|
|
Returns teams with member counts and permission flags.
|
|
|
|
Permission flags:
|
|
- is_owner: User created this team
|
|
- can_manage: User can manage team (owner or admin/developer)
|
|
"""
|
|
logger.info(f"Listing teams for user {current_user['sub']}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
teams = await service.get_user_teams()
|
|
|
|
return TeamListResponse(
|
|
data=teams,
|
|
total=len(teams)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing teams: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("", response_model=TeamResponse, status_code=201)
|
|
async def create_team(
|
|
team_data: TeamCreate,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Create a new team with the current user as owner.
|
|
|
|
The creator is automatically the team owner with full management permissions.
|
|
"""
|
|
logger.info(f"Creating team '{team_data.name}' for user {current_user['sub']}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
team = await service.create_team(
|
|
name=team_data.name,
|
|
description=team_data.description or ""
|
|
)
|
|
|
|
return TeamResponse(data=team)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating team: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ==============================================================================
|
|
# TEAM INVITATION ENDPOINTS (must come before /{team_id} routes)
|
|
# ==============================================================================
|
|
|
|
@router.get("/invitations", response_model=InvitationListResponse)
|
|
async def list_my_invitations(
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get current user's pending team invitations.
|
|
|
|
Returns list of invitations with team details and inviter information.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
invitations = await service.get_pending_invitations()
|
|
|
|
return InvitationListResponse(
|
|
data=invitations,
|
|
total=len(invitations)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing invitations: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/invitations/{invitation_id}/accept", response_model=MemberResponse)
|
|
async def accept_team_invitation(
|
|
invitation_id: str,
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Accept a team invitation.
|
|
|
|
Updates the invitation status to 'accepted' and grants team membership.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
member = await service.accept_invitation(invitation_id)
|
|
|
|
return MemberResponse(data=member)
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error accepting invitation: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/invitations/{invitation_id}/decline", status_code=204)
|
|
async def decline_team_invitation(
|
|
invitation_id: str,
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Decline a team invitation.
|
|
|
|
Removes the invitation from the system.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
await service.decline_invitation(invitation_id)
|
|
|
|
return None
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error declining invitation: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/observable-requests", response_model=ObservableRequestListResponse)
|
|
async def get_observable_requests(
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get pending Observable requests for the current user.
|
|
|
|
Returns list of teams requesting Observable access.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
requests = await service.get_observable_requests()
|
|
|
|
return ObservableRequestListResponse(
|
|
data=[ObservableRequest(**req) for req in requests],
|
|
total=len(requests)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Observable requests: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ==============================================================================
|
|
# TEAM CRUD ENDPOINTS (dynamic routes with {team_id})
|
|
# ==============================================================================
|
|
|
|
@router.get("/{team_id}", response_model=TeamResponse)
|
|
async def get_team(
|
|
team_id: str,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get team details by ID.
|
|
|
|
Only accessible to team members or tenant admins.
|
|
"""
|
|
logger.info(f"Getting team {team_id} for user {current_user['sub']}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
team = await service.get_team_by_id(team_id)
|
|
|
|
if not team:
|
|
raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
|
|
|
|
return TeamResponse(data=team)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting team: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.put("/{team_id}", response_model=TeamResponse)
|
|
async def update_team(
|
|
team_id: str,
|
|
updates: TeamUpdate,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Update team name/description.
|
|
|
|
Requires: Team ownership or admin/developer role
|
|
"""
|
|
logger.info(f"Updating team {team_id} for user {current_user['sub']}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
|
|
# Convert Pydantic model to dict, excluding None values
|
|
update_dict = updates.model_dump(exclude_none=True)
|
|
|
|
team = await service.update_team(team_id, update_dict)
|
|
|
|
if not team:
|
|
raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
|
|
|
|
return TeamResponse(data=team)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error updating team: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{team_id}", status_code=204)
|
|
async def delete_team(
|
|
team_id: str,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Delete a team and all its memberships (CASCADE).
|
|
|
|
Requires: Team ownership or admin/developer role
|
|
"""
|
|
logger.info(f"Deleting team {team_id} for user {current_user['sub']}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
success = await service.delete_team(team_id)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
|
|
|
|
return None # 204 No Content
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error deleting team: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ============================================================================
|
|
# TEAM MEMBER ENDPOINTS
|
|
# ============================================================================
|
|
|
|
@router.get("/{team_id}/members", response_model=MemberListResponse)
|
|
async def list_team_members(
|
|
team_id: str,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
List all members of a team with their permissions.
|
|
|
|
Only accessible to team members or tenant admins.
|
|
|
|
Returns:
|
|
- user_id, user_email, user_name
|
|
- team_permission: 'read' or 'share'
|
|
- resource_permissions: JSONB dict of resource-level permissions
|
|
"""
|
|
logger.info(f"Listing members for team {team_id}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
members = await service.get_team_members(team_id)
|
|
|
|
return MemberListResponse(
|
|
data=members,
|
|
total=len(members)
|
|
)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error listing team members: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/{team_id}/members", response_model=MemberResponse, status_code=201)
|
|
async def add_team_member(
|
|
team_id: str,
|
|
member_data: AddMemberRequest,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Add a user to the team with specified permission.
|
|
|
|
Requires: Team ownership or admin/developer role
|
|
|
|
Team Permissions:
|
|
- 'read': Can access resources shared to this team
|
|
- 'share': Can access resources AND share own resources to this team
|
|
|
|
Note: Observability access is automatically requested when inviting users.
|
|
The invited user can approve or decline the observability request separately.
|
|
"""
|
|
logger.info(f"Adding member {member_data.user_email} to team {team_id}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
member = await service.add_member(
|
|
team_id=team_id,
|
|
user_email=member_data.user_email,
|
|
team_permission=member_data.team_permission
|
|
)
|
|
|
|
return MemberResponse(data=member)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except RuntimeError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error adding team member: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.put("/{team_id}/members/{user_id}", response_model=MemberResponse)
|
|
async def update_member_permission(
|
|
team_id: str,
|
|
user_id: str,
|
|
permission_data: UpdateMemberPermissionRequest,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Update a team member's permission level.
|
|
|
|
Requires: Team ownership or admin/developer role
|
|
|
|
Note: DB trigger auto-clears resource_permissions when downgraded from 'share' to 'read'
|
|
"""
|
|
logger.info(f"PUT /teams/{team_id}/members/{user_id} - Permission update request")
|
|
logger.info(f"Request body: {permission_data.model_dump()}")
|
|
logger.info(f"Current user: {current_user.get('email')}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
member = await service.update_member_permission(
|
|
team_id=team_id,
|
|
user_id=user_id,
|
|
new_permission=permission_data.team_permission
|
|
)
|
|
|
|
return MemberResponse(data=member)
|
|
|
|
except PermissionError as e:
|
|
logger.error(f"PermissionError updating member permission: {str(e)}")
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
logger.error(f"ValueError updating member permission: {str(e)}")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except RuntimeError as e:
|
|
logger.error(f"RuntimeError updating member permission: {str(e)}")
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error updating member permission: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{team_id}/members/{user_id}", status_code=204)
|
|
async def remove_team_member(
|
|
team_id: str,
|
|
user_id: str,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Remove a user from the team.
|
|
|
|
Requires: Team ownership or admin/developer role
|
|
"""
|
|
logger.info(f"Removing member {user_id} from team {team_id}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
success = await service.remove_member(team_id, user_id)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail=f"Member {user_id} not found in team {team_id}")
|
|
|
|
return None # 204 No Content
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error removing team member: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ============================================================================
|
|
# RESOURCE SHARING ENDPOINTS
|
|
# ============================================================================
|
|
|
|
@router.post("/{team_id}/share", status_code=201)
|
|
async def share_resource_to_team(
|
|
team_id: str,
|
|
share_data: ShareResourceRequest,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Share a resource (agent/dataset) to team with per-user permissions.
|
|
|
|
Requires: Team ownership or 'share' team permission
|
|
|
|
Request body:
|
|
{
|
|
"resource_type": "agent" | "dataset",
|
|
"resource_id": "uuid",
|
|
"user_permissions": {
|
|
"user_uuid_1": "read",
|
|
"user_uuid_2": "edit"
|
|
}
|
|
}
|
|
|
|
Resource Permissions:
|
|
- 'read': View-only access to resource
|
|
- 'edit': Full edit access to resource
|
|
"""
|
|
logger.info(f"Sharing {share_data.resource_type}:{share_data.resource_id} to team {team_id}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
|
|
# Use new junction table method (Phase 2)
|
|
await service.share_resource_to_teams(
|
|
resource_id=share_data.resource_id,
|
|
resource_type=share_data.resource_type,
|
|
shared_by=current_user["user_id"],
|
|
team_shares=[{
|
|
"team_id": team_id,
|
|
"user_permissions": share_data.user_permissions
|
|
}]
|
|
)
|
|
|
|
return {"message": "Resource shared successfully", "success": True}
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error sharing resource: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{team_id}/share/{resource_type}/{resource_id}", status_code=204)
|
|
async def unshare_resource_from_team(
|
|
team_id: str,
|
|
resource_type: str,
|
|
resource_id: str,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Remove resource sharing from team (removes from all members' resource_permissions).
|
|
|
|
Requires: Team ownership or 'share' team permission
|
|
"""
|
|
logger.info(f"Unsharing {resource_type}:{resource_id} from team {team_id}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
|
|
# Use new junction table method (Phase 2)
|
|
await service.unshare_resource_from_team(
|
|
resource_id=resource_id,
|
|
resource_type=resource_type,
|
|
team_id=team_id
|
|
)
|
|
|
|
return None # 204 No Content
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error unsharing resource: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/{team_id}/resources", response_model=SharedResourcesResponse)
|
|
async def list_shared_resources(
|
|
team_id: str,
|
|
resource_type: str = Query(None, description="Filter by resource type: 'agent' or 'dataset'"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
List all resources shared to a team.
|
|
|
|
Only accessible to team members or tenant admins.
|
|
|
|
Returns list of:
|
|
{
|
|
"resource_type": "agent" | "dataset",
|
|
"resource_id": "uuid",
|
|
"user_permissions": {"user_id": "read|edit", ...}
|
|
}
|
|
"""
|
|
logger.info(f"Listing shared resources for team {team_id}")
|
|
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
resources = await service.get_shared_resources(
|
|
team_id=team_id,
|
|
resource_type=resource_type
|
|
)
|
|
|
|
return SharedResourcesResponse(
|
|
data=resources,
|
|
total=len(resources)
|
|
)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error listing shared resources: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
@router.get("/{team_id}/invitations", response_model=InvitationListResponse)
|
|
async def list_team_invitations(
|
|
team_id: str,
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get pending invitations for a team (owner view).
|
|
|
|
Shows all users who have been invited but haven't accepted yet.
|
|
Requires team ownership or admin role.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
invitations = await service.get_team_pending_invitations(team_id)
|
|
|
|
return InvitationListResponse(
|
|
data=invitations,
|
|
total=len(invitations)
|
|
)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error listing team invitations: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{team_id}/invitations/{invitation_id}", status_code=204)
|
|
async def cancel_team_invitation(
|
|
team_id: str,
|
|
invitation_id: str,
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Cancel a pending invitation (owner only).
|
|
|
|
Removes the invitation before the user accepts it.
|
|
Requires team ownership or admin role.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
await service.cancel_invitation(team_id, invitation_id)
|
|
|
|
return None
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error canceling invitation: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ============================================================================
|
|
# Observable Member Management Endpoints
|
|
# ============================================================================
|
|
|
|
@router.post("/{team_id}/members/{user_id}/request-observable", status_code=200)
|
|
async def request_observable_access(
|
|
team_id: str,
|
|
user_id: str,
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Request Observable access from a team member.
|
|
|
|
Sets Observable status to pending for the target user.
|
|
Requires owner or manager permission.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
result = await service.request_observable_status(team_id, user_id)
|
|
|
|
return {"success": True, "data": result}
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error requesting Observable access: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/{team_id}/observable/approve", status_code=200)
|
|
async def approve_observable_request(
|
|
team_id: str,
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Approve Observable status for current user in a team.
|
|
|
|
User explicitly consents to team managers viewing their activity.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
result = await service.approve_observable_consent(team_id)
|
|
|
|
return {"success": True, "data": result}
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error approving Observable request: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{team_id}/observable", status_code=200)
|
|
async def revoke_observable_status(
|
|
team_id: str,
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Revoke Observable status for current user in a team.
|
|
|
|
Immediately removes manager access to user's activity data.
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
result = await service.revoke_observable_status(team_id)
|
|
|
|
return {"success": True, "data": result}
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error revoking Observable status: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/{team_id}/activity", response_model=TeamActivityResponse)
|
|
async def get_team_activity(
|
|
team_id: str,
|
|
days: int = Query(7, ge=1, le=365, description="Number of days to include in metrics"),
|
|
current_user: Dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get team activity metrics for Observable members.
|
|
|
|
Returns aggregated activity data for team members who have approved Observable status.
|
|
Requires owner or manager permission.
|
|
|
|
Args:
|
|
team_id: Team UUID
|
|
days: Number of days to include (1-365, default 7)
|
|
"""
|
|
try:
|
|
service = await get_team_service_for_user(current_user)
|
|
activity = await service.get_team_activity(team_id, days)
|
|
|
|
return TeamActivityResponse(
|
|
data=TeamActivityMetrics(**activity)
|
|
)
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error getting team activity: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|