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:
261
backend/migrate_db.py
Normal file
261
backend/migrate_db.py
Normal 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)
|
||||
Reference in New Issue
Block a user