Files
fast-next-template/backend/app/schemas/syndarix/project.py
Felipe Cardoso cea97afe25 fix: Add missing API endpoints and validation improvements
- Add cancel_sprint and delete_sprint endpoints to sprints.py
- Add unassign_issue endpoint to issues.py
- Add remove_issue_from_sprint endpoint to sprints.py
- Add CRUD methods: remove_sprint_from_issues, unassign, remove_from_sprint
- Add validation to prevent closed issues in active/planned sprints
- Add authorization tests for SSE events endpoint
- Fix IDOR vulnerabilities in agents.py and projects.py
- Add Syndarix models migration (0004)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:39:51 +01:00

132 lines
3.9 KiB
Python

# app/schemas/syndarix/project.py
"""
Pydantic schemas for Project entity.
"""
import re
from datetime import datetime
from typing import Any
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_validator
from .enums import AutonomyLevel, ProjectStatus
class ProjectBase(BaseModel):
"""Base project schema with common fields."""
name: str = Field(..., min_length=1, max_length=255)
slug: str | None = Field(None, min_length=1, max_length=255)
description: str | None = None
autonomy_level: AutonomyLevel = AutonomyLevel.MILESTONE
status: ProjectStatus = ProjectStatus.ACTIVE
settings: dict[str, Any] = Field(default_factory=dict)
@field_validator("slug")
@classmethod
def validate_slug(cls, v: str | None) -> str | None:
"""Validate slug format: lowercase, alphanumeric, hyphens only."""
if v is None:
return v
if not re.match(r"^[a-z0-9-]+$", v):
raise ValueError(
"Slug must contain only lowercase letters, numbers, and hyphens"
)
if v.startswith("-") or v.endswith("-"):
raise ValueError("Slug cannot start or end with a hyphen")
if "--" in v:
raise ValueError("Slug cannot contain consecutive hyphens")
return v
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""Validate project name."""
if not v or v.strip() == "":
raise ValueError("Project name cannot be empty")
return v.strip()
class ProjectCreate(ProjectBase):
"""Schema for creating a new project."""
name: str = Field(..., min_length=1, max_length=255)
slug: str = Field(..., min_length=1, max_length=255)
owner_id: UUID | None = None
class ProjectUpdate(BaseModel):
"""Schema for updating a project.
Note: owner_id is intentionally excluded to prevent IDOR vulnerabilities.
Project ownership transfer should be done via a dedicated endpoint with
proper authorization checks.
"""
name: str | None = Field(None, min_length=1, max_length=255)
slug: str | None = Field(None, min_length=1, max_length=255)
description: str | None = None
autonomy_level: AutonomyLevel | None = None
status: ProjectStatus | None = None
settings: dict[str, Any] | None = None
@field_validator("slug")
@classmethod
def validate_slug(cls, v: str | None) -> str | None:
"""Validate slug format."""
if v is None:
return v
if not re.match(r"^[a-z0-9-]+$", v):
raise ValueError(
"Slug must contain only lowercase letters, numbers, and hyphens"
)
if v.startswith("-") or v.endswith("-"):
raise ValueError("Slug cannot start or end with a hyphen")
if "--" in v:
raise ValueError("Slug cannot contain consecutive hyphens")
return v
@field_validator("name")
@classmethod
def validate_name(cls, v: str | None) -> str | None:
"""Validate project name."""
if v is not None and (not v or v.strip() == ""):
raise ValueError("Project name cannot be empty")
return v.strip() if v else v
class ProjectInDB(ProjectBase):
"""Schema for project in database."""
id: UUID
owner_id: UUID | None = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class ProjectResponse(ProjectBase):
"""Schema for project API responses."""
id: UUID
owner_id: UUID | None = None
created_at: datetime
updated_at: datetime
agent_count: int | None = 0
issue_count: int | None = 0
active_sprint_name: str | None = None
model_config = ConfigDict(from_attributes=True)
class ProjectListResponse(BaseModel):
"""Schema for paginated project list responses."""
projects: list[ProjectResponse]
total: int
page: int
page_size: int
pages: int