# ----------------------------------------------------------------------------- # Description: SQLite database initialization and persistence helpers for users, projects, cells, and project metadata. # Inside functions: connect_db, init_db, get_user, get_user_profile, get_user_auth_by_id, update_user_occupation, update_user_password, add_user_log, list_user_logs # Developer : Qin Yue @ 2026 # Organization : OptiHK Limited # ----------------------------------------------------------------------------- # backend/database.py import sqlite3 import os from werkzeug.security import generate_password_hash from datetime import datetime # Store application data in the shared database folder so all backend modules # use the same SQLite file regardless of their import path. DB_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "database", "mxpic_data.db")) def connect_db(): """Open a SQLite connection with row-style access for application data queries.""" conn = sqlite3.connect(DB_FILE, timeout=20) conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA busy_timeout=20000") return conn def init_db(): """Create the user, profile, and audit-log tables required by the backend.""" os.makedirs(os.path.dirname(DB_FILE), exist_ok=True) conn = connect_db() cursor = conn.cursor() # Core account table used by login, profile, and role-based PDK access. cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL ) ''') # Audit log table used by dashboard activity history and backend action # tracing. cursor.execute(''' CREATE TABLE IF NOT EXISTS user_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT NOT NULL, action TEXT NOT NULL, project TEXT, cell TEXT, detail TEXT, ip_address TEXT, created_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ) ''') # Lightweight migrations keep older local SQLite files compatible after # profile, credit, occupation, and role fields were added. cursor.execute("PRAGMA table_info(users)") existing_columns = {row[1] for row in cursor.fetchall()} migrations = { "created_at": "ALTER TABLE users ADD COLUMN created_at TEXT", "credits": "ALTER TABLE users ADD COLUMN credits INTEGER NOT NULL DEFAULT 0", "occupation": "ALTER TABLE users ADD COLUMN occupation TEXT NOT NULL DEFAULT 'intern'", "user_group": "ALTER TABLE users ADD COLUMN user_group TEXT NOT NULL DEFAULT 'user'", } for column, statement in migrations.items(): if column not in existing_columns: cursor.execute(statement) now = datetime.utcnow().strftime("%Y-%m-%d") cursor.execute("UPDATE users SET created_at = ? WHERE created_at IS NULL OR created_at = ''", (now,)) cursor.execute("UPDATE users SET user_group = 'manager' WHERE username = 'admin'") cursor.execute("UPDATE users SET user_group = 'developers' WHERE username = 'engineer'") cursor.execute("UPDATE users SET user_group = 'user' WHERE user_group IS NULL OR user_group = ''") # Default local accounts let developers test manager/developer/user access # without manually editing the database. cursor.execute("SELECT * FROM users WHERE username = 'admin'") if not cursor.fetchone(): test_hash = generate_password_hash("123456") cursor.execute( "INSERT INTO users (username, password_hash, created_at, credits, occupation, user_group) VALUES (?, ?, ?, ?, ?, ?)", ("admin", test_hash, now, 0, "principle engineer", "manager") ) print("Test user created. Username: admin | Password: 123456") cursor.execute("SELECT * FROM users WHERE username = 'engineer'") if not cursor.fetchone(): test_hash = generate_password_hash("123456") cursor.execute( "INSERT INTO users (username, password_hash, created_at, credits, occupation, user_group) VALUES (?, ?, ?, ?, ?, ?)", ("engineer", test_hash, now, 0, "junior engineer", "developers") ) print("Second test user created. Username: engineer | Password: 123456") conn.commit() conn.close() def get_user(username): """Fetch login credentials and account status for a username.""" conn = connect_db() cursor = conn.cursor() cursor.execute("SELECT id, username, password_hash, user_group FROM users WHERE username = ?", (username,)) user = cursor.fetchone() conn.close() return user def get_user_profile(user_id): """Fetch editable profile details for an authenticated user.""" conn = connect_db() cursor = conn.cursor() cursor.execute( "SELECT id, username, created_at, credits, occupation, user_group FROM users WHERE id = ?", (user_id,) ) user = cursor.fetchone() conn.close() return user def get_user_auth_by_id(user_id): """Fetch password and account metadata by user id.""" conn = connect_db() cursor = conn.cursor() cursor.execute("SELECT id, username, password_hash FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() conn.close() return user def update_user_occupation(user_id, occupation): """Persist a user profile occupation update.""" conn = connect_db() cursor = conn.cursor() cursor.execute("UPDATE users SET occupation = ? WHERE id = ?", (occupation, user_id)) conn.commit() conn.close() def update_user_password(user_id, password): """Persist a newly hashed password for a user.""" conn = connect_db() cursor = conn.cursor() cursor.execute("UPDATE users SET password_hash = ? WHERE id = ?", (generate_password_hash(password), user_id)) conn.commit() conn.close() def add_user_log(user_id, username, action, project=None, cell=None, detail=None, ip_address=None): """Record an auditable user action with optional project and cell context.""" conn = connect_db() cursor = conn.cursor() cursor.execute( ''' INSERT INTO user_logs (user_id, username, action, project, cell, detail, ip_address, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', ( user_id, username, action, project, cell, detail, ip_address, datetime.utcnow().isoformat(timespec="seconds") + "Z" ) ) conn.commit() conn.close() def list_user_logs(user_id, limit=200): """Return recent audit-log entries for display in the account page.""" conn = connect_db() cursor = conn.cursor() cursor.execute( ''' SELECT id, action, project, cell, detail, ip_address, created_at FROM user_logs WHERE user_id = ? ORDER BY id DESC LIMIT ? ''', (user_id, limit) ) rows = cursor.fetchall() conn.close() return rows if __name__ == "__main__": # Allow this module to be run directly when a fresh local database needs to # be initialized outside the Flask server. init_db() print("Database initialized successfully.")