Added project separation logic

This commit is contained in:
gulimabr
2025-12-01 11:01:13 -03:00
parent 6d02736cba
commit 07005788ed
17 changed files with 1337 additions and 115 deletions

View File

@@ -8,12 +8,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.models import (
TokenResponse, UserInfo, GroupResponse,
TagResponse, RequirementResponse, PriorityResponse,
RequirementCreateRequest, RequirementUpdateRequest
RequirementCreateRequest, RequirementUpdateRequest,
ProjectResponse, ProjectCreateRequest, ProjectUpdateRequest, ProjectMemberRequest
)
from src.controller import AuthController
from src.config import get_openid, get_settings
from src.database import init_db, close_db, get_db
from src.repositories import RoleRepository, GroupRepository, TagRepository, RequirementRepository, PriorityRepository
from src.repositories import RoleRepository, GroupRepository, TagRepository, RequirementRepository, PriorityRepository, ProjectRepository
import logging
# Configure logging
@@ -237,6 +238,233 @@ async def get_priorities(db: AsyncSession = Depends(get_db)):
return [PriorityResponse.model_validate(p) for p in priorities]
# ===========================================
# Projects Endpoints
# ===========================================
async def _get_current_user_db(request: Request, db: AsyncSession):
"""Helper to get the current authenticated user from the database."""
user_info = AuthController.get_current_user(request)
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
return user
async def _verify_project_membership(project_id: int, user_id: int, db: AsyncSession):
"""Helper to verify user is a member of a project."""
project_repo = ProjectRepository(db)
# Check if project exists
project = await project_repo.get_by_id(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Project with id {project_id} not found"
)
# Check if user is a member
is_member = await project_repo.is_member(project_id, user_id)
if not is_member:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You are not a member of this project"
)
return project
@app.get("/api/projects", response_model=List[ProjectResponse])
async def get_my_projects(
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get all projects the authenticated user is a member of.
Returns:
List of projects the user belongs to.
"""
user = await _get_current_user_db(request, db)
project_repo = ProjectRepository(db)
projects = await project_repo.get_by_user_id(user.id)
return [ProjectResponse.model_validate(p) for p in projects]
@app.get("/api/projects/{project_id}", response_model=ProjectResponse)
async def get_project(
project_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get a specific project by ID.
User must be a member of the project.
Args:
project_id: The project ID
Returns:
The project if found and user is a member.
"""
user = await _get_current_user_db(request, db)
project = await _verify_project_membership(project_id, user.id, db)
return ProjectResponse.model_validate(project)
@app.post("/api/projects", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
async def create_project(
request: Request,
project_data: ProjectCreateRequest,
db: AsyncSession = Depends(get_db)
):
"""
Create a new project.
The creating user will automatically be added as a member.
Args:
project_data: The project data
Returns:
The created project.
"""
user = await _get_current_user_db(request, db)
project_repo = ProjectRepository(db)
project = await project_repo.create(
project_name=project_data.project_name,
project_desc=project_data.project_desc,
creator_id=user.id,
)
await db.commit()
return ProjectResponse.model_validate(project)
@app.put("/api/projects/{project_id}", response_model=ProjectResponse)
async def update_project(
project_id: int,
request: Request,
project_data: ProjectUpdateRequest,
db: AsyncSession = Depends(get_db)
):
"""
Update an existing project.
User must be a member of the project.
Args:
project_id: The project ID to update
project_data: The updated project data
Returns:
The updated project.
"""
user = await _get_current_user_db(request, db)
await _verify_project_membership(project_id, user.id, db)
project_repo = ProjectRepository(db)
project = await project_repo.update(
project_id=project_id,
project_name=project_data.project_name,
project_desc=project_data.project_desc,
)
await db.commit()
return ProjectResponse.model_validate(project)
@app.delete("/api/projects/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_project(
project_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Delete a project.
User must be a member of the project.
Args:
project_id: The project ID to delete
"""
user = await _get_current_user_db(request, db)
await _verify_project_membership(project_id, user.id, db)
project_repo = ProjectRepository(db)
await project_repo.delete(project_id)
await db.commit()
@app.post("/api/projects/{project_id}/members", status_code=status.HTTP_201_CREATED)
async def add_project_member(
project_id: int,
request: Request,
member_data: ProjectMemberRequest,
db: AsyncSession = Depends(get_db)
):
"""
Add a member to a project.
User must be a member of the project.
Args:
project_id: The project ID
member_data: The user to add
Returns:
Success message.
"""
user = await _get_current_user_db(request, db)
await _verify_project_membership(project_id, user.id, db)
project_repo = ProjectRepository(db)
added = await project_repo.add_member(project_id, member_data.user_id)
if not added:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User is already a member of this project"
)
await db.commit()
return {"message": "Member added successfully"}
@app.delete("/api/projects/{project_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_project_member(
project_id: int,
user_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Remove a member from a project.
User must be a member of the project.
Args:
project_id: The project ID
user_id: The user ID to remove
"""
current_user = await _get_current_user_db(request, db)
await _verify_project_membership(project_id, current_user.id, db)
project_repo = ProjectRepository(db)
removed = await project_repo.remove_member(project_id, user_id)
if not removed:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User is not a member of this project"
)
await db.commit()
# ===========================================
# Requirements Endpoints
# ===========================================
@@ -252,6 +480,7 @@ def _build_requirement_response(req) -> RequirementResponse:
return RequirementResponse(
id=req.id,
project_id=req.project_id,
req_name=req.req_name,
req_desc=req.req_desc,
version=req.version,
@@ -264,44 +493,79 @@ def _build_requirement_response(req) -> RequirementResponse:
)
@app.get("/api/requirements", response_model=List[RequirementResponse])
async def get_requirements(
@app.get("/api/projects/{project_id}/requirements", response_model=List[RequirementResponse])
async def get_project_requirements(
project_id: int,
request: Request,
group_id: Optional[int] = None,
tag_id: Optional[int] = None,
db: AsyncSession = Depends(get_db)
):
"""
Get all requirements for the authenticated user, optionally filtered by group or tag.
Get all requirements for a specific project, optionally filtered by group or tag.
User must be a member of the project.
Args:
project_id: The project ID
group_id: Optional group ID to filter by
tag_id: Optional tag ID to filter by
Returns:
List of requirements owned by the authenticated user.
List of requirements in the project.
"""
# Get the current user from cookie
user_info = AuthController.get_current_user(request)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
user = await _get_current_user_db(request, db)
await _verify_project_membership(project_id, user.id, db)
req_repo = RequirementRepository(db)
if group_id:
requirements = await req_repo.get_by_group_id(group_id, user_id=user.id)
requirements = await req_repo.get_by_group_id(group_id, project_id=project_id)
elif tag_id:
requirements = await req_repo.get_by_tag_id(tag_id, user_id=user.id)
requirements = await req_repo.get_by_tag_id(tag_id, project_id=project_id)
else:
requirements = await req_repo.get_by_user_id(user.id)
requirements = await req_repo.get_by_project_id(project_id)
return [_build_requirement_response(req) for req in requirements]
@app.get("/api/requirements", response_model=List[RequirementResponse])
async def get_requirements(
request: Request,
project_id: Optional[int] = None,
group_id: Optional[int] = None,
tag_id: Optional[int] = None,
db: AsyncSession = Depends(get_db)
):
"""
Get requirements. If project_id is provided, returns requirements for that project.
User must be a member of the project.
Args:
project_id: Required project ID to filter by
group_id: Optional group ID to filter by
tag_id: Optional tag ID to filter by
Returns:
List of requirements.
"""
user = await _get_current_user_db(request, db)
if not project_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="project_id is required"
)
await _verify_project_membership(project_id, user.id, db)
req_repo = RequirementRepository(db)
if group_id:
requirements = await req_repo.get_by_group_id(group_id, project_id=project_id)
elif tag_id:
requirements = await req_repo.get_by_tag_id(tag_id, project_id=project_id)
else:
requirements = await req_repo.get_by_project_id(project_id)
return [_build_requirement_response(req) for req in requirements]
@@ -314,25 +578,15 @@ async def get_requirement(
):
"""
Get a specific requirement by ID.
User must be a member of the requirement's project.
Args:
requirement_id: The requirement ID
Returns:
The requirement if found and owned by the authenticated user.
The requirement if found and user has access.
"""
# Get the current user from cookie
user_info = AuthController.get_current_user(request)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
user = await _get_current_user_db(request, db)
req_repo = RequirementRepository(db)
requirement = await req_repo.get_by_id(requirement_id)
@@ -342,12 +596,8 @@ async def get_requirement(
detail=f"Requirement with id {requirement_id} not found"
)
# Verify user owns this requirement
if requirement.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to access this requirement"
)
# Verify user is a member of the requirement's project
await _verify_project_membership(requirement.project_id, user.id, db)
return _build_requirement_response(requirement)
@@ -360,28 +610,22 @@ async def create_requirement(
):
"""
Create a new requirement.
User must be a member of the project.
Args:
req_data: The requirement data
req_data: The requirement data (must include project_id)
Returns:
The created requirement.
"""
# Get the current user from cookie
user_info = AuthController.get_current_user(request)
user = await _get_current_user_db(request, db)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
# Verify user is a member of the project
await _verify_project_membership(req_data.project_id, user.id, db)
req_repo = RequirementRepository(db)
requirement = await req_repo.create(
project_id=req_data.project_id,
user_id=user.id,
tag_id=req_data.tag_id,
req_name=req_data.req_name,
@@ -403,6 +647,7 @@ async def update_requirement(
):
"""
Update an existing requirement.
User must be a member of the requirement's project.
Args:
requirement_id: The requirement ID to update
@@ -411,22 +656,11 @@ async def update_requirement(
Returns:
The updated requirement.
"""
# Get the current user from cookie
user_info = AuthController.get_current_user(request)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
user = await _get_current_user_db(request, db)
req_repo = RequirementRepository(db)
# First check if requirement exists and user owns it
# First check if requirement exists
existing_req = await req_repo.get_by_id(requirement_id)
if not existing_req:
raise HTTPException(
@@ -434,11 +668,8 @@ async def update_requirement(
detail=f"Requirement with id {requirement_id} not found"
)
if existing_req.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to update this requirement"
)
# Verify user is a member of the requirement's project
await _verify_project_membership(existing_req.project_id, user.id, db)
requirement = await req_repo.update(
requirement_id=requirement_id,
@@ -462,26 +693,16 @@ async def delete_requirement(
):
"""
Delete a requirement.
User must be a member of the requirement's project.
Args:
requirement_id: The requirement ID to delete
"""
# Get the current user from cookie
user_info = AuthController.get_current_user(request)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
user = await _get_current_user_db(request, db)
req_repo = RequirementRepository(db)
# First check if requirement exists and user owns it
# First check if requirement exists
existing_req = await req_repo.get_by_id(requirement_id)
if not existing_req:
raise HTTPException(
@@ -489,11 +710,8 @@ async def delete_requirement(
detail=f"Requirement with id {requirement_id} not found"
)
if existing_req.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to delete this requirement"
)
# Verify user is a member of the requirement's project
await _verify_project_membership(existing_req.project_id, user.id, db)
await req_repo.delete(requirement_id)
await db.commit()