import ipaddress import os from fastapi import Request # Dashboard v1.5.3 — Testing simplified dev workflow GITEA_URL = os.environ.get("GITEA_URL", "http://gitea:3000") GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "") DOCKER_HOST = os.environ.get("DOCKER_HOST", "tcp://docker-socket-proxy:2375") VOLUME_ROOT = os.environ.get("VOLUME_ROOT", "/volume1") # Auth SECRET_KEY = os.environ.get("SECRET_KEY", "") if not SECRET_KEY or len(SECRET_KEY) < 32: raise RuntimeError("SECRET_KEY must be set and at least 32 characters") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days ADMIN_USER = os.environ.get("ADMIN_USER", "admin") ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "") TOTP_SECRET = os.environ.get("TOTP_SECRET", "") # SSH terminal # NOTE: The default values below are for development only. # In production, override these via environment variables: # SSH_HOST, SSH_USER, VPS_SSH_HOST, VPS_SSH_USER, # CADDY_VPS_SSH_HOST, CADDY_VPS_SSH_USER, MAC_SSH_HOST, MAC_SSH_USER SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal") SSH_USER = os.environ.get("SSH_USER", "zjgump") SSH_KEY_PATH = os.environ.get("SSH_KEY_PATH", "/app/ssh/id_ed25519") SSH_KNOWN_HOSTS = os.environ.get("SSH_KNOWN_HOSTS", "/app/ssh/known_hosts") VPS_SSH_HOST = os.environ.get("VPS_SSH_HOST", "158.101.140.85") VPS_SSH_USER = os.environ.get("VPS_SSH_USER", "jimmyg") VPS_SSH_KEY_PATH = os.environ.get("VPS_SSH_KEY_PATH", "/app/ssh/vps_id_ed25519") CADDY_VPS_SSH_HOST = os.environ.get("CADDY_VPS_SSH_HOST", "161.33.182.109") CADDY_VPS_SSH_USER = os.environ.get("CADDY_VPS_SSH_USER", "ubuntu") MAC_SSH_HOST = os.environ.get("MAC_SSH_HOST", "192.168.31.22") MAC_SSH_USER = os.environ.get("MAC_SSH_USER", "jimmyg") MAC_SSH_KEY_PATH = os.environ.get("MAC_SSH_KEY_PATH", "/app/ssh/mac_id_ed25519") # Telegram notifications TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") TELEGRAM_PROXY = os.environ.get("TELEGRAM_PROXY", "") WEBAUTHN_RP_ID = os.environ.get("WEBAUTHN_RP_ID", "jimmygan.com") WEBAUTHN_ORIGINS = os.environ.get("WEBAUTHN_ORIGINS", "https://nas.jimmygan.com,https://nas.jimmygan.com:8443").split( "," ) # CORS CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "https://nas.jimmygan.com,https://nas.jimmygan.com:8443").split(",") # Dashboard public URL (used in email templates, etc.) DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "https://nas.jimmygan.com") # External service URLs (used by frontend sidebar) EXTERNAL_SERVICES = { "navidrome": os.environ.get("NAVIDROME_URL", "https://music.jimmygan.com:8443"), "jellyfin": os.environ.get("JELLYFIN_URL", "https://media.jimmygan.com:8443"), "audiobookshelf": os.environ.get("AUDIOBOOKSHELF_URL", "https://books.jimmygan.com:8443"), "immich": os.environ.get("IMMICH_URL", "https://photos.jimmygan.com:8443"), "t_youtube": os.environ.get("T_YOUTUBE_URL", "https://yt.jimmygan.com:8443"), "media_management": os.environ.get("MEDIA_MANAGEMENT_URL", "https://media.jimmygan.com:8443"), "hermes": os.environ.get("HERMES_URL", "https://hermes-agent.nousresearch.com/docs"), "gitea_web": os.environ.get("GITEA_WEB_URL", "https://git.jimmygan.com:8443"), "stirling_pdf": os.environ.get("STIRLING_PDF_URL", "https://pdf.jimmygan.com:8443"), "vaultwarden": os.environ.get("VAULTWARDEN_URL", "https://vault.jimmygan.com:8443"), "n8n": os.environ.get("N8N_URL", "https://n8n.jimmygan.com:8443"), "speedtest": os.environ.get("SPEEDTEST_URL", "https://speed.jimmygan.com:8443"), } # Network addresses (used by frontend for LAN/Tailscale direct access) LAN_IP = os.environ.get("LAN_IP", "192.168.31.222") TS_IP = os.environ.get("TS_IP", "100.78.131.124") # Chat Summary CHAT_SUMMARY_DB = os.environ.get("CHAT_SUMMARY_DB", "/app/data/chat-summarizer/chat_summary.db") # Conversation Tracker CONVERSATION_TRACKER_DB = os.environ.get("CONVERSATION_TRACKER_DB", "/app/data/claude-code-tracker/conversations.db") # Info Engine INFO_ENGINE_DB = os.environ.get("INFO_ENGINE_DB", "/app/data/info-engine/info_engine.db") # LiteLLM LITELLM_URL = os.environ.get("LITELLM_URL", "http://127.0.0.1:4005") LITELLM_HEALTH_API_KEY = os.environ.get("LITELLM_HEALTH_API_KEY", "") # CC Connect CC_CONNECT_URL = os.environ.get("CC_CONNECT_URL", "") # OpenClaw OPENCLAW_GATEWAY_TOKEN = os.environ.get("OPENCLAW_GATEWAY_TOKEN", "") # Transmission RPC TRANSMISSION_URL = os.environ.get("TRANSMISSION_URL", "http://host.docker.internal:9091/transmission/rpc") TRANSMISSION_USER = os.environ.get("TRANSMISSION_USER", "") TRANSMISSION_PASS = os.environ.get("TRANSMISSION_PASS", "") if not TRANSMISSION_USER or not TRANSMISSION_PASS: raise RuntimeError("TRANSMISSION_USER and TRANSMISSION_PASS must be set") # Networking configs LAN_IP_RANGES = os.environ.get("LAN_IP_RANGES", "192.168.31.0/24").split(",") TRUSTED_PROXIES = os.environ.get("TRUSTED_PROXIES", "127.0.0.1,::1,172.21.0.1,172.17.0.1").split(",") def _parse_ip(ip_str: str): try: return ipaddress.ip_address((ip_str or "").strip()) except ValueError: return None def _ip_matches_ranges(ip_str: str, ranges: list[str]) -> bool: ip_obj = _parse_ip(ip_str) if ip_obj is None: return False for subnet_str in ranges: subnet_str = subnet_str.strip() if not subnet_str: continue try: if "/" in subnet_str: if ip_obj in ipaddress.ip_network(subnet_str, strict=False): return True elif ip_obj == ipaddress.ip_address(subnet_str): return True except ValueError: continue return False def is_lan_ip(ip_str: str) -> bool: """Check if IP is in local LAN (not Tailscale)""" return _ip_matches_ranges(ip_str, LAN_IP_RANGES) def is_tailscale_ip(ip_str: str) -> bool: """Check if IP is in Tailscale CGNAT range (100.64.0.0/10)""" return _ip_matches_ranges(ip_str, ["100.64.0.0/10"]) def is_trusted_proxy(ip_str: str) -> bool: return _ip_matches_ranges(ip_str, TRUSTED_PROXIES) def get_forwarded_client_ip(request: Request) -> str | None: source_ip = request.client.host if request.client else "" if not is_trusted_proxy(source_ip): return None forwarded_for = request.headers.get("x-forwarded-for", "") forwarded_ip = forwarded_for.split(",", 1)[0].strip() return forwarded_ip or None def get_client_ip(request: Request) -> str: return get_forwarded_client_ip(request) or (request.client.host if request.client else "")