Files
core/services/env.py
2025-07-31 18:55:59 +03:00

325 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from dataclasses import dataclass
from typing import ClassVar, Optional
from services.redis import redis
from utils.logger import root_logger as logger
@dataclass
class EnvVariable:
"""Переменная окружения"""
key: str
value: str
description: str
is_secret: bool = False
@dataclass
class EnvSection:
"""Секция переменных окружения"""
name: str
description: str
variables: list[EnvVariable]
class EnvService:
"""Сервис для работы с переменными окружения"""
redis_prefix = "env:"
# Определение секций с их описаниями
SECTIONS: ClassVar[dict[str, str]] = {
"database": "Настройки базы данных",
"auth": "Настройки аутентификации",
"redis": "Настройки Redis",
"search": "Настройки поиска",
"integrations": "Внешние интеграции",
"security": "Настройки безопасности",
"logging": "Настройки логирования",
"features": "Флаги функций",
"other": "Прочие настройки",
}
# Маппинг переменных на секции
VARIABLE_SECTIONS: ClassVar[dict[str, str]] = {
# Database
"DB_URL": "database",
"DATABASE_URL": "database",
"POSTGRES_USER": "database",
"POSTGRES_PASSWORD": "database",
"POSTGRES_DB": "database",
"POSTGRES_HOST": "database",
"POSTGRES_PORT": "database",
# Auth
"JWT_SECRET": "auth",
"JWT_ALGORITHM": "auth",
"JWT_EXPIRATION": "auth",
"SECRET_KEY": "auth",
"AUTH_SECRET": "auth",
"OAUTH_GOOGLE_CLIENT_ID": "auth",
"OAUTH_GOOGLE_CLIENT_SECRET": "auth",
"OAUTH_GITHUB_CLIENT_ID": "auth",
"OAUTH_GITHUB_CLIENT_SECRET": "auth",
# Redis
"REDIS_URL": "redis",
"REDIS_HOST": "redis",
"REDIS_PORT": "redis",
"REDIS_PASSWORD": "redis",
"REDIS_DB": "redis",
# Search
"SEARCH_API_KEY": "search",
"ELASTICSEARCH_URL": "search",
"SEARCH_INDEX": "search",
# Integrations
"GOOGLE_ANALYTICS_ID": "integrations",
"SENTRY_DSN": "integrations",
"SMTP_HOST": "integrations",
"SMTP_PORT": "integrations",
"SMTP_USER": "integrations",
"SMTP_PASSWORD": "integrations",
"EMAIL_FROM": "integrations",
# Security
"CORS_ORIGINS": "security",
"ALLOWED_HOSTS": "security",
"SECURE_SSL_REDIRECT": "security",
"SESSION_COOKIE_SECURE": "security",
"CSRF_COOKIE_SECURE": "security",
# Logging
"LOG_LEVEL": "logging",
"LOG_FORMAT": "logging",
"LOG_FILE": "logging",
"DEBUG": "logging",
# Features
"FEATURE_REGISTRATION": "features",
"FEATURE_COMMENTS": "features",
"FEATURE_ANALYTICS": "features",
"FEATURE_SEARCH": "features",
}
# Секретные переменные (не показываем их значения в UI)
SECRET_VARIABLES: ClassVar[set[str]] = {
"JWT_SECRET",
"SECRET_KEY",
"AUTH_SECRET",
"OAUTH_GOOGLE_CLIENT_SECRET",
"OAUTH_GITHUB_CLIENT_SECRET",
"POSTGRES_PASSWORD",
"REDIS_PASSWORD",
"SEARCH_API_KEY",
"SENTRY_DSN",
"SMTP_PASSWORD",
}
def __init__(self) -> None:
"""Инициализация сервиса"""
def get_variable_description(self, key: str) -> str:
"""Получает описание переменной окружения"""
descriptions = {
"DB_URL": "URL подключения к базе данных",
"DATABASE_URL": "URL подключения к базе данных",
"POSTGRES_USER": "Пользователь PostgreSQL",
"POSTGRES_PASSWORD": "Пароль PostgreSQL",
"POSTGRES_DB": "Имя базы данных PostgreSQL",
"POSTGRES_HOST": "Хост PostgreSQL",
"POSTGRES_PORT": "Порт PostgreSQL",
"JWT_SECRET": "Секретный ключ для JWT токенов",
"JWT_ALGORITHM": "Алгоритм подписи JWT",
"JWT_EXPIRATION": "Время жизни JWT токенов",
"SECRET_KEY": "Секретный ключ приложения",
"AUTH_SECRET": "Секретный ключ аутентификации",
"OAUTH_GOOGLE_CLIENT_ID": "Google OAuth Client ID",
"OAUTH_GOOGLE_CLIENT_SECRET": "Google OAuth Client Secret",
"OAUTH_GITHUB_CLIENT_ID": "GitHub OAuth Client ID",
"OAUTH_GITHUB_CLIENT_SECRET": "GitHub OAuth Client Secret",
"REDIS_URL": "URL подключения к Redis",
"REDIS_HOST": "Хост Redis",
"REDIS_PORT": "Порт Redis",
"REDIS_PASSWORD": "Пароль Redis",
"REDIS_DB": "Номер базы данных Redis",
"SEARCH_API_KEY": "API ключ для поиска",
"ELASTICSEARCH_URL": "URL Elasticsearch",
"SEARCH_INDEX": "Индекс поиска",
"GOOGLE_ANALYTICS_ID": "Google Analytics ID",
"SENTRY_DSN": "Sentry DSN",
"SMTP_HOST": "SMTP сервер",
"SMTP_PORT": "Порт SMTP",
"SMTP_USER": "Пользователь SMTP",
"SMTP_PASSWORD": "Пароль SMTP",
"EMAIL_FROM": "Email отправителя",
"CORS_ORIGINS": "Разрешенные CORS источники",
"ALLOWED_HOSTS": "Разрешенные хосты",
"SECURE_SSL_REDIRECT": "Принудительное SSL перенаправление",
"SESSION_COOKIE_SECURE": "Безопасные cookies сессий",
"CSRF_COOKIE_SECURE": "Безопасные CSRF cookies",
"LOG_LEVEL": "Уровень логирования",
"LOG_FORMAT": "Формат логов",
"LOG_FILE": "Файл логов",
"DEBUG": "Режим отладки",
"FEATURE_REGISTRATION": "Включить регистрацию",
"FEATURE_COMMENTS": "Включить комментарии",
"FEATURE_ANALYTICS": "Включить аналитику",
"FEATURE_SEARCH": "Включить поиск",
}
return descriptions.get(key, f"Переменная окружения {key}")
async def get_variables_from_redis(self) -> dict[str, str]:
"""Получает переменные из Redis"""
try:
keys = await redis.keys(f"{self.redis_prefix}*")
if not keys:
return {}
redis_vars: dict[str, str] = {}
for key in keys:
var_key = key.replace(self.redis_prefix, "")
value = await redis.get(key)
if value:
redis_vars[var_key] = str(value)
return redis_vars
except Exception:
return {}
async def set_variables_to_redis(self, variables: dict[str, str]) -> bool:
"""Сохраняет переменные в Redis"""
try:
for key, value in variables.items():
await redis.set(f"{self.redis_prefix}{key}", value)
return True
except Exception:
return False
def get_variables_from_env(self) -> dict[str, str]:
"""Получает переменные из системного окружения"""
env_vars = {}
# Получаем все переменные известные системе
for key in self.VARIABLE_SECTIONS:
value = os.getenv(key)
if value is not None:
env_vars[key] = value
# Получаем дополнительные переменные окружения
env_vars.update(
{
env_key: env_value
for env_key, env_value in os.environ.items()
if any(env_key.startswith(prefix) for prefix in ["APP_", "SITE_", "FEATURE_", "OAUTH_"])
}
)
return env_vars
async def get_all_variables(self) -> list[EnvSection]:
"""Получает все переменные окружения, сгруппированные по секциям"""
# Получаем переменные из Redis и системного окружения
redis_vars = await self.get_variables_from_redis()
env_vars = self.get_variables_from_env()
# Объединяем переменные (Redis имеет приоритет)
all_vars = {**env_vars, **redis_vars}
# Группируем по секциям
sections_dict: dict[str, list[EnvVariable]] = {section: [] for section in self.SECTIONS}
other_variables: list[EnvVariable] = [] # Для переменных, которые не попали ни в одну секцию
for key, value in all_vars.items():
is_secret = key in self.SECRET_VARIABLES
description = self.get_variable_description(key)
# Скрываем значение секретных переменных
display_value = "***" if is_secret else value
env_var = EnvVariable(
key=key,
value=display_value,
description=description,
is_secret=is_secret,
)
# Определяем секцию для переменной
section = self.VARIABLE_SECTIONS.get(key, "other")
if section in sections_dict:
sections_dict[section].append(env_var)
else:
other_variables.append(env_var)
# Создаем объекты секций
sections = []
for section_name, section_description in self.SECTIONS.items():
variables = sections_dict.get(section_name, [])
if variables: # Добавляем только непустые секции
sections.append(EnvSection(name=section_name, description=section_description, variables=variables))
# Добавляем секцию "other" если есть переменные
if other_variables:
sections.append(EnvSection(name="other", description="Прочие настройки", variables=other_variables))
return sorted(sections, key=lambda x: x.name)
async def update_variables(self, variables: list[EnvVariable]) -> bool:
"""Обновляет переменные окружения"""
try:
# Подготавливаем переменные для сохранения
vars_dict = {}
for var in variables:
if not var.is_secret or var.value != "***":
vars_dict[var.key] = var.value
# Сохраняем в Redis
return await self.set_variables_to_redis(vars_dict)
except Exception:
return False
async def delete_variable(self, key: str) -> bool:
"""Удаляет переменную окружения"""
try:
redis_key = f"{self.redis_prefix}{key}"
result = await redis.delete(redis_key)
if result > 0:
logger.info(f"Переменная {key} удалена")
return True
logger.warning(f"Переменная {key} не найдена")
return False
except Exception as e:
logger.error(f"Ошибка при удалении переменной {key}: {e}")
return False
async def get_variable(self, key: str) -> Optional[str]:
"""Получает значение конкретной переменной"""
# Сначала проверяем Redis
try:
redis_key = f"{self.redis_prefix}{key}"
value = await redis.get(redis_key)
if value:
return value.decode("utf-8") if isinstance(value, bytes) else str(value)
except Exception as e:
logger.error(f"Ошибка при получении переменной {key} из Redis: {e}")
# Fallback на системное окружение
return os.getenv(key)
async def set_variable(self, key: str, value: str) -> bool:
"""Устанавливает значение переменной"""
try:
redis_key = f"{self.redis_prefix}{key}"
await redis.set(redis_key, value)
logger.info(f"Переменная {key} установлена")
return True
except Exception as e:
logger.error(f"Ошибка при установке переменной {key}: {e}")
return False
env_manager = EnvService()