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. brute force attacks and DoS attempts on sensitive endpoints.
""" """
import logging
import os import os
from slowapi import Limiter from slowapi import Limiter
@@ -12,10 +13,50 @@ from slowapi.util import get_remote_address
from app.core.config import settings from app.core.config import settings
# Use memory storage for testing, Redis for production logger = logging.getLogger(__name__)
# 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 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 # Create limiter instance with appropriate storage
# Uses the client's remote address (IP) as the key for rate limiting # Uses the client's remote address (IP) as the key for rate limiting

View File

@@ -1,95 +1,34 @@
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
export default function Dashboard() { export default function Dashboard() {
const { user, logout } = useAuth() const { user } = useAuth()
const handleLogout = async () => {
await logout()
}
return ( return (
<div style={styles.container}> <div style={styles.container}>
<header style={styles.header}> <div style={styles.welcomeCard}>
<h1 style={styles.title}>Project Control</h1> <h2>Welcome, {user?.name}!</h2>
<div style={styles.userInfo}> <p>Email: {user?.email}</p>
<span style={styles.userName}>{user?.name}</span> <p>Role: {user?.role || 'No role assigned'}</p>
{user?.is_system_admin && ( {user?.is_system_admin && (
<span style={styles.badge}>Admin</span> <p style={styles.adminNote}>
)} You have system administrator privileges.
<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.
</p> </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> </div>
) )
} }
const styles: { [key: string]: React.CSSProperties } = { const styles: { [key: string]: React.CSSProperties } = {
container: { 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', padding: '24px',
maxWidth: '1200px', maxWidth: '1200px',
margin: '0 auto', margin: '0 auto',

View File

@@ -213,6 +213,94 @@ check_redis() {
return 0 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() { check_mysql() {
local env_file="$BACKEND_DIR/.env" local env_file="$BACKEND_DIR/.env"
local mysql_host mysql_port local mysql_host mysql_port
@@ -397,7 +485,8 @@ start() {
check_env_file check_env_file
check_python_deps check_python_deps
check_node_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_mysql || true # Warn but don't fail
check_ports check_ports
@@ -425,6 +514,7 @@ stop() {
fi fi
stop_service "backend" "$dir/backend.pid" stop_service "backend" "$dir/backend.pid"
stop_service "frontend" "$dir/frontend.pid" stop_service "frontend" "$dir/frontend.pid"
stop_redis
} }
status() { status() {