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

261
backend/migrate_db.py Normal file
View File

@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""
Database migration script - Adds missing columns for new features.
Run this script to update an existing database without losing data.
Usage:
python migrate_db.py [database_path]
If no path provided, uses the default /config/config.db
"""
import sqlite3
import sys
from pathlib import Path
def get_existing_columns(cursor, table_name):
"""Get list of existing column names for a table."""
cursor.execute(f"PRAGMA table_info({table_name})")
return {row[1] for row in cursor.fetchall()}
def get_existing_tables(cursor):
"""Get list of existing tables."""
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
return {row[0] for row in cursor.fetchall()}
def migrate(db_path):
"""Run migrations."""
print(f"Migrating database: {db_path}")
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
existing_tables = get_existing_tables(cursor)
print(f"Existing tables: {existing_tables}")
# ====================
# USERS TABLE MIGRATIONS
# ====================
if "users" in existing_tables:
existing_cols = get_existing_columns(cursor, "users")
print(f"Users columns: {existing_cols}")
# Add 2FA columns
if "totp_secret" not in existing_cols:
print(" Adding totp_secret column...")
cursor.execute("ALTER TABLE users ADD COLUMN totp_secret VARCHAR(32)")
if "totp_enabled" not in existing_cols:
print(" Adding totp_enabled column...")
cursor.execute("ALTER TABLE users ADD COLUMN totp_enabled BOOLEAN DEFAULT 0")
if "totp_backup_codes" not in existing_cols:
print(" Adding totp_backup_codes column...")
cursor.execute("ALTER TABLE users ADD COLUMN totp_backup_codes TEXT")
# ====================
# CREATE NEW TABLES IF MISSING
# ====================
# Audit Logs
if "audit_logs" not in existing_tables:
print("Creating audit_logs table...")
cursor.execute("""
CREATE TABLE audit_logs (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36),
username VARCHAR(100),
action VARCHAR(50) NOT NULL,
resource_type VARCHAR(50),
resource_id VARCHAR(255),
details TEXT,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
status VARCHAR(20) DEFAULT 'success',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
)
""")
cursor.execute("CREATE INDEX ix_audit_logs_action ON audit_logs(action)")
cursor.execute("CREATE INDEX ix_audit_logs_resource_type ON audit_logs(resource_type)")
cursor.execute("CREATE INDEX ix_audit_logs_created_at ON audit_logs(created_at)")
# API Keys
if "api_keys" not in existing_tables:
print("Creating api_keys table...")
cursor.execute("""
CREATE TABLE api_keys (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
name VARCHAR(100) NOT NULL,
key_hash VARCHAR(64) NOT NULL UNIQUE,
key_prefix VARCHAR(8) NOT NULL,
scopes TEXT,
is_active BOOLEAN DEFAULT 1,
expires_at DATETIME,
last_used_at DATETIME,
last_used_ip VARCHAR(45),
usage_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
""")
cursor.execute("CREATE INDEX ix_api_keys_key_hash ON api_keys(key_hash)")
# Notifications
if "notifications" not in existing_tables:
print("Creating notifications table...")
cursor.execute("""
CREATE TABLE notifications (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
title VARCHAR(200) NOT NULL,
message TEXT NOT NULL,
type VARCHAR(20) DEFAULT 'info',
link VARCHAR(500),
extra_data TEXT,
is_read BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
read_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
""")
cursor.execute("CREATE INDEX ix_notifications_user_id ON notifications(user_id)")
cursor.execute("CREATE INDEX ix_notifications_is_read ON notifications(is_read)")
# User Sessions
if "user_sessions" not in existing_tables:
print("Creating user_sessions table...")
cursor.execute("""
CREATE TABLE user_sessions (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
token_hash VARCHAR(64) NOT NULL UNIQUE,
device_name VARCHAR(200),
device_type VARCHAR(50),
browser VARCHAR(100),
os VARCHAR(100),
user_agent VARCHAR(500),
ip_address VARCHAR(45),
location VARCHAR(200),
is_active BOOLEAN DEFAULT 1,
is_current BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_active_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME,
revoked_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
""")
cursor.execute("CREATE INDEX ix_user_sessions_token_hash ON user_sessions(token_hash)")
cursor.execute("CREATE INDEX ix_user_sessions_user_id ON user_sessions(user_id)")
# Webhooks
if "webhooks" not in existing_tables:
print("Creating webhooks table...")
cursor.execute("""
CREATE TABLE webhooks (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
url VARCHAR(500) NOT NULL,
secret VARCHAR(64),
events TEXT DEFAULT '["*"]',
is_active BOOLEAN DEFAULT 1,
retry_count INTEGER DEFAULT 3,
timeout_seconds INTEGER DEFAULT 30,
created_by VARCHAR(36),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_triggered_at DATETIME,
success_count INTEGER DEFAULT 0,
failure_count INTEGER DEFAULT 0,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
)
""")
# Webhook Deliveries
if "webhook_deliveries" not in existing_tables:
print("Creating webhook_deliveries table...")
cursor.execute("""
CREATE TABLE webhook_deliveries (
id VARCHAR(36) PRIMARY KEY,
webhook_id VARCHAR(36) NOT NULL,
event_type VARCHAR(50) NOT NULL,
payload TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
status_code INTEGER,
response_body TEXT,
error_message TEXT,
attempt_count INTEGER DEFAULT 0,
next_retry_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
delivered_at DATETIME,
FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON DELETE CASCADE
)
""")
# Stored Files
if "stored_files" not in existing_tables:
print("Creating stored_files table...")
cursor.execute("""
CREATE TABLE stored_files (
id VARCHAR(36) PRIMARY KEY,
original_filename VARCHAR(255) NOT NULL,
content_type VARCHAR(100),
size_bytes BIGINT NOT NULL,
storage_path VARCHAR(500) NOT NULL,
storage_type VARCHAR(20) DEFAULT 'local',
description TEXT,
tags TEXT,
is_public BOOLEAN DEFAULT 0,
uploaded_by VARCHAR(36),
file_hash VARCHAR(64),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT 0,
deleted_at DATETIME,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
)
""")
cursor.execute("CREATE INDEX ix_stored_files_file_hash ON stored_files(file_hash)")
conn.commit()
conn.close()
print("Migration completed successfully!")
if __name__ == "__main__":
# Default path for Docker container
default_path = "/config/config.db"
if len(sys.argv) > 1:
db_path = sys.argv[1]
else:
# Check common locations
paths_to_try = [
Path(default_path),
Path("./config.db"),
Path("./data/config.db"),
Path("../config/config.db"),
]
db_path = None
for p in paths_to_try:
if p.exists():
db_path = str(p)
break
if not db_path:
print(f"Database not found. Please provide path as argument.")
print(f"Usage: python migrate_db.py /path/to/config.db")
sys.exit(1)
if not Path(db_path).exists():
print(f"Error: Database file not found: {db_path}")
sys.exit(1)
migrate(db_path)