fix: resolve duplicate header and improve Redis management

- Dashboard: Remove redundant header (Layout already provides it)
- projectctl.sh: Add start_redis/stop_redis functions for automatic
  Redis lifecycle management on project start/stop
- rate_limiter.py: Add fallback to memory storage when Redis unavailable

🤖 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-07 21:53:16 +08:00
parent 4b5a9c1d0a
commit a7c452ffd8
3 changed files with 154 additions and 84 deletions

View File

@@ -5,6 +5,7 @@ This module provides rate limiting functionality to protect against
brute force attacks and DoS attempts on sensitive endpoints.
"""
import logging
import os
from slowapi import Limiter
@@ -12,10 +13,50 @@ 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
logger = logging.getLogger(__name__)
def _get_storage_uri() -> str:
"""
Determine the appropriate storage URI for rate limiting.
Priority:
1. Use memory storage if TESTING environment variable is set
2. Try Redis if available
3. Fall back to memory storage if Redis is unavailable (with warning)
Note: Memory storage is acceptable for development but Redis should
be used in production for consistent rate limiting across workers.
"""
# Use memory storage for testing
testing = os.environ.get("TESTING", "").lower() in ("true", "1", "yes")
if testing:
return "memory://"
# Try to connect to Redis
redis_url = settings.REDIS_URL
try:
import redis
r = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB,
socket_connect_timeout=1,
)
r.ping()
logger.info("Rate limiter using Redis storage")
return redis_url
except Exception as e:
logger.warning(
f"Redis unavailable for rate limiting ({e}). "
"Falling back to in-memory storage. "
"Note: In production, Redis should be running for consistent "
"rate limiting across multiple workers."
)
return "memory://"
_storage_uri = _get_storage_uri()
# Create limiter instance with appropriate storage
# Uses the client's remote address (IP) as the key for rate limiting

View File

@@ -1,95 +1,34 @@
import { useAuth } from '../contexts/AuthContext'
export default function Dashboard() {
const { user, logout } = useAuth()
const handleLogout = async () => {
await logout()
}
const { user } = useAuth()
return (
<div style={styles.container}>
<header style={styles.header}>
<h1 style={styles.title}>Project Control</h1>
<div style={styles.userInfo}>
<span style={styles.userName}>{user?.name}</span>
{user?.is_system_admin && (
<span style={styles.badge}>Admin</span>
)}
<button onClick={handleLogout} style={styles.logoutButton}>
Logout
</button>
</div>
</header>
<main style={styles.main}>
<div style={styles.welcomeCard}>
<h2>Welcome, {user?.name}!</h2>
<p>Email: {user?.email}</p>
<p>Role: {user?.role || 'No role assigned'}</p>
{user?.is_system_admin && (
<p style={styles.adminNote}>
You have system administrator privileges.
</p>
)}
</div>
<div style={styles.infoCard}>
<h3>Getting Started</h3>
<p>
This is the Project Control system dashboard. Features will be
added as development progresses.
<div style={styles.welcomeCard}>
<h2>Welcome, {user?.name}!</h2>
<p>Email: {user?.email}</p>
<p>Role: {user?.role || 'No role assigned'}</p>
{user?.is_system_admin && (
<p style={styles.adminNote}>
You have system administrator privileges.
</p>
</div>
</main>
)}
</div>
<div style={styles.infoCard}>
<h3>Getting Started</h3>
<p>
This is the Project Control system dashboard. Features will be
added as development progresses.
</p>
</div>
</div>
)
}
const styles: { [key: string]: React.CSSProperties } = {
container: {
minHeight: '100vh',
backgroundColor: '#f5f5f5',
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px 24px',
backgroundColor: 'white',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
},
title: {
fontSize: '20px',
fontWeight: 600,
color: '#333',
margin: 0,
},
userInfo: {
display: 'flex',
alignItems: 'center',
gap: '12px',
},
userName: {
color: '#666',
},
badge: {
backgroundColor: '#0066cc',
color: 'white',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 500,
},
logoutButton: {
padding: '8px 16px',
backgroundColor: '#f5f5f5',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
},
main: {
padding: '24px',
maxWidth: '1200px',
margin: '0 auto',

View File

@@ -213,6 +213,94 @@ check_redis() {
return 0
}
start_redis() {
local env_file="$BACKEND_DIR/.env"
local redis_host redis_port
redis_host="$(get_env_value "REDIS_HOST" "$env_file")"
redis_port="$(get_env_value "REDIS_PORT" "$env_file")"
redis_host="${redis_host:-localhost}"
redis_port="${redis_port:-6379}"
# Only manage local Redis
if [[ "$redis_host" != "localhost" && "$redis_host" != "127.0.0.1" ]]; then
log "Redis host is remote ($redis_host), skipping local Redis management"
return 0
fi
# Check if Redis is already running
if command -v redis-cli >/dev/null 2>&1; then
if redis-cli -h "$redis_host" -p "$redis_port" ping >/dev/null 2>&1; then
log "Redis already running at $redis_host:$redis_port"
return 0
fi
fi
# Try to start Redis using brew services (macOS)
if command -v brew >/dev/null 2>&1; then
if brew services list 2>/dev/null | grep -q "redis"; then
log "Starting Redis via brew services..."
brew services start redis >/dev/null 2>&1 || true
sleep 2
if redis-cli -h "$redis_host" -p "$redis_port" ping >/dev/null 2>&1; then
log "Redis started successfully"
env_log "Redis: Started via brew services"
return 0
fi
fi
fi
# Try to start redis-server directly
if command -v redis-server >/dev/null 2>&1; then
log "Starting Redis server directly..."
redis-server --daemonize yes --port "$redis_port" >/dev/null 2>&1 || true
sleep 1
if redis-cli -h "$redis_host" -p "$redis_port" ping >/dev/null 2>&1; then
log "Redis started successfully"
env_log "Redis: Started directly"
return 0
fi
fi
log "WARN: Could not start Redis automatically"
return 1
}
stop_redis() {
local env_file="$BACKEND_DIR/.env"
local redis_host redis_port
redis_host="$(get_env_value "REDIS_HOST" "$env_file")"
redis_port="$(get_env_value "REDIS_PORT" "$env_file")"
redis_host="${redis_host:-localhost}"
redis_port="${redis_port:-6379}"
# Only manage local Redis
if [[ "$redis_host" != "localhost" && "$redis_host" != "127.0.0.1" ]]; then
echo "Redis host is remote ($redis_host), skipping local Redis management"
return 0
fi
# Try to stop Redis using brew services (macOS)
if command -v brew >/dev/null 2>&1; then
if brew services list 2>/dev/null | grep -q "redis.*started"; then
echo "Stopping Redis via brew services..."
brew services stop redis >/dev/null 2>&1 || true
return 0
fi
fi
# Try to stop redis-server via redis-cli
if command -v redis-cli >/dev/null 2>&1; then
if redis-cli -h "$redis_host" -p "$redis_port" ping >/dev/null 2>&1; then
echo "Stopping Redis via redis-cli shutdown..."
redis-cli -h "$redis_host" -p "$redis_port" shutdown nosave >/dev/null 2>&1 || true
return 0
fi
fi
echo "Redis: not running or not managed locally"
return 0
}
check_mysql() {
local env_file="$BACKEND_DIR/.env"
local mysql_host mysql_port
@@ -397,7 +485,8 @@ start() {
check_env_file
check_python_deps
check_node_deps
check_redis || true # Warn but don't fail
start_redis || true # Start Redis if not running (warn but don't fail)
check_redis || true # Verify Redis is available
check_mysql || true # Warn but don't fail
check_ports
@@ -425,6 +514,7 @@ stop() {
fi
stop_service "backend" "$dir/backend.pid"
stop_service "frontend" "$dir/frontend.pid"
stop_redis
}
status() {