""" Environment Variable Validator 환경 변수 검증 및 설정 확인 서버 시작 시 필수 환경 변수 검증으로 런타임 에러 방지 @changelog - v1.1.0 (2026-01-25): 환경변수 검증 강화 - 플레이스홀더 값 감지 (your_, placeholder, xxx 등) - 최소 길이 검증 (API 키는 20자 이상) - 패턴 기반 검증 추가 """ import os import re import logging from typing import List, Dict, Any, Optional logger = logging.getLogger(__name__) class EnvValidationError(Exception): """환경 변수 검증 실패 예외""" pass # 플레이스홀더 패턴 (이 패턴으로 시작하면 에러) PLACEHOLDER_PATTERNS = [ r'^your_', # your_api_key_here r'^placeholder', # placeholder r'^xxx', # xxx r'^example', # example_key r'^test_', # test_key (테스트용) r'^sk-xxx', # sk-xxx... r'^AIza.*xxx', # AIzaxxxxx (Google API 플레이스홀더) ] # API 키 최소 길이 MIN_API_KEY_LENGTH = 20 class EnvValidator: """환경 변수 검증기""" def __init__(self): """초기화""" self.errors: List[str] = [] self.warnings: List[str] = [] self.validated: Dict[str, Any] = {} def _is_placeholder(self, value: str) -> bool: """ 값이 플레이스홀더인지 확인 Args: value: 검사할 값 Returns: 플레이스홀더 여부 """ value_lower = value.lower().strip() for pattern in PLACEHOLDER_PATTERNS: if re.match(pattern, value_lower, re.IGNORECASE): return True return False def _check_min_length(self, key: str, value: str, min_length: int = MIN_API_KEY_LENGTH) -> bool: """ 최소 길이 확인 (API 키 검증용) Args: key: 환경 변수 이름 value: 값 min_length: 최소 길이 Returns: 길이 충족 여부 """ if len(value) < min_length: warning_msg = ( f"⚠️ '{key}' is shorter than expected ({len(value)} chars < {min_length}). " f"This may be an invalid or placeholder value." ) self.warnings.append(warning_msg) return False return True def require(self, key: str, description: str = "", is_api_key: bool = False) -> Optional[str]: """ 필수 환경 변수 확인 Args: key: 환경 변수 이름 description: 설명 is_api_key: API 키 여부 (플레이스홀더 및 최소 길이 검증) Returns: 환경 변수 값 (없으면 None) """ value = os.getenv(key) if not value: error_msg = f"❌ Required environment variable '{key}' is not set" if description: error_msg += f" ({description})" self.errors.append(error_msg) return None # 플레이스홀더 검증 (API 키인 경우) if is_api_key and self._is_placeholder(value): error_msg = ( f"❌ '{key}' contains a placeholder value (starts with 'your_', 'placeholder', etc.). " f"Please set the actual API key." ) self.errors.append(error_msg) return None # 최소 길이 검증 (API 키인 경우) if is_api_key: self._check_min_length(key, value) self.validated[key] = value logger.debug(f"✅ {key} is set") return value def optional( self, key: str, default: str = None, description: str = "" ) -> Optional[str]: """ 선택적 환경 변수 확인 Args: key: 환경 변수 이름 default: 기본값 description: 설명 Returns: 환경 변수 값 또는 기본값 """ value = os.getenv(key) if not value: if default: warning_msg = f"⚠️ Optional environment variable '{key}' not set, using default" if description: warning_msg += f" ({description})" self.warnings.append(warning_msg) self.validated[key] = default return default else: logger.debug(f"ℹ️ Optional variable '{key}' not set") return None self.validated[key] = value logger.debug(f"✅ {key} is set") return value def validate_url(self, key: str, description: str = "") -> Optional[str]: """ URL 형식 환경 변수 검증 Args: key: 환경 변수 이름 description: 설명 Returns: URL 값 """ value = self.require(key, description) if value and not (value.startswith("http://") or value.startswith("https://")): error_msg = f"❌ '{key}' must be a valid URL (http:// or https://)" self.errors.append(error_msg) return None return value def validate_port(self, key: str, default: int = None) -> int: """ 포트 번호 검증 Args: key: 환경 변수 이름 default: 기본 포트 Returns: 포트 번호 """ value = os.getenv(key) if not value: if default is not None: self.validated[key] = default return default else: self.errors.append(f"❌ Port '{key}' is not set") return None try: port = int(value) if not 1 <= port <= 65535: self.errors.append(f"❌ '{key}' must be between 1 and 65535") return None self.validated[key] = port return port except ValueError: self.errors.append(f"❌ '{key}' must be a valid number") return None def check(self) -> bool: """ 검증 결과 확인 Returns: 모든 검증 통과 여부 """ if self.warnings: logger.warning("Environment variable warnings:") for warning in self.warnings: logger.warning(f" {warning}") if self.errors: logger.error("Environment variable validation failed:") for error in self.errors: logger.error(f" {error}") return False logger.info("✅ All required environment variables are set") return True def raise_if_invalid(self): """검증 실패 시 예외 발생""" if not self.check(): raise EnvValidationError( "Environment variable validation failed. " "Please check your .env file and environment settings." ) def validate_backend_env() -> Dict[str, Any]: """ 백엔드 필수 환경 변수 검증 Returns: 검증된 환경 변수 딕셔너리 Raises: EnvValidationError: 필수 환경 변수 누락 시 @changelog - v1.1.0: API 키는 is_api_key=True로 플레이스홀더 및 최소 길이 검증 """ validator = EnvValidator() # ===== 필수 환경 변수 ===== # Gemini AI (is_api_key=True로 플레이스홀더 및 길이 검증) validator.require("GEMINI_API_KEY", "Gemini AI API 키", is_api_key=True) # Supabase URL (서버용 SUPABASE_URL 우선, VITE_* 폴백) supabase_url = validator.validate_url("SUPABASE_URL", "Supabase 프로젝트 URL") if not supabase_url: # Fallback supabase_url = validator.validate_url("VITE_SUPABASE_URL", "Supabase 프로젝트 URL (VITE 폴백)") # Supabase Service Key (is_api_key=True로 플레이스홀더 및 길이 검증) supabase_service_key = validator.require( "SUPABASE_SERVICE_ROLE_KEY", "Supabase 서비스 역할 키", is_api_key=True ) if not supabase_service_key: validator.errors.append("❌ 'SUPABASE_SERVICE_ROLE_KEY' is required but not set") # ===== 선택적 환경 변수 ===== # 서버 설정 validator.optional("PORT", "7860", "서버 포트") validator.optional("HOST", "0.0.0.0", "서버 호스트") validator.optional("LOG_LEVEL", "INFO", "로그 레벨") validator.optional("LOG_FORMAT_JSON", "true", "JSON 로그 형식 사용") # Rate Limiting validator.optional("RATE_LIMIT_PER_MINUTE", "60", "분당 요청 한도") validator.optional("RATE_LIMIT_PER_HOUR", "1000", "시간당 요청 한도") # HuggingFace (프론트엔드에서 필요, 백엔드는 선택적) validator.optional("HF_TOKEN", None, "HuggingFace 인증 토큰") # 검증 실패 시 예외 발생 validator.raise_if_invalid() return validator.validated def print_env_summary(): """환경 변수 요약 출력""" logger.info("=" * 60) logger.info("Environment Configuration Summary") logger.info("=" * 60) # Gemini gemini_key = os.getenv("GEMINI_API_KEY") logger.info(f"Gemini API: {'✅ Configured' if gemini_key else '❌ Not configured'}") # Supabase supabase_url = os.getenv("SUPABASE_URL") or os.getenv("VITE_SUPABASE_URL") supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") logger.info(f"Supabase URL: {'✅ Configured' if supabase_url else '❌ Not configured'}") logger.info(f"Supabase Key: {'✅ Configured' if supabase_key else '❌ Not configured'}") # Server port = os.getenv("PORT", "7860") log_level = os.getenv("LOG_LEVEL", "INFO") logger.info(f"Server Port: {port}") logger.info(f"Log Level: {log_level}") logger.info("=" * 60)