## Security Enhancements (P0) - Add input validation with max_length and numeric range constraints - Implement WebSocket token authentication via first message - Add path traversal prevention in file storage service ## Permission Enhancements (P0) - Add project member management for cross-department access - Implement is_department_manager flag for workload visibility ## Cycle Detection (P0) - Add DFS-based cycle detection for task dependencies - Add formula field circular reference detection - Display user-friendly cycle path visualization ## Concurrency & Reliability (P1) - Implement optimistic locking with version field (409 Conflict on mismatch) - Add trigger retry mechanism with exponential backoff (1s, 2s, 4s) - Implement cascade restore for soft-deleted tasks ## Rate Limiting (P1) - Add tiered rate limits: standard (60/min), sensitive (20/min), heavy (5/min) - Apply rate limits to tasks, reports, attachments, and comments ## Frontend Improvements (P1) - Add responsive sidebar with hamburger menu for mobile - Improve touch-friendly UI with proper tap target sizes - Complete i18n translations for all components ## Backend Reliability (P2) - Configure database connection pool (size=10, overflow=20) - Add Redis fallback mechanism with message queue - Add blocker check before task deletion ## API Enhancements (P3) - Add standardized response wrapper utility - Add /health/ready and /health/live endpoints - Implement project templates with status/field copying ## Tests Added - test_input_validation.py - Schema and path traversal tests - test_concurrency_reliability.py - Optimistic locking and retry tests - test_backend_reliability.py - Connection pool and Redis tests - test_api_enhancements.py - Health check and template tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
8.8 KiB
Python
281 lines
8.8 KiB
Python
"""Workload API endpoints.
|
|
|
|
Provides endpoints for workload heatmap, user workload details,
|
|
and capacity management.
|
|
"""
|
|
from datetime import date
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.database import get_db
|
|
from app.middleware.auth import get_current_user
|
|
from app.models.user import User
|
|
from app.schemas.workload import (
|
|
WorkloadHeatmapResponse,
|
|
UserWorkloadDetail,
|
|
CapacityUpdate,
|
|
UserWorkloadSummary,
|
|
)
|
|
from app.services.workload_service import (
|
|
get_week_bounds,
|
|
get_current_week_start,
|
|
get_workload_heatmap,
|
|
get_user_workload_detail,
|
|
)
|
|
from app.services.workload_cache import (
|
|
get_cached_heatmap,
|
|
set_cached_heatmap,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def check_workload_access(
|
|
current_user: User,
|
|
target_user_id: Optional[str] = None,
|
|
target_user_department_id: Optional[str] = None,
|
|
department_id: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Check if current user has access to view workload data.
|
|
|
|
Access rules:
|
|
- System admin: can access all workloads
|
|
- Department manager: can access workloads of users in their department
|
|
- Regular user: can only access their own workload
|
|
|
|
Raises HTTPException if access is denied.
|
|
"""
|
|
# System admin can access all
|
|
if current_user.is_system_admin:
|
|
return
|
|
|
|
# If querying specific user
|
|
if target_user_id and target_user_id != current_user.id:
|
|
# Department manager can view subordinates' workload
|
|
if current_user.is_department_manager:
|
|
# Manager can view users in their department
|
|
# target_user_department_id must be provided for this check
|
|
if target_user_department_id and target_user_department_id == current_user.department_id:
|
|
return
|
|
# Access denied for non-manager or user not in same department
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied: Cannot view other users' workload",
|
|
)
|
|
|
|
# If querying by department
|
|
if department_id and department_id != current_user.department_id:
|
|
# Department manager can only query their own department
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied: Cannot view other departments' workload",
|
|
)
|
|
|
|
|
|
def filter_accessible_users(
|
|
current_user: User,
|
|
user_ids: Optional[List[str]] = None,
|
|
db: Optional[Session] = None,
|
|
) -> Optional[List[str]]:
|
|
"""
|
|
Filter user IDs to only those accessible by current user.
|
|
Returns None if user can access all (system admin).
|
|
|
|
Access rules:
|
|
- System admin: can see all users
|
|
- Department manager: can see all users in their department
|
|
- Regular user: can only see themselves
|
|
"""
|
|
# System admin can access all
|
|
if current_user.is_system_admin:
|
|
return user_ids
|
|
|
|
# Department manager can see all users in their department
|
|
if current_user.is_department_manager and current_user.department_id and db:
|
|
# Get all users in the same department
|
|
department_users = db.query(User.id).filter(
|
|
User.department_id == current_user.department_id,
|
|
User.is_active == True
|
|
).all()
|
|
department_user_ids = {u.id for u in department_users}
|
|
|
|
if user_ids:
|
|
# Filter to only users in manager's department
|
|
accessible = [uid for uid in user_ids if uid in department_user_ids]
|
|
if not accessible:
|
|
return [current_user.id]
|
|
return accessible
|
|
else:
|
|
# No filter specified, return all department users
|
|
return list(department_user_ids)
|
|
|
|
# Regular user can only see themselves
|
|
if user_ids:
|
|
# Filter to only accessible users
|
|
accessible = [uid for uid in user_ids if uid == current_user.id]
|
|
if not accessible:
|
|
return [current_user.id] # Default to self if no accessible users
|
|
return accessible
|
|
else:
|
|
# No filter specified, return only self
|
|
return [current_user.id]
|
|
|
|
|
|
@router.get("/heatmap", response_model=WorkloadHeatmapResponse)
|
|
async def get_heatmap(
|
|
week_start: Optional[date] = Query(
|
|
None,
|
|
description="Start of week (ISO date, defaults to current Monday)"
|
|
),
|
|
department_id: Optional[str] = Query(
|
|
None,
|
|
description="Filter by department ID"
|
|
),
|
|
user_ids: Optional[str] = Query(
|
|
None,
|
|
description="Comma-separated list of user IDs to include"
|
|
),
|
|
hide_empty: bool = Query(
|
|
True,
|
|
description="Hide users with no tasks assigned for the week"
|
|
),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Get workload heatmap for users.
|
|
|
|
Access Rules:
|
|
- System admin: Can view all users' workload
|
|
- Department manager: Can view workload of all users in their department
|
|
- Regular user: Can only view their own workload
|
|
|
|
Returns workload summaries for users showing:
|
|
- allocated_hours: Total estimated hours from tasks due this week
|
|
- capacity_hours: User's weekly capacity
|
|
- load_percentage: Percentage of capacity used
|
|
- load_level: normal (<80%), warning (80-99%), overloaded (>=100%)
|
|
"""
|
|
# Parse user_ids if provided
|
|
parsed_user_ids = None
|
|
if user_ids:
|
|
parsed_user_ids = [uid.strip() for uid in user_ids.split(",") if uid.strip()]
|
|
|
|
# Check department access
|
|
if department_id:
|
|
check_workload_access(current_user, department_id=department_id)
|
|
|
|
# Filter user_ids based on access (pass db for manager department lookup)
|
|
accessible_user_ids = filter_accessible_users(current_user, parsed_user_ids, db)
|
|
|
|
# Normalize week_start
|
|
if week_start is None:
|
|
week_start = get_current_week_start()
|
|
else:
|
|
week_start = get_week_bounds(week_start)[0]
|
|
|
|
week_start, week_end = get_week_bounds(week_start)
|
|
|
|
# Try cache first (only use cache for default hide_empty=True)
|
|
cached = None
|
|
if hide_empty:
|
|
cached = get_cached_heatmap(week_start, department_id, accessible_user_ids)
|
|
if cached:
|
|
return WorkloadHeatmapResponse(
|
|
week_start=week_start,
|
|
week_end=week_end,
|
|
users=cached,
|
|
)
|
|
|
|
# Calculate from database
|
|
summaries = get_workload_heatmap(
|
|
db=db,
|
|
week_start=week_start,
|
|
department_id=department_id,
|
|
user_ids=accessible_user_ids,
|
|
hide_empty=hide_empty,
|
|
)
|
|
|
|
# Cache the result (only cache when hide_empty=True, the default)
|
|
if hide_empty:
|
|
set_cached_heatmap(week_start, summaries, department_id, accessible_user_ids)
|
|
|
|
return WorkloadHeatmapResponse(
|
|
week_start=week_start,
|
|
week_end=week_end,
|
|
users=summaries,
|
|
)
|
|
|
|
|
|
@router.get("/user/{user_id}", response_model=UserWorkloadDetail)
|
|
async def get_user_workload(
|
|
user_id: str,
|
|
week_start: Optional[date] = Query(
|
|
None,
|
|
description="Start of week (ISO date, defaults to current Monday)"
|
|
),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Get detailed workload for a specific user.
|
|
|
|
Access rules:
|
|
- System admin: can view any user's workload
|
|
- Department manager: can view workload of users in their department
|
|
- Regular user: can only view their own workload
|
|
|
|
Returns:
|
|
- Workload summary (same as heatmap)
|
|
- List of tasks contributing to the workload
|
|
"""
|
|
# Get target user's department for manager access check
|
|
target_user = db.query(User).filter(User.id == user_id).first()
|
|
target_user_department_id = target_user.department_id if target_user else None
|
|
|
|
# Check access (pass target user's department for manager check)
|
|
check_workload_access(
|
|
current_user,
|
|
target_user_id=user_id,
|
|
target_user_department_id=target_user_department_id
|
|
)
|
|
|
|
# Calculate workload detail
|
|
detail = get_user_workload_detail(db, user_id, week_start)
|
|
|
|
if detail is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="User not found",
|
|
)
|
|
|
|
return detail
|
|
|
|
|
|
@router.get("/me", response_model=UserWorkloadDetail)
|
|
async def get_my_workload(
|
|
week_start: Optional[date] = Query(
|
|
None,
|
|
description="Start of week (ISO date, defaults to current Monday)"
|
|
),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Get workload for the current authenticated user.
|
|
|
|
Convenience endpoint that doesn't require specifying user ID.
|
|
"""
|
|
detail = get_user_workload_detail(db, current_user.id, week_start)
|
|
|
|
if detail is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to calculate workload",
|
|
)
|
|
|
|
return detail
|