2025-05-21 01:34:02 +03:00
|
|
|
|
import os
|
2025-05-29 12:37:39 +03:00
|
|
|
|
from dataclasses import dataclass
|
2025-07-31 18:55:59 +03:00
|
|
|
|
from typing import ClassVar, Optional
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
from services.redis import redis
|
2025-05-16 09:23:48 +03:00
|
|
|
|
from utils.logger import root_logger as logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class EnvVariable:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
"""Переменная окружения"""
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
2025-05-16 09:23:48 +03:00
|
|
|
|
key: str
|
2025-07-31 18:55:59 +03:00
|
|
|
|
value: str
|
|
|
|
|
description: str
|
2025-05-16 09:23:48 +03:00
|
|
|
|
is_secret: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class EnvSection:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
"""Секция переменных окружения"""
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
2025-05-16 09:23:48 +03:00
|
|
|
|
name: str
|
2025-06-02 02:56:11 +03:00
|
|
|
|
description: str
|
2025-07-31 18:55:59 +03:00
|
|
|
|
variables: list[EnvVariable]
|
|
|
|
|
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
class EnvService:
|
|
|
|
|
"""Сервис для работы с переменными окружения"""
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
redis_prefix = "env:"
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
# Определение секций с их описаниями
|
2025-07-31 18:55:59 +03:00
|
|
|
|
SECTIONS: ClassVar[dict[str, str]] = {
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"database": "Настройки базы данных",
|
|
|
|
|
"auth": "Настройки аутентификации",
|
|
|
|
|
"redis": "Настройки Redis",
|
|
|
|
|
"search": "Настройки поиска",
|
|
|
|
|
"integrations": "Внешние интеграции",
|
|
|
|
|
"security": "Настройки безопасности",
|
|
|
|
|
"logging": "Настройки логирования",
|
|
|
|
|
"features": "Флаги функций",
|
|
|
|
|
"other": "Прочие настройки",
|
2025-05-21 01:34:02 +03:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
# Маппинг переменных на секции
|
2025-07-31 18:55:59 +03:00
|
|
|
|
VARIABLE_SECTIONS: ClassVar[dict[str, str]] = {
|
2025-06-02 02:56:11 +03:00
|
|
|
|
# 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",
|
2025-05-21 01:34:02 +03:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
# Секретные переменные (не показываем их значения в UI)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
SECRET_VARIABLES: ClassVar[set[str]] = {
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"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",
|
|
|
|
|
}
|
2025-05-21 01:34:02 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
def __init__(self) -> None:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
"""Инициализация сервиса"""
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def get_variable_description(self, key: str) -> str:
|
|
|
|
|
"""Получает описание переменной окружения"""
|
2025-06-02 02:56:11 +03:00
|
|
|
|
descriptions = {
|
|
|
|
|
"DB_URL": "URL подключения к базе данных",
|
2025-07-31 18:55:59 +03:00
|
|
|
|
"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",
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"REDIS_URL": "URL подключения к Redis",
|
2025-07-31 18:55:59 +03:00
|
|
|
|
"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",
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"SMTP_USER": "Пользователь SMTP",
|
|
|
|
|
"SMTP_PASSWORD": "Пароль SMTP",
|
2025-07-31 18:55:59 +03:00
|
|
|
|
"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": "Включить поиск",
|
2025-06-02 02:56:11 +03:00
|
|
|
|
}
|
|
|
|
|
return descriptions.get(key, f"Переменная окружения {key}")
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
async def get_variables_from_redis(self) -> dict[str, str]:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"""Получает переменные из Redis"""
|
2025-05-16 09:23:48 +03:00
|
|
|
|
try:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
keys = await redis.keys(f"{self.redis_prefix}*")
|
2025-06-02 02:56:11 +03:00
|
|
|
|
if not keys:
|
|
|
|
|
return {}
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
redis_vars: dict[str, str] = {}
|
2025-05-16 09:23:48 +03:00
|
|
|
|
for key in keys:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
var_key = key.replace(self.redis_prefix, "")
|
|
|
|
|
value = await redis.get(key)
|
2025-05-16 09:23:48 +03:00
|
|
|
|
if value:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
redis_vars[var_key] = str(value)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
|
|
|
|
return redis_vars
|
2025-07-31 18:55:59 +03:00
|
|
|
|
except Exception:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
return {}
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
async def set_variables_to_redis(self, variables: dict[str, str]) -> bool:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"""Сохраняет переменные в Redis"""
|
|
|
|
|
try:
|
|
|
|
|
for key, value in variables.items():
|
2025-07-31 18:55:59 +03:00
|
|
|
|
await redis.set(f"{self.redis_prefix}{key}", value)
|
2025-05-26 13:31:25 +03:00
|
|
|
|
return True
|
2025-07-31 18:55:59 +03:00
|
|
|
|
except Exception:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
return False
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def get_variables_from_env(self) -> dict[str, str]:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"""Получает переменные из системного окружения"""
|
|
|
|
|
env_vars = {}
|
|
|
|
|
|
|
|
|
|
# Получаем все переменные известные системе
|
2025-07-31 18:55:59 +03:00
|
|
|
|
for key in self.VARIABLE_SECTIONS:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
value = os.getenv(key)
|
|
|
|
|
if value is not None:
|
|
|
|
|
env_vars[key] = value
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
# Получаем дополнительные переменные окружения
|
|
|
|
|
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_"])
|
|
|
|
|
}
|
|
|
|
|
)
|
2025-05-21 01:34:02 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
return env_vars
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
async def get_all_variables(self) -> list[EnvSection]:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"""Получает все переменные окружения, сгруппированные по секциям"""
|
2025-07-31 18:55:59 +03:00
|
|
|
|
# Получаем переменные из Redis и системного окружения
|
2025-06-02 02:56:11 +03:00
|
|
|
|
redis_vars = await self.get_variables_from_redis()
|
2025-07-31 18:55:59 +03:00
|
|
|
|
env_vars = self.get_variables_from_env()
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
# Объединяем переменные (Redis имеет приоритет)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
all_vars = {**env_vars, **redis_vars}
|
|
|
|
|
|
|
|
|
|
# Группируем по секциям
|
2025-07-31 18:55:59 +03:00
|
|
|
|
sections_dict: dict[str, list[EnvVariable]] = {section: [] for section in self.SECTIONS}
|
|
|
|
|
other_variables: list[EnvVariable] = [] # Для переменных, которые не попали ни в одну секцию
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
|
|
|
|
for key, value in all_vars.items():
|
|
|
|
|
is_secret = key in self.SECRET_VARIABLES
|
2025-07-31 18:55:59 +03:00
|
|
|
|
description = self.get_variable_description(key)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
# Скрываем значение секретных переменных
|
|
|
|
|
display_value = "***" if is_secret else value
|
|
|
|
|
|
|
|
|
|
env_var = EnvVariable(
|
2025-06-02 02:56:11 +03:00
|
|
|
|
key=key,
|
2025-07-31 18:55:59 +03:00
|
|
|
|
value=display_value,
|
|
|
|
|
description=description,
|
2025-06-02 02:56:11 +03:00
|
|
|
|
is_secret=is_secret,
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
# Определяем секцию для переменной
|
|
|
|
|
section = self.VARIABLE_SECTIONS.get(key, "other")
|
|
|
|
|
if section in sections_dict:
|
|
|
|
|
sections_dict[section].append(env_var)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
else:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
other_variables.append(env_var)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
|
|
|
|
# Создаем объекты секций
|
|
|
|
|
sections = []
|
2025-07-31 18:55:59 +03:00
|
|
|
|
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))
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
return sorted(sections, key=lambda x: x.name)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
async def update_variables(self, variables: list[EnvVariable]) -> bool:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
"""Обновляет переменные окружения"""
|
2025-05-16 09:23:48 +03:00
|
|
|
|
try:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
# Подготавливаем переменные для сохранения
|
|
|
|
|
vars_dict = {}
|
2025-06-02 02:56:11 +03:00
|
|
|
|
for var in variables:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
if not var.is_secret or var.value != "***":
|
|
|
|
|
vars_dict[var.key] = var.value
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
2025-05-21 01:34:02 +03:00
|
|
|
|
# Сохраняем в Redis
|
2025-07-31 18:55:59 +03:00
|
|
|
|
return await self.set_variables_to_redis(vars_dict)
|
|
|
|
|
except Exception:
|
2025-05-16 09:23:48 +03:00
|
|
|
|
return False
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def delete_variable(self, key: str) -> bool:
|
|
|
|
|
"""Удаляет переменную окружения"""
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
try:
|
|
|
|
|
redis_key = f"{self.redis_prefix}{key}"
|
|
|
|
|
result = await redis.delete(redis_key)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
if result > 0:
|
|
|
|
|
logger.info(f"Переменная {key} удалена")
|
|
|
|
|
return True
|
|
|
|
|
logger.warning(f"Переменная {key} не найдена")
|
|
|
|
|
return False
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-21 01:34:02 +03:00
|
|
|
|
except Exception as e:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
logger.error(f"Ошибка при удалении переменной {key}: {e}")
|
2025-05-21 01:34:02 +03:00
|
|
|
|
return False
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def get_variable(self, key: str) -> Optional[str]:
|
|
|
|
|
"""Получает значение конкретной переменной"""
|
|
|
|
|
|
|
|
|
|
# Сначала проверяем Redis
|
2025-05-16 09:23:48 +03:00
|
|
|
|
try:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
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}")
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
# Fallback на системное окружение
|
|
|
|
|
return os.getenv(key)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def set_variable(self, key: str, value: str) -> bool:
|
|
|
|
|
"""Устанавливает значение переменной"""
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
try:
|
|
|
|
|
redis_key = f"{self.redis_prefix}{key}"
|
|
|
|
|
await redis.set(redis_key, value)
|
|
|
|
|
logger.info(f"Переменная {key} установлена")
|
2025-05-16 09:23:48 +03:00
|
|
|
|
return True
|
2025-06-02 02:56:11 +03:00
|
|
|
|
|
2025-05-16 09:23:48 +03:00
|
|
|
|
except Exception as e:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
logger.error(f"Ошибка при установке переменной {key}: {e}")
|
2025-05-16 09:23:48 +03:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
env_manager = EnvService()
|