Add comprehensive backend features and mobile UI improvements

Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

184
backend/app/crud/api_key.py Normal file
View File

@@ -0,0 +1,184 @@
"""CRUD operations for API Key model."""
import json
import hashlib
from datetime import datetime
from typing import Optional, List, Tuple
from sqlalchemy.orm import Session
from app.models.api_key import APIKey, generate_api_key, generate_key_prefix
from app.schemas.api_key import APIKeyCreate, APIKeyUpdate
def hash_api_key(key: str) -> str:
"""Hash an API key for secure storage."""
return hashlib.sha256(key.encode()).hexdigest()
class CRUDAPIKey:
"""CRUD operations for API Key model."""
def create(
self,
db: Session,
*,
obj_in: APIKeyCreate,
user_id: str
) -> Tuple[APIKey, str]:
"""
Create a new API key.
Returns both the database object and the plain key (shown only once).
"""
# Generate the actual key
plain_key = generate_api_key()
key_hash = hash_api_key(plain_key)
key_prefix = generate_key_prefix(plain_key)
# Serialize scopes to JSON
scopes_json = json.dumps(obj_in.scopes) if obj_in.scopes else None
db_obj = APIKey(
user_id=user_id,
name=obj_in.name,
key_hash=key_hash,
key_prefix=key_prefix,
scopes=scopes_json,
expires_at=obj_in.expires_at,
is_active=True,
usage_count="0"
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj, plain_key
def get(self, db: Session, id: str) -> Optional[APIKey]:
"""Get an API key by ID."""
return db.query(APIKey).filter(APIKey.id == id).first()
def get_by_key(self, db: Session, plain_key: str) -> Optional[APIKey]:
"""Get an API key by the plain key (for authentication)."""
key_hash = hash_api_key(plain_key)
return db.query(APIKey).filter(APIKey.key_hash == key_hash).first()
def get_multi_by_user(
self,
db: Session,
*,
user_id: str,
skip: int = 0,
limit: int = 100
) -> List[APIKey]:
"""Get all API keys for a user."""
return db.query(APIKey)\
.filter(APIKey.user_id == user_id)\
.order_by(APIKey.created_at.desc())\
.offset(skip)\
.limit(limit)\
.all()
def count_by_user(self, db: Session, user_id: str) -> int:
"""Count API keys for a user."""
return db.query(APIKey).filter(APIKey.user_id == user_id).count()
def update(
self,
db: Session,
*,
db_obj: APIKey,
obj_in: APIKeyUpdate
) -> APIKey:
"""Update an API key."""
update_data = obj_in.model_dump(exclude_unset=True)
# Handle scopes serialization
if "scopes" in update_data:
update_data["scopes"] = json.dumps(update_data["scopes"]) if update_data["scopes"] else None
for field, value in update_data.items():
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def delete(self, db: Session, *, id: str) -> bool:
"""Delete an API key."""
obj = db.query(APIKey).filter(APIKey.id == id).first()
if obj:
db.delete(obj)
db.commit()
return True
return False
def delete_by_user(self, db: Session, *, user_id: str) -> int:
"""Delete all API keys for a user."""
count = db.query(APIKey).filter(APIKey.user_id == user_id).delete()
db.commit()
return count
def revoke(self, db: Session, *, id: str) -> Optional[APIKey]:
"""Revoke (deactivate) an API key."""
obj = db.query(APIKey).filter(APIKey.id == id).first()
if obj:
obj.is_active = False
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def record_usage(
self,
db: Session,
*,
db_obj: APIKey,
ip_address: Optional[str] = None
) -> APIKey:
"""Record API key usage."""
db_obj.last_used_at = datetime.utcnow()
db_obj.last_used_ip = ip_address
db_obj.usage_count = str(int(db_obj.usage_count or "0") + 1)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def authenticate(
self,
db: Session,
*,
plain_key: str,
ip_address: Optional[str] = None
) -> Optional[APIKey]:
"""
Authenticate with an API key.
Returns the key if valid, None otherwise.
Also records usage on successful auth.
"""
api_key = self.get_by_key(db, plain_key)
if not api_key:
return None
if not api_key.is_valid:
return None
# Record usage
self.record_usage(db, db_obj=api_key, ip_address=ip_address)
return api_key
def get_scopes(self, api_key: APIKey) -> List[str]:
"""Get scopes for an API key."""
if api_key.scopes:
try:
return json.loads(api_key.scopes)
except json.JSONDecodeError:
return []
return []
# Create instance
api_key = CRUDAPIKey()