Fix activities endpoints to match established project-access convention

get_project_activities, get_task_activities, and get_recent_activities were
gating on "project member OR admin", incorrectly blocking coordinators,
directors, and developers from projects they weren't explicitly added to as
members. Every other project-scoped router (shots.py, assets.py) only
restricts the artist role this way — everyone else has access regardless of
membership. Brought activities.py in line with that convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 04:18:07 +08:00
parent 4f9deeb57a
commit 9527f06b3f
+51 -47
View File
@@ -5,7 +5,7 @@ from typing import List, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from database import get_db from database import get_db
from models.user import User from models.user import User, UserRole
from models.activity import Activity, ActivityType from models.activity import Activity, ActivityType
from models.project import ProjectMember from models.project import ProjectMember
from schemas.activity import ActivityResponse from schemas.activity import ActivityResponse
@@ -26,15 +26,17 @@ def get_project_activities(
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""Get activity feed for a specific project (excludes activities for deleted records).""" """Get activity feed for a specific project (excludes activities for deleted records)."""
# Verify user has access to the project # Only artists are restricted to their explicit project memberships; coordinators,
member = db.query(ProjectMember).filter( # directors, developers, and admins have access to all projects (matches shots.py/assets.py).
ProjectMember.project_id == project_id, if current_user.role == UserRole.ARTIST:
ProjectMember.user_id == current_user.id member = db.query(ProjectMember).filter(
).first() ProjectMember.project_id == project_id,
ProjectMember.user_id == current_user.id
if not member and not current_user.is_admin: ).first()
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Access denied to this project") if not member:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Access denied to this project")
# Use ActivityService to get activities excluding deleted records # Use ActivityService to get activities excluding deleted records
activities = ActivityService.get_activities_excluding_deleted( activities = ActivityService.get_activities_excluding_deleted(
@@ -66,15 +68,17 @@ def get_task_activities(
from fastapi import HTTPException from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Task not found") raise HTTPException(status_code=404, detail="Task not found")
# Check if user is a member of the project # Only artists are restricted to their explicit project memberships; coordinators,
member = db.query(ProjectMember).filter( # directors, developers, and admins have access to all tasks (matches shots.py/assets.py).
ProjectMember.project_id == task.project_id, if current_user.role == UserRole.ARTIST:
ProjectMember.user_id == current_user.id member = db.query(ProjectMember).filter(
).first() ProjectMember.project_id == task.project_id,
ProjectMember.user_id == current_user.id
if not member and not current_user.is_admin: ).first()
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Access denied to this task") if not member:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Access denied to this task")
# Use ActivityService to get activities excluding deleted records # Use ActivityService to get activities excluding deleted records
activities = ActivityService.get_activities_excluding_deleted( activities = ActivityService.get_activities_excluding_deleted(
@@ -122,34 +126,10 @@ def get_recent_activities(
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""Get recent activities from all projects the user has access to (excludes activities for deleted records).""" """Get recent activities from all projects the user has access to (excludes activities for deleted records)."""
# Get all projects the user is a member of # Only artists are restricted to their explicit project memberships; coordinators,
project_ids = db.query(ProjectMember.project_id).filter( # directors, developers, and admins see recent activity across all projects
ProjectMember.user_id == current_user.id # (matches shots.py/assets.py).
).all() if current_user.role != UserRole.ARTIST:
project_ids = [pid[0] for pid in project_ids]
if not project_ids and not current_user.is_admin:
return []
# For non-admin users, filter by their project access
if not current_user.is_admin:
# Get activities from user's projects, excluding deleted records
all_activities = []
for project_id in project_ids:
activities = ActivityService.get_activities_excluding_deleted(
db=db,
project_id=project_id,
skip=0,
limit=limit * 2 # Get more to account for filtering
)
all_activities.extend(activities)
# Sort by created_at and apply pagination
all_activities.sort(key=lambda x: x.created_at, reverse=True)
return all_activities[skip:skip + limit]
else:
# Admin gets all activities excluding deleted records
activities = ActivityService.get_activities_excluding_deleted( activities = ActivityService.get_activities_excluding_deleted(
db=db, db=db,
skip=skip, skip=skip,
@@ -157,6 +137,30 @@ def get_recent_activities(
) )
return activities return activities
# Get all projects the artist is a member of
project_ids = db.query(ProjectMember.project_id).filter(
ProjectMember.user_id == current_user.id
).all()
project_ids = [pid[0] for pid in project_ids]
if not project_ids:
return []
all_activities = []
for project_id in project_ids:
activities = ActivityService.get_activities_excluding_deleted(
db=db,
project_id=project_id,
skip=0,
limit=limit * 2 # Get more to account for filtering
)
all_activities.extend(activities)
# Sort by created_at and apply pagination
all_activities.sort(key=lambda x: x.created_at, reverse=True)
return all_activities[skip:skip + limit]
# Admin-only endpoints that include activities for deleted records # Admin-only endpoints that include activities for deleted records
@router.get("/admin/project/{project_id}/all", response_model=List[ActivityResponse]) @router.get("/admin/project/{project_id}/all", response_model=List[ActivityResponse])