Spaces:
Runtime error
Runtime error
feat: synchronize text-to-sql-bot codebase with Hugging Face Space repository, including Docker build configurations
6086e71 | """ | |
| User Repository — MySQL-backed user persistence. | |
| Replaces the in-memory dict user store that was wiped on every restart. | |
| Uses the existing DatabasePool for connection management. | |
| """ | |
| import structlog | |
| from typing import Optional | |
| logger = structlog.get_logger() | |
| class UserRepository: | |
| """ | |
| Persistent user storage backed by MySQL. | |
| Provides CRUD operations for user management. | |
| """ | |
| def __init__(self, db_pool): | |
| self.db_pool = db_pool | |
| self._ensure_table() | |
| def _ensure_table(self): | |
| """Create the users table if it doesn't exist.""" | |
| try: | |
| self.db_pool._execute_write_internal(""" | |
| CREATE TABLE IF NOT EXISTS plainsql_users ( | |
| id VARCHAR(64) PRIMARY KEY, | |
| username VARCHAR(100) NOT NULL UNIQUE, | |
| email VARCHAR(255) NOT NULL, | |
| password_hash VARCHAR(255) NOT NULL, | |
| role ENUM('admin', 'analyst', 'viewer') NOT NULL DEFAULT 'analyst', | |
| tenant_id VARCHAR(100) NOT NULL DEFAULT 'default', | |
| is_active BOOLEAN NOT NULL DEFAULT TRUE, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |
| INDEX idx_username (username), | |
| INDEX idx_tenant (tenant_id) | |
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 | |
| """) | |
| logger.info("users_table_ready") | |
| except Exception as e: | |
| logger.warning("users_table_migration_failed", error=str(e)) | |
| def get_by_username(self, username: str) -> Optional[dict]: | |
| """Retrieve a user by username.""" | |
| try: | |
| rows = self.db_pool.execute_query( | |
| "SELECT id, username, email, password_hash, role, tenant_id, is_active " | |
| "FROM plainsql_users WHERE username = :username AND is_active = TRUE", | |
| {"username": username}, | |
| ) | |
| return rows[0] if rows else None | |
| except Exception as e: | |
| logger.error("user_lookup_failed", username=username, error=str(e)) | |
| return None | |
| def get_by_id(self, user_id: str) -> Optional[dict]: | |
| """Retrieve a user by ID.""" | |
| try: | |
| rows = self.db_pool.execute_query( | |
| "SELECT id, username, email, password_hash, role, tenant_id, is_active " | |
| "FROM plainsql_users WHERE id = :user_id AND is_active = TRUE", | |
| {"user_id": user_id}, | |
| ) | |
| return rows[0] if rows else None | |
| except Exception as e: | |
| logger.error("user_lookup_by_id_failed", user_id=user_id, error=str(e)) | |
| return None | |
| def create( | |
| self, | |
| user_id: str, | |
| username: str, | |
| email: str, | |
| password_hash: str, | |
| role: str = "analyst", | |
| tenant_id: str = "default", | |
| ) -> bool: | |
| """ | |
| Create a new user. Returns True on success, False if user already exists. | |
| Uses INSERT IGNORE to handle race conditions gracefully. | |
| """ | |
| try: | |
| self.db_pool._execute_write_internal( | |
| """INSERT IGNORE INTO plainsql_users | |
| (id, username, email, password_hash, role, tenant_id) | |
| VALUES (:p0, :p1, :p2, :p3, :p4, :p5)""", | |
| (user_id, username, email, password_hash, role, tenant_id), | |
| ) | |
| logger.info("user_created", username=username, role=role) | |
| return True | |
| except Exception as e: | |
| logger.error("user_creation_failed", username=username, error=str(e)) | |
| return False | |
| def update_password(self, username: str, new_password_hash: str) -> bool: | |
| """Update a user's password hash.""" | |
| try: | |
| self.db_pool._execute_write_internal( | |
| "UPDATE plainsql_users SET password_hash = :p0 WHERE username = :p1", | |
| (new_password_hash, username), | |
| ) | |
| logger.info("password_updated", username=username) | |
| return True | |
| except Exception as e: | |
| logger.error("password_update_failed", username=username, error=str(e)) | |
| return False | |
| def list_users(self, tenant_id: str = "default") -> list[dict]: | |
| """List all active users for a tenant (without password hashes).""" | |
| try: | |
| rows = self.db_pool.execute_query( | |
| "SELECT id, username, email, role, tenant_id, created_at " | |
| "FROM plainsql_users WHERE tenant_id = :tenant_id AND is_active = TRUE", | |
| {"tenant_id": tenant_id}, | |
| ) | |
| return rows | |
| except Exception as e: | |
| logger.error("user_list_failed", tenant_id=tenant_id, error=str(e)) | |
| return [] | |
| def deactivate(self, username: str) -> bool: | |
| """Soft-delete a user by marking as inactive.""" | |
| try: | |
| self.db_pool._execute_write_internal( | |
| "UPDATE plainsql_users SET is_active = FALSE WHERE username = :p0", | |
| (username,), | |
| ) | |
| logger.info("user_deactivated", username=username) | |
| return True | |
| except Exception as e: | |
| logger.error("user_deactivation_failed", username=username, error=str(e)) | |
| return False | |
| def count(self) -> int: | |
| """Count active users.""" | |
| try: | |
| rows = self.db_pool.execute_query( | |
| "SELECT COUNT(*) as cnt FROM plainsql_users WHERE is_active = TRUE" | |
| ) | |
| return rows[0]["cnt"] if rows else 0 | |
| except Exception: | |
| return 0 | |