feat: implement 8 OpenSpec proposals for security, reliability, and UX improvements

## 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>
This commit is contained in:
beabigegg
2026-01-10 22:13:43 +08:00
parent 96210c7ad4
commit 3bdc6ff1c9
106 changed files with 9704 additions and 429 deletions

View File

@@ -10,13 +10,18 @@ from sqlalchemy.orm import Session
from typing import Optional
from app.core.database import get_db
from app.core.rate_limiter import limiter
from app.core.config import settings
from app.models import User, Task, TaskDependency, AuditAction
from app.schemas.task_dependency import (
TaskDependencyCreate,
TaskDependencyUpdate,
TaskDependencyResponse,
TaskDependencyListResponse,
TaskInfo
TaskInfo,
BulkDependencyCreate,
BulkDependencyValidationResult,
BulkDependencyCreateResponse,
)
from app.middleware.auth import get_current_user, check_task_access, check_task_edit_access
from app.middleware.audit import get_audit_metadata
@@ -429,3 +434,184 @@ async def list_project_dependencies(
dependencies=[dependency_to_response(d) for d in dependencies],
total=len(dependencies)
)
@router.post(
"/api/projects/{project_id}/dependencies/validate",
response_model=BulkDependencyValidationResult
)
async def validate_bulk_dependencies(
project_id: str,
bulk_data: BulkDependencyCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Validate a batch of dependencies without creating them.
This endpoint checks for:
- Self-references
- Cross-project dependencies
- Duplicate dependencies
- Circular dependencies (including cycles that would be created by the batch)
Returns validation results without modifying the database.
"""
from app.models import Project
from app.middleware.auth import check_project_access
project = db.query(Project).filter(Project.id == project_id).first()
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Project not found"
)
if not check_project_access(current_user, project):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Convert to tuple format for validation
dependencies = [
(dep.predecessor_id, dep.successor_id)
for dep in bulk_data.dependencies
]
errors = DependencyService.validate_bulk_dependencies(db, dependencies, project_id)
return BulkDependencyValidationResult(
valid=len(errors) == 0,
errors=errors
)
@router.post(
"/api/projects/{project_id}/dependencies/bulk",
response_model=BulkDependencyCreateResponse,
status_code=status.HTTP_201_CREATED
)
@limiter.limit(settings.RATE_LIMIT_HEAVY)
async def create_bulk_dependencies(
request: Request,
project_id: str,
bulk_data: BulkDependencyCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Create multiple dependencies at once.
This endpoint:
1. Validates all dependencies together for cycle detection
2. Creates valid dependencies
3. Returns both created dependencies and any failures
Cycle detection considers all dependencies in the batch together,
so cycles that would only appear when all dependencies are added
will be caught.
Rate limited: 5 requests per minute (heavy tier).
"""
from app.models import Project
from app.middleware.auth import check_project_edit_access
project = db.query(Project).filter(Project.id == project_id).first()
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Project not found"
)
if not check_project_edit_access(current_user, project):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied"
)
# Convert to tuple format for validation
dependencies_to_validate = [
(dep.predecessor_id, dep.successor_id)
for dep in bulk_data.dependencies
]
# Validate all dependencies together
errors = DependencyService.validate_bulk_dependencies(
db, dependencies_to_validate, project_id
)
# Build a set of failed dependency pairs for quick lookup
failed_pairs = set()
for error in errors:
pair = (error.get("predecessor_id"), error.get("successor_id"))
failed_pairs.add(pair)
created_dependencies = []
failed_items = errors # Include validation errors
# Create dependencies that passed validation
for dep_data in bulk_data.dependencies:
pair = (dep_data.predecessor_id, dep_data.successor_id)
if pair in failed_pairs:
continue
# Additional check: verify dependency limit for successor
current_count = db.query(TaskDependency).filter(
TaskDependency.successor_id == dep_data.successor_id
).count()
if current_count >= DependencyService.MAX_DIRECT_DEPENDENCIES:
failed_items.append({
"error_type": "limit_exceeded",
"predecessor_id": dep_data.predecessor_id,
"successor_id": dep_data.successor_id,
"message": f"Successor task already has {DependencyService.MAX_DIRECT_DEPENDENCIES} dependencies"
})
continue
# Create the dependency
dependency = TaskDependency(
id=str(uuid.uuid4()),
predecessor_id=dep_data.predecessor_id,
successor_id=dep_data.successor_id,
dependency_type=dep_data.dependency_type.value,
lag_days=dep_data.lag_days
)
db.add(dependency)
created_dependencies.append(dependency)
# Audit log for each created dependency
AuditService.log_event(
db=db,
event_type="task.dependency.create",
resource_type="task_dependency",
action=AuditAction.CREATE,
user_id=current_user.id,
resource_id=dependency.id,
changes=[{
"field": "dependency",
"old_value": None,
"new_value": {
"predecessor_id": dependency.predecessor_id,
"successor_id": dependency.successor_id,
"dependency_type": dependency.dependency_type,
"lag_days": dependency.lag_days
}
}],
request_metadata=get_audit_metadata(request)
)
db.commit()
# Refresh created dependencies to get relationships
for dep in created_dependencies:
db.refresh(dep)
return BulkDependencyCreateResponse(
created=[dependency_to_response(d) for d in created_dependencies],
failed=failed_items,
total_created=len(created_dependencies),
total_failed=len(failed_items)
)