| """ |
| API Keys Management for HomePilot |
| |
| Securely stores and retrieves API keys for: |
| - Hugging Face (for gated model downloads) |
| - Civitai (for NSFW/restricted model downloads) |
| |
| Keys are stored in .env.json with restricted permissions. |
| Environment variables take precedence over stored keys. |
| |
| This module is OPTIONAL - HomePilot works without API keys configured. |
| Keys are only needed for: |
| - Gated HuggingFace models (FLUX, SVD XT 1.1) |
| - NSFW/restricted Civitai models |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| from pathlib import Path |
| from typing import Optional, Dict, Literal |
|
|
| |
| ApiKeyProvider = Literal["huggingface", "civitai"] |
|
|
| |
| _DATA_DIR = Path(os.getenv("DATA_DIR", "")) or Path(__file__).parent.parent / "data" |
| ENV_JSON_FILE = _DATA_DIR / ".env.json" |
|
|
|
|
| def _load_keys() -> Dict[str, str]: |
| """Load API keys from storage file.""" |
| if not ENV_JSON_FILE.exists(): |
| return {} |
| try: |
| with open(ENV_JSON_FILE, "r", encoding="utf-8") as f: |
| data = json.load(f) |
| |
| return data.get("api_keys", {}) |
| except (json.JSONDecodeError, IOError): |
| return {} |
|
|
|
|
| def _save_keys(keys: Dict[str, str]) -> None: |
| """Save API keys to storage file with secure permissions.""" |
| ENV_JSON_FILE.parent.mkdir(parents=True, exist_ok=True) |
|
|
| |
| existing_data = {} |
| if ENV_JSON_FILE.exists(): |
| try: |
| with open(ENV_JSON_FILE, "r", encoding="utf-8") as f: |
| existing_data = json.load(f) |
| except (json.JSONDecodeError, IOError): |
| pass |
|
|
| |
| existing_data["api_keys"] = keys |
|
|
| with open(ENV_JSON_FILE, "w", encoding="utf-8") as f: |
| json.dump(existing_data, f, indent=2) |
|
|
| |
| try: |
| os.chmod(ENV_JSON_FILE, 0o600) |
| except OSError: |
| pass |
|
|
|
|
| def get_api_key(provider: ApiKeyProvider) -> Optional[str]: |
| """ |
| Get API key for a provider. |
| Priority: Environment variable > Stored key |
| |
| Returns None if no key is configured (this is normal - keys are optional). |
| """ |
| |
| env_map = { |
| "huggingface": "HF_TOKEN", |
| "civitai": "CIVITAI_API_KEY", |
| } |
| env_var = env_map.get(provider, "") |
| env_key = os.getenv(env_var, "").strip() |
| if env_key: |
| return env_key |
|
|
| |
| keys = _load_keys() |
| stored_key = keys.get(provider, "").strip() |
| return stored_key if stored_key else None |
|
|
|
|
| def set_api_key(provider: ApiKeyProvider, key: str) -> None: |
| """Store an API key for a provider.""" |
| keys = _load_keys() |
| key = key.strip() |
| if key: |
| keys[provider] = key |
| elif provider in keys: |
| |
| del keys[provider] |
| _save_keys(keys) |
|
|
|
|
| def delete_api_key(provider: ApiKeyProvider) -> bool: |
| """Remove an API key. Returns True if key existed.""" |
| keys = _load_keys() |
| if provider in keys: |
| del keys[provider] |
| _save_keys(keys) |
| return True |
| return False |
|
|
|
|
| def get_api_keys_status() -> Dict[str, Dict]: |
| """ |
| Get status of all API keys (for UI display). |
| Returns masked keys and source (env/stored/none). |
| Never returns actual key values. |
| """ |
| result = {} |
| providers = ["huggingface", "civitai"] |
| env_map = {"huggingface": "HF_TOKEN", "civitai": "CIVITAI_API_KEY"} |
|
|
| for provider in providers: |
| env_key = os.getenv(env_map[provider], "").strip() |
| stored_keys = _load_keys() |
| stored_key = stored_keys.get(provider, "").strip() |
|
|
| if env_key: |
| result[provider] = { |
| "configured": True, |
| "source": "environment", |
| "masked": _mask_key(env_key), |
| "env_var": env_map[provider], |
| } |
| elif stored_key: |
| result[provider] = { |
| "configured": True, |
| "source": "stored", |
| "masked": _mask_key(stored_key), |
| "env_var": env_map[provider], |
| } |
| else: |
| result[provider] = { |
| "configured": False, |
| "source": "none", |
| "masked": None, |
| "env_var": env_map[provider], |
| } |
|
|
| return result |
|
|
|
|
| def _mask_key(key: str) -> str: |
| """Mask API key for display (show first 4 and last 4 chars).""" |
| if not key: |
| return "" |
| if len(key) <= 8: |
| return "*" * len(key) |
| return key[:4] + "*" * (len(key) - 8) + key[-4:] |
|
|