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

View File

@@ -2,5 +2,11 @@
from app.models.user import User
from app.models.settings import Settings
from app.models.audit_log import AuditLog
from app.models.api_key import APIKey
from app.models.notification import Notification
from app.models.session import UserSession
from app.models.webhook import Webhook, WebhookDelivery
from app.models.file import StoredFile
__all__ = ["User", "Settings"]
__all__ = ["User", "Settings", "AuditLog", "APIKey", "Notification", "UserSession", "Webhook", "WebhookDelivery", "StoredFile"]

View File

@@ -0,0 +1,69 @@
"""API Key database model for programmatic access."""
import uuid
import secrets
from datetime import datetime
from sqlalchemy import Column, String, Boolean, ForeignKey, Text
from sqlalchemy.sql import func
from sqlalchemy.types import DateTime
from app.db.base import Base
def generate_api_key() -> str:
"""Generate a secure API key."""
return f"sk_{secrets.token_urlsafe(32)}"
def generate_key_prefix(key: str) -> str:
"""Generate a display prefix for the key (first 8 chars after sk_)."""
return key[:11] + "..." if len(key) > 11 else key
class APIKey(Base):
"""API Key model for programmatic authentication."""
__tablename__ = "api_keys"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# Owner
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
# Key info
name = Column(String(100), nullable=False) # User-friendly name
key_hash = Column(String(255), nullable=False, unique=True) # Hashed key (for lookup)
key_prefix = Column(String(20), nullable=False) # First chars for display (sk_xxx...)
# Permissions & scope
scopes = Column(Text, nullable=True) # JSON array of allowed scopes/permissions
# Status
is_active = Column(Boolean, default=True, nullable=False)
# Usage tracking
last_used_at = Column(DateTime, nullable=True)
last_used_ip = Column(String(45), nullable=True)
usage_count = Column(String(20), default="0") # String for SQLite compatibility
# Expiration
expires_at = Column(DateTime, nullable=True) # null = never expires
# Timestamps
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
def __repr__(self):
return f"<APIKey(id={self.id}, name='{self.name}', prefix='{self.key_prefix}')>"
@property
def is_expired(self) -> bool:
"""Check if key is expired."""
if self.expires_at is None:
return False
return datetime.utcnow() > self.expires_at
@property
def is_valid(self) -> bool:
"""Check if key is valid (active and not expired)."""
return self.is_active and not self.is_expired

View File

@@ -0,0 +1,48 @@
"""Audit log database model for tracking user actions."""
import uuid
from sqlalchemy import Column, String, Text, ForeignKey, Index
from sqlalchemy.sql import func
from sqlalchemy.types import DateTime
from app.db.base import Base
class AuditLog(Base):
"""Audit log model for tracking user actions and system events."""
__tablename__ = "audit_logs"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# Who performed the action
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
username = Column(String(100), nullable=True) # Stored for history even if user deleted
# What action was performed
action = Column(String(50), nullable=False, index=True) # login, logout, create, update, delete, etc.
resource_type = Column(String(50), nullable=True, index=True) # user, setting, api_key, etc.
resource_id = Column(String(255), nullable=True) # ID of affected resource
# Additional details
details = Column(Text, nullable=True) # JSON string with extra info
# Request context
ip_address = Column(String(45), nullable=True) # IPv6 max length
user_agent = Column(String(500), nullable=True)
# Status
status = Column(String(20), default="success") # success, failure, error
# Timestamp
created_at = Column(DateTime, server_default=func.now(), nullable=False, index=True)
# Composite indexes for common queries
__table_args__ = (
Index('ix_audit_user_action', 'user_id', 'action'),
Index('ix_audit_resource', 'resource_type', 'resource_id'),
Index('ix_audit_created_at_desc', created_at.desc()),
)
def __repr__(self):
return f"<AuditLog(id={self.id}, user={self.username}, action={self.action})>"

View File

@@ -0,0 +1,43 @@
"""File storage model."""
import uuid
from sqlalchemy import Column, String, Boolean, DateTime, BigInteger, ForeignKey, Text
from sqlalchemy.sql import func
from app.db.base import Base
class StoredFile(Base):
"""Model for tracking uploaded files."""
__tablename__ = "stored_files"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# Original file info
original_filename = Column(String(255), nullable=False)
content_type = Column(String(100), nullable=True)
size_bytes = Column(BigInteger, nullable=False)
# Storage info
storage_path = Column(String(500), nullable=False) # Relative path in storage
storage_type = Column(String(20), default="local", nullable=False) # local, s3, etc.
# Metadata
description = Column(Text, nullable=True)
tags = Column(Text, nullable=True) # JSON array as text
# Access control
is_public = Column(Boolean, default=False, nullable=False)
uploaded_by = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
# Hash for deduplication/integrity
file_hash = Column(String(64), nullable=True, index=True) # SHA-256
# Timestamps
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
# Soft delete
is_deleted = Column(Boolean, default=False, nullable=False)
deleted_at = Column(DateTime, nullable=True)

View File

@@ -0,0 +1,51 @@
"""Notification database model for in-app notifications."""
import json
import uuid
from sqlalchemy import Column, String, Boolean, ForeignKey, Text
from sqlalchemy.sql import func
from sqlalchemy.types import DateTime
from app.db.base import Base
class Notification(Base):
"""Notification model for in-app user notifications."""
__tablename__ = "notifications"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# Recipient
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
# Notification content
title = Column(String(200), nullable=False)
message = Column(Text, nullable=True)
type = Column(String(50), default="info") # info, success, warning, error, system
# Link (optional) - where to navigate when clicked
link = Column(String(500), nullable=True)
# Extra data (JSON string)
extra_data = Column(Text, nullable=True)
# Status
is_read = Column(Boolean, default=False, nullable=False, index=True)
# Timestamps
created_at = Column(DateTime, server_default=func.now(), nullable=False, index=True)
read_at = Column(DateTime, nullable=True)
@property
def parsed_extra_data(self) -> dict | None:
"""Get extra_data as parsed dict."""
if self.extra_data:
try:
return json.loads(self.extra_data)
except json.JSONDecodeError:
return None
return None
def __repr__(self):
return f"<Notification(id={self.id}, user_id={self.user_id}, title='{self.title[:30]}')>"

View File

@@ -0,0 +1,46 @@
"""User session database model for tracking active sessions."""
import uuid
from sqlalchemy import Column, String, Boolean, ForeignKey, Text
from sqlalchemy.sql import func
from sqlalchemy.types import DateTime
from app.db.base import Base
class UserSession(Base):
"""User session model for tracking active sessions."""
__tablename__ = "user_sessions"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# User
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
# Token hash (to identify and revoke specific sessions)
token_hash = Column(String(64), unique=True, nullable=False, index=True)
# Device/client info
device_name = Column(String(200), nullable=True) # User-friendly name
device_type = Column(String(50), nullable=True) # desktop, mobile, tablet
browser = Column(String(100), nullable=True)
os = Column(String(100), nullable=True)
user_agent = Column(String(500), nullable=True)
# Location info
ip_address = Column(String(45), nullable=True)
location = Column(String(200), nullable=True) # City, Country
# Status
is_active = Column(Boolean, default=True, nullable=False)
is_current = Column(Boolean, default=False, nullable=False) # Marks the current session
# Timestamps
created_at = Column(DateTime, server_default=func.now(), nullable=False, index=True)
last_active_at = Column(DateTime, server_default=func.now(), nullable=False)
expires_at = Column(DateTime, nullable=True)
revoked_at = Column(DateTime, nullable=True)
def __repr__(self):
return f"<UserSession(id={self.id}, user_id={self.user_id}, device={self.device_name})>"

View File

@@ -27,6 +27,11 @@ class User(Base):
# null means inherit from global settings (all enabled by default)
_permissions = Column("permissions", Text, nullable=True)
# 2FA fields
totp_secret = Column(String(32), nullable=True) # Base32 encoded TOTP secret
totp_enabled = Column(Boolean, default=False, nullable=False)
totp_backup_codes = Column(Text, nullable=True) # JSON array of backup codes
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
last_login = Column(DateTime, nullable=True)
@@ -49,5 +54,23 @@ class User(Base):
else:
self._permissions = json.dumps(value)
@property
def backup_codes(self) -> list:
"""Get backup codes as a list."""
if self.totp_backup_codes:
try:
return json.loads(self.totp_backup_codes)
except json.JSONDecodeError:
return []
return []
@backup_codes.setter
def backup_codes(self, value: list):
"""Set backup codes from a list."""
if value is None:
self.totp_backup_codes = None
else:
self.totp_backup_codes = json.dumps(value)
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}', email='{self.email}')>"

View File

@@ -0,0 +1,63 @@
"""Webhook model for external integrations."""
import uuid
from sqlalchemy import Column, String, Boolean, DateTime, Text, Integer, ForeignKey
from sqlalchemy.sql import func
from app.db.base import Base
class Webhook(Base):
"""Webhook configuration model."""
__tablename__ = "webhooks"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String(100), nullable=False)
url = Column(String(500), nullable=False)
secret = Column(String(64), nullable=True) # For signature verification
# Events to trigger on (JSON array stored as text)
events = Column(Text, nullable=False, default='["*"]') # ["user.created", "user.updated", etc.]
# Configuration
is_active = Column(Boolean, default=True, nullable=False)
retry_count = Column(Integer, default=3, nullable=False)
timeout_seconds = Column(Integer, default=30, nullable=False)
# Metadata
created_by = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
# Statistics
last_triggered_at = Column(DateTime, nullable=True)
success_count = Column(Integer, default=0, nullable=False)
failure_count = Column(Integer, default=0, nullable=False)
class WebhookDelivery(Base):
"""Webhook delivery log model."""
__tablename__ = "webhook_deliveries"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
webhook_id = Column(String(36), ForeignKey("webhooks.id", ondelete="CASCADE"), nullable=False)
# Event details
event_type = Column(String(50), nullable=False)
payload = Column(Text, nullable=False) # JSON payload
# Delivery status
status = Column(String(20), default="pending", nullable=False) # pending, success, failed
status_code = Column(Integer, nullable=True)
response_body = Column(Text, nullable=True)
error_message = Column(Text, nullable=True)
# Retry tracking
attempt_count = Column(Integer, default=0, nullable=False)
next_retry_at = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, server_default=func.now(), nullable=False)
delivered_at = Column(DateTime, nullable=True)