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:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user