""" AUTARCH User Profile Manager Handles user identity, preferences, and session management. On first launch, creates a default profile in the user data directory. Supports multiple users for shared deployments. The profile stores non-sensitive user info (username, display name, role, etc.) Sensitive data (API keys, passwords) goes in the keystore. """ import json import logging import os import time from pathlib import Path from typing import Dict, Optional _log = logging.getLogger('autarch.user_profile') class UserProfile: """User profile and preferences.""" def __init__(self, profile_path: Path = None): from core.paths import get_user_profile_path self._path = profile_path or get_user_profile_path() self._data: Dict = {} self._load() def _load(self): """Load profile from disk, or create default.""" if self._path.exists(): try: with open(self._path) as f: self._data = json.load(f) _log.info(f'Loaded profile for {self._data.get("username", "unknown")}') return except Exception as e: _log.error(f'Failed to load profile: {e}') # Create default profile self._data = self._create_default() self._save() _log.info('Created default user profile') def _create_default(self) -> Dict: """Create the default user profile.""" username = os.getenv('USER', os.getenv('USERNAME', 'operator')) return { 'username': username, 'display_name': username.capitalize(), 'role': 'admin', 'created': int(time.time()), 'last_login': int(time.time()), 'preferences': { 'theme': 'dark', 'llm_backend': 'local', 'hal_auto_analyze': True, 'hal_max_steps': 200, 'verbose': False, 'default_scan_target': 'local', # 'local' scans user device, 'server' scans server 'chat_memory_enabled': True, }, 'session': { 'last_module': '', 'last_target': '', 'last_scan_type': '', }, } def _save(self): """Save profile to disk.""" try: self._path.parent.mkdir(parents=True, exist_ok=True) with open(self._path, 'w') as f: json.dump(self._data, f, indent=2) except Exception as e: _log.error(f'Failed to save profile: {e}') @property def username(self) -> str: return self._data.get('username', 'default') @property def display_name(self) -> str: return self._data.get('display_name', self.username) @property def role(self) -> str: return self._data.get('role', 'user') def get_preference(self, key: str, default=None): """Get a user preference value.""" return self._data.get('preferences', {}).get(key, default) def set_preference(self, key: str, value): """Set a user preference and save.""" if 'preferences' not in self._data: self._data['preferences'] = {} self._data['preferences'][key] = value self._save() def get_session(self, key: str, default='') -> str: """Get a session value (non-persistent state hints).""" return self._data.get('session', {}).get(key, default) def set_session(self, key: str, value: str): """Set a session value.""" if 'session' not in self._data: self._data['session'] = {} self._data['session'][key] = value self._save() def update_last_login(self): """Record login timestamp.""" self._data['last_login'] = int(time.time()) self._save() def to_dict(self) -> Dict: """Export profile as a dict (for API/display).""" return { 'username': self.username, 'display_name': self.display_name, 'role': self.role, 'created': self._data.get('created', 0), 'last_login': self._data.get('last_login', 0), 'preferences': self._data.get('preferences', {}), } # Singleton _profile_instance: Optional[UserProfile] = None def get_profile() -> UserProfile: """Get the global user profile instance.""" global _profile_instance if _profile_instance is None: _profile_instance = UserProfile() return _profile_instance def init_profile_on_launch(): """Called at app startup to ensure profile exists and update login time.""" profile = get_profile() profile.update_last_login() # Migrate old vault secrets to new keystore if needed from core.paths import get_keystore_path ks_path = get_keystore_path() if not ks_path.exists(): from core.keystore import get_keystore ks = get_keystore() migrated = ks.migrate_from_vault() if migrated: _log.info(f'Migrated {migrated} secrets from vault to keystore on first run') return profile