From 40ffbb7de7db494f363fcd53919a96dd4d8dbaf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 31 Mar 2026 13:23:56 +0200 Subject: [PATCH] fix with .env/password --- .env.example | 77 ++++++++++++++++++--------- README.md | 7 +++ config.py | 136 ++++++++++++++++++++++++++++------------------- deploy_docker.sh | 97 +++++++++++++++++++++++++++------ 4 files changed, 222 insertions(+), 95 deletions(-) diff --git a/.env.example b/.env.example index b671dd0..7983e53 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,8 @@ +# UWAGA: +# Po zmianie pliku .env samo `docker compose restart` może nie wystarczyć. +# Aby nowe wartości zostały na pewno wczytane do kontenerów, użyj: +# docker compose up -d --force-recreate + # APP_PORT: # Domyślny port, na którym uruchamiana jest aplikacja Flask # Domyślnie: 8000 @@ -6,27 +11,30 @@ APP_PORT=8000 # SECRET_KEY: # Klucz używany przez Flask do zabezpieczenia sesji, tokenów i formularzy # Powinien być długi i trudny do odgadnięcia -SECRET_KEY=supersekretnyklucz123 +# Może zawierać znaki specjalne +SECRET_KEY="supersekretnyklucz123" # SYSTEM_PASSWORD: # Hasło główne administratora systemowego, używane np. przy inicjalizacji # Domyślnie: admin -SYSTEM_PASSWORD=admin +# Może zawierać znaki specjalne +SYSTEM_PASSWORD="admin" # DEFAULT_ADMIN_USERNAME: # Domyślna nazwa użytkownika administratora (tworzona przy starcie) # Domyślnie: admin -DEFAULT_ADMIN_USERNAME=admin +DEFAULT_ADMIN_USERNAME="admin" # DEFAULT_ADMIN_PASSWORD: # Domyślne hasło administratora # Domyślnie: admin123 -DEFAULT_ADMIN_PASSWORD=admin123 +# Może zawierać znaki specjalne +DEFAULT_ADMIN_PASSWORD="admin123" # UPLOAD_FOLDER: # Ścieżka (względna) do katalogu, gdzie zapisywane są wgrywane pliki # Domyślnie: uploads -UPLOAD_FOLDER=uploads +UPLOAD_FOLDER="uploads" # SESSION_TIMEOUT_MINUTES: # Czas bezczynności użytkownika (w minutach), po którym sesja wygasa @@ -41,8 +49,12 @@ AUTH_COOKIE_MAX_AGE=86400 # AUTHORIZED_COOKIE_VALUE: # Wartość ciasteczka uprawniającego do dostępu (np. do zasobów zabezpieczonych) # Powinna być trudna do przewidzenia -# Chodzi to o zabezpieczenie strony "hasłęm głównym czyli endpointem /system-auth" -AUTHORIZED_COOKIE_VALUE=twoj_wlasny_hash +# Chodzi o zabezpieczenie strony "hasłem głównym", czyli endpointem /system-auth +# Może zawierać znaki specjalne +# UWAGA: zmiana SYSTEM_PASSWORD nie unieważnia automatycznie wcześniej wydanych ciasteczek. +# Aby wymusić ponowną autoryzację, zmień także AUTHORIZED_COOKIE_VALUE +# lub wyczyść ciasteczka w przeglądarce. +AUTHORIZED_COOKIE_VALUE="twoj_wlasny_hash" # SESSION_COOKIE_SECURE: # Określa, czy ciasteczko sesyjne (Flask session) ma mieć ustawiony atrybut "Secure". @@ -54,39 +66,54 @@ SESSION_COOKIE_SECURE=0 # BCRYPT_PEPPER: # Dodatkowy „sekretny klucz” (pepper) dodawany do hasła przed zahashowaniem # Zwiększa bezpieczeństwo przechowywanych haseł -BCRYPT_PEPPER=sekretnyKluczbcrypt +# Może zawierać znaki specjalne +BCRYPT_PEPPER="sekretnyKluczbcrypt" # HEALTHCHECK_TOKEN: # Token wykorzystywany do sprawdzania stanu aplikacji (np. w Docker Compose) # Domyślnie: alamapsaikota123 -HEALTHCHECK_TOKEN=alamapsaikota123 +# Może zawierać znaki specjalne +HEALTHCHECK_TOKEN="alamapsaikota123" # Rodzaj bazy: sqlite, pgsql, mysql -# Mozliwe wartosci: sqlite / pgsql / mysql -DB_ENGINE=sqlite +# Możliwe wartości: sqlite / pgsql / mysql +DB_ENGINE="sqlite" # --- Konfiguracja dla sqlite --- -# Plik bazy bedzie utworzony automatycznie w katalogu ./instance -# Pozostale zmienne sa ignorowane przy DB_ENGINE=sqlite +# Plik bazy będzie utworzony automatycznie w katalogu ./instance +# Pozostałe zmienne są ignorowane przy DB_ENGINE=sqlite # --- Konfiguracja dla pgsql --- # Ustaw DB_ENGINE=pgsql -# Domyslny port PostgreSQL to 5432 -# Wymaga dzialajacego serwera PostgreSQL (np. kontener `postgres`) +# Domyślny port PostgreSQL to 5432 +# Wymaga działającego serwera PostgreSQL (np. kontener `postgres`) # --- Konfiguracja dla mysql --- # Ustaw DB_ENGINE=mysql -# Domyslny port MySQL to 3306 -# Wymaga kontenera z MySQL i uzytkownika z dostepem do bazy +# Domyślny port MySQL to 3306 +# Wymaga kontenera z MySQL i użytkownika z dostępem do bazy -# Wspolne zmienne (dla pgsql, mysql) +# Wspólne zmienne (dla pgsql, mysql) # DB_HOST = pgsql lub mysql zgodnie z deployem (profil w docker-compose.yml) -DB_HOST=pgsql +DB_HOST="pgsql" DB_PORT=5432 -DB_NAME=myapp -DB_USER=user -DB_PASSWORD=pass + +# DB_NAME: +# Nazwa bazy danych +# Może zawierać znaki specjalne, ale zalecane są proste nazwy +DB_NAME="myapp" + +# DB_USER: +# Użytkownik bazy danych +# Może zawierać znaki specjalne +DB_USER="user" + +# DB_PASSWORD: +# Hasło do bazy danych +# Może zawierać znaki specjalne +# Zalecane jest używanie wartości w cudzysłowach +DB_PASSWORD="pass" # ======================== # Nagłówki bezpieczeństwa @@ -164,8 +191,8 @@ UPLOADS_CACHE_CONTROL="max-age=3600, immutable" # Lista domyślnych kategorii tworzonych automatycznie przy starcie aplikacji, # jeśli nie istnieją w bazie danych. # Podaj w formacie CSV (oddzielone przecinkami) – kolejność zostanie zachowana. -# Możesz dodać własne kategorie -# UWAGA: Wielkość liter w nazwach jest zachowywana, ale porównywanie odbywa się +# Możesz dodać własne kategorie. +# UWAGA: wielkość liter w nazwach jest zachowywana, ale porównywanie odbywa się # bez rozróżniania wielkości liter (case-insensitive). # Domyślnie: poniższa lista -DEFAULT_CATEGORIES="Spożywcze,Budowlane,Zabawki,Chemia,Inne,Elektronika,Odzież i obuwie,Artykuły biurowe,Kosmetyki i higiena,Motoryzacja,Ogród i rośliny,Zwierzęta,Sprzęt sportowy,Książki i prasa,Narzędzia i majsterkowanie,RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo" +DEFAULT_CATEGORIES="Spożywcze,Budowlane,Zabawki,Chemia,Inne,Elektronika,Odzież i obuwie,Artykuły biurowe,Kosmetyki i higiena,Motoryzacja,Ogród i rośliny,Zwierzęta,Sprzęt sportowy,Książki i prasa,Narzędzia i majsterkowanie,RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo" \ No newline at end of file diff --git a/README.md b/README.md index 7c678ed..b5de1f2 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,13 @@ Prosta aplikacja webowa do zarządzania listami zakupów z obsługą użytkownik bash deploy_docker.sh sqlite ``` +2.3 Restart: + ```bash + bash deploy_docker.sh pgsql restart + lub + bash deploy_docker.sh sqlite restart + ``` + Aplikacja będzie dostępna pod `http://localhost:8000`. ## Domyślne dane logowania diff --git a/config.py b/config.py index e2c3e55..c960ac7 100644 --- a/config.py +++ b/config.py @@ -1,85 +1,113 @@ import os +from urllib.parse import quote_plus basedir = os.path.abspath(os.path.dirname(__file__)) +def env_str(name, default=None): + value = os.environ.get(name) + return default if value is None else value + + +def env_int(name, default): + value = os.environ.get(name) + if value is None or value == "": + return default + try: + return int(value) + except (TypeError, ValueError): + return default + + +def env_bool(name, default=False): + value = os.environ.get(name) + if value is None: + return default + return str(value).strip().lower() in ("1", "true", "yes", "on") + + class Config: - SESSION_COOKIE_HTTPONLY = True - SESSION_COOKIE_SAMESITE = "Lax" # działa w HTTP i HTTPS - - SECRET_KEY = os.environ.get("SECRET_KEY", "D8pceNZ8q%YR7^7F&9wAC2") + SESSION_COOKIE_SAMESITE = "Lax" - APP_PORT = int(os.environ.get("APP_PORT", "8000") or "8000") + SECRET_KEY = env_str("SECRET_KEY", "D8pceNZ8q%YR7^7F&9wAC2") + + APP_PORT = env_int("APP_PORT", 8000) + + DB_ENGINE = env_str("DB_ENGINE", "sqlite").lower() - DB_ENGINE = os.environ.get("DB_ENGINE", "sqlite").lower() if DB_ENGINE == "sqlite": SQLALCHEMY_DATABASE_URI = ( f"sqlite:///{os.path.join(basedir, 'db', 'shopping.db')}" ) + elif DB_ENGINE == "pgsql": - SQLALCHEMY_DATABASE_URI = f"postgresql://{os.environ['DB_USER']}:{os.environ['DB_PASSWORD']}@{os.environ['DB_HOST']}:{os.environ.get('DB_PORT', 5432)}/{os.environ['DB_NAME']}" + db_user = quote_plus(env_str("DB_USER", "user")) + db_password = quote_plus(env_str("DB_PASSWORD", "pass")) + db_host = env_str("DB_HOST", "pgsql") + db_port = env_str("DB_PORT", "5432") + db_name = quote_plus(env_str("DB_NAME", "myapp")) + + SQLALCHEMY_DATABASE_URI = ( + f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" + ) + elif DB_ENGINE == "mysql": - SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{os.environ['DB_USER']}:{os.environ['DB_PASSWORD']}@{os.environ['DB_HOST']}:{os.environ.get('DB_PORT', 3306)}/{os.environ['DB_NAME']}" + db_user = quote_plus(env_str("DB_USER", "user")) + db_password = quote_plus(env_str("DB_PASSWORD", "pass")) + db_host = env_str("DB_HOST", "mysql") + db_port = env_str("DB_PORT", "3306") + db_name = quote_plus(env_str("DB_NAME", "myapp")) + + SQLALCHEMY_DATABASE_URI = ( + f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" + ) + else: raise ValueError("Nieobsługiwany typ bazy danych.") SQLALCHEMY_TRACK_MODIFICATIONS = False - SYSTEM_PASSWORD = os.environ.get("SYSTEM_PASSWORD", "admin") - DEFAULT_ADMIN_USERNAME = os.environ.get("DEFAULT_ADMIN_USERNAME", "admin") - DEFAULT_ADMIN_PASSWORD = os.environ.get("DEFAULT_ADMIN_PASSWORD", "admin123") - UPLOAD_FOLDER = os.environ.get("UPLOAD_FOLDER", "uploads") - AUTHORIZED_COOKIE_VALUE = os.environ.get("AUTHORIZED_COOKIE_VALUE", "cookievalue") - BCRYPT_PEPPER = os.environ.get("BCRYPT_PEPPER", "sekretnyKluczBcrypt") - SESSION_COOKIE_SECURE = os.environ.get("SESSION_COOKIE_SECURE", "0") == "1" - HEALTHCHECK_TOKEN = os.environ.get("HEALTHCHECK_TOKEN", "alamapsaikota1234") - try: - AUTH_COOKIE_MAX_AGE = int( - os.environ.get("AUTH_COOKIE_MAX_AGE", "86400") or "86400" - ) - except ValueError: - AUTH_COOKIE_MAX_AGE = 86400 + SYSTEM_PASSWORD = env_str("SYSTEM_PASSWORD", "admin") + DEFAULT_ADMIN_USERNAME = env_str("DEFAULT_ADMIN_USERNAME", "admin") + DEFAULT_ADMIN_PASSWORD = env_str("DEFAULT_ADMIN_PASSWORD", "admin123") + UPLOAD_FOLDER = env_str("UPLOAD_FOLDER", "uploads") + AUTHORIZED_COOKIE_VALUE = env_str("AUTHORIZED_COOKIE_VALUE", "cookievalue") + BCRYPT_PEPPER = env_str("BCRYPT_PEPPER", "sekretnyKluczBcrypt") + SESSION_COOKIE_SECURE = env_bool("SESSION_COOKIE_SECURE", False) + HEALTHCHECK_TOKEN = env_str("HEALTHCHECK_TOKEN", "alamapsaikota1234") - try: - SESSION_TIMEOUT_MINUTES = int( - os.environ.get("SESSION_TIMEOUT_MINUTES", "10080") or "10080" - ) - except ValueError: - SESSION_TIMEOUT_MINUTES = 10080 + AUTH_COOKIE_MAX_AGE = env_int("AUTH_COOKIE_MAX_AGE", 86400) + SESSION_TIMEOUT_MINUTES = env_int("SESSION_TIMEOUT_MINUTES", 10080) - ENABLE_HSTS = os.environ.get("ENABLE_HSTS", "0") == "1" - ENABLE_XFO = os.environ.get("ENABLE_XFO", "0") == "1" - ENABLE_XCTO = os.environ.get("ENABLE_XCTO", "0") == "1" - ENABLE_CSP = os.environ.get("ENABLE_CSP", "0") == "1" - ENABLE_PP = os.environ.get("ENABLE_PP", "0") == "1" - REFERRER_POLICY = os.environ.get("REFERRER_POLICY") or None + ENABLE_HSTS = env_bool("ENABLE_HSTS", False) + ENABLE_XFO = env_bool("ENABLE_XFO", False) + ENABLE_XCTO = env_bool("ENABLE_XCTO", False) + ENABLE_CSP = env_bool("ENABLE_CSP", False) + ENABLE_PP = env_bool("ENABLE_PP", False) - DEBUG_MODE = os.environ.get("DEBUG_MODE", "1") == "1" - DISABLE_ROBOTS = os.environ.get("DISABLE_ROBOTS", "0") == "1" + REFERRER_POLICY = env_str("REFERRER_POLICY") or None - JS_CACHE_CONTROL = os.environ.get( - "JS_CACHE_CONTROL", "no-cache" - ) - CSS_CACHE_CONTROL = os.environ.get( - "CSS_CACHE_CONTROL", "no-cache" - ) - LIB_JS_CACHE_CONTROL = os.environ.get( - "LIB_JS_CACHE_CONTROL", "max-age=604800" - ) - LIB_CSS_CACHE_CONTROL = os.environ.get( - "LIB_CSS_CACHE_CONTROL", "max-age=604800" - ) - UPLOADS_CACHE_CONTROL = os.environ.get( - "UPLOADS_CACHE_CONTROL", "public, max-age=2592000, immutable" + DEBUG_MODE = env_bool("DEBUG_MODE", True) + DISABLE_ROBOTS = env_bool("DISABLE_ROBOTS", False) + + JS_CACHE_CONTROL = env_str("JS_CACHE_CONTROL", "no-cache") + CSS_CACHE_CONTROL = env_str("CSS_CACHE_CONTROL", "no-cache") + LIB_JS_CACHE_CONTROL = env_str("LIB_JS_CACHE_CONTROL", "max-age=604800") + LIB_CSS_CACHE_CONTROL = env_str("LIB_CSS_CACHE_CONTROL", "max-age=604800") + UPLOADS_CACHE_CONTROL = env_str( + "UPLOADS_CACHE_CONTROL", + "public, max-age=2592000, immutable", ) DEFAULT_CATEGORIES = [ - c.strip() for c in os.environ.get( + c.strip() + for c in env_str( "DEFAULT_CATEGORIES", "Spożywcze,Budowlane,Zabawki,Chemia,Inne,Elektronika,Odzież i obuwie,Jedzenie poza domem," "Artykuły biurowe,Kosmetyki i higiena,Motoryzacja,Ogród i rośliny," "Zwierzęta,Sprzęt sportowy,Książki i prasa,Narzędzia i majsterkowanie," - "RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo,Różne,Chiny,Dom,Leki,Odzież,Samochód,Dzieci" - ).split(",") if c.strip() - ] + "RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo,Różne,Chiny,Dom,Leki,Odzież,Samochód,Dzieci", + ).split(",") + if c.strip() + ] \ No newline at end of file diff --git a/deploy_docker.sh b/deploy_docker.sh index fc45d18..1505184 100644 --- a/deploy_docker.sh +++ b/deploy_docker.sh @@ -1,5 +1,7 @@ #!/bin/bash -set -e +set -euo pipefail + +COMPOSE_FILE="docker/compose.yml" if [[ -f .env ]]; then set -a @@ -8,23 +10,63 @@ if [[ -f .env ]]; then fi APP_PORT="${APP_PORT:-8080}" -PROFILE=$1 -COMPOSE_FILE="docker/compose.yml" +DEFAULT_ENGINE="${DB_ENGINE:-sqlite}" -if [[ -z "$PROFILE" ]]; then - echo "Użycie: $0 {pgsql|mysql|sqlite}" - exit 1 +print_usage() { + echo "Użycie:" + echo " $0 [sqlite|pgsql|mysql] [deploy|restart]" + echo + echo "Przykłady:" + echo " $0 pgsql deploy" + echo " $0 mysql restart" + echo " $0 sqlite" + echo + echo "Domyślnie:" + echo " silnik: z DB_ENGINE z .env albo sqlite" + echo " akcja: deploy" +} + +validate_engine() { + local engine="$1" + case "$engine" in + sqlite|pgsql|mysql) + return 0 + ;; + *) + echo "Błąd: nieobsługiwany silnik bazy: '$engine'" + echo "Dozwolone wartości: sqlite, pgsql, mysql" + exit 1 + ;; + esac +} + +validate_action() { + local action="$1" + case "$action" in + deploy|restart) + return 0 + ;; + *) + echo "Błąd: nieobsługiwana akcja: '$action'" + echo "Dozwolone wartości: deploy, restart" + exit 1 + ;; + esac +} + +PROFILE="${1:-$DEFAULT_ENGINE}" +ACTION="${2:-deploy}" + +validate_engine "$PROFILE" +validate_action "$ACTION" + +if [[ -n "${DB_ENGINE:-}" && "$DB_ENGINE" != "$PROFILE" ]]; then + echo "Uwaga: DB_ENGINE w .env ma wartość '$DB_ENGINE', a uruchamiasz profil '$PROFILE'." + echo "Kontynuuję z profilem z argumentu: '$PROFILE'" fi -echo "Zatrzymuję kontenery aplikacji i bazy..." -if [[ "$PROFILE" == "sqlite" ]]; then - docker compose -f "$COMPOSE_FILE" stop -else - docker compose -f "$COMPOSE_FILE" --profile "$PROFILE" stop -fi - -echo "Pobieram najnowszy kod z repozytorium..." -git pull +echo "Wybrany silnik bazy: $PROFILE" +echo "Wybrana akcja: $ACTION" echo "Generowanie default.vcl z APP_PORT=$APP_PORT" envsubst < deploy/varnish/default.vcl.template > deploy/varnish/default.vcl @@ -32,7 +74,30 @@ envsubst < deploy/varnish/default.vcl.template > deploy/varnish/default.vcl echo "Zapisuję hash commita do version.txt..." git rev-parse --short HEAD > version.txt -echo "Buduję i uruchamiam kontenery..." +if [[ "$ACTION" == "restart" ]]; then + echo "Restart kontenerów bez przebudowy obrazu..." + + if [[ "$PROFILE" == "sqlite" ]]; then + docker compose -f "$COMPOSE_FILE" restart + else + DB_ENGINE="$PROFILE" docker compose -f "$COMPOSE_FILE" --profile "$PROFILE" restart + fi + + echo "Gotowe! Wersja aplikacji: $(cat version.txt)" + exit 0 +fi + +echo "Zatrzymuję kontenery aplikacji i bazy..." +if [[ "$PROFILE" == "sqlite" ]]; then + docker compose -f "$COMPOSE_FILE" stop +else + DB_ENGINE="$PROFILE" docker compose -f "$COMPOSE_FILE" --profile "$PROFILE" stop +fi + +echo "Pobieram najnowszy kod z repozytorium..." +git pull + +echo "Uruchamiam kontenery..." if [[ "$PROFILE" == "sqlite" ]]; then docker compose -f "$COMPOSE_FILE" up -d --build else