DEPLOY.md
Rama main · réplica de lectura del repositorio original.
# استقرار Phonix (Docker)
کل سرویس (API + فروشگاه) با Docker Compose روی یک سرور Ubuntu/Debian بالا میآید.
## نصب یکخطی (سرور تازه، مثلاً Hetzner)
روی سروری که فقط IP دارد:
```bash
curl -fsSL https://raw.githubusercontent.com/<user>/<repo>/main/scripts/install.sh \
| sudo REPO_URL=https://github.com/<user>/<repo>.git bash
```
اسکریپت Docker را نصب میکند، ریپو را در `/opt/phonix` کلون میکند، یک `.env` با IP سرور میسازد و کانتینرها را بالا میآورد. **وسط کار، نامکاربری و رمز اکانت مالک/ادمین را همینجا روی ترمینال از شما میپرسد** — این تنها لحظهایست که این اطلاعات تعیین میشوند، جای دیگری خودکار ساخته یا نمایش داده نمیشوند. در پایان آدرس فروشگاه و API را چاپ میکند.
با دامنه (پشت reverse proxy با TLS):
```bash
curl -fsSL https://raw.githubusercontent.com/<user>/<repo>/main/scripts/install.sh \
| sudo REPO_URL=https://github.com/<user>/<repo>.git DOMAIN=example.com bash
```
## نصب دستی
```bash
git clone https://github.com/<user>/<repo>.git /opt/phonix
cd /opt/phonix
cp .env.example .env # مقادیر را ویرایش کنید (URLها و پورتها)
docker compose up -d --build
```
## اجرای محلی (توسعه)
برای اینکه همزمان API و فروشگاه را با یک دستور بالا بیاوری (به همان ترتیبِ سرور):
```powershell
# ویندوز (PowerShell) — هرکدام در پنجرهی جدا با لاگ زنده
./scripts/dev.ps1
```
```bash
# لینوکس / مک / Git Bash — Ctrl+C هر دو را میبندد
bash scripts/dev.sh
```
API روی `http://localhost:5228` و فروشگاه روی `http://localhost:3000` بالا میآید. ورودهای تستی: مدیر `reza/1234`، کاربر `ali/1234`.
> اگر بکاند بالا نباشد، لاگین و هر صفحهای که به API نیاز دارد کار نمیکند — حتماً هر دو با هم اجرا شوند.
## راهاندازی خودکار بعد از ریست سرور
روی سرور دو لایه این را تضمین میکنند و نیازی به دخالت دستی بعد از ریبوت نیست:
1. کانتینرها با `restart: unless-stopped` تعریف شدهاند و سرویس Docker روی بوت `enable` است، پس بعد از ریبوت خودکار برمیگردند.
2. اسکریپت نصب یک یونیت systemd به نام `phonix.service` میسازد و enable میکند که موقع بوت (بعد از `docker.service`) دستور `docker compose up -d` را اجرا میکند.
کنترل دستی:
```bash
systemctl status phonix # وضعیت
systemctl restart phonix # رفرش کل استک
systemctl stop phonix # توقف (docker compose down)
journalctl -u phonix # لاگ راهاندازی
```
ترتیب اجرا (اول بکاند، بعد فرانت) را خودِ Compose با `depends_on` رعایت میکند.
## پیکربندی (`.env`)
| متغیر | توضیح |
|------|-------|
| `NEXT_PUBLIC_API_URL` | آدرس عمومی API که **مرورگر** صدا میزند. در زمان build داخل باندل کلاینت قرار میگیرد، پس قبل از build باید درست باشد. |
| `PHONIX_FRONTEND_URL` | آدرس عمومی فروشگاه؛ برای CORS و لینکهای ایمیل (تأیید/بازنشانی) استفاده میشود. |
| `FRONTEND_PORT` / `BACKEND_PORT` | پورتهای منتشرشده روی هاست (پیشفرض 3000 و 5228). |
| `PHONIX_BEHIND_PROXY` | اگر یک reverse proxy جلوی برنامه TLS را terminate میکند `true` کنید تا IP واقعی کاربر و scheme درست شناسایی شود. |
| `PHONIX_FORCE_HTTPS` | همراه proxy؛ ریدایرکت HTTPS و HSTS را فعال میکند. |
| `PHONIX_OWNER_USERNAME` / `PHONIX_OWNER_PASSWORD` | **الزامی برای اولین راهاندازی.** `install.sh` اینها را **روی همین ترمینال، بهصورت تعاملی** از شما میپرسد (هیچوقت خودکار تولید یا چاپ نمیشوند) — فقط اگر بدون این اسکریپت نصب میکنید لازم است خودتان این دو خط را در `.env` بنویسید. این تنها اکانتیست که از ابتدا وجود دارد؛ همان مالک بعداً از داخل پنل ادمین بقیهی اکانتهای ادمین/کارمند را میسازد. هر بار سرور بالا میآید اعمال میشود — یعنی اگر رمز مدیر را گم کردید، کافیست مقدار جدید در `.env` بگذارید و `docker compose up -d` را دوباره بزنید. |
| `PHONIX_BACKUP_KEY` | اختیاری ولی توصیهشده. بکاپهای خروجی از `/admin/backup` (دانلود یا تلگرام) را با AES-256-GCM رمزنگاری میکند. بدون آن بکاپها ساده (JSON/zip) ذخیره میشوند. یکبار قبل از اولین بکاپ واقعی تنظیم کنید و جایی خارج از سرور نگه دارید — گمکردن این کلید یعنی بکاپهای رمزنگاریشده با آن برای همیشه غیرقابلبازیابی میشوند. |
| `PHONIX_BACKUP_MAX_MEDIA_MB` | اختیاری، پیشفرض `512`. سقف حجم پوشهی آپلودها برای ارسال رسانه به تلگرام. آرشیو کامل در حافظه ساخته و بعد رمزنگاری میشود (یعنی چند برابر حجم خودش رم میخواهد)، پس اگر مدارک کاربران از این سقف بگذرد بکاپ با پیام روشن رد میشود بهجای اینکه سرور با کمبود حافظه بخوابد. روی سرور بزرگتر میتوانید بالاتر ببرید؛ برای گرفتن نسخهی کامل بدون این محدودیت از «دانلود پشتیبان» روی خود سرور استفاده کنید. |
| `PHONIX_CLUSTER_MODE` | اختیاری. `standalone` (پیشفرض) یا `primary`/`standby` برای خوشهبندی HA — به بخش [خوشهبندی و دسترسپذیری بالا](#خوشهبندی-و-دسترسپذیری-بالا-ha) مراجعه کنید. |
| `PHONIX_NODE_ID` | فقط در حالت `primary`/`standby`. یک برچسب کوتاه (مثلاً `germany`) تا صفحهی «مدیریت خوشه» در پنل ادمین سرورها را از هم تشخیص دهد. |
| `PHONIX_CLUSTER_PEER` | فقط در حالت `primary`/`standby`. آدرس قابلدسترس سرور مقابل (مثلاً `https://other-server:5228`). |
| `PHONIX_CLUSTER_SECRET` | فقط در حالت `primary`/`standby`. رمز مشترک بین دو سرور برای تأیید ارتباط (HMAC) — فقط برای همین منظور، جدا از `PHONIX_BACKUP_KEY`. |
| `PHONIX_CLUSTER_WITNESS_URLS` | اختیاری. آدرسهای مستقلی که Standby پیش از ترفیع خودکار با آنها تشخیص میدهد قطعی از سمت سرور مقابل بوده یا از سمت خودش. پیشفرض: `cloudflare.com` و `google.com`. |
> اگر `NEXT_PUBLIC_API_URL` را عوض کردید، فرانت باید دوباره build شود: `docker compose up -d --build frontend`.
## ماندگاری دادهها (Persistence)
کل state داخل volume به نام `phonix-data` (مسیر `/app/App_Data`) ذخیره میشود و با rebuild از بین نمیرود. این شامل **همهٔ** موارد زیر است: فایل `store.json`، لاگ ممیزی (`audit_store.json`)، **تصاویر و فایلهای آپلودشده** (`ProtectedUploads/` — عکس پروفایل/محصول/سایت و مدارک احراز هویت)، کلیدهای نشست (`keys/`) و لاگها.
> ⚠️ بکاپِ فقط `store.json` **کافی نیست** — تصاویر جدا از store.json روی دیسکاند. برای بکاپ کامل از گزینهٔ «بکاپ کامل» پنل `/admin/backup` (که store.json + همهٔ مدیا را در یک zip میگذارد) یا بات بکاپ تلگرام استفاده کنید.
## TLS / دامنه
برای HTTPS یک reverse proxy (مثلاً Caddy یا nginx) جلوی سرویس بگذارید که دامنه را به `frontend:3000` و مسیر `/api` را به `backend:5228` پراکسی کند، سپس در `.env` دو گزینهی `PHONIX_BEHIND_PROXY` و `PHONIX_FORCE_HTTPS` را `true` کنید و فرانت را با `NEXT_PUBLIC_API_URL=https://<domain>/api` دوباره build کنید.
## خوشهبندی و دسترسپذیری بالا (HA)
این قابلیت فقط برای **تداوم کسبوکار** است (قطعی اینترنت/دیتاسنتر) — نه توزیع بار، نه اجرای همزمان دو سرور فعال. در هر لحظه فقط **یک** سرور نوشتنی (Primary) است؛ سرور دوم (Standby) فقطخواندنی میماند و پیوسته با Primary همگام میشود. برای اکثر نصبهای تکسروری این بخش کاملاً قابلصرفنظر است — پیشفرض (`PHONIX_CLUSTER_MODE=standalone`) دقیقاً همان رفتار همیشگی است.
### راهاندازی اولیه
روی هر دو سرور جداگانه `scripts/install.sh` را اجرا کنید:
- **سرور اول (Primary):** در پرسش «Cluster mode» گزینهی ۲ را بزنید. اسکریپت یک `PHONIX_CLUSTER_SECRET` تصادفی میسازد و **فقط همان یکبار** آن را چاپ میکند — آن را کپی کنید.
- **سرور دوم (Standby):** گزینهی ۳ را بزنید، آدرس سرور اول را بهعنوان peer بدهید، و همان `PHONIX_CLUSTER_SECRET` که از سرور اول کپی کردید را پیست کنید.
- در هر دو، آدرس سرور مقابل (`PHONIX_CLUSTER_PEER`) باید از سمت دیگری هم قابلدسترس باشد (پورت باز، یا از طریق VPN/تونل بین دو دیتاسنتر).
> **الزام HTTPS:** آدرس `PHONIX_CLUSTER_PEER` باید حتماً `https://` باشد. ترافیک بین دو سرور شامل سفارشها، اطلاعات کاربران و اسنپشات اولیه است، پس باید رمزنگاریشده باشد؛ سرور با آدرس `http://` بالا نمیآید و پیام خطای واضحی میدهد. فقط برای تست محلی میتوانید `PHONIX_CLUSTER_ALLOW_INSECURE=true` بگذارید.
بعد از بالا آمدن هر دو کانتینر، همگامسازی خودکار و در پسزمینه شروع میشود — بدون نیاز به هیچ اقدام دستی. وضعیت را از پنل ادمین → «مدیریت خوشه (HA)» ببینید: نقش هر سرور، وضعیت اتصال، تعداد رویدادهای در انتظار همگامسازی، و تعداد رویدادهای ناموفق (dead-letter).
### تنظیم از پنل ادمین (بدون ترمینال)
راهاندازی اولیه بالا از طریق `install.sh`/env varها است، ولی هر سه مقدار (حالت، آدرس سرور مقابل، کلید مشترک) از پنل ادمین → «مدیریت خوشه (HA)» → «تنظیمات دستی» هم قابل تنظیم/تغییرند — زنده و بدون نیاز به ریاستارت:
- فعالسازی خوشه از حالت Standalone: انتخاب Primary/Standby + آدرس Peer + کلید مشترک.
- روی سروری که خوشه از قبل فعال است: تغییر آدرس Peer یا چرخش (rotate) کلید مشترک، در هر لحظه.
- اگر نقش یک سرور اشتباه تنظیم شده باشد (مثلاً باید Primary میبود ولی Standby ست شده) و نه Promote نه Recovery برایش فعال نیست، دکمهی «اصلاح دستی نقش» نقش را مستقیم تغییر میدهد — این مسیر بدون هماهنگی با سرور مقابل است، پس فقط برای اصلاح یک اشتباه استفاده شود، نه بهجای Promote/Recovery عادی (وگرنه ریسک split-brain دارد).
همین صفحه یک گزارش دقیقتر هم دارد: تاریخچهی آخرین failover/ترفیع/تنزل، وضعیت رزرو بازهی id، Data Epoch این سرور و سرور مقابل، و یک لاگ رویدادهای اخیر (فقط در حافظه، با ریاستارت پاک میشود).
### اتصال یک Standby جدید به Primary دارای داده (Bootstrap)
اگر Primary از قبل دادهی واقعی دارد (مثلاً سرور آلمان) و Standby تازه نصب شده (مثلاً سرور ایران)، لازم نیست هر دو از صفر شروع کنند. یک Standby **خالی** هنگام اولین اجرا بهطور خودکار یک اسنپشات کامل از Primary میگیرد، آن را بازیابی میکند، مکاننمای همگامسازی را روی نقطهی درست تنظیم میکند و سپس فایلهای رسانه را میکشد. اگر خواستید این کار را دستی انجام دهید (یا دوباره اجرا کنید)، از پنل ادمین → «مدیریت خوشه» روی Standby دکمهی **«راهاندازی اولیه از Primary»** را بزنید. این عمل فقط روی Standby ممکن است تا هرگز دادهی زندهی Primary بازنویسی نشود.
### همگامسازی فایلهای رسانه
علاوه بر پایگاهداده، فایلهای آپلودشده (مدارک احراز هویت KYC، تصاویر کاربران، عکس محصولات/سایت، آواتارها) هم بهصورت افزایشی و با بررسی checksum (SHA-256) همگام میشوند: فقط فایلهایی که Standby ندارد یا checksum متفاوت دارند منتقل میشوند، انتقال خراب هرگز نوشته نمیشود، و هیچ فایل معتبری حذف نمیشود. بازهی این کار با `PHONIX_CLUSTER_MEDIA_SYNC_INTERVAL_SECONDS` (پیشفرض ۶۰ ثانیه) تنظیم میشود.
### قطعی (Failover) — خودکار
> **تفکیک «سرور مقابل خوابیده» از «من قطع شدهام»:** ندیدن سرور مقابل بهتنهایی چیزی را ثابت نمیکند. اگر اینترنت بینالملل همین سرور قطع شود یا IP سرور مقابل فیلتر شود، سرور اصلی همچنان سالم است و به مشتریها سرویس میدهد؛ ترفیع در چنین حالتی یعنی دو Primary همزمان و از دست رفتن نوشتههای یک طرف وقتی ارتباط برگردد. دو محافظ جلوی این را میگیرند:
>
> ۱. **ارتباط ورودی از سرور مقابل.** اگر همین Standby نتواند به Primary برسد ولی Primary همچنان به او درخواست بفرستد، یعنی مسیر فقط یکطرفه بسته است و سرور مقابل زنده است. چون این درخواستها با کلید مشترک امضا میشوند و فقط سرور مقابل میتواند بسازدشان، وجودشان مدرک قاطع زندهبودن اوست و بر سکوت خروجی ارجحیت دارد. سرور مقابلِ واقعاً خوابیده هیچ درخواستی نمیفرستد، پس failover واقعی دستنخورده میماند. **این حالت روی مسیرهای فیلترشده رایجترین سناریوست.**
>
> ۲. **سنجش دسترسی خودِ سرور.** پیش از ترفیع خودکار، Standby دسترسی خودش به اینترنت را با چند آدرس مستقل (`PHONIX_CLUSTER_WITNESS_URLS`) میسنجد: اگر آنها هم جواب ندهند، ترفیع انجام **نمیشود**، سرور فقطخواندنی میماند و هشدار تلگرام ارسال میشود. توجه: این محافظ فقط قطعی کامل اینترنت را میگیرد؛ وقتی فقط IP سرور مقابل فیلتر شده باشد این آدرسها سالم جواب میدهند و تشخیص بر عهدهی محافظ اول است.
اگر Standby برای مدتی (پیشفرض ۹۰ ثانیه، قابلتنظیم با `PHONIX_CLUSTER_FAILOVER_GRACE_SECONDS`) نتواند Primary را ببیند، **خودش را بهطور خودکار Primary میکند** و همهچیز (سفارشها، تیکتها، پرداختها، پنل ادمین) روی همان سرور ادامه پیدا میکند — بدون نیاز به دخالت انسانی.
### بازگشت (Recovery و Failback) — همیشه دستی
وقتی سرور اصلی دوباره در دسترس قرار میگیرد، **خودش را دوباره Primary نمیکند** — این عمدی است. بهجای آن وارد حالت «Recovering» میشود: فقطخواندنی میماند و هرچه در نبودش رخ داده را از سرور دیگر میگیرد. در پنل ادمین → «مدیریت خوشه»:
1. صبر کنید تا «رویدادهای در انتظار همگامسازی» صفر شود (یا دکمهی «همگامسازی دستی» را بزنید).
2. دکمهی «ترفیع به Primary» را بزنید — این کار فقط وقتی همگامسازی کامل باشد و سرور مقابل در دسترس باشد انجام میشود؛ سرور مقابل بهطور هماهنگ به Standby تنزل مییابد.
اگر لازم شد این سرور را عمداً از حالت Primary خارج کنید (مثلاً برای نگهداری)، دکمهی «شروع بازیابی (Recovery)» آن را به حالت فقطخواندنی/در حال همگامسازی میبرد.
### نکات مهم
- بکاپگیری/بازیابی (`/admin/backup`) کاملاً مستقل از خوشهبندی کار میکند و رفتارش تغییری نکرده — یک بازیابی کامل، همگامسازی خوشه را از نو شروع میکند (نه اینکه با آن تداخل کند).
- ورود ادمین/مشتری، ۲FA، CSRF و بقیهی لایههای امنیتی دقیقاً همان قبل هستند؛ ارتباط بین دو سرور با `PHONIX_CLUSTER_SECRET` (HMAC) جدا تأیید میشود، نه با نشست کاربر.
- در حالت `standalone` مسیرهای `/api/cluster/*` مربوط به ارتباط بین سرورها اصلاً پاسخ نمیدهند (404).
- روی Standby، درخواستهای ورود/ثبتنام/بازیابی رمز/نشست (`/api/auth/*`) همچنان کار میکنند تا کاربران در زمان failover هم بتوانند احراز هویت شوند؛ فقط عملیات کسبوکار (سفارش، محصول، تنظیمات، عملیات مالی) روی Standby مسدود میشوند و با کد 503 به سمت Primary هدایت میشوند.
- یک رویداد خرابِ همگامسازی کل خوشه را متوقف نمیکند: چنین رویدادی در صف dead-letter ثبت و چند بار بازآزمایی میشود و تعدادش در صفحهی «مدیریت خوشه» دیده میشود؛ مکاننمای همگامسازی پشت آن گیر نمیکند.
- صف رویدادهای همگامسازی خودش را جمع میکند: رویدادی که Standby مصرفش را اعلام کرده و از یک بازهی زمانی هم گذشته باشد حذف میشود. اگر Standby عقب، خاموش یا اصلاً تنظیمنشده باشد این مکاننما جلو نمیرود و هیچ رویدادی که هنوز لازم دارد پاک نمیشود. بدون این، صف تا ابد بزرگ میشد و حجم دیتابیس، اسنپشات اولیه و هر بکاپ را با خودش بالا میبرد.
## مانیتورینگ و هشدار
- **Health check:** بکاند مسیر `GET /health` را دارد (وضعیت + بررسی بارگذاری store، خروجی JSON). هم Docker از آن برای healthcheck استفاده میکند و هم میتوانید یک مانیتور بیرونی (مثل UptimeRobot) را روی همین آدرس تنظیم کنید.
- **Docker healthcheck:** هر دو سرویس healthcheck دارند؛ فرانت تا زمانی که بکاند `healthy` نشود بالا نمیآید و کانتینرِ ناسالم طبق سیاست `restart` راهاندازی مجدد میشود.
- **هشدار تلگرام:** در پنل `/admin/backup` گزینهی «هشدار خطا و راهاندازی سرور» را روشن کنید (از همان توکن بات و چت بکاپ استفاده میکند). با فعالبودن، هر خطای داخلی سرور (۵۰۰) و هر بار راهاندازی مجدد به تلگرام اطلاع داده میشود؛ هشدارهای تکراری حداکثر هر ۵ دقیقه یکبار ارسال میشوند. با دکمهی «ارسال هشدار آزمایشی» اتصال را بسنجید.
## تست خودکار
```bash
cd backend
dotnet test
```
پوشهی `backend/tests/Phonix.Api.Tests` شامل تستهای واحد (هش گذرواژه، صفحهبندی، منطق سفارش/تخفیف/لغو و کنترل موجودی) و تستهای یکپارچهی HTTP (ورود، حساب مسدود، سلامت سرویس) است. این تستها در CI هم اجرا میشوند.
## CI/CD
- **CI** (`.github/workflows/ci.yml`): با هر push/PR روی `main`، بکاند (Release) + **تستهای خودکار** و فرانت (type-check + build) و هر دو ایمیج Docker ساخته/اجرا میشوند.
- **CD** (`.github/workflows/deploy.yml`): غیرفعال است تا زمانی که فعالش کنید. در Settings → Secrets and variables → Actions:
- متغیر (Variable): `DEPLOY_ENABLED=true` و در صورت نیاز `DEPLOY_PATH`
- Secretها: `DEPLOY_HOST`، `DEPLOY_USER`، `DEPLOY_SSH_KEY`
بعد از آن هر push روی `main` بهصورت خودکار روی سرور `git pull` و `docker compose up -d --build` اجرا میکند (سرور باید یکبار با اسکریپت نصب راهاندازی شده باشد).