Files
PROJECT-CONTORL/frontend/src/pages/Dashboard.tsx
beabigegg 35c90fe76b feat: implement 5 QA-driven security and quality proposals
Implemented proposals from comprehensive QA review:

1. extend-csrf-protection
   - Add POST to CSRF protected methods in frontend
   - Global CSRF middleware for all state-changing operations
   - Update tests with CSRF token fixtures

2. tighten-cors-websocket-security
   - Replace wildcard CORS with explicit method/header lists
   - Disable query parameter auth in production (code 4002)
   - Add per-user WebSocket connection limit (max 5, code 4005)

3. shorten-jwt-expiry
   - Reduce JWT expiry from 7 days to 60 minutes
   - Add refresh token support with 7-day expiry
   - Implement token rotation on refresh
   - Frontend auto-refresh when token near expiry (<5 min)

4. fix-frontend-quality
   - Add React.lazy() code splitting for all pages
   - Fix useCallback dependency arrays (Dashboard, Comments)
   - Add localStorage data validation in AuthContext
   - Complete i18n for AttachmentUpload component

5. enhance-backend-validation
   - Add SecurityAuditMiddleware for access denied logging
   - Add ErrorSanitizerMiddleware for production error messages
   - Protect /health/detailed with admin authentication
   - Add input length validation (comment 5000, desc 10000)

All 521 backend tests passing. Frontend builds successfully.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:19:05 +08:00

270 lines
7.6 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useAuth } from '../contexts/AuthContext'
import { dashboardApi, DashboardResponse } from '../services/dashboard'
import {
StatisticsCard,
WorkloadWidget,
HealthSummaryWidget,
QuickActions,
} from '../components/dashboard'
import { Skeleton } from '../components/Skeleton'
export default function Dashboard() {
const { t } = useTranslation('dashboard')
const { user } = useAuth()
const [data, setData] = useState<DashboardResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchDashboard = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await dashboardApi.getDashboard()
setData(response)
} catch (err) {
setError(t('common:messages.networkError'))
console.error('Dashboard fetch error:', err)
} finally {
setLoading(false)
}
}, [t])
useEffect(() => {
fetchDashboard()
}, [fetchDashboard])
// Loading state
if (loading) {
return (
<div style={styles.container}>
<div style={styles.welcomeSection}>
<Skeleton variant="text" width={200} height={28} />
<Skeleton variant="text" width={150} height={16} style={{ marginTop: 8 }} />
</div>
{/* Statistics Cards Skeleton */}
<div style={styles.statsGrid}>
{[1, 2, 3, 4].map((i) => (
<div key={i} style={styles.skeletonCard}>
<Skeleton variant="circle" width={48} height={48} />
<div style={{ flex: 1 }}>
<Skeleton variant="text" width="60%" height={28} />
<Skeleton variant="text" width="80%" height={14} style={{ marginTop: 4 }} />
</div>
</div>
))}
</div>
{/* Widgets Skeleton */}
<div style={styles.widgetsGrid}>
<div style={styles.skeletonWidget}>
<Skeleton variant="text" width={120} height={20} />
<Skeleton variant="text" width="100%" height={32} style={{ marginTop: 16 }} />
<Skeleton variant="rect" width="100%" height={8} style={{ marginTop: 12, borderRadius: 4 }} />
</div>
<div style={styles.skeletonWidget}>
<Skeleton variant="text" width={120} height={20} />
<div style={{ display: 'flex', gap: 24, marginTop: 16 }}>
<Skeleton variant="circle" width={80} height={80} />
<div style={{ flex: 1 }}>
<Skeleton variant="text" width="100%" height={16} />
<Skeleton variant="text" width="100%" height={16} style={{ marginTop: 8 }} />
<Skeleton variant="text" width="100%" height={16} style={{ marginTop: 8 }} />
</div>
</div>
</div>
</div>
{/* Quick Actions Skeleton */}
<div style={styles.skeletonWidget}>
<Skeleton variant="text" width={120} height={20} />
<div style={{ display: 'flex', gap: 12, marginTop: 16 }}>
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} variant="rect" width={100} height={80} />
))}
</div>
</div>
</div>
)
}
// Error state
if (error) {
return (
<div style={styles.container}>
<div style={styles.welcomeSection}>
<h1 style={styles.welcomeTitle}>{t('welcome', { name: user?.name })}</h1>
</div>
<div style={styles.errorCard}>
<div style={styles.errorIcon}>!</div>
<h3 style={styles.errorTitle}>{t('common:messages.error')}</h3>
<p style={styles.errorMessage}>{error}</p>
<button
style={styles.retryButton}
onClick={fetchDashboard}
type="button"
>
{t('common:buttons.refresh')}
</button>
</div>
</div>
)
}
// Success state
return (
<div style={styles.container}>
{/* Welcome Section */}
<div style={styles.welcomeSection}>
<h1 style={styles.welcomeTitle}>{t('welcome', { name: user?.name })}</h1>
<p style={styles.welcomeSubtitle}>
{t('sections.projectOverview')}
</p>
</div>
{/* Statistics Cards */}
{data && (
<>
<div style={styles.statsGrid}>
<StatisticsCard
icon="✓"
value={data.task_stats.assigned_count}
label={t('stats.myTasks')}
color="#2196f3"
/>
<StatisticsCard
icon="⏰"
value={data.task_stats.due_this_week}
label={t('deadlines.thisWeek')}
color="#ff9800"
highlight={data.task_stats.due_this_week > 0}
/>
<StatisticsCard
icon="⚠"
value={data.task_stats.overdue_count}
label={t('deadlines.overdue')}
color="#f44336"
highlight={data.task_stats.overdue_count > 0}
/>
<StatisticsCard
icon="✅"
value={data.task_stats.completion_rate}
label={t('stats.completedTasks')}
color="#4caf50"
suffix="%"
/>
</div>
{/* Widgets Grid */}
<div style={styles.widgetsGrid}>
<WorkloadWidget workload={data.workload} />
<HealthSummaryWidget health={data.health_summary} />
</div>
{/* Quick Actions */}
<QuickActions isAdmin={user?.is_system_admin} />
</>
)}
</div>
)
}
const styles: { [key: string]: React.CSSProperties } = {
container: {
padding: '24px',
maxWidth: '1200px',
margin: '0 auto',
display: 'flex',
flexDirection: 'column',
gap: '24px',
},
welcomeSection: {
marginBottom: '8px',
},
welcomeTitle: {
margin: 0,
fontSize: '24px',
fontWeight: 600,
color: '#333',
},
welcomeSubtitle: {
margin: '8px 0 0 0',
fontSize: '14px',
color: '#666',
},
statsGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: '16px',
},
widgetsGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '16px',
},
skeletonCard: {
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
padding: '20px',
display: 'flex',
alignItems: 'center',
gap: '16px',
},
skeletonWidget: {
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
padding: '20px',
},
errorCard: {
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
padding: '40px',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
},
errorIcon: {
width: '60px',
height: '60px',
borderRadius: '50%',
backgroundColor: '#ffebee',
color: '#f44336',
fontSize: '32px',
fontWeight: 700,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
errorTitle: {
margin: 0,
fontSize: '18px',
fontWeight: 600,
color: '#333',
},
errorMessage: {
margin: 0,
fontSize: '14px',
color: '#666',
maxWidth: '400px',
},
retryButton: {
padding: '10px 24px',
fontSize: '14px',
fontWeight: 500,
color: 'white',
backgroundColor: '#2196f3',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
transition: 'background-color 0.2s ease',
},
}