Move client key to database with admin UI for key management and QR code generation

- Store client key in SQLite database instead of environment variable
- Add database migration from CLIENT_KEY environment variable to preserve existing keys
- Add admin UI with tabbed interface (Configuration and QR Code tabs)
- Implement QR code generation containing server config (address, port, key) for Android app
- Add functionality to regenerate client key with warning dialog
- Add buttons to download QR code as PNG and copy QR image to clipboard
- Add manual input fields for server address and port configuration
- Update requirements.txt with pyotp and qrcode dependencies
This commit is contained in:
2025-12-27 14:29:58 -05:00
parent 872ae74668
commit 164e2162a2
7 changed files with 786 additions and 32 deletions

View File

@@ -1,7 +1,7 @@
from .file_metadata import FileMetadata
# Database initialization
from .database import init_db, get_db_connection, migrate_from_json
from .database import init_db, get_db_connection, migrate_from_json, migrate_client_key_to_db
# Admin model functions
from .admin_model import (
@@ -33,3 +33,13 @@ from .chunk_model import (
cleanup_abandoned,
update_received_chunks
)
# Config model functions
from .config_model import (
get_config,
get_client_key,
init_config,
update_server_address,
update_server_port,
regenerate_client_key
)

157
src/models/config_model.py Normal file
View File

@@ -0,0 +1,157 @@
import sqlite3
import secrets
from datetime import datetime
from models.database import get_db
import utils.mLogger as log
def generate_client_key():
"""Generate a random 64-character hex string (256 bits)"""
return secrets.token_hex(32) # 32 bytes = 64 hex chars
def get_config():
"""Get server configuration (singleton)
Returns:
dict: Configuration with client_key, server_address, server_port, timestamps
None: If config doesn't exist or error occurred
"""
try:
with get_db() as conn:
cursor = conn.cursor()
row = cursor.execute("""
SELECT client_key, server_address, server_port, created_at, updated_at
FROM server_config
WHERE id = 1
""").fetchone()
if row:
return dict(row)
return None
except sqlite3.Error as e:
log.e(f"Error getting config: {e}")
return None
def get_client_key():
"""Get only the client key for authentication
Returns:
str: The client key
None: If config doesn't exist or error occurred
"""
config = get_config()
return config['client_key'] if config else None
def init_config(client_key=None):
"""Initialize server config with provided or generated key
Args:
client_key (str, optional): Client key to use. If None, generates new key
Returns:
bool: True if successful, False otherwise
"""
try:
with get_db() as conn:
# Check if config already exists
existing = conn.execute("SELECT COUNT(*) FROM server_config WHERE id = 1").fetchone()[0]
if existing > 0:
log.i("Server config already exists, skipping initialization")
return True
# Generate key if not provided
if not client_key:
client_key = generate_client_key()
log.i("Generated new client key")
else:
log.i("Using provided client key from environment")
# Insert initial config
now = datetime.now().isoformat()
conn.execute("""
INSERT INTO server_config (id, client_key, created_at, updated_at)
VALUES (1, ?, ?, ?)
""", (client_key, now, now))
log.i("Server config initialized successfully")
return True
except sqlite3.Error as e:
log.e(f"Error initializing config: {e}")
return False
def update_server_address(address):
"""Update server address
Args:
address (str): Server address (IP or hostname)
Returns:
bool: True if successful, False otherwise
"""
try:
with get_db() as conn:
conn.execute("""
UPDATE server_config
SET server_address = ?, updated_at = ?
WHERE id = 1
""", (address, datetime.now().isoformat()))
log.i(f"Updated server address: {address}")
return True
except sqlite3.Error as e:
log.e(f"Error updating server address: {e}")
return False
def update_server_port(port):
"""Update server port
Args:
port (int): Server port number
Returns:
bool: True if successful, False otherwise
"""
try:
with get_db() as conn:
conn.execute("""
UPDATE server_config
SET server_port = ?, updated_at = ?
WHERE id = 1
""", (port, datetime.now().isoformat()))
log.i(f"Updated server port: {port}")
return True
except sqlite3.Error as e:
log.e(f"Error updating server port: {e}")
return False
def regenerate_client_key():
"""Generate and set a new client key
Returns:
str: The new client key
None: If error occurred
"""
try:
new_key = generate_client_key()
with get_db() as conn:
conn.execute("""
UPDATE server_config
SET client_key = ?, updated_at = ?
WHERE id = 1
""", (new_key, datetime.now().isoformat()))
log.w("Client key regenerated - all clients must update!")
return new_key
except sqlite3.Error as e:
log.e(f"Error regenerating client key: {e}")
return None

View File

@@ -110,6 +110,19 @@ def init_db():
CREATE INDEX IF NOT EXISTS idx_chunk_sessions_created_at ON chunk_sessions(created_at)
""")
# Server configuration table (singleton)
conn.execute("""
CREATE TABLE IF NOT EXISTS server_config (
id INTEGER PRIMARY KEY CHECK(id = 1),
client_key TEXT NOT NULL,
server_address TEXT,
server_port INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
CONSTRAINT client_key_length CHECK(length(client_key) = 64)
)
""")
log.i("Database initialized successfully")
def migrate_from_json():
@@ -224,3 +237,27 @@ def migrate_from_json():
log.i("Migration completed successfully")
else:
log.i("No migration performed")
def migrate_client_key_to_db():
"""Migrate CLIENT_KEY from environment to database (one-time)"""
from models.config_model import init_config
# Only run if config doesn't exist
with get_db() as conn:
existing = conn.execute("SELECT COUNT(*) FROM server_config WHERE id = 1").fetchone()[0]
if existing > 0:
log.i("Server config already exists in database, skipping migration")
return
# Get CLIENT_KEY from environment if set
env_key = os.environ.get('CLIENT_KEY')
# Initialize config (will generate new key if env_key is None or default)
if env_key and env_key != 'default-secret-key':
log.i("Migrating CLIENT_KEY from environment to database")
init_config(client_key=env_key)
else:
log.i("No valid CLIENT_KEY in environment, generating new key")
init_config()