feat(stack): full overhaul — hardening, ops tooling, monitoring, analytics, chatbot

Security: deep /api/health (DB+MinIO probes, 503 on failure) + /api/live;
rate-limited public forms with honeypots; real client IP behind Traefik
(--proxy-headers); TipTap URL sanitization on write (nh3); non-root
containers + slim .dockerignore; prod MinIO console bound to 127.0.0.1;
HSTS/nosniff on TLS routers; JWT 8h; no default credentials in code.

Reliability: Alembic migrations (create_all baseline); graceful shutdown
(exec uvicorn); hardened backups (checksums, sanity check, freshness gauge,
optional rclone off-site) + restore.sh + hourly db-backup sidecar (ops
profile); stack.sh ops tooling (deploy/logs/health/backup/restore/monitoring);
.gitattributes LF for shell scripts.

Orchestration: json-file log rotation, pinned images, per-service memory
limits, edge/internal networks, Traefik ping/accesslog/metrics + flood
limiter, conditional minio-setup, pgvector db image, one-shot db-init
(umami + aramesh_chatbot databases).

Chatbot: unified from site-chatbot branch (RAG + pgvector, SSE, ChatWidget).

Monitoring: Prometheus/Alertmanager/Loki/Promtail/Grafana + cAdvisor,
node/postgres/blackbox exporters, provisioned dashboards and 10 alert
rules; alerts delivered to Bale (tapi.bale.ai — Telegram-compatible).

Analytics: Umami on shared Postgres, build-time tracker injection, custom
events (contact/newsletter/phone/chat/lang) via frontend/src/analytics.ts.

Hygiene: CSV export for contact messages and subscribers; upload image
optimization (Pillow); CI script + GitHub-compatible workflow; audit.sh;
gen-env.sh for strong credentials.
This commit is contained in:
MMD
2026-09-19 00:24:57 +03:30
parent 805f4594d4
commit ce0735d775
75 changed files with 4287 additions and 153 deletions
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# ممیزی وابستگی‌ها — آسیب‌پذیری‌های شناخته‌شده در پکیج‌های پایتون/نود و ایمیج‌های داکر.
# bash scripts/audit.sh
# ابزارها اگر نصب نباشند رد می‌شوند (پیام هشدار)؛ خروجی nonzero یعنی مشکل پیدا شد.
set -uo pipefail
cd "$(dirname "$0")/.."
STATUS=0
echo "== pip-audit (بک‌اند) =="
if command -v pip-audit >/dev/null 2>&1; then
(cd backend && pip-audit -r requirements.txt --progress-spinner off) || STATUS=1
else
echo "رد شد: pip-audit نصب نیست (pip install pip-audit)"
fi
echo "== npm audit (فرانت‌اند) =="
if [ -d frontend/node_modules ]; then
(cd frontend && npm audit --omit=dev) || STATUS=1
else
echo "رد شد: frontend/node_modules نیست (npm install)"
fi
echo "== trivy (ایمیج‌های داکر) =="
if command -v trivy >/dev/null 2>&1; then
for img in aramesh-backend-backend aramesh-backend-frontend aramesh-backend-chatbot; do
if docker image inspect "$img" >/dev/null 2>&1; then
trivy image --severity HIGH,CRITICAL "$img" || STATUS=1
fi
done
else
echo "رد شد: trivy نصب نیست"
fi
if [ "$STATUS" -eq 0 ]; then
echo "✔ ممیزی تمام شد — مشکل شناخته‌شده‌ای پیدا نشد."
else
echo "✗ ممیزی مشکل پیدا کرد — خروجی بالا را بررسی کنید." >&2
fi
exit "$STATUS"
+32 -3
View File
@@ -4,8 +4,12 @@
# اجرا: bash scripts/backup.sh
# زمان‌بندی: cron (لینوکس) یا Task Scheduler (ویندوز)، مثلا هر شب ساعت ۳:
# 0 3 * * * cd /path/to/aramesh-backend && bash scripts/backup.sh >> backups/backup.log 2>&1
# یا سرویس db-backup در پروفایل ops (پشتیبان ساعتی DB بدون cron میزبان).
#
# نگهداری: فایل‌های قدیمی‌تر از RETENTION_DAYS (پیش‌فرض ۱۴) حذف می‌شوند.
# انتشار خارج از سرور: اگر OFFSITE_RCLONE_REMOTE تنظیم شده باشد (در .env، مثل
# OFFSITE_RCLONE_REMOTE=storage:aramesh-backups)، بعد از هر پشتیبان rclone copy انجام می‌شود.
# تازگی پشتیبان: backups/backup_freshness.prom برای هشدار «پشتیبان کهنه» در Grafana نوشته می‌شود.
set -euo pipefail
cd "$(dirname "$0")/.."
@@ -14,7 +18,7 @@ RETENTION_DAYS="${RETENTION_DAYS:-14}"
STAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p backups
# متغیرهای .env (برای MINIO_ROOT_*) در دسترس قرار می‌گیرند
# متغیرهای .env (برای MINIO_ROOT_* و OFFSITE_RCLONE_REMOTE) در دسترس قرار می‌گیرند
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
@@ -26,7 +30,18 @@ echo "[backup] $STAMP"
# ۱) دیتابیس — همه متن‌ها، مقاله‌ها و تنظیمات سایت
DB_FILE="backups/db-${STAMP}.sql.gz"
docker compose exec -T db pg_dump -U aramesh arameshplaza | gzip > "$DB_FILE"
# --exit-on-error: اگر pg_dump وسط کار خطا بگیرد، پشتیبان ناقص تولید نمی‌شود
docker compose exec -T db pg_dump --exit-on-error -U aramesh arameshplaza | gzip > "$DB_FILE"
# پشتیبان خالی/کوچک یعنی خرابی — زودتر بفهمیم تا «روز نیاز»
MIN_DB_BYTES=1024
DB_BYTES=$(wc -c < "$DB_FILE" | tr -d ' ')
if [ "$DB_BYTES" -lt "$MIN_DB_BYTES" ]; then
echo "[backup] ✗ پشتیبان دیتابیس شکسته است (${DB_BYTES} بایت)" >&2
rm -f "$DB_FILE"
exit 1
fi
sha256sum "$DB_FILE" > "${DB_FILE}.sha256"
echo "[backup] db: $DB_FILE ($(du -h "$DB_FILE" | cut -f1))"
# ۲) تصاویر MinIO — باکت aramesh-media
@@ -36,9 +51,23 @@ docker compose run --rm -v "$(pwd)/backups:/backups" --entrypoint sh mc -c "
"
echo "[backup] images: backups/images-${STAMP}"
# ۳) پاک‌سازی نسخه‌های قدیمی‌تر
# ۳) انتشار خارج از سرور (اختیاری) — هر شب کپی به حافظهٔ دیگری
if [ -n "${OFFSITE_RCLONE_REMOTE:-}" ]; then
if command -v rclone >/dev/null 2>&1; then
rclone copy backups "${OFFSITE_RCLONE_REMOTE}" --max-age 48h
echo "[backup] offsite: ${OFFSITE_RCLONE_REMOTE}"
else
echo "[backup] هشدار: OFFSITE_RCLONE_REMOTE تنظیم است اما rclone نصب نیست" >&2
fi
fi
# ۴) پاک‌سازی نسخه‌های قدیمی‌تر
find backups -name 'db-*.sql.gz' -mtime "+${RETENTION_DAYS}" -delete
find backups -name 'db-*.sql.gz.sha256' -mtime "+${RETENTION_DAYS}" -delete
find backups -maxdepth 1 -type d -name 'images-*' -mtime "+${RETENTION_DAYS}" -exec rm -rf {} +
echo "[backup] پاک‌سازی نسخه‌های قدیمی‌تر از ${RETENTION_DAYS} روز انجام شد"
# ۵) سنجهٔ تازگی پشتیبان — Prometheus آن را می‌خواند؛ کهنه‌شدن یعنی کرون خفتی است
printf 'backup_success_time %s\n' "$(date +%s)" > backups/backup_freshness.prom
echo "[backup] ✔ تمام شد"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# تست‌ها + بیلد ایمیج‌ها — قابل اجرا محلی یا در هر CI (Gitea Actions/GitLab CI سازگار است)
# bash scripts/ci.sh
set -euo pipefail
cd "$(dirname "$0")/.."
echo "== تست‌های بک‌اند =="
if [ -d backend/.venv ]; then
# shellcheck disable=SC1091
source backend/.venv/bin/activate 2>/dev/null || source backend/.venv/Scripts/activate
(cd backend && pytest -q)
elif command -v pytest >/dev/null 2>&1; then
(cd backend && pytest -q)
else
echo "رد شد: pytest در دسترس نیست (backend/.venv را بسازید)"
fi
echo "== بیلد ایمیج‌های داکر =="
docker compose -f docker-compose.yml build backend frontend chatbot
echo "== ممیزی وابستگی‌ها =="
bash scripts/audit.sh || true
echo "✔ CI تمام شد"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# ساخت .env با مقادیر قوی تصادفی — پیش از اولین استقرار تولید یک‌بار اجرا کنید.
#
# bash scripts/gen-env.sh # فایل .env می‌سازد (موجود را بازنویسی نمی‌کند)
# bash scripts/gen-env.sh --force # .env موجود را بازنویسی می‌کند (احتیاط!)
#
# توجه: تغییر SECRET_KEY همه ادمین‌ها را از پنل خارج می‌کند؛ تغییر MINIO_ROOT_PASSWORD
# فقط با همگام‌سازی S3_SECRET_KEY بک‌اند اعتبار دارد.
set -euo pipefail
cd "$(dirname "$0")/.."
if [ -f .env ] && [ "${1:-}" != "--force" ]; then
echo "خطا: .env از قبل موجود است. برای بازنویسی: bash scripts/gen-env.sh --force" >&2
exit 1
fi
rand() { openssl rand -"$1" 2>/dev/null || head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n'; }
SECRET_KEY="$(rand hex 48)"
POSTGRES_PASSWORD="$(rand hex 16)"
MINIO_ROOT_PASSWORD="$(rand hex 16)"
ADMIN_PASSWORD="$(rand hex 12)"
cat > .env <<EOF
# تولیدشده با scripts/gen-env.sh — این فایل هرگز به گیت نمی‌رود.
# پیش از انتشار واقعی، ADMIN_PASSWORD را به مقداری که به یاد می‌سپارید اما قوی است تغییر دهید:
# docker compose exec backend python -c "from app.auth import hash_password; print(hash_password('رمز-جدید'))"
# سپس در پنل مدیریت از تغییر رمز (change-password) استفاده کنید.
SECRET_KEY=${SECRET_KEY}
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
MINIO_ROOT_USER=aramesh
MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
ADMIN_USERNAME=admin
ADMIN_PASSWORD=${ADMIN_PASSWORD}
TRAEFIK_HTTP_PORT=18081
SITE_URL=
# --- فقط تولید ---
# DOMAIN=example.com
# ACME_EMAIL=you@example.com
# --- تحلیل رفتار (Umami) ---
# VITE_API_URL=
# VITE_UMAMI_SRC=
# VITE_UMAMI_WEBSITE_ID=
# --- چت‌بات ---
# LLM_BASE_URL=
# LLM_API_KEY=
# LLM_MODEL=openai/gpt-oss-120b
# CHAT_REINDEX_TOKEN=
# --- مانیتورینگ ---
# GRAFANA_ADMIN_PASSWORD=
# ALERT_BOT_TOKEN=
# ALERT_CHAT_ID=
EOF
echo "✔ .env ساخته شد با مقادیر قوی تصادفی."
echo " ADMIN_PASSWORD فعلی: ${ADMIN_PASSWORD}"
echo " (آن را امن نگه دارید — پس از ورود به پنل، رمز را عوض کنید.)"
+13 -1
View File
@@ -27,11 +27,23 @@ import uuid
from collections import defaultdict
from io import BytesIO
# اگر .env ریشه هست، متغیرهایش (ADMIN_PASSWORD و ...) خوانده می‌شوند
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_ENV_FILE = os.path.join(_REPO_ROOT, ".env")
if os.path.exists(_ENV_FILE):
with open(_ENV_FILE, encoding="utf-8") as _f:
for _line in _f:
_line = _line.strip()
if _line and not _line.startswith("#") and "=" in _line:
_k, _, _v = _line.partition("=")
os.environ.setdefault(_k.strip(), _v.strip())
import requests
API = os.environ.get("VITE_API_URL", "http://localhost:5173")
USER = os.environ.get("ADMIN_USERNAME", "admin")
PASS = os.environ.get("ADMIN_PASSWORD", "aramesh123")
# رمز پیش‌فرض ندارد — از .env یا متغیر محیطی خوانده می‌شود؛ نبودش یعنی توقف با خطا
PASS = os.environ["ADMIN_PASSWORD"]
FORCE = "--force" in sys.argv
print(f"API بک‌اند: {API}")
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# بازگردانی پشتیبان دیتابیس — از بازگردانی در محیط واقعی قبلش تمرین کنید!
#
# bash scripts/restore.sh backups/db-20260918-030000.sql.gz
#
# احتیاط: محتوای فعلی دیتابیس با محتوای پشتیبان جایگزین می‌شود.
# تصاویر MinIO هم اگر پوشه images-* همان زمان را دارید:
# docker compose run --rm -v "$(pwd)/backups:/backups" --entrypoint sh mc -c \
# "mc alias set local http://minio:9000 \"\$MINIO_ROOT_USER\" \"\$MINIO_ROOT_PASSWORD\" && \
# mc mirror --overwrite /backups/images-20260918-030000 local/aramesh-media"
set -euo pipefail
cd "$(dirname "$0")/.."
if [ $# -ne 1 ]; then
echo "استفاده: bash scripts/restore.sh backups/db-<زمان>.sql.gz" >&2
exit 1
fi
SQL_GZ="$1"
if [ ! -f "$SQL_GZ" ]; then
echo "خطا: فایل ${SQL_GZ} پیدا نشد" >&2
exit 1
fi
# راستی‌آزمایی چک‌سام اگر کنار فایل هست
if [ -f "${SQL_GZ}.sha256" ]; then
sha256sum -c "${SQL_GZ}.sha256"
fi
echo "[restore] هشدار: محتوای فعلی arameshplaza جایگزین می‌شود. ۵ ثانیه فرصت لغو (Ctrl+C)…"
sleep 5
gunzip -c "$SQL_GZ" | docker compose exec -T db psql -v ON_ERROR_STOP=1 -U aramesh -d arameshplaza
echo "[restore] ✔ بازگردانی تمام شد — حالا: docker compose restart backend"
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# ابزار orchestration استک — یک نقطه ورود برای همه عملیات رایج.
#
# bash scripts/stack.sh deploy # بیلد فرانت‌اند (روی هاست) + بیلد و بالا آوردن استک
# bash scripts/stack.sh deploy-prod # همین، با لایه TLS/دامنه (docker-compose.prod.yml)
# bash scripts/stack.sh logs [svc] # دنبال‌کردن لاگ (همه یا یک سرویس)
# bash scripts/stack.sh ps # وضعیت سرویس‌ها
# bash scripts/stack.sh health # وضعیت healthcheck همه سرویس‌ها
# bash scripts/stack.sh backup # پشتیبان‌گیری کامل (scripts/backup.sh)
# bash scripts/stack.sh restore FILE # بازگردانی دیتابیس (scripts/restore.sh)
# bash scripts/stack.sh update # کشیدن ایمیج‌های پین‌شده و بالا آوردن مجدد
# bash scripts/stack.sh monitoring # لایه مانیتورینگ (Prometheus/Grafana/Loki/بله)
# bash scripts/stack.sh seed-force # seed مجدد محتوا (--force — داده‌های جدول‌ها ریست می‌شود)
#
# متغیرهای بیلد فرانت‌اند (VITE_API_URL، VITE_UMAMI_SRC، VITE_UMAMI_WEBSITE_ID)
# از .env ریشه خوانده و به npm run build پاس می‌شوند — متغیرهای Vite در زمان بیلد
# روی هاست معنا دارند، نه در ایمیج Caddy.
set -euo pipefail
cd "$(dirname "$0")/.."
DEV_FILES=(-f docker-compose.yml)
[ -f docker-compose.override.yml ] && DEV_FILES+=(-f docker-compose.override.yml)
# compose dev ... | compose prod ... — انتخاب فایل‌های لایه‌ای
compose() {
local mode="$1"
shift
if [ "$mode" = "prod" ]; then
docker compose -f docker-compose.yml -f docker-compose.prod.yml "$@"
else
docker compose "${DEV_FILES[@]}" "$@"
fi
}
load_env_vars() {
# فقط متغیرهای بیلد فرانت‌اند — بدون خروجی
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
fi
}
render_alertmanager() {
# قالب alertmanager.yml با مقادیر .env رندر می‌شود — Alertmanager متغیر محیطی
# را در کانفیگ جایگزین نمی‌کند؛ خروجی (rendered) در گیت نمی‌آید.
load_env_vars
local tpl="monitoring/alertmanager/alertmanager.yml"
local out="monitoring/alertmanager/alertmanager.rendered.yml"
local bot="${ALERT_BOT_TOKEN:-0000000000:PLACEHOLDER-TOKEN}"
local chat="${ALERT_CHAT_ID:-0}"
sed -e "s|\${ALERT_BOT_TOKEN}|${bot}|g" \
-e "s|\${ALERT_CHAT_ID}|${chat}|g" \
"$tpl" > "$out"
if [ -n "${ALERT_BOT_TOKEN:-}" ]; then
echo "[stack] alertmanager config rendered (bale token set)"
else
echo "[stack] alertmanager config rendered (no bale token — alerts will not deliver)"
fi
}
build_frontend() {
load_env_vars
echo "[stack] بیلد فرانت‌اند روی هاست…"
(cd frontend \
&& VITE_API_URL="${VITE_API_URL:-}" \
VITE_UMAMI_SRC="${VITE_UMAMI_SRC:-}" \
VITE_UMAMI_WEBSITE_ID="${VITE_UMAMI_WEBSITE_ID:-}" \
npm run build)
if [ ! -f frontend/dist/index.html ]; then
echo "[stack] ✗ بیلد فرانت‌اند خروجی نداد (frontend/dist)" >&2
exit 1
fi
}
wait_healthy() {
local mode="$1"
echo "[stack] منتظر سلامت سرویس‌ها (حداکثر ۱۸۰ ثانیه)…"
local deadline=$((SECONDS + 180))
local unhealthy=""
while [ $SECONDS -lt $deadline ]; do
unhealthy=$(compose "$mode" ps --format '{{.Service}} {{.Health}}' 2>/dev/null \
| awk '$2=="starting" || $2=="unhealthy" {print $1":"$2}')
if [ -z "$unhealthy" ]; then
echo "[stack] ✔ همه سرویس‌ها سالم‌اند"
return 0
fi
sleep 5
done
echo "[stack] ✗ برخی سرویس‌ها سالم نشدند: ${unhealthy:-نامشخص}" >&2
compose "$mode" ps
return 1
}
do_deploy() {
local mode="$1"
build_frontend
echo "[stack] بیلد و بالا آوردن استک…"
compose "$mode" up -d --build
wait_healthy "$mode"
docker system prune -f >/dev/null 2>&1 || true
echo "[stack] ✔ استقرار تمام شد"
if [ -z "${VITE_UMAMI_WEBSITE_ID:-}" ]; then
echo "[stack] یادآوری: VITE_UMAMI_WEBSITE_ID در .env خالی است — تحلیل رفتار غیرفعال می‌ماند."
fi
}
case "${1:-}" in
deploy)
do_deploy dev
;;
deploy-prod)
load_env_vars
if [ -z "${DOMAIN:-}" ] || [ -z "${ACME_EMAIL:-}" ]; then
echo "[stack] ✗ برای deploy-prod، DOMAIN و ACME_EMAIL باید در .env تنظیم باشند" >&2
exit 1
fi
do_deploy prod
;;
logs)
shift || true
compose dev logs -f --tail=100 "$@"
;;
ps)
compose dev ps
;;
health)
compose dev ps --format '{{.Service}}: {{.Status}}'
;;
backup)
bash scripts/backup.sh
;;
restore)
shift
bash scripts/restore.sh "$@"
;;
update)
compose dev pull
compose dev up -d
wait_healthy dev
;;
monitoring)
# لایه مانیتورینگ — Prometheus/Grafana/Loki/Alertmanager (+ رندر کانفیگ بله)
render_alertmanager
echo "[stack] بالا آوردن لایه مانیتورینگ…"
docker compose "${DEV_FILES[@]}" -f docker-compose.monitoring.yml up -d --build
wait_healthy dev
echo "[stack] ✔ مانیتورینگ بالا آمد — داشبورد: http://grafana.localhost:${TRAEFIK_HTTP_PORT:-5173}"
;;
seed-force)
compose dev run --rm backend python -m app.seed --force
;;
*)
grep '^# ' "$0" | sed 's/^# //'
exit 1
;;
esac