223 lines
16 KiB
Markdown
223 lines
16 KiB
Markdown
<div dir="rtl">
|
||
|
||
# سامانهٔ جامع آمار و اطلاعات «دیدوان»
|
||
|
||
داشبورد زندهٔ بازار فلزات، فولاد، ارز، طلا/سکه، کامودیتی و کریپتو — بههمراه آرشیو گزارشهای منابع معتبر جهانی. این سند برای **توسعهدهندهٔ بعدی** نوشته شده؛ کل معماری، نحوهٔ کار، راهاندازی، امنیت و دامها (gotchas) را پوشش میدهد. کامل بخوان.
|
||
|
||
---
|
||
|
||
## ۱) این سیستم چیست؟
|
||
|
||
یک اپلیکیشن سهبخشی:
|
||
|
||
| بخش | تکنولوژی | پورت | نقش |
|
||
|---|---|---|---|
|
||
| **فرانتاند** | TanStack Start + React 19 + Vite 7 + Tailwind v4 | `5174` | لندینگ، ورود/ثبتنام، داشبورد |
|
||
| **بکاند (API)** | FastAPI + SQLite + Playwright | `8000` | اسکرپرها، API دادهٔ بازار، احراز هویت |
|
||
| **پنل ادمین** | FastAPI (HTML درونخطی) | `8001` | بازبینی/تأیید کاربران، مدیریت توکن، سلامت سرویسها، گزارشها |
|
||
|
||
دیتای بازار با **اسکرپرهای پسزمینه** جمع میشود و در **SQLite** ذخیره میشود (فقط مقادیرِ تغییرکرده — dedup). فرانتاند هر چند ثانیه از API میخواند و «زنده» نشان میدهد.
|
||
|
||
---
|
||
|
||
## ۲) ساختار پوشهها
|
||
|
||
```
|
||
asianmetal-dashboard/
|
||
├─ src/ # فرانتاند
|
||
│ ├─ routes/ # مسیرهای فایلمحورِ TanStack
|
||
│ │ ├─ index.tsx # لندینگ (LaserFlow، پنل زنده، گالریها، …)
|
||
│ │ ├─ login.tsx # ورود (کاراکترهای کارتونی + احراز واقعی)
|
||
│ │ ├─ register.tsx # ثبتنام (→ pending)
|
||
│ │ ├─ dashboard.tsx # داشبورد + همهٔ تبها/کارتها/چارتها
|
||
│ │ └─ __root.tsx # لایهٔ ریشه (RTL، فونت Vazirmatn، لود styles)
|
||
│ ├─ components/ # LaserFlow, CircularGallery, CardSwap, TubelightNavbar, …
|
||
│ ├─ components/ui/ # shadcn/ui
|
||
│ ├─ lib/ # labels.ts، metals-data.ts، utils.ts
|
||
│ ├─ styles.css # توکنهای تم (oklch)، .tile/.glass/.panel، انیمیشنها
|
||
│ └─ routeTree.gen.ts # خودکار توسط TanStack (دستی ویرایش نکن)
|
||
│
|
||
├─ backend/ # API + اسکرپرها
|
||
│ ├─ main.py # اپ FastAPI، CORS/هدرها/ریتلیمیت، همهٔ اندپوینتها، lifespan
|
||
│ ├─ db.py # اتصال SQLite + init_db (ساخت/مهاجرت جداول)
|
||
│ ├─ auth.py # هشِ رمز (PBKDF2) + نشستهای سمتسرور
|
||
│ ├─ currency_stream.py # اسکرپِ ارز/طلا/سکهٔ داخلی (Playwright، هر ۶۰ث)
|
||
│ ├─ crypto_market.py # کریپتو از API رایگانِ CoinGecko (هر ۶۰ث)
|
||
│ ├─ world_economy.py # شاخصهای کلانِ اقتصادهای بزرگ (هفتگی)
|
||
│ ├─ steel_stocks.py # سهام شرکتهای فولادی (companiesmarketcap، هر ۵ دقیقه)
|
||
│ ├─ live_stream.py # کامودیتی لحظهای (Yahoo Finance) روی WebSocket
|
||
│ ├─ reports.py # اسکنِ آرشیو PDFِ گزارشها (فایلسیستم)
|
||
│ └─ sync.py # اسکریپت دستیِ سینکِ قیمت فلزات از API منبعِ بالادست
|
||
│
|
||
├─ admin/admin_app.py # پنل ادمین (مستقل)
|
||
├─ vite.config.ts # پورت ۵۱۷۴ + proxy (/api,/ws → :8000) + allowedHosts
|
||
├─ .gitignore # شاملِ .env، __pycache__، *.db
|
||
└─ README.md # همین فایل
|
||
```
|
||
|
||
---
|
||
|
||
## ۳) راهاندازی محلی
|
||
|
||
**پیشنیازها:** Node 20+ (یا Bun)، Python 3.11+، و برای اسکرپرها `playwright install chromium`.
|
||
|
||
```powershell
|
||
# نصب وابستگیهای فرانت
|
||
npm install
|
||
|
||
# نصب وابستگیهای پایتون (نمونه)
|
||
pip install fastapi uvicorn playwright requests yfinance pandas websockets
|
||
python -m playwright install chromium
|
||
```
|
||
|
||
### اجرای هر سه سرویس (هرکدام در ترمینال جدا)
|
||
|
||
```powershell
|
||
# ۱) بکاند (پورت 8000)
|
||
$env:PYTHONUTF8="1"; python -m uvicorn main:app --host 127.0.0.1 --port 8000 --app-dir backend
|
||
|
||
# ۲) فرانتاند (پورت 5174)
|
||
npm run dev
|
||
|
||
# ۳) پنل ادمین (پورت 8001) — ADMIN_PASSWORD الزامی است
|
||
$env:ADMIN_USER="admin"; $env:ADMIN_PASSWORD="یکرمزِقوی"; python -m uvicorn admin_app:app --host 127.0.0.1 --port 8001 --app-dir admin
|
||
```
|
||
|
||
سپس: فرانت روی `http://localhost:5174` ، پنل ادمین روی `http://localhost:8001`.
|
||
|
||
---
|
||
|
||
## ۴) متغیرهای محیطی
|
||
|
||
| متغیر | کجا | پیشفرض | توضیح |
|
||
|---|---|---|---|
|
||
| `DB_PATH` | backend, admin | مسیر محلی | محل فایل SQLite (هر دو سرویس باید یکی باشند) |
|
||
| `ASIANMETAL_TOKEN` | sync.py | — | توکنِ API بالادست (یا از پنل ادمین در DB) — **هیچوقت در کد هاردکد نکن** |
|
||
| `ADMIN_USER` / `ADMIN_PASSWORD` | admin | `admin` / **الزامی** | احراز Basic پنل ادمین؛ بدون `ADMIN_PASSWORD` بالا نمیآید (fail-closed) |
|
||
| `ALLOWED_ORIGINS` | backend | `localhost:5174,127.0.0.1:5174` | originهای مجاز CORS (با کاما) |
|
||
| `COOKIE_SECURE` | backend | `0` | در پروداکشن (HTTPS) روی `1` بگذار تا کوکیِ نشست Secure شود |
|
||
| `REPORTS_DIR` | reports.py | پوشهٔ آرشیو | محل PDFهای گزارش (ساختار `<منبع> - <دسته>/<file>.pdf`) |
|
||
|
||
---
|
||
|
||
## ۵) دیتابیس (SQLite)
|
||
|
||
`init_db()` در `db.py` جداول را میسازد و **مهاجرتِ امن** انجام میدهد (ستونها را اگر نبودند `ALTER` میکند):
|
||
|
||
- **`prices`** — تاریخچهٔ روزانهٔ قیمت فلزات: `grp, category, title, date, low, mid, high` (یکتا روی `title,date`). منبعِ تب «فلزات و فولاد».
|
||
- **`users`** — کاربران: `id, name, email, phone, organization, password_hash, status('pending'|'approved'|'rejected'), created_at`.
|
||
- **`sessions`** — نشستهای سمتسرور: `token, user_id, created_at, expires_at`.
|
||
- **`app_config`** — جفتهای `key/value` (مثلاً `asianmetal_token`، هارتبیت اسکرپرها).
|
||
- جداول اسکرپرها: `currency_prices, gold_prices, coin_prices`, `te_prices`, `world_economy`, `steel_stocks` — هرکدام آخرین/تاریخچهٔ مقادیر را نگه میدارند (dedup روی تغییر).
|
||
|
||
> نکته: قیمتهای لحظهای (ارز/کریپتو/کامودیتی) عمدتاً در **حافظه** کش میشوند (`_latest`) و فقط تغییرها در DB ذخیره میشوند تا حجم کم بماند.
|
||
|
||
---
|
||
|
||
## ۶) اسکرپرها (در `lifespan` بکاند خودکار استارت میشوند)
|
||
|
||
| ماژول | منبع | بازه | جدول/خروجی |
|
||
|---|---|---|---|
|
||
| `currency_stream` | نرخ داخلیِ ارز/طلا/سکه (URL در همان فایل) | ۶۰ ثانیه | `currency_prices`/`gold_prices`/`coin_prices` |
|
||
| `crypto_market` | **CoinGecko** (API رایگان، بدون کلید) | ۶۰ ثانیه | حافظه (`get_latest`) |
|
||
| `world_economy` | شاخصهای کلانِ اقتصادهای بزرگ | هفتگی | `world_economy` |
|
||
| `steel_stocks` | companiesmarketcap.com | ۵ دقیقه | `steel_stocks` |
|
||
| `live_stream` | **Yahoo Finance** | لحظهای | WebSocket `/ws/live` |
|
||
| `reports` | فایلسیستمِ `REPORTS_DIR` | کشِ ۱۰ دقیقه | اسکنِ PDF |
|
||
|
||
**دامِ مهم:** سایتهای منبع قیمتها را با JS لِیزیلود میکنند. اسکرپرهای Playwright بعد از `goto` یک `wait_for_timeout(5000–9000)` دارند؛ **حذفش نکن** وگرنه مقادیرِ کهنه/خالی خوانده میشود. هر اسکرپر هم try/except دارد تا یک خطای گذرا کلِ تسک پسزمینه را نکُشد.
|
||
|
||
`sync.py` یک اسکریپتِ **دستی** است (در lifespan نیست) که قیمت فلزات را از API بالادست میگیرد؛ توکن را از `app_config` یا env `ASIANMETAL_TOKEN` میخواند.
|
||
|
||
---
|
||
|
||
## ۷) اندپوینتهای API (بکاند `:8000`)
|
||
|
||
**عمومی (دادهٔ بازار):** `GET /api/tree`, `/api/history?title=&days=`, `/api/search?q=`, `/api/latest`, `/api/live`, `/api/currency`, `/api/gold`, `/api/coin`, `/api/world-economy`, `/api/steel-stocks`, `/api/crypto`, `/api/reports`, `/api/reports/files?folder=`, `/api/reports/file?folder=&name=` (استریمِ inline PDF با گاردِ traversal)، WebSocket `/ws/live`.
|
||
|
||
**احراز هویت:** `POST /api/register`، `POST /api/login`، `GET /api/me`، `POST /api/logout`.
|
||
|
||
**پنل ادمین `:8001`** (پشت HTTP Basic): `/api/users`، `POST /api/users/{id}/approve|reject`، `GET|POST /api/token`، `/api/health`، `/api/test-scrape`، `/api/reports`، `/api/export?dataset=`.
|
||
|
||
---
|
||
|
||
## ۸) احراز هویت و گردشِ تأیید (مهم)
|
||
|
||
> اصل: **هیچ اعتمادی به کلاینت نیست.** ورود کاملاً سمتسرور تأیید میشود؛ نشست در **کوکیِ httpOnly** است، نه `localStorage`.
|
||
|
||
```
|
||
ثبتنام (نام، نامخانوادگی، سازمان، ایمیل، موبایل، رمز)
|
||
│ POST /api/register → رمز با PBKDF2 هش میشود، رکورد با status='pending'
|
||
▼
|
||
کاربر در وضعیت «در انتظار تأیید» ──► ادمین در پنل (تبِ کاربران) تأیید/رد میکند
|
||
│ (POST /api/users/{id}/approve | reject)
|
||
▼
|
||
ورود POST /api/login → اگر approved بود: کوکیِ نشست (httpOnly, SameSite=Lax) ست میشود
|
||
│ pending → 403 «در انتظار تأیید» ، rejected → 403 ، رمز غلط → 401
|
||
▼
|
||
داشبورد → گاردِ سمتسرور: fetch('/api/me')؛ اگر 401 شد → ریدایرکت به /login
|
||
خروج POST /api/logout → نشست از DB حذف + کوکی پاک میشود
|
||
```
|
||
|
||
نکات پیادهسازی:
|
||
- هشِ رمز: `auth.hash_password` با `hashlib.pbkdf2_hmac('sha256', ..., 200000)` — بدونِ وابستگیِ خارجی. مقایسه با `hmac.compare_digest`.
|
||
- توکنِ نشست: `secrets.token_urlsafe(32)`، انقضای ۷ روزه، در جدول `sessions`.
|
||
- ریتلیمیتِ per-IP (در حافظه) روی `register`/`login` (۵ در دقیقه).
|
||
- اعتبارسنجیِ Pydantic: فرمت/طولِ ایمیل و موبایل، حداقل ۸ کاراکترِ رمز.
|
||
- رد کردنِ کاربر، نشستهای فعالش را هم پاک میکند (دسترسی فوراً قطع).
|
||
|
||
---
|
||
|
||
## ۹) پنل ادمین (`:8001`)
|
||
|
||
- ورود با HTTP Basic؛ `ADMIN_PASSWORD` **الزامی** است (بدونش بالا نمیآید).
|
||
- تبها: **کاربران** (لیست + تأیید/رد + سازمان/وضعیت)، **توکن منبع داده** (ذخیره/تستِ توکن بالادست)، **سلامت سرویسها** (هارتبیتِ هر اسکرپر + تست اسکرپ)، **گزارشات** (دیتاستها + خروجی CSV).
|
||
- خروجیِ کاربران در UI با `esc()` فرار داده میشود (جلوگیری از XSSِ ذخیرهشده).
|
||
|
||
---
|
||
|
||
## ۱۰) فرانتاند
|
||
|
||
- مسیرها: `/` (لندینگ)، `/login`، `/register`، `/dashboard`.
|
||
- داشبورد، تبها (`VIEW_NAV`): **بازار داخلی**، **کامودیتی**، **فلزات و فولاد** (کاتالوگِ کارت + چارت)، **اقتصاد جهانی** (جدول حرارتی)، **سهام فولادی**، **کریپتو**، **گزارشها**.
|
||
- زبان بصری: تم **لایتِ کرم + لهجهٔ کهربایی (فولاد مذاب)**؛ توکنها در `styles.css` (oklch). قیمتکارتها سبکِ **Spark-canvas** (نمودار پسزمینه + رقمهای غلتانِ `Odometer` + پیلِ درصد).
|
||
- API نسبی است (`const API = ""`) تا از طریق **پراکسیِ Vite** هممبدأ کار کند → برای share فقط یک تونل لازم است.
|
||
- RTL سراسری، فونت **Vazirmatn**. اعداد همه با همین فونت (نه مونو).
|
||
|
||
---
|
||
|
||
## ۱۱) اشتراکگذاری (ngrok)
|
||
|
||
چون `vite.config.ts` مسیرهای `/api` و `/ws` را به `:8000` پراکسی میکند و فرانت از URLهای نسبی استفاده میکند، **فقط یک تونل روی ۵۱۷۴ کافی است**:
|
||
|
||
```powershell
|
||
ngrok http 5174
|
||
# یا مستقیم باینری: & "$env:APPDATA\npm\node_modules\ngrok\bin\ngrok.exe" http 5174
|
||
```
|
||
|
||
نکته: شیمِ `ngrok.ps1`ی npm گاهی پورت را اشتباه پاس میدهد؛ اگر تونل به پورت غلط رفت، **مستقیم `ngrok.exe`** را صدا بزن. URL را از `http://127.0.0.1:4040/api/tunnels` هم میتوانی بخوانی. (پلنِ رایگان: هر بار URL عوض میشود و یک صفحهٔ هشدارِ اولیه دارد.)
|
||
|
||
---
|
||
|
||
## ۱۲) امنیت — وضعیت و کارهای واجب
|
||
|
||
سختسازیهای انجامشده: CORS محدود، هدرهای امنیتی (`nosniff`, `X-Frame-Options`, `Referrer-Policy`)، اعتبارسنجی + ریتلیمیتِ ورودیها، محدودیتِ کوئریِ `history`/`search`، گاردِ path-traversal روی استریمِ PDF، کوئریهای پارامتری، احراز هویتِ سمتسرور با کوکیِ httpOnly، و حذفِ توکنِ هاردکد از کد.
|
||
|
||
**واجب قبل از پروداکشن:**
|
||
1. توکنِ بالادست (SteelStatista) که قبلاً در git history لو رفته را **باطل/تمدید** کن؛ مقدارِ جدید فقط در پنل ادمین/`env`.
|
||
2. `ADMIN_PASSWORD` قوی بگذار (پیشفرضِ ضعیف حذف شده).
|
||
3. در HTTPS واقعی: `COOKIE_SECURE=1` و `ALLOWED_ORIGINS` را به دامنهٔ واقعی محدود کن.
|
||
4. پنل ادمین (`:8001`) را **عمومی نکن** (در تونل نگذار؛ فقط لوکال/VPN/SSH).
|
||
|
||
---
|
||
|
||
## ۱۳) دامها و بدهیِ فنی (برای دفعهٔ بعد)
|
||
|
||
- `DB_PATH`/مسیرها در `db.py` و `admin_app.py` پیشفرضِ ویندوزیِ مطلق دارند (شاملِ نام کاربری) — بهتره به مسیر نسبی/`env` منتقل شود.
|
||
- وابستگیهای `package.json` با `^` پین نشدهاند؛ برای بیلدِ تکرارپذیر `npm ci` + پین.
|
||
- فایلهای `__pycache__/*.pyc` قبلاً کامیت شدهاند؛ untrack کن: `git rm -r --cached backend/__pycache__`.
|
||
- `routeTree.gen.ts` خودکار است — دستی ویرایش نکن (با اضافهشدن فایل route، خودِ Vite بازتولید میکند).
|
||
- اسکرپرها به ساختار HTMLِ منابع وابستهاند؛ اگر منبع قالبش را عوض کند، سلکتورها/`wait_for_timeout` باید تنظیم شوند.
|
||
|
||
</div>
|