diff --git a/backend/app/core/rate_limiter.py b/backend/app/core/rate_limiter.py
index 3e19d3d..8e9a5cd 100644
--- a/backend/app/core/rate_limiter.py
+++ b/backend/app/core/rate_limiter.py
@@ -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
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index cd51dea..cf2b17d 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -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 (
-
-
-
-
-
Welcome, {user?.name}!
-
Email: {user?.email}
-
Role: {user?.role || 'No role assigned'}
- {user?.is_system_admin && (
-
- You have system administrator privileges.
-
- )}
-
-
-
-
Getting Started
-
- This is the Project Control system dashboard. Features will be
- added as development progresses.
+
+
Welcome, {user?.name}!
+
Email: {user?.email}
+
Role: {user?.role || 'No role assigned'}
+ {user?.is_system_admin && (
+
+ You have system administrator privileges.
-
-
+ )}
+
+
+
+
Getting Started
+
+ This is the Project Control system dashboard. Features will be
+ added as development progresses.
+
+
)
}
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',
diff --git a/projectctl.sh b/projectctl.sh
index 2cb6cdb..25e0926 100755
--- a/projectctl.sh
+++ b/projectctl.sh
@@ -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() {