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

@@ -0,0 +1,49 @@
"""Add permission enhancements - manager flag and project members table
Revision ID: 014
Revises: a0a0f2710e01
Create Date: 2026-01-10
Add is_department_manager flag to users and create project_members table
for cross-department collaboration support.
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '014'
down_revision: Union[str, None] = 'a0a0f2710e01'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add is_department_manager column to pjctrl_users table
op.add_column(
'pjctrl_users',
sa.Column('is_department_manager', sa.Boolean(), nullable=False, server_default='0')
)
# Create project_members table for cross-department collaboration
op.create_table(
'pjctrl_project_members',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('project_id', sa.String(36), sa.ForeignKey('pjctrl_projects.id', ondelete='CASCADE'), nullable=False),
sa.Column('user_id', sa.String(36), sa.ForeignKey('pjctrl_users.id', ondelete='CASCADE'), nullable=False),
sa.Column('role', sa.String(50), nullable=False, server_default='member'),
sa.Column('added_by', sa.String(36), sa.ForeignKey('pjctrl_users.id'), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=False),
# Ensure a user can only be added once per project
sa.UniqueConstraint('project_id', 'user_id', name='uq_project_member'),
)
# Create indexes for efficient lookups
op.create_index('ix_pjctrl_project_members_project_id', 'pjctrl_project_members', ['project_id'])
op.create_index('ix_pjctrl_project_members_user_id', 'pjctrl_project_members', ['user_id'])
def downgrade() -> None:
op.drop_index('ix_pjctrl_project_members_user_id', table_name='pjctrl_project_members')
op.drop_index('ix_pjctrl_project_members_project_id', table_name='pjctrl_project_members')
op.drop_table('pjctrl_project_members')
op.drop_column('pjctrl_users', 'is_department_manager')

View File

@@ -0,0 +1,29 @@
"""Add version field to tasks for optimistic locking
Revision ID: 015
Revises: 014
Create Date: 2026-01-10
Add version integer field to tasks table for optimistic locking.
This prevents concurrent update conflicts by tracking version numbers.
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '015'
down_revision: Union[str, None] = '014'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add version column to pjctrl_tasks table for optimistic locking
op.add_column(
'pjctrl_tasks',
sa.Column('version', sa.Integer(), nullable=False, server_default='1')
)
def downgrade() -> None:
op.drop_column('pjctrl_tasks', 'version')

View File

@@ -0,0 +1,47 @@
"""Add project templates table
Revision ID: 016
Revises: 015
Create Date: 2026-01-10
Adds project_templates table for storing reusable project configurations
with predefined task statuses and custom fields.
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '016'
down_revision: Union[str, None] = '015'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create pjctrl_project_templates table
op.create_table(
'pjctrl_project_templates',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('name', sa.String(200), nullable=False),
sa.Column('description', sa.Text, nullable=True),
sa.Column('owner_id', sa.String(36), sa.ForeignKey('pjctrl_users.id'), nullable=False),
sa.Column('is_public', sa.Boolean, default=False, nullable=False),
sa.Column('is_active', sa.Boolean, default=True, nullable=False),
sa.Column('task_statuses', sa.JSON, nullable=True),
sa.Column('custom_fields', sa.JSON, nullable=True),
sa.Column('default_security_level', sa.String(20), default='department', nullable=True),
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
# Create indexes
op.create_index('ix_pjctrl_project_templates_owner_id', 'pjctrl_project_templates', ['owner_id'])
op.create_index('ix_pjctrl_project_templates_is_public', 'pjctrl_project_templates', ['is_public'])
op.create_index('ix_pjctrl_project_templates_is_active', 'pjctrl_project_templates', ['is_active'])
def downgrade() -> None:
op.drop_index('ix_pjctrl_project_templates_is_active', table_name='pjctrl_project_templates')
op.drop_index('ix_pjctrl_project_templates_is_public', table_name='pjctrl_project_templates')
op.drop_index('ix_pjctrl_project_templates_owner_id', table_name='pjctrl_project_templates')
op.drop_table('pjctrl_project_templates')