Harden platform security, reliability, and CI after full audit.

Close deployment IDOR and gate stub payment endpoints, add production
secret validation, health probes, Redis-backed build progress, GitHub
Actions CI, expanded tests, billing/k8s refactors, and ops runbooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-06-29 20:59:49 +03:30
parent a87bc49393
commit 837f0fa63f
83 changed files with 3953 additions and 1308 deletions
+229
View File
@@ -0,0 +1,229 @@
# CloudHost — راهنمای معماری و اجرای وبسایت (Runbook)
این سند دو بخش دارد:
1. **اپلیکیشن چطور کار می‌کند** — معماری و جریان‌ها.
2. **اجرای وبسایت، مرحله‌به‌مرحله** — هم برای توسعه‌ی محلی، هم برای استقرار (deploy) روی کلاستر k3s سرور (abrban) به‌همراه راه‌حل‌های مخصوص شبکه‌ی ایران.
> اصطلاح‌ها: «پنل» = اپ احرازشده (`panel.abrban.com`)، «لندینگ» = صفحه‌ی معرفی (`abrban.com`)، «اپ کاربر» = اپلیکیشنی که مشتری روی CloudHost دیپلوی می‌کند.
> **به‌روزرسانی ۲۰۲۶:** pipeline بیلد فعلی از **Kaniko** + Dockerfileهای نگهداری‌شده توسط پلتفرم استفاده می‌کند (نه Nixpacks/MinIO). آرشیو سورس روی دیسک/PVC آپلود می‌شود. manifest بوت‌استرپ namespace بیلد: [`backend/k8s/builds/cloudhost-builds-bootstrap.yaml`](backend/k8s/builds/cloudhost-builds-bootstrap.yaml).
---
### ۱.۱ CloudHost چیست
یک **PaaS خودسرویس** برای بازار ایران: کاربر کد/ریپوی خودش را می‌دهد و CloudHost آن را build و روی Kubernetes اجرا می‌کند، با مدیریت دامنه، دیتابیس، لاگ، فاکتور و کیف پول.
### ۱.۲ اجزای اصلی
| جزء | تکنولوژی | نقش |
|---|---|---|
| **Frontend** | Next.js (App Router, SSR) | لندینگ + پنل کاربری/ادمین |
| **Backend** | NestJS (REST `/api/v1`) | منطق کسب‌وکار، ساخت اپ، احراز هویت |
| **Postgres** | postgres:16 | دیتابیس اصلی (کاربر، اپ، فاکتور، …) |
| **Redis** | redis:7 | کش، صف Bull (مهاجرت اپ، دسترسی موقت)، پیشرفت بیلد |
| **Registry داخلی** | registry:2 | ایمیج‌های build‌شده |
| **Build pipeline** | Kaniko | تبدیل سورس به ایمیج Docker داخل کلاستر (بدون Docker daemon) |
| **Kubernetes** | k3s (تک‌نود) | اجرای همه‌ی موارد بالا + اپ‌های کاربر |
### ۱.۳ دامنه‌ها (همه روی `78.157.39.52`، HTTPS با wildcard cert)
- `abrban.com` → لندینگ
- `panel.abrban.com` → پنل احرازشده
- `api.abrban.com` → بک‌اند
- `registry.abrban.com` → رجیستری داخلی (pull توسط kubelet)
- `apps.abrban.com` → دامنه‌ی پیش‌فرض اپ‌های کاربر
### ۱.۴ جریان احراز هویت
- ورود مبتنی بر **موبایل + OTP** (پیامک از طریق MizbanSMS) یا رمز عبور.
- توکن **JWT** (access ~15m، refresh ~7d).
- `JwtStrategy` در هر درخواست نقش و فعال‌بودن کاربر را **از دیتابیس** می‌خواند (نه از توکن) تا تغییر نقش/غیرفعال‌سازی بلافاصله اثر کند.
### ۱.۵ جریان دیپلویِ «اپ کاربر» (مهم‌ترین بخش)
وقتی کاربر یک اپ را build/redeploy می‌کند:
```
کاربر (پنل)
│ آپلود zip ──────────────► MinIO (bucket: app-sources) ┐
│ یا git URL │ منبع سورس
▼ │
Backend: یک job در صف Bull («app-deploy») می‌گذارد │
▼ │
ساخت یک Kubernetes Job در namespace «cloudhost-builds»: │
1) init: fetch-source (دانلود از MinIO) یا git-clone ◄───────┘
2) init: nixpacks-prepare
• اگر سورس Dockerfile دارد → همان (BYO)
• وگرنه → با Nixpacks یک Dockerfile می‌سازد
3) container: Kaniko → build ایمیج → push به registry داخلی
(غیرمسدودکننده) اسکن Trivy → خلاصه‌ی آسیب‌پذیری
Backend با Helm، اپ را روی کلاستر بالا می‌آورد (Deployment + Service + Ingress)
GC رجیستری: روزانه فقط N نسخه‌ی آخر هر اپ را نگه می‌دارد
```
- پیشرفت بیلد در Redis نگه‌داری می‌شود؛ فرانت هر ۱.۵ ثانیه `build-progress`/`build-logs` را poll می‌کند.
- توکن git در یک Secret موقتِ هر بیلد می‌نشیند و در `finally` پاک می‌شود (در manifest درج نمی‌شود).
---
## بخش ۲ — اجرای محلی (Local Dev)
پیش‌نیاز: Node 20، Docker، Docker Compose.
```bash
# ۱) دیتابیس و Redis
docker compose up -d # از docker-compose.yml ریشه‌ی پروژه
# ۲) بک‌اند
cd backend
cp .env.example .env # مقادیر را پر کن (DB، JWT، SMS، …)
npm install
npm run start:dev # روی http://localhost:4000 (پیشوند /api/v1)
# ۳) فرانت
cd ../frontend
npm install
npm run dev # روی http://localhost:3000
```
- در حالت dev، TypeORM `synchronize` روشن است و جدول‌ها خودکار ساخته می‌شوند.
- `NEXT_PUBLIC_API_URL` فرانت باید به آدرس بک‌اند اشاره کند.
---
## بخش ۳ — استقرار روی کلاستر (abrban / k3s)
> این بخش فرض می‌کند کلاستر k3s و wildcard cert از قبل آماده‌اند. context کوبه: `default`.
### ۳.۰ ثابت‌های محیط
| | مقدار |
|---|---|
| namespace اپ | `cloudhost` |
| namespace بیلد | `cloudhost-builds` |
| Helm release | `cloudhost` |
| چارت | `backend/helm/cloudhost-platform` |
| values | `/tmp/abrban/values-abrban.yaml` |
| رجیستری (push داخلی) | `registry.cloudhost.svc.cluster.local:5000` (HTTP, insecure) |
| رجیستری (pull توسط kubelet) | `registry.abrban.com` (HTTPS, wildcard cert) — همان storage |
| پروکسی build | `http://builder:<pw>@45.129.38.203:9911` |
### ۳.۱ نکات شبکه‌ی ایران (چرا کارها این‌شکلی‌اند)
- کلاستر به **Let's Encrypt، github، docker.io، ghcr.io، gcr.io** مستقیم نمی‌رسد (یا خیلی کند).
- **gTLS/cert**: دستی، secret `abrban-wildcard-tls` (نه cert-manager).
- **npm**: از `registry.npmmirror.com` مستقیم (نه پروکسی).
- **ایمیج‌های پایه**: اول داخل رجیستری داخلی **mirror** می‌شوند، بعد استفاده.
- **دانلودهای build (apk/nix/pip/…)**: از طریق پروکسی بالا.
### ۳.۲ گام‌های استقرار
**گام ۱ — بررسی دسترسی کلاستر**
```bash
kubectl config current-context # باید default باشد
kubectl get nodes
```
**گام ۲ — mirror کردن ایمیج‌های پایه به رجیستری داخلی**
ایمیج‌هایی که کلاستر مستقیم نمی‌تواند pull کند را با یک Job داخل کلاستر کپی کن.
- برای ایمیج‌های **کوچک**: `crane copy <src> registry.cloudhost.svc.cluster.local:5000/<dst> --insecure`
- برای ایمیج‌های **بزرگ** (مثل nixpacks): از **skopeo** استفاده کن — چون بلاب را با PUT یکجا آپلود می‌کند و گیر `PROTOCOL_ERROR` آپلود تکه‌ای crane را ندارد:
```
skopeo copy --override-os linux --override-arch amd64 --dest-tls-verify=false \
docker://<src> docker://registry.cloudhost.svc.cluster.local:5000/<dst>
```
(با `REGISTRY_AUTH_FILE` از secret `kaniko-docker-config` و env پروکسی.)
ایمیج‌های لازم: `minio/minio`, `railwayapp/nixpacks`, `library/alpine:3.19`, `library/postgres`, `library/redis`, و base ای که Nixpacks تولید می‌کند.
**گام ۳ — MinIO (ذخیره‌ی سورس اپ‌ها)**
به‌طور خودکار فقط هنگام **ثبت کلاستر جدید** ساخته می‌شود؛ روی کلاستر موجود دستی بساز: secret `minio-credentials` (`accesskey`/`secretkey`) + PVC ۲۰Gi + Deployment (`registry.abrban.com/minio/minio:latest`) + Service، همه در `cloudhost-builds`. کردنشال پیش‌فرض با config بک‌اند می‌خواند.
**گام ۴ — pull-secret برای namespace بیلد**
```bash
# کپی pull-secret رجیستری به ns بیلد
kubectl get secret registry-pull-secret -n cloudhost -o json \
| jq '.metadata.namespace="cloudhost-builds" | del(.metadata.uid,.metadata.resourceVersion,.metadata.creationTimestamp)' \
| kubectl apply -f -
# وصل به هر دو ServiceAccount که pod بیلد ممکن است از آن‌ها استفاده کند
kubectl patch sa default -n cloudhost-builds -p '{"imagePullSecrets":[{"name":"registry-pull-secret"}]}'
kubectl patch sa kaniko-builder -n cloudhost-builds -p '{"imagePullSecrets":[{"name":"registry-pull-secret"}]}'
```
> ⚠️ `kaniko-builder` حتماً لازم است: pod بیلد با همین SA اجرا می‌شود و init container نیکس‌پکس ایمیجش را از `registry.abrban.com` می‌کشد.
**گام ۵ — build ایمیج‌های frontend/backend (Kaniko)**
سورس را در PVC بیلد (`build-src`) از طریق pod `srcsync` قرار بده، سپس Job‌های Kaniko را اجرا کن.
```bash
# سینک سورس (از working tree؛ tsbuildinfo و dist را حذف کن!)
cd <repo>
tar czf - --exclude=node_modules --exclude=.next --exclude=.git frontend \
| kubectl exec -i srcsync -n cloudhost -- sh -c 'rm -rf /workspace/frontend && tar xzf - -C /workspace'
rm -f backend/tsconfig.tsbuildinfo # ← مهم
tar czf - --exclude=node_modules --exclude=dist --exclude=.git backend \
| kubectl exec -i srcsync -n cloudhost -- sh -c 'rm -rf /workspace/backend && tar xzf - -C /workspace'
# build (manifestهای kaniko-*.yaml: registry-mirror + proxy + npmmirror)
kubectl apply -f /tmp/abrban/kaniko-frontend-<tag>.yaml
kubectl apply -f /tmp/abrban/kaniko-backend-<tag>.yaml
```
**گام ۶ — استقرار با Helm**
```bash
helm upgrade cloudhost backend/helm/cloudhost-platform \
-n cloudhost -f /tmp/abrban/values-abrban.yaml \
--set images.backend.tag=<tag> \
--set images.frontend.tag=<tag> \
--set migrations.enabled=false # ← مهاجرت‌ها روی DB زنده تداخل دارند
```
> بدون `--wait` اجرا کن (وگرنه به‌خاطر کندیِ pull، status اشتباهاً `failed` می‌شود درحالی‌که rollout موفق است).
**گام ۷ — bootstrap اسکیمای دیتابیس (فقط روی DB تازه)**
در پروداکشن `synchronize` خاموش است. روی DB کاملاً تازه: موقتاً `NODE_ENV=development` کن تا synchronize جدول‌ها را بسازد و pricing خودش seed شود، بعد به `production` برگردان. روی DB موجود، فقط مهاجرت‌های idempotent جدید را با یک Job جدا اعمال کن (نه helm hook).
**گام ۸ — تأیید**
```bash
kubectl get deploy -n cloudhost # backend/frontend 1/1
helm status cloudhost -n cloudhost # STATUS: deployed
curl -s -o /dev/null -w '%{http_code}\n' https://panel.abrban.com # 307
curl -s -X POST https://api.abrban.com/api/v1/auth/otp/request \
-H 'Content-Type: application/json' -d '{"phone":"09xxxxxxxxx"}' # {"sent":true}
```
---
## بخش ۴ — env‌های کلیدی build pipeline (روی بک‌اند)
این‌ها در `backend.env` فایل values ست می‌شوند:
| env | مقدار/توضیح |
|---|---|
| `MINIO_SECRET_KEY` | کلید MinIO (هماهنگ با secret) |
| `NIXPACKS_IMAGE` | `registry.abrban.com/railwayapp/nixpacks:latest` (mirror) |
| `NIXPACKS_BUILD_ENV` | `NPM_CONFIG_REGISTRY=https://registry.npmmirror.com` |
| `BUILD_HTTP_PROXY` | پروکسی build (تزریق به Kaniko + init containerها) |
| `BUILD_REGISTRY_MIRROR` | `registry.cloudhost.svc.cluster.local:5000` (pull پایه از mirror) |
| `BUILD_SCAN_ENABLED` | `false` تا وقتی ایمیج Trivy mirror شود |
| `SMS_PROVIDER` + `MIZBANSMS_*` | بدون این‌ها ارسال OTP خطای 503 می‌دهد |
---
## بخش ۵ — عیب‌یابی رایج
| نشانه | علت | راه‌حل |
|---|---|---|
| backend کرش: `Cannot find module '/app/dist/main.js'` | `tsconfig.tsbuildinfo` کهنه در سورس → tsc فایل‌ها را دوباره emit نمی‌کند | قبل از build، `tsconfig.tsbuildinfo` را حذف کن |
| init container بیلد: `no basic auth credentials` | SA `kaniko-builder` بدون pull-secret | گام ۴ را اجرا کن |
| pull ایمیج: `not found` با اینکه push شده | crane در آپلود تکه‌ایِ بلاب بزرگ شکست خورده (tag ناقص) | با **skopeo** دوباره mirror کن |
| pull از docker.io: `TLS handshake timeout` | docker.io از کلاستر بسته است | ایمیج را mirror کن |
| `nixpacks: not found` در init | باینری نیکس‌پکس روی PATH پیش‌فرض نیست | command را با مسیر/ENTRYPOINT درست صدا بزن |
| helm: `no template "...namespace"` یا `nil pointer redis.enabled` | فایل‌های چارت (`_helpers.tpl`/`values.yaml`) از `/tmp` پاک شده‌اند | از سورس اصلی بازیابی + ادیت‌های deploy را دوباره اعمال کن |
| OTP خطای 503 | env پیامک ست نیست | `SMS_PROVIDER`/`MIZBANSMS_*` را ست کن |
> ⚠️ پوشه‌ی `/tmp` در macOS فایل‌های قدیمی‌تر از ~۳ روز را پاک می‌کند؛ درخت کاری deploy در `/tmp` ممکن است فایل از دست بدهد — قبل از build بررسی کن.
---
## بخش ۶ — به‌روزرسانی نسخه (خلاصه)
1. تغییرات کد را در سورس اعمال کن (`tsc --noEmit` بگیر).
2. tag جدید انتخاب کن.
3. سورس را در `srcsync` سینک کن (با حذف `tsbuildinfo`).
4. Job Kaniko را با tag جدید بساز.
5. `helm upgrade ... --set images.*.tag=<tag> --set migrations.enabled=false` (بدون `--wait`).
6. تأیید کن: podها 1/1، helm `deployed`، endpoint‌ها سالم.