🚀 Major Enhancement: Complete AI-Powered LifeRPG Platform with Git LFS

 New Features:
- AI-powered habit creation with natural language processing
- HuggingFace transformers integration for sentiment analysis (tracked via Git LFS)
- Advanced predictive analytics and behavioral insights
- Voice & image input capabilities for hands-free habit tracking
- Real-time notifications and community features
- Plugin system with extensible architecture

🔧 Technical Improvements:
- Comprehensive FastAPI backend with 30+ endpoints
- React frontend with PWA capabilities
- Advanced authentication with 2FA support
- RBAC authorization system
- Comprehensive security features (CSRF, rate limiting, audit logging)
- Database migrations and health monitoring
- Docker containerization support
- Git LFS configured for large AI model files (2+ GB)

📚 Documentation & DevOps:
- Complete deployment guides for multiple platforms
- Professional README with feature highlights
- GitHub Actions CI/CD workflows
- Comprehensive API documentation
- Security audit roadmap and compliance framework
- Setup scripts for development environment

🧪 Testing & Quality:
- Comprehensive test suite with 20+ test modules
- Setup verification scripts
- Working development environment with both backend and frontend
- Health checks and monitoring systems

🌟 Ready for:
- Portfolio showcasing
- Community contributions
- Production deployment
- Professional presentation
This commit is contained in:
TLimoges33
2025-09-28 21:29:19 +00:00
committed by GitHub
parent 7fe4ae5365
commit 2b961611fd
131 changed files with 29938 additions and 1450 deletions
+195 -23
View File
@@ -1,32 +1,180 @@
import time
from typing import Dict, Tuple, Optional
import os
from typing import Dict, Tuple, Optional, Any
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from config import settings
from security_monitor import security_monitor, log_rate_limit_exceeded, check_ip_blocked
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Add comprehensive security headers to all responses"""
def __init__(self, app):
super().__init__(app)
self.security_headers = self._get_security_headers()
def _get_security_headers(self):
"""Get comprehensive security headers configuration"""
return {
# Content Security Policy
"Content-Security-Policy": settings.csp_header(),
# Prevent MIME type sniffing
"X-Content-Type-Options": "nosniff",
# XSS Protection (legacy but still useful)
"X-XSS-Protection": "1; mode=block",
# Prevent framing
"X-Frame-Options": "DENY",
# Referrer policy
"Referrer-Policy": "strict-origin-when-cross-origin",
# Permissions policy (Feature Policy successor)
"Permissions-Policy": (
"camera=(), microphone=(), geolocation=(), "
"payment=(), usb=(), magnetometer=(), gyroscope=(), "
"accelerometer=(), ambient-light-sensor=(), "
"autoplay=(), encrypted-media=(), fullscreen=(), "
"picture-in-picture=()"
),
# Cross-Origin policies
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "same-origin",
# Additional security headers
"X-Permitted-Cross-Domain-Policies": "none",
"X-DNS-Prefetch-Control": "off",
"Expect-CT": "max-age=86400, enforce",
# Cache control for sensitive pages
"Cache-Control": "no-store, no-cache, must-revalidate, private",
"Pragma": "no-cache",
"Expires": "0",
# Server information hiding
"Server": "WizardsGrimoire/1.0",
"X-Powered-By": "Magic",
}
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
# Apply security headers
for header_name, header_value in self.security_headers.items():
# Skip cache headers for static resources
if (header_name in ["Cache-Control", "Pragma", "Expires"] and
self._is_static_resource(request.url.path)):
continue
response.headers[header_name] = header_value
# HSTS (only for HTTPS)
if settings.HSTS_ENABLE and request.url.scheme == "https":
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains; preload"
)
# Remove potentially revealing headers
headers_to_remove = ["server", "x-powered-by", "x-aspnet-version"]
for header in headers_to_remove:
if header in response.headers:
del response.headers[header]
# Add API version and security level info
if request.url.path.startswith("/api/"):
response.headers["X-API-Version"] = "v1"
response.headers["X-Security-Level"] = "enhanced"
response.headers["X-Content-Security"] = "validated"
return response
def _is_static_resource(self, path: str) -> bool:
"""Check if the path is for a static resource"""
static_extensions = ['.css', '.js', '.png', '.jpg', '.jpeg', '.gif',
'.svg', '.ico', '.woff', '.woff2', '.ttf']
return any(path.endswith(ext) for ext in static_extensions)
class BodySizeLimitMiddleware(BaseHTTPMiddleware):
def __init__(self, app, max_body_bytes: int):
super().__init__(app)
self.max_body_bytes = max_body_bytes
# Per-endpoint size limits
self.endpoint_limits = {
# File upload endpoints
"/api/files/upload": 50 * 1024 * 1024, # 50MB for file uploads
"/api/profile/avatar": 5 * 1024 * 1024, # 5MB for avatar uploads
# Data export endpoints
"/api/gdpr/export-data": 100 * 1024 * 1024, # 100MB for export
# Authentication endpoints (strict limits)
"/api/auth/login": 1024, # 1KB for login
"/api/auth/register": 2048, # 2KB for registration
"/api/auth/2fa": 1024, # 1KB for 2FA
# Application data endpoints
"/api/habits": 10 * 1024, # 10KB for habit operations
"/api/projects": 50 * 1024, # 50KB for project operations
# Admin endpoints
"/api/admin/": 1024 * 1024, # 1MB for admin operations
}
def _get_size_limit_for_path(self, path: str) -> int:
"""Get appropriate size limit for the given path"""
# Check exact matches first
if path in self.endpoint_limits:
return self.endpoint_limits[path]
# Check prefix matches for admin endpoints
for endpoint_path, limit in self.endpoint_limits.items():
if endpoint_path.endswith("/") and path.startswith(endpoint_path):
return limit
# Return default limit
return self.max_body_bytes
async def dispatch(self, request: Request, call_next):
# Skip when no body (GET/DELETE/etc.)
if request.method in {"GET", "DELETE", "OPTIONS", "HEAD"}:
return await call_next(request)
# Get appropriate size limit for this endpoint
size_limit = self._get_size_limit_for_path(request.url.path)
cl = request.headers.get("content-length")
try:
if cl and int(cl) > self.max_body_bytes:
return JSONResponse({"detail": "request entity too large"}, status_code=413)
if cl and int(cl) > size_limit:
return JSONResponse(
{
"detail": "request entity too large",
"max_size": size_limit,
"received_size": int(cl)
},
status_code=413
)
except Exception:
pass
# Read body once and reuse cached body downstream
body = await request.body()
if len(body) > self.max_body_bytes:
return JSONResponse({"detail": "request entity too large"}, status_code=413)
if len(body) > size_limit:
return JSONResponse(
{
"detail": "request entity too large",
"max_size": size_limit,
"received_size": len(body)
},
status_code=413
)
# Starlette caches body in request, so downstream can still call .json()/.form()
return await call_next(request)
@@ -42,6 +190,8 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
self.rpm = max(1, int(requests_per_minute))
self._counts: Dict[Tuple[str, int], int] = {}
self._redis = self._init_redis()
# Special rate limits for authentication endpoints
self.auth_rpm = max(1, int(requests_per_minute // 4)) # More restrictive for auth
def _init_redis(self):
import os
@@ -67,6 +217,12 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
if request.method == "OPTIONS":
return await call_next(request)
# Use stricter limits for authentication endpoints
current_rpm = self.rpm
path = request.url.path
if any(auth_path in path for auth_path in ['/auth/login', '/auth/signup', '/2fa/']):
current_rpm = self.auth_rpm
now = int(time.time())
window = now // 60
ip = self._client_ip(request)
@@ -74,25 +230,32 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
# Use a single Redis counter per ip+window
rkey = f"rl:{ip}:{window}"
try:
current = self._redis.incr(rkey)
# Redis incr returns an integer
redis_result = self._redis.incr(rkey)
# Type safety: ensure we have an integer
current = redis_result if isinstance(redis_result, int) else 1
if current == 1:
# Set TTL to end of current minute
self._redis.expire(rkey, 60 - (now % 60))
if current > self.rpm:
if current > current_rpm:
retry_after = 60 - (now % 60)
return JSONResponse(
{"detail": "rate limit exceeded"},
status_code=429,
headers={
"Retry-After": str(retry_after),
"X-RateLimit-Limit": str(self.rpm),
"X-RateLimit-Limit": str(current_rpm),
"X-RateLimit-Remaining": "0",
},
)
resp: Response = await call_next(request)
remaining = max(0, self.rpm - int(current))
resp.headers.setdefault("X-RateLimit-Limit", str(self.rpm))
resp.headers.setdefault("X-RateLimit-Remaining", str(remaining))
remaining = max(0, current_rpm - current)
resp.headers.setdefault("X-RateLimit-Limit", str(current_rpm))
resp.headers.setdefault(
"X-RateLimit-Remaining", str(remaining)
)
return resp
except Exception:
# If Redis fails, fall back to memory
@@ -101,29 +264,31 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
# In-memory windowing fallback
key = (ip, window)
count = self._counts.get(key, 0)
if count >= self.rpm:
if count >= current_rpm:
retry_after = 60 - (now % 60)
return JSONResponse(
{"detail": "rate limit exceeded"},
status_code=429,
headers={
"Retry-After": str(retry_after),
"X-RateLimit-Limit": str(self.rpm),
"X-RateLimit-Limit": str(current_rpm),
"X-RateLimit-Remaining": "0",
},
)
self._counts[key] = count + 1
resp: Response = await call_next(request)
remaining = max(0, self.rpm - self._counts.get(key, 0))
resp.headers.setdefault("X-RateLimit-Limit", str(self.rpm))
remaining = max(0, current_rpm - self._counts.get(key, 0))
resp.headers.setdefault("X-RateLimit-Limit", str(current_rpm))
resp.headers.setdefault("X-RateLimit-Remaining", str(remaining))
return resp
class CSRFMiddleware(BaseHTTPMiddleware):
"""Double-submit cookie CSRF protection for cookie-authenticated, state-changing requests.
"""Double-submit cookie CSRF protection for
cookie-authenticated, state-changing requests.
Enforced when settings.CSRF_ENABLE is true and request has a session cookie and no Bearer token.
Enforced when settings.CSRF_ENABLE is true and request has a
session cookie and no Bearer token.
Excludes safe methods and OPTIONS.
"""
@@ -140,14 +305,21 @@ class CSRFMiddleware(BaseHTTPMiddleware):
return await call_next(request)
# If using Bearer token, skip CSRF (not cookie-based auth)
auth = request.headers.get('authorization') or request.headers.get('Authorization')
if auth and auth.lower().startswith('bearer '):
auth_header = (request.headers.get('authorization') or
request.headers.get('Authorization'))
if auth_header and auth_header.lower().startswith('bearer '):
return await call_next(request)
# Only enforce if session cookie present
if request.cookies.get('session'):
header = request.headers.get(settings.CSRF_HEADER_NAME) or request.headers.get(settings.CSRF_HEADER_NAME.upper())
cookie = request.cookies.get(settings.CSRF_COOKIE_NAME)
if not header or not cookie or header != cookie:
return JSONResponse({"detail": "CSRF token missing or invalid"}, status_code=403)
csrf_header = (request.headers.get(settings.CSRF_HEADER_NAME) or
request.headers.get(
settings.CSRF_HEADER_NAME.upper()))
csrf_cookie = request.cookies.get(settings.CSRF_COOKIE_NAME)
if (not csrf_header or not csrf_cookie or
csrf_header != csrf_cookie):
return JSONResponse(
{"detail": "CSRF token missing or invalid"},
status_code=403
)
return await call_next(request)