feat: complete issue fixes and implement remaining features

## Critical Issues (CRIT-001~003) - All Fixed
- JWT secret key validation with pydantic field_validator
- Login audit logging for success/failure attempts
- Frontend API path prefix removal

## High Priority Issues (HIGH-001~008) - All Fixed
- Project soft delete using is_active flag
- Redis session token bytes handling
- Rate limiting with slowapi (5 req/min for login)
- Attachment API permission checks
- Kanban view with drag-and-drop
- Workload heatmap UI (WorkloadPage, WorkloadHeatmap)
- TaskDetailModal integrating Comments/Attachments
- UserSelect component for task assignment

## Medium Priority Issues (MED-001~012) - All Fixed
- MED-001~005: DB commits, N+1 queries, datetime, error format, blocker flag
- MED-006: Project health dashboard (HealthService, ProjectHealthPage)
- MED-007: Capacity update API (PUT /api/users/{id}/capacity)
- MED-008: Schedule triggers (cron parsing, deadline reminders)
- MED-009: Watermark feature (image/PDF watermarking)
- MED-010~012: useEffect deps, DOM operations, PDF export

## New Files
- backend/app/api/health/ - Project health API
- backend/app/services/health_service.py
- backend/app/services/trigger_scheduler.py
- backend/app/services/watermark_service.py
- backend/app/core/rate_limiter.py
- frontend/src/pages/ProjectHealthPage.tsx
- frontend/src/components/ProjectHealthCard.tsx
- frontend/src/components/KanbanBoard.tsx
- frontend/src/components/WorkloadHeatmap.tsx

## Tests
- 113 new tests passing (health: 32, users: 14, triggers: 35, watermark: 32)

## OpenSpec Archives
- add-project-health-dashboard
- add-capacity-update-api
- add-schedule-triggers
- add-watermark-feature
- add-rate-limiting
- enhance-frontend-ux
- add-resource-management-ui

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-04 21:49:52 +08:00
parent 64874d5425
commit 9b220523ff
90 changed files with 9426 additions and 194 deletions

View File

@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings
from pydantic import field_validator
from typing import List
import os
@@ -24,11 +25,33 @@ class Settings(BaseSettings):
def REDIS_URL(self) -> str:
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
# JWT
JWT_SECRET_KEY: str = "your-secret-key-change-in-production"
# JWT - Must be set in environment, no default allowed
JWT_SECRET_KEY: str = ""
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_MINUTES: int = 10080 # 7 days
@field_validator("JWT_SECRET_KEY")
@classmethod
def validate_jwt_secret_key(cls, v: str) -> str:
"""Validate that JWT_SECRET_KEY is set and not a placeholder."""
if not v or v.strip() == "":
raise ValueError(
"JWT_SECRET_KEY must be set in environment variables. "
"Please configure it in the .env file."
)
placeholder_values = [
"your-secret-key-change-in-production",
"change-me",
"secret",
"your-secret-key",
]
if v.lower() in placeholder_values:
raise ValueError(
"JWT_SECRET_KEY appears to be a placeholder value. "
"Please set a secure secret key in the .env file."
)
return v
# External Auth API
AUTH_API_URL: str = "https://pj-auth-api.vercel.app"

View File

@@ -0,0 +1,26 @@
"""
Rate limiting configuration using slowapi with Redis backend.
This module provides rate limiting functionality to protect against
brute force attacks and DoS attempts on sensitive endpoints.
"""
import os
from slowapi import Limiter
from slowapi.util import get_remote_address
from app.core.config import settings
# Use memory storage for testing, Redis for production
# This allows tests to run without a Redis connection
_testing = os.environ.get("TESTING", "").lower() in ("true", "1", "yes")
_storage_uri = "memory://" if _testing else settings.REDIS_URL
# Create limiter instance with appropriate storage
# Uses the client's remote address (IP) as the key for rate limiting
limiter = Limiter(
key_func=get_remote_address,
storage_uri=_storage_uri,
strategy="fixed-window", # Fixed window strategy for predictable rate limiting
)

View File

@@ -1,9 +1,11 @@
import logging
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from app.core.database import SessionLocal
from app.services.report_service import ReportService
from app.services.trigger_scheduler import TriggerSchedulerService
logger = logging.getLogger(__name__)
@@ -24,6 +26,24 @@ async def weekly_report_job():
db.close()
async def schedule_trigger_job():
"""Job function to evaluate and execute schedule triggers.
This runs every minute and checks:
1. Cron-based schedule triggers
2. Deadline reminder triggers
"""
db = SessionLocal()
try:
logs = TriggerSchedulerService.evaluate_schedule_triggers(db)
if logs:
logger.info(f"Schedule trigger job executed {len(logs)} triggers")
except Exception as e:
logger.error(f"Error in schedule trigger job: {e}")
finally:
db.close()
def init_scheduler():
"""Initialize the scheduler with jobs."""
# Weekly report - Every Friday at 16:00
@@ -35,7 +55,16 @@ def init_scheduler():
replace_existing=True,
)
logger.info("Scheduler initialized with weekly report job (Friday 16:00)")
# Schedule trigger evaluation - Every minute
scheduler.add_job(
schedule_trigger_job,
IntervalTrigger(minutes=1),
id='schedule_triggers',
name='Evaluate Schedule Triggers',
replace_existing=True,
)
logger.info("Scheduler initialized with jobs: weekly_report (Friday 16:00), schedule_triggers (every minute)")
def start_scheduler():

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Optional, Any
from jose import jwt, JWTError
from app.core.config import settings
@@ -16,13 +16,14 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
Encoded JWT token string
"""
to_encode = data.copy()
now = datetime.now(timezone.utc)
if expires_delta:
expire = datetime.utcnow() + expires_delta
expire = now + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
expire = now + timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
to_encode.update({"exp": expire, "iat": now})
encoded_jwt = jwt.encode(
to_encode,