From 20014d3a815804af4a6bce554ca6c42d087a2919 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Tue, 4 Nov 2025 21:18:15 +0900 Subject: [PATCH] environment emprovements --- .env.example | 76 ++++++++++++----- backend/backend/settings.py | 64 +++++++------- scripts/README_env_generator.md | 94 +++++++++++++++++++++ scripts/generate_env.sh | 143 +++++++++++++++++++++++++++++--- setup.sh | 111 ++++++++++--------------- 5 files changed, 351 insertions(+), 137 deletions(-) create mode 100644 scripts/README_env_generator.md diff --git a/.env.example b/.env.example index b8c22f8..d5f7346 100644 --- a/.env.example +++ b/.env.example @@ -1,34 +1,66 @@ # Django настройки -DJANGO_SECRET_KEY=django-insecure-your-secret-key-here-change-this-in-production -DJANGO_DEBUG=True -DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,0.0.0.0 +DJANGO_SECRET_KEY= +DJANGO_DEBUG=False +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 +DJANGO_CSRF_TRUSTED_ORIGINS= + +# CORS настройки +CORS_ALLOWED_ORIGINS= +CORS_ALLOW_ALL_ORIGINS=False +CORS_ALLOW_CREDENTIALS=True +CORS_ALLOW_HEADERS=accept,accept-encoding,authorization,content-type,dnt,origin,user-agent,x-csrftoken,x-requested-with + +# Локализация +DJANGO_LANGUAGE_CODE=ru-ru +DJANGO_TIME_ZONE=UTC +DJANGO_USE_I18N=True +DJANGO_USE_TZ=True + +# Статические файлы +DJANGO_STATIC_URL=/static/ +DJANGO_MEDIA_URL=/storage/ + +# API настройки +DJANGO_APPEND_SLASH=False + +# JWT настройки +JWT_ACCESS_TOKEN_LIFETIME_MINUTES=60 +JWT_REFRESH_TOKEN_LIFETIME_DAYS=1 # База данных PostgreSQL DATABASE_ENGINE=django.db.backends.postgresql -DATABASE_NAME=links_db -DATABASE_USER=links_user -DATABASE_PASSWORD=links_password +DATABASE_NAME= +DATABASE_USER= +DATABASE_PASSWORD= DATABASE_HOST=db DATABASE_PORT=5432 # PostgreSQL настройки для контейнера -POSTGRES_DB=links_db -POSTGRES_USER=links_user -POSTGRES_PASSWORD=links_password +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= # Frontend настройки -NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_API_URL= -# Опциональные настройки -# DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend -# DJANGO_EMAIL_HOST= -# DJANGO_EMAIL_PORT=587 -# DJANGO_EMAIL_HOST_USER= -# DJANGO_EMAIL_HOST_PASSWORD= -# DJANGO_EMAIL_USE_TLS=True +# Безопасность (для продакшена) +DJANGO_SECURE_SSL_REDIRECT=False +DJANGO_SECURE_HSTS_SECONDS=0 +DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=False +DJANGO_SECURE_HSTS_PRELOAD=False +DJANGO_SECURE_CONTENT_TYPE_NOSNIFF=True +DJANGO_SECURE_BROWSER_XSS_FILTER=True +DJANGO_X_FRAME_OPTIONS=DENY -# Для продакшена -# DJANGO_SECURE_SSL_REDIRECT=True -# DJANGO_SECURE_HSTS_SECONDS=31536000 -# DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=True -# DJANGO_SECURE_HSTS_PRELOAD=True \ No newline at end of file +# Email настройки (опционально) +DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend +DJANGO_EMAIL_HOST= +DJANGO_EMAIL_PORT=587 +DJANGO_EMAIL_HOST_USER= +DJANGO_EMAIL_HOST_PASSWORD= +DJANGO_EMAIL_USE_TLS=True + +# SSL настройки (для Let's Encrypt) +DOMAIN= +EMAIL= +ACME_CA_URI=https://acme-v02.api.letsencrypt.org/directory \ No newline at end of file diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 8a73a96..db21b77 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -29,35 +29,20 @@ SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True' -ALLOWED_HOSTS = ['*'] # Разрешаем доступ с любых хостов для разработки +# Allowed hosts из переменных окружения +ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') + +# CSRF trusted origins из переменных окружения +CSRF_TRUSTED_ORIGINS = os.getenv('DJANGO_CSRF_TRUSTED_ORIGINS', '').split(',') if os.getenv('DJANGO_CSRF_TRUSTED_ORIGINS') else [] # Отключаем APPEND_SLASH для корректной работы API с Next.js proxy -APPEND_SLASH = False +APPEND_SLASH = os.getenv('DJANGO_APPEND_SLASH', 'False') == 'True' -CORS_ALLOWED_ORIGINS = [ - "http://127.0.0.1:3000", - "http://localhost:3000", - "http://127.0.0.1:3001", - "http://localhost:3001", - "http://192.168.219.108:3000", - "http://192.168.219.108:3001", - "http://192.168.219.108:8000", - "http://192.168.219.108:8001", -] - -CORS_ALLOW_ALL_ORIGINS = True # Для разработки -CORS_ALLOW_CREDENTIALS = True -CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-csrftoken', - 'x-requested-with', -] +# CORS настройки из переменных окружения +CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', '').split(',') if os.getenv('CORS_ALLOWED_ORIGINS') else [] +CORS_ALLOW_ALL_ORIGINS = os.getenv('CORS_ALLOW_ALL_ORIGINS', 'False') == 'True' +CORS_ALLOW_CREDENTIALS = os.getenv('CORS_ALLOW_CREDENTIALS', 'True') == 'True' +CORS_ALLOW_HEADERS = os.getenv('CORS_ALLOW_HEADERS', 'accept,accept-encoding,authorization,content-type,dnt,origin,user-agent,x-csrftoken,x-requested-with').split(',') # Application definition @@ -126,8 +111,8 @@ REST_FRAMEWORK = { from datetime import timedelta SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=int(os.getenv('JWT_ACCESS_TOKEN_LIFETIME_MINUTES', '60'))), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=int(os.getenv('JWT_REFRESH_TOKEN_LIFETIME_DAYS', '1'))), 'AUTH_HEADER_TYPES': ('Bearer',), } @@ -170,13 +155,13 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'ru-ru' +LANGUAGE_CODE = os.getenv('DJANGO_LANGUAGE_CODE', 'ru-ru') -TIME_ZONE = 'UTC' +TIME_ZONE = os.getenv('DJANGO_TIME_ZONE', 'UTC') -USE_I18N = True +USE_I18N = os.getenv('DJANGO_USE_I18N', 'True') == 'True' -USE_TZ = True +USE_TZ = os.getenv('DJANGO_USE_TZ', 'True') == 'True' # Static files (CSS, JavaScript, Images) @@ -185,7 +170,7 @@ USE_TZ = True STATIC_ROOT = BASE_DIR / 'staticfiles' # URL, по которому статика будет доступна -STATIC_URL = '/static/' +STATIC_URL = os.getenv('DJANGO_STATIC_URL', '/static/') # WhiteNoise настройки STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' @@ -195,5 +180,14 @@ STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -MEDIA_URL = '/storage/' -MEDIA_ROOT = BASE_DIR / 'storage' \ No newline at end of file +MEDIA_URL = os.getenv('DJANGO_MEDIA_URL', '/storage/') +MEDIA_ROOT = BASE_DIR / 'storage' + +# Настройки безопасности из переменных окружения +SECURE_SSL_REDIRECT = os.getenv('DJANGO_SECURE_SSL_REDIRECT', 'False') == 'True' +SECURE_HSTS_SECONDS = int(os.getenv('DJANGO_SECURE_HSTS_SECONDS', '0')) +SECURE_HSTS_INCLUDE_SUBDOMAINS = os.getenv('DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS', 'False') == 'True' +SECURE_HSTS_PRELOAD = os.getenv('DJANGO_SECURE_HSTS_PRELOAD', 'False') == 'True' +SECURE_CONTENT_TYPE_NOSNIFF = os.getenv('DJANGO_SECURE_CONTENT_TYPE_NOSNIFF', 'True') == 'True' +SECURE_BROWSER_XSS_FILTER = os.getenv('DJANGO_SECURE_BROWSER_XSS_FILTER', 'True') == 'True' +X_FRAME_OPTIONS = os.getenv('DJANGO_X_FRAME_OPTIONS', 'DENY') \ No newline at end of file diff --git a/scripts/README_env_generator.md b/scripts/README_env_generator.md new file mode 100644 index 0000000..1d5676d --- /dev/null +++ b/scripts/README_env_generator.md @@ -0,0 +1,94 @@ +# Environment Configuration Generator + +## Описание + +Скрипт `scripts/generate_env.sh` создает файл `.env` на основе шаблона `.env.example` с интерактивным вводом параметров или автоматической генерацией безопасных значений. + +## Использование + +### Интерактивный режим (разработка) +```bash +./scripts/generate_env.sh +``` + +### Автоматический режим для продакшена +```bash +./scripts/generate_env.sh --production +``` + +### Неинтерактивный режим +```bash +./scripts/generate_env.sh --yes +``` + +### Комбинированный режим +```bash +./scripts/generate_env.sh --production --yes +``` + +## Функции + +### 🔒 Автоматическая генерация безопасных паролей +- Django SECRET_KEY (криптографически стойкий) +- Пароли базы данных (случайные 25-символьные) +- Все пароли генерируются с использованием OpenSSL или Python secrets + +### 🚀 Продакшен режим (`--production`) +- Автоматически устанавливает `DJANGO_DEBUG=False` +- Включает SSL настройки (HSTS, SSL redirect) +- Генерирует криптографически стойкие пароли +- Запрашивает домен и email для SSL сертификатов +- Настраивает CORS только для указанного домена + +### ⚡ Неинтерактивный режим (`--yes`) +- Использует значения из переменных окружения +- Подходит для CI/CD и автоматизированного развертывания +- Сохраняет значения по умолчанию из `.env.example` + +## Примеры + +### Быстрая настройка для разработки +```bash +# Создаст .env с базовыми настройками для localhost +./scripts/generate_env.sh --yes +``` + +### Настройка продакшена +```bash +# Интерактивная настройка с вводом домена +./scripts/generate_env.sh --production + +# Или автоматически, если домен в переменной окружения +DOMAIN=example.com EMAIL=admin@example.com ./scripts/generate_env.sh --production --yes +``` + +### Переменные окружения для автоматизации +```bash +export DJANGO_SECRET_KEY="your-secret-key" +export DATABASE_PASSWORD="your-db-password" +export DOMAIN="example.com" +export EMAIL="admin@example.com" + +./scripts/generate_env.sh --yes +``` + +## Безопасность + +- Пароли генерируются криптографически стойкими методами +- Секретные ключи никогда не выводятся в консоль +- В продакшен режиме автоматически отключается DEBUG +- Включаются все необходимые заголовки безопасности + +## Интеграция + +Скрипт интегрирован в: +- `setup.sh` - основной скрипт установки +- `Makefile` - команды для управления проектом +- `scripts/master-deploy.sh` - скрипт развертывания + +## Совместимость + +- ✅ Linux/macOS/WSL +- ✅ Docker environments +- ✅ CI/CD pipelines +- ✅ OpenSSL и Python 3.x \ No newline at end of file diff --git a/scripts/generate_env.sh b/scripts/generate_env.sh index 22428f5..467408b 100755 --- a/scripts/generate_env.sh +++ b/scripts/generate_env.sh @@ -3,23 +3,39 @@ set -euo pipefail # scripts/generate_env.sh # Interactive generator for .env from .env.example -# Usage: ./scripts/generate_env.sh [--yes] +# Usage: ./scripts/generate_env.sh [--yes] [--production] BASE_DIR="$(cd "$(dirname "$0")/.." && pwd)" EXAMPLE="$BASE_DIR/.env.example" TARGET="$BASE_DIR/.env" confirm=false -if [[ "${1:-}" == "--yes" ]]; then - confirm=true -fi +production=false + +# Parse arguments +for arg in "$@"; do + case $arg in + --yes) + confirm=true + ;; + --production) + production=true + ;; + *) + echo "Usage: $0 [--yes] [--production]" + echo " --yes Non-interactive mode, use defaults or environment variables" + echo " --production Setup for production environment" + exit 1 + ;; + esac +done if [[ ! -f "$EXAMPLE" ]]; then echo "Error: .env.example not found at $EXAMPLE" exit 1 fi -echo "Generating $TARGET from $EXAMPLE" +echo "🔧 Generating $TARGET from $EXAMPLE" if [[ -f "$TARGET" && "$confirm" = false ]]; then read -p "$TARGET exists. Overwrite? (y/N): " ans @@ -29,9 +45,75 @@ if [[ -f "$TARGET" && "$confirm" = false ]]; then esac fi +# Function to generate secure passwords +generate_password() { + openssl rand -base64 32 | tr -d "=+/" | cut -c1-25 +} + +# Function to generate Django secret key +generate_secret_key() { + python3 -c " +from django.core.management.utils import get_random_secret_key +print(get_random_secret_key()) +" 2>/dev/null || openssl rand -base64 50 | tr -d "=+/" +} + +# Function to safely update env variable +update_env_var() { + local key="$1" + local value="$2" + local file="$3" + + # Remove existing line and add new one + grep -v "^${key}=" "$file" > "${file}.tmp" 2>/dev/null || true + echo "${key}=${value}" >> "${file}.tmp" + mv "${file}.tmp" "$file" +} + # Copy example first cp "$EXAMPLE" "$TARGET" +# Production mode adjustments +if [[ "$production" = true ]]; then + echo "🚀 Setting up for production environment..." + + # Generate secure values + SECRET_KEY=$(generate_secret_key) + DB_PASSWORD=$(generate_password) + + # Set production values + update_env_var "DJANGO_SECRET_KEY" "$SECRET_KEY" "$TARGET" + update_env_var "DJANGO_DEBUG" "False" "$TARGET" + update_env_var "DATABASE_PASSWORD" "$DB_PASSWORD" "$TARGET" + update_env_var "POSTGRES_PASSWORD" "$DB_PASSWORD" "$TARGET" + update_env_var "DJANGO_SECURE_SSL_REDIRECT" "True" "$TARGET" + update_env_var "DJANGO_SECURE_HSTS_SECONDS" "31536000" "$TARGET" + update_env_var "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS" "True" "$TARGET" + update_env_var "DJANGO_SECURE_HSTS_PRELOAD" "True" "$TARGET" + + # Prompt for domain + if [[ "$confirm" = false ]]; then + read -p "Enter your domain (e.g., example.com): " domain + read -p "Enter your email for SSL certificate: " email + + if [[ -n "$domain" ]]; then + update_env_var "DOMAIN" "$domain" "$TARGET" + update_env_var "DJANGO_ALLOWED_HOSTS" "${domain},localhost,127.0.0.1" "$TARGET" + update_env_var "DJANGO_CSRF_TRUSTED_ORIGINS" "https://${domain}" "$TARGET" + update_env_var "CORS_ALLOWED_ORIGINS" "https://${domain}" "$TARGET" + update_env_var "NEXT_PUBLIC_API_URL" "https://${domain}" "$TARGET" + fi + + if [[ -n "$email" ]]; then + update_env_var "EMAIL" "$email" "$TARGET" + fi + fi + + echo "✅ Production configuration generated with secure passwords" + echo "📝 Database password: $DB_PASSWORD" + echo "⚠️ Save this password securely!" +fi + # For each variable in example, prompt the user to keep or change while IFS= read -r line; do if [[ "$line" =~ ^# ]] || [[ -z "$line" ]]; then @@ -56,26 +138,65 @@ while IFS= read -r line; do env_val="${!key}" fi if [[ -n "$env_val" ]]; then - sed -i "s|^$key=.*|$key=$env_val|" "$TARGET" + update_env_var "$key" "$env_val" "$TARGET" fi continue fi + # Skip if production mode already set the value + if [[ "$production" = true ]]; then + case "$key" in + DJANGO_SECRET_KEY|DATABASE_PASSWORD|POSTGRES_PASSWORD|DJANGO_DEBUG|DJANGO_SECURE_*|DOMAIN|EMAIL) + continue + ;; + esac + fi + # Interactive mode: prompt user for each variable if read -t 1 -n 0 2>/dev/null; then - # stdin is available for reading - read -p "$key [$current_val]: " new_val || true + # Special prompts for certain variables + case "$key" in + DJANGO_SECRET_KEY) + if [[ -z "$current_val" ]]; then + read -p "$key (press Enter to generate): " new_val || true + if [[ -z "$new_val" ]]; then + new_val=$(generate_secret_key) + fi + else + read -p "$key [HIDDEN]: " new_val || true + fi + ;; + DATABASE_PASSWORD|POSTGRES_PASSWORD) + if [[ -z "$current_val" ]]; then + read -p "$key (press Enter to generate): " new_val || true + if [[ -z "$new_val" ]]; then + new_val=$(generate_password) + fi + else + read -p "$key [HIDDEN]: " new_val || true + fi + ;; + *) + read -p "$key [$current_val]: " new_val || true + ;; + esac else # no stdin available, skip prompting new_val="" fi if [[ -n "$new_val" ]]; then - sed -i "s|^$key=.*|$key=$new_val|" "$TARGET" + update_env_var "$key" "$new_val" "$TARGET" fi done < <(grep -E '^[A-Z0-9_]+=.*' "$EXAMPLE") -echo "Written $TARGET" +echo "✅ Written $TARGET" -echo "You can re-run this script with --yes to auto-fill from environment variables." +if [[ "$production" = false ]]; then + echo "" + echo "💡 Tips:" + echo " - Run with --production for production setup with secure defaults" + echo " - Run with --yes to auto-fill from environment variables" + echo " - Check the generated .env file and adjust values as needed" +fi diff --git a/setup.sh b/setup.sh index e369f80..7b548bb 100644 --- a/setup.sh +++ b/setup.sh @@ -78,81 +78,54 @@ create_env_file() { if [ -f .env ]; then print_info ".env файл уже существует" - read -p "Пересоздать? (y/N): " recreate - if [[ ! $recreate =~ ^[Yy]$ ]]; then + read -p "Пересоздать с помощью интерактивного генератора? (y/N): " recreate + if [[ $recreate =~ ^[Yy]$ ]]; then + print_info "Запуск интерактивного генератора .env..." + ./scripts/generate_env.sh + return + else print_info "Пропуск создания .env" return fi fi - # Генерация SECRET_KEY - SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))" 2>/dev/null || openssl rand -base64 32) + print_info "Запуск интерактивного генератора .env..." - # Запрос данных от пользователя - echo "" - read -p "Имя базы данных [links_db]: " DB_NAME - DB_NAME=${DB_NAME:-links_db} - - read -p "Пользователь БД [links_user]: " DB_USER - DB_USER=${DB_USER:-links_user} - - read -p "Пароль БД [links_password]: " DB_PASSWORD - DB_PASSWORD=${DB_PASSWORD:-links_password} - - read -p "Хост БД [db]: " DB_HOST - DB_HOST=${DB_HOST:-db} - - read -p "Порт БД [5432]: " DB_PORT - DB_PORT=${DB_PORT:-5432} - - read -p "Домен для продакшена [localhost]: " DOMAIN - DOMAIN=${DOMAIN:-localhost} - - read -p "Email для SSL сертификатов: " SSL_EMAIL - - # Создание .env файла - cat > .env << EOF -# Django Configuration -SECRET_KEY=$SECRET_KEY -DEBUG=True -ALLOWED_HOSTS=localhost,127.0.0.1,$DOMAIN - -# Database Configuration -DATABASE_URL=postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME -DB_NAME=$DB_NAME -DB_USER=$DB_USER -DB_PASSWORD=$DB_PASSWORD -DB_HOST=$DB_HOST -DB_PORT=$DB_PORT - -# Production Settings -DOMAIN=$DOMAIN -SSL_EMAIL=$SSL_EMAIL - -# CORS Settings -CORS_ALLOWED_ORIGINS=http://localhost:3000,https://$DOMAIN - -# File Upload Settings -MAX_UPLOAD_SIZE=10485760 -MEDIA_ROOT=/app/storage - -# Cache Settings -CACHE_TTL=300 - -# Email Settings (optional) -EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend -EMAIL_HOST= -EMAIL_PORT=587 -EMAIL_USE_TLS=True -EMAIL_HOST_USER= -EMAIL_HOST_PASSWORD= - -# Redis Settings (optional) -REDIS_URL=redis://redis:6379/0 - -# Logging -LOG_LEVEL=INFO -EOF + # Проверяем наличие скрипта генерации + if [ -f "scripts/generate_env.sh" ]; then + chmod +x scripts/generate_env.sh + ./scripts/generate_env.sh + else + print_warning "Скрипт генерации не найден, создаем базовый .env" + + # Генерация SECRET_KEY + SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))" 2>/dev/null || openssl rand -base64 32) + + # Запрос основных данных + echo "" + read -p "Имя базы данных [links_db]: " DB_NAME + DB_NAME=${DB_NAME:-links_db} + + read -p "Пользователь БД [links_user]: " DB_USER + DB_USER=${DB_USER:-links_user} + + read -p "Пароль БД (пустой для генерации): " DB_PASSWORD + if [ -z "$DB_PASSWORD" ]; then + DB_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25) + print_info "Сгенерирован пароль БД: $DB_PASSWORD" + fi + + # Копируем из примера и заполняем основные поля + cp .env.example .env + sed -i "s|^DJANGO_SECRET_KEY=.*|DJANGO_SECRET_KEY=$SECRET_KEY|" .env + sed -i "s|^DATABASE_NAME=.*|DATABASE_NAME=$DB_NAME|" .env + sed -i "s|^DATABASE_USER=.*|DATABASE_USER=$DB_USER|" .env + sed -i "s|^DATABASE_PASSWORD=.*|DATABASE_PASSWORD=$DB_PASSWORD|" .env + sed -i "s|^POSTGRES_DB=.*|POSTGRES_DB=$DB_NAME|" .env + sed -i "s|^POSTGRES_USER=.*|POSTGRES_USER=$DB_USER|" .env + sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$DB_PASSWORD|" .env + sed -i "s|^NEXT_PUBLIC_API_URL=.*|NEXT_PUBLIC_API_URL=http://localhost:8000|" .env + fi print_success ".env файл создан" }