Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| auth.py | |
| Authentication utilities for MorphGuard using SQLite database. | |
| """ | |
| import os | |
| import json | |
| import time | |
| from typing import Dict, Any, List, Optional, Tuple | |
| from werkzeug.security import generate_password_hash, check_password_hash | |
| from datetime import datetime | |
| # Try to import psycopg2 | |
| try: | |
| import psycopg2 | |
| from psycopg2.extras import RealDictCursor | |
| HAS_POSTGRES = True | |
| except ImportError: | |
| HAS_POSTGRES = False | |
| RealDictCursor = None | |
| import sqlite3 | |
| IS_HF_SPACE = os.environ.get('HF_SPACE', '0') == '1' | |
| USE_SQLITE = IS_HF_SPACE or not HAS_POSTGRES | |
| # Database configuration | |
| DB_HOST = os.environ.get('MORPHGUARD_DB_HOST', 'localhost') | |
| DB_PORT = int(os.environ.get('MORPHGUARD_DB_PORT', 5432)) | |
| DB_NAME = os.environ.get('MORPHGUARD_DB_NAME', 'morphguard') | |
| DB_USER = os.environ.get('MORPHGUARD_DB_USER', 'morphguard') | |
| DB_PASS = os.environ.get('MORPHGUARD_DB_PASS', 'morphguard') | |
| def _is_sqlite(conn): | |
| return isinstance(conn, sqlite3.Connection) | |
| def _get_conn(): | |
| if USE_SQLITE: | |
| try: | |
| conn = sqlite3.connect('users.db') | |
| conn.row_factory = sqlite3.Row | |
| return conn | |
| except Exception as e: | |
| print(f"SQLite connection failed: {e}") | |
| return None | |
| else: | |
| try: | |
| conn = psycopg2.connect( | |
| host=DB_HOST, | |
| port=DB_PORT, | |
| dbname=DB_NAME, | |
| user=DB_USER, | |
| password=DB_PASS | |
| ) | |
| return conn | |
| except Exception as e: | |
| print(f"Database connection failed: {e}") | |
| # Try falling back to SQLite if Postgres connection fails | |
| try: | |
| conn = sqlite3.connect('users.db') | |
| conn.row_factory = sqlite3.Row | |
| return conn | |
| except Exception: | |
| return None | |
| def _execute(conn, query: str, params: tuple = (), fetch_all: bool = False, fetch_one: bool = False, commit: bool = False): | |
| is_sqlite = _is_sqlite(conn) | |
| if is_sqlite: | |
| query = query.replace('%s', '?') | |
| new_params = [] | |
| for p in params: | |
| if isinstance(p, datetime): | |
| new_params.append(p.timestamp()) | |
| elif isinstance(p, bool): | |
| new_params.append(1 if p else 0) | |
| else: | |
| new_params.append(p) | |
| params = tuple(new_params) | |
| cur = conn.cursor() | |
| else: | |
| cur = conn.cursor(cursor_factory=RealDictCursor) | |
| cur.execute(query, params) | |
| result = None | |
| if fetch_all: | |
| rows = cur.fetchall() | |
| result = [dict(r) for r in rows] | |
| elif fetch_one: | |
| row = cur.fetchone() | |
| result = dict(row) if row else None | |
| if commit: | |
| conn.commit() | |
| return cur, result | |
| def init_db(): | |
| conn = _get_conn() | |
| if conn: | |
| db_type = "SQLite" if _is_sqlite(conn) else "PostgreSQL" | |
| if _is_sqlite(conn): | |
| try: | |
| cur = conn.cursor() | |
| cur.execute(""" | |
| CREATE TABLE IF NOT EXISTS users ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| username TEXT UNIQUE NOT NULL, | |
| password_hash TEXT NOT NULL, | |
| role TEXT NOT NULL, | |
| email TEXT, | |
| created_at REAL, | |
| last_login REAL, | |
| active INTEGER NOT NULL DEFAULT 1 | |
| ); | |
| """) | |
| conn.commit() | |
| cur.execute("SELECT id FROM users WHERE username = ?;", ("admin",)) | |
| if not cur.fetchone(): | |
| pwd_hash = generate_password_hash("morphguard_admin", method='scrypt') | |
| cur.execute( | |
| "INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, ?, ?);", | |
| ("admin", pwd_hash, "admin", datetime.now().timestamp()) | |
| ) | |
| conn.commit() | |
| print("Default admin user created in SQLite database.") | |
| except Exception as e: | |
| print(f"Error initializing SQLite database: {e}") | |
| conn.close() | |
| print(f"Auth module connected to {db_type} successfully") | |
| else: | |
| print("WARNING: Auth module failed to connect to database") | |
| # Initialize check | |
| init_db() | |
| def get_users() -> List[Dict[str, Any]]: | |
| """Fetch all users""" | |
| conn = _get_conn() | |
| if not conn: return [] | |
| try: | |
| cur, rows = _execute(conn, 'SELECT id, username, role, email, last_login, active FROM users;', fetch_all=True) | |
| if _is_sqlite(conn) and rows: | |
| for row in rows: | |
| if row.get('last_login'): | |
| try: | |
| row['last_login'] = datetime.fromtimestamp(row['last_login']) | |
| except Exception: | |
| pass | |
| if 'active' in row: | |
| row['active'] = bool(row['active']) | |
| return rows | |
| finally: | |
| conn.close() | |
| def get_user_by_username(username: str) -> Optional[Dict[str, Any]]: | |
| """Get a user by username""" | |
| conn = _get_conn() | |
| if not conn: return None | |
| try: | |
| cur, row = _execute(conn, 'SELECT * FROM users WHERE username = %s;', (username,), fetch_one=True) | |
| if row and _is_sqlite(conn): | |
| row = dict(row) | |
| if row.get('created_at'): | |
| try: | |
| row['created_at'] = datetime.fromtimestamp(row['created_at']) | |
| except Exception: | |
| pass | |
| if row.get('last_login'): | |
| try: | |
| row['last_login'] = datetime.fromtimestamp(row['last_login']) | |
| except Exception: | |
| pass | |
| if 'active' in row: | |
| row['active'] = bool(row['active']) | |
| return row | |
| finally: | |
| conn.close() | |
| def authenticate_user(username: str, password: str) -> Tuple[bool, Any]: | |
| """Authenticate user credentials""" | |
| user = get_user_by_username(username) | |
| if not user or not check_password_hash(user['password_hash'], password): | |
| return False, {'error': 'Invalid credentials.'} | |
| # Update last login | |
| conn = _get_conn() | |
| if conn: | |
| try: | |
| now = datetime.now() | |
| _execute(conn, 'UPDATE users SET last_login = %s WHERE id = %s;', (now, user['id']), commit=True) | |
| except Exception as e: | |
| print(f"Warning: Failed to update last_login: {e}") | |
| finally: | |
| conn.close() | |
| return True, user | |
| def create_user(username: str, password: str, email: Optional[str] = None, role: str = 'user') -> Tuple[bool, Any]: | |
| """Create a new user""" | |
| if get_user_by_username(username): | |
| return False, {'error': 'Username already exists.'} | |
| pwd_hash = generate_password_hash(password, method='scrypt') | |
| conn = _get_conn() | |
| if not conn: return False, {'error': 'Database connection failed'} | |
| now = datetime.now() | |
| try: | |
| if _is_sqlite(conn): | |
| cur, _ = _execute( | |
| conn, | |
| 'INSERT INTO users (username, password_hash, role, email, created_at) VALUES (%s, %s, %s, %s, %s);', | |
| (username, pwd_hash, role, email, now), | |
| commit=True | |
| ) | |
| user_id = cur.lastrowid | |
| else: | |
| cur, row = _execute( | |
| conn, | |
| 'INSERT INTO users (username, password_hash, role, email, created_at) VALUES (%s, %s, %s, %s, %s) RETURNING id;', | |
| (username, pwd_hash, role, email, now), | |
| fetch_one=True, | |
| commit=True | |
| ) | |
| user_id = row['id'] if isinstance(row, dict) else row[0] | |
| return True, {'id': user_id, 'username': username, 'role': role} | |
| except Exception as e: | |
| conn.rollback() | |
| print(f"Error creating user: {e}") | |
| return False, {'error': str(e)} | |
| finally: | |
| conn.close() | |
| def update_user(user_id: int, data: Dict[str, Any]) -> Tuple[bool, Any]: | |
| """Update user's password and/or role""" | |
| fields = [] | |
| params = [] | |
| if data.get('password'): | |
| pwd_hash = generate_password_hash(data['password'], method='scrypt') | |
| fields.append('password_hash = %s') | |
| params.append(pwd_hash) | |
| if data.get('role'): | |
| fields.append('role = %s') | |
| params.append(data['role']) | |
| if not fields: | |
| return False, {'error': 'No fields to update.'} | |
| params.append(user_id) | |
| conn = _get_conn() | |
| if not conn: return False, {'error': 'Database connection failed'} | |
| try: | |
| if _is_sqlite(conn): | |
| cur, row = _execute(conn, 'SELECT username FROM users WHERE id = %s;', (user_id,), fetch_one=True) | |
| if not row: | |
| return False, {'error': 'User not found.'} | |
| username = row['username'] | |
| sql_query = f"UPDATE users SET {', '.join(fields)} WHERE id = %s;" | |
| _execute(conn, sql_query, params, commit=True) | |
| return True, {'username': username} | |
| else: | |
| sql_query = f"UPDATE users SET {', '.join(fields)} WHERE id = %s RETURNING username;" | |
| cur, row = _execute(conn, sql_query, params, fetch_one=True, commit=True) | |
| if row: | |
| return True, {'username': row['username']} | |
| return False, {'error': 'User not found.'} | |
| except Exception as e: | |
| conn.rollback() | |
| return False, {'error': str(e)} | |
| finally: | |
| conn.close() | |
| def delete_user(user_id: int) -> bool: | |
| """Delete a user, preventing deletion of last admin""" | |
| conn = _get_conn() | |
| if not conn: return False | |
| try: | |
| cur, row = _execute(conn, 'SELECT role FROM users WHERE id = %s;', (user_id,), fetch_one=True) | |
| if not row: | |
| return False | |
| if row['role'] == 'admin': | |
| _, count_row = _execute(conn, "SELECT COUNT(*) AS cnt FROM users WHERE role = 'admin';", fetch_one=True) | |
| if count_row and count_row['cnt'] <= 1: | |
| return False | |
| _execute(conn, 'DELETE FROM users WHERE id = %s;', (user_id,), commit=True) | |
| return True | |
| finally: | |
| conn.close() | |
| def get_all_users() -> List[Dict[str, Any]]: | |
| """Alias for get_users to match expected API usage""" | |
| return get_users() | |
| def update_user_details(user_id: int, role: str = None) -> bool: | |
| """Helper for simple user updates""" | |
| data = {} | |
| if role: | |
| data['role'] = role | |
| success, _ = update_user(user_id, data) | |
| return success | |
| def get_user_by_id(user_id: int) -> Optional[Dict[str, Any]]: | |
| """Fetch user by ID""" | |
| conn = _get_conn() | |
| if not conn: return None | |
| try: | |
| cur, row = _execute(conn, 'SELECT * FROM users WHERE id = %s;', (user_id,), fetch_one=True) | |
| if row and _is_sqlite(conn): | |
| row = dict(row) | |
| if row.get('created_at'): | |
| try: | |
| row['created_at'] = datetime.fromtimestamp(row['created_at']) | |
| except Exception: | |
| pass | |
| if row.get('last_login'): | |
| try: | |
| row['last_login'] = datetime.fromtimestamp(row['last_login']) | |
| except Exception: | |
| pass | |
| if 'active' in row: | |
| row['active'] = bool(row['active']) | |
| return row | |
| finally: | |
| conn.close() | |
| def update_user_status(user_id: int, active: bool) -> bool: | |
| """Toggle user active status""" | |
| conn = _get_conn() | |
| if not conn: return False | |
| try: | |
| _execute(conn, 'UPDATE users SET active = %s WHERE id = %s;', (active, user_id), commit=True) | |
| return True | |
| except Exception as e: | |
| conn.rollback() | |
| return False | |
| finally: | |
| conn.close() |