mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
Jackify provides native Linux support for Wabbajack modlist installation and management with automated Steam integration and Proton configuration. Key Features: - Almost Native Linux implementation (texconv.exe run via proton) - Automated Steam shortcut creation and Proton prefix management - Both CLI and GUI interfaces, with Steam Deck optimization Supported Games: - Skyrim Special Edition - Fallout 4 - Fallout New Vegas - Oblivion, Starfield, Enderal, and diverse other games Technical Architecture: - Clean separation between frontend and backend services - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
271 lines
10 KiB
Python
271 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
API Key Service Module
|
|
Centralized service for managing Nexus API keys across CLI and GUI frontends
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Tuple
|
|
from ..handlers.config_handler import ConfigHandler
|
|
|
|
# Initialize logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class APIKeyService:
|
|
"""
|
|
Centralized service for managing Nexus API keys
|
|
Handles saving, loading, and validation of API keys
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the API key service"""
|
|
self.config_handler = ConfigHandler()
|
|
logger.debug("APIKeyService initialized")
|
|
|
|
def save_api_key(self, api_key: str) -> bool:
|
|
"""
|
|
Save an API key to configuration
|
|
|
|
Args:
|
|
api_key (str): The API key to save
|
|
|
|
Returns:
|
|
bool: True if saved successfully, False otherwise
|
|
"""
|
|
try:
|
|
# Validate API key format (basic check)
|
|
if not self._validate_api_key_format(api_key):
|
|
logger.warning("Invalid API key format provided")
|
|
return False
|
|
|
|
# Check if we can write to config directory
|
|
import os
|
|
config_dir = os.path.dirname(self.config_handler.config_file)
|
|
if not os.path.exists(config_dir):
|
|
try:
|
|
os.makedirs(config_dir, exist_ok=True)
|
|
logger.debug(f"Created config directory: {config_dir}")
|
|
except PermissionError:
|
|
logger.error(f"Permission denied creating config directory: {config_dir}")
|
|
return False
|
|
except Exception as dir_error:
|
|
logger.error(f"Error creating config directory: {dir_error}")
|
|
return False
|
|
|
|
# Check write permissions
|
|
if not os.access(config_dir, os.W_OK):
|
|
logger.error(f"No write permission for config directory: {config_dir}")
|
|
return False
|
|
|
|
success = self.config_handler.save_api_key(api_key)
|
|
if success:
|
|
logger.info("API key saved successfully")
|
|
# Verify the save worked by reading it back
|
|
saved_key = self.config_handler.get_api_key()
|
|
if saved_key != api_key:
|
|
logger.error("API key save verification failed - key mismatch")
|
|
return False
|
|
else:
|
|
logger.error("Failed to save API key via config handler")
|
|
|
|
return success
|
|
except Exception as e:
|
|
logger.error(f"Error in save_api_key: {e}")
|
|
return False
|
|
|
|
def get_saved_api_key(self) -> Optional[str]:
|
|
"""
|
|
Retrieve the saved API key from configuration
|
|
|
|
Returns:
|
|
str: The decoded API key or None if not saved
|
|
"""
|
|
try:
|
|
api_key = self.config_handler.get_api_key()
|
|
if api_key:
|
|
logger.debug("Retrieved saved API key")
|
|
else:
|
|
logger.debug("No saved API key found")
|
|
return api_key
|
|
except Exception as e:
|
|
logger.error(f"Error retrieving API key: {e}")
|
|
return None
|
|
|
|
def has_saved_api_key(self) -> bool:
|
|
"""
|
|
Check if an API key is saved in configuration
|
|
|
|
Returns:
|
|
bool: True if API key exists, False otherwise
|
|
"""
|
|
try:
|
|
return self.config_handler.has_saved_api_key()
|
|
except Exception as e:
|
|
logger.error(f"Error checking for saved API key: {e}")
|
|
return False
|
|
|
|
def clear_saved_api_key(self) -> bool:
|
|
"""
|
|
Clear the saved API key from configuration
|
|
|
|
Returns:
|
|
bool: True if cleared successfully, False otherwise
|
|
"""
|
|
try:
|
|
success = self.config_handler.clear_api_key()
|
|
if success:
|
|
logger.info("API key cleared successfully")
|
|
else:
|
|
logger.error("Failed to clear API key")
|
|
return success
|
|
except Exception as e:
|
|
logger.error(f"Error clearing API key: {e}")
|
|
return False
|
|
|
|
def get_api_key_for_session(self, provided_key: Optional[str] = None,
|
|
use_saved: bool = True) -> Tuple[Optional[str], str]:
|
|
"""
|
|
Get the API key to use for a session, with priority logic
|
|
|
|
Args:
|
|
provided_key (str, optional): API key provided by user for this session
|
|
use_saved (bool): Whether to use saved API key if no key provided
|
|
|
|
Returns:
|
|
tuple: (api_key, source) where source is 'provided', 'saved', or 'none'
|
|
"""
|
|
try:
|
|
# Priority 1: Use provided key if given
|
|
if provided_key and self._validate_api_key_format(provided_key):
|
|
logger.debug("Using provided API key for session")
|
|
return provided_key, 'provided'
|
|
|
|
# Priority 2: Use saved key if enabled and available
|
|
if use_saved and self.has_saved_api_key():
|
|
saved_key = self.get_saved_api_key()
|
|
if saved_key:
|
|
logger.debug("Using saved API key for session")
|
|
return saved_key, 'saved'
|
|
|
|
# No valid API key available
|
|
logger.debug("No valid API key available for session")
|
|
return None, 'none'
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting API key for session: {e}")
|
|
return None, 'none'
|
|
|
|
def _validate_api_key_format(self, api_key: str) -> bool:
|
|
"""
|
|
Validate basic API key format
|
|
|
|
Args:
|
|
api_key (str): API key to validate
|
|
|
|
Returns:
|
|
bool: True if format appears valid, False otherwise
|
|
"""
|
|
if not api_key or not isinstance(api_key, str):
|
|
return False
|
|
|
|
# Basic validation: should be alphanumeric string of reasonable length
|
|
# Nexus API keys are typically 32+ characters, alphanumeric with some special chars
|
|
api_key = api_key.strip()
|
|
if len(api_key) < 10: # Too short to be valid
|
|
return False
|
|
|
|
if len(api_key) > 200: # Unreasonably long
|
|
return False
|
|
|
|
# Should contain some alphanumeric characters
|
|
if not any(c.isalnum() for c in api_key):
|
|
return False
|
|
|
|
return True
|
|
|
|
def get_api_key_display(self, api_key: str, mask_after_chars: int = 4) -> str:
|
|
"""
|
|
Get a masked version of the API key for display purposes
|
|
|
|
Args:
|
|
api_key (str): The API key to mask
|
|
mask_after_chars (int): Number of characters to show before masking
|
|
|
|
Returns:
|
|
str: Masked API key for display
|
|
"""
|
|
if not api_key:
|
|
return ""
|
|
|
|
if len(api_key) <= mask_after_chars:
|
|
return "*" * len(api_key)
|
|
|
|
visible_part = api_key[:mask_after_chars]
|
|
masked_part = "*" * (len(api_key) - mask_after_chars)
|
|
return visible_part + masked_part
|
|
|
|
def validate_api_key_works(self, api_key: str) -> Tuple[bool, str]:
|
|
"""
|
|
Validate that an API key actually works with Nexus API
|
|
Tests the key against the Nexus Mods validation endpoint
|
|
|
|
Args:
|
|
api_key (str): API key to validate
|
|
|
|
Returns:
|
|
tuple: (is_valid, message)
|
|
"""
|
|
# First check format
|
|
if not self._validate_api_key_format(api_key):
|
|
return False, "API key format is invalid"
|
|
|
|
try:
|
|
import requests
|
|
import time
|
|
|
|
# Nexus API validation endpoint
|
|
url = "https://api.nexusmods.com/v1/users/validate.json"
|
|
headers = {
|
|
'apikey': api_key,
|
|
'User-Agent': 'Jackify/1.0' # Required by Nexus API
|
|
}
|
|
|
|
# Set a reasonable timeout
|
|
response = requests.get(url, headers=headers, timeout=10)
|
|
|
|
if response.status_code == 200:
|
|
# API key is valid
|
|
try:
|
|
data = response.json()
|
|
username = data.get('name', 'Unknown')
|
|
# Don't log the actual API key - use masking
|
|
masked_key = self.get_api_key_display(api_key)
|
|
logger.info(f"API key validation successful for user: {username} (key: {masked_key})")
|
|
return True, f"API key valid for user: {username}"
|
|
except Exception as json_error:
|
|
logger.warning(f"API key valid but couldn't parse user info: {json_error}")
|
|
return True, "API key is valid"
|
|
elif response.status_code == 401:
|
|
# Invalid API key
|
|
logger.warning("API key validation failed: Invalid key")
|
|
return False, "Invalid API key"
|
|
elif response.status_code == 429:
|
|
# Rate limited
|
|
logger.warning("API key validation rate limited")
|
|
return False, "Rate limited - try again later"
|
|
else:
|
|
# Other error
|
|
logger.warning(f"API key validation failed with status {response.status_code}")
|
|
return False, f"Validation failed (HTTP {response.status_code})"
|
|
|
|
except requests.exceptions.Timeout:
|
|
logger.warning("API key validation timed out")
|
|
return False, "Validation timed out - check connection"
|
|
except requests.exceptions.ConnectionError:
|
|
logger.warning("API key validation connection error")
|
|
return False, "Connection error - check internet"
|
|
except Exception as e:
|
|
logger.error(f"API key validation error: {e}")
|
|
return False, f"Validation error: {str(e)}" |