diff --git a/README.md b/README.md index 983d607..a95cb53 100644 --- a/README.md +++ b/README.md @@ -5,113 +5,175 @@ احراز هویت با OTP، محتوای درسی و پیشرفت کاربر، دستیار هوش مصنوعی، اشتراک و پرداخت، تخفیف، اعلان‌ها، همکاری در فروش، پشتیبانی و حساب خانواده. -## اجرا +| اپ | مسئولیت | +|---|---| +| `accounts` | احراز هویت، session، پروفایل کاربر | +| `content` | پایه‌ها، مباحث، درس‌ها، سوال‌ها، پیشرفت | +| `progress` | آمار، لیدربرد، streak | +| `ai_tutor` | مکالمات دستیار هوش مصنوعی | +| `subscription` / `payment` / `discount` | پلن، سفارش، تراکنش، کد تخفیف | +| `notifications`, `affiliate`, `support`, `family` | امکانات جانبی | +| `statuslog` | لاگ عمومی تغییر status، مستقل از بقیه‌ی اپ‌ها | +--- + +## ۱. راه‌اندازی محیط توسعه + +### پیش‌نیازها +- پایتون ۳.۱۲ +- pip + +### تنظیم محیط ```bash python3 -m venv venv source venv/bin/activate # ویندوز: venv\Scripts\activate pip install -r requirements.txt +cp .env.example .env +``` -# مهم: از این به بعد DEBUG دیفالتش False هست (برای امنیتِ پروداکشن). -# برای اجرای محلی/تست با Swagger (که به DEV_MASTER_OTP و devOtp توی -# پاسخ نیاز داره)، این env var رو ست کنید: +`.env.example` (تمام متغیرهایی که واقعاً توسط اپ خونده می‌شن): +```env +DJANGO_DEBUG=1 +DJANGO_SECRET_KEY= +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 +CACHE_URL= +KAVENEGAR_API_KEY= +GRAFANA_ADMIN_PASSWORD=admin +``` +مقادیر `.env` رو به شل خودتون export کنید (یا از `direnv`/`python-dotenv` استفاده کنید): +```bash export DJANGO_DEBUG=1 # ویندوز (PowerShell): $env:DJANGO_DEBUG=1 +``` +> `DEBUG` دیفالتش `False` است. برای فعال‌کردنِ کمک‌کننده‌های dev-only در Swagger +> (`DEV_MASTER_OTP` و فیلدِ `devOtp` توی پاسخ‌ها)، محلی `DJANGO_DEBUG=1` رو ست کنید. +### سریع‌ترین راه اجرای پروژه +```bash python manage.py migrate -python manage.py createsuperuser # اختیاری، برای /admin python manage.py runserver ``` +اپ روی `http://127.0.0.1:8000` بالا میاد. -در پروداکشن، `DJANGO_DEBUG` رو ست نکنید (یا `0` بذارید) و حتماً -`DJANGO_SECRET_KEY` و `DJANGO_ALLOWED_HOSTS` (کاما-جدا) رو هم از env بدید. - -## اجرا با Docker (اپ + استکِ لاگِ متمرکز) - -برای اجرای محلی، در کنارِ Django یک استکِ لاگِ متمرکز هم فراهم شده: -`Loki` (ذخیره‌سازیِ لاگ) + `Grafana Alloy` (جمع‌آوریِ لاگ از فایلِ اپ) + -`Grafana` (جستجو/فیلترِ لاگ‌ها). این فقط برای dev/local هست، نه یک -تنظیمِ پروداکشن (مثلاً هنوز از `runserver` استفاده می‌شه، نه gunicorn). - +### مایگریشن‌ها ```bash -# اختیاری: تنظیماتِ خودتون رو ست کنید (وگرنه دیفالت‌های dev استفاده می‌شن) -cp .env.example .env - -docker compose up --build +python manage.py migrate ``` -- اپ Django: `http://localhost:8000` -- Grafana: `http://localhost:3000` (بدونِ نیاز به لاگین، anonymous admin فعاله؛ فقط برای محیطِ محلی) +### دیتای تستی (Seed) +یک دیتاست کوچک و idempotent می‌سازه که همه‌ی اپ‌ها رو پوشش می‌ده (کاربر، محتوا، پیشرفت، +اشتراک، پرداخت، تخفیف، دستیار هوش مصنوعی، اعلان، افیلیت، پشتیبانی، خانواده). اجرای مکرر +امن است — با `get_or_create` هیچ رکورد تکراری‌ای نمی‌سازه. پیش‌فرض فقط وقتی `DEBUG=True` +اجرا می‌شه، مگر با `--force`. -داشبوردِ «Tadrisino Django Logs» به‌صورتِ خودکار در Grafana لود می‌شه و -امکانِ فیلتر بر اساسِ سطحِ لاگ (`level`) و جستجوی متنی رو می‌ده. - -**جریانِ لاگ:** Django همون خطوطِ JSON موجود رو (بدونِ تغییرِ پیام‌ها یا -محافظت‌های حساسیتی) هم روی stdout و هم در `/var/log/django/app.log` -(یک ولومِ Docker) می‌نویسه ← Grafana Alloy این فایل رو می‌خونه ← به Loki -push می‌کنه ← Grafana از Loki می‌خونه. جزئیاتِ بیشتر در کامنت‌های بالای -`docker-compose.yml`، `docker/alloy/config.alloy` و `config/settings.py` -(بخشِ `LOG_DIR`). - -اجرای بدونِ Docker (روش قبلی، بالای همین فایل) دقیقاً همون‌طور که بود -کار می‌کنه — `LOG_DIR` فقط داخلِ Docker ست می‌شه. - -## مستندات API +```bash +python manage.py seed_test_data # لوکال (DEBUG=True) +python manage.py seed_test_data --force # staging (DEBUG=False) +python manage.py seed_test_data --reset # پاک‌کردنِ دیتای قبلیِ seed و ساختِ دوباره +``` +### مستندات API | | | |---|---| | Swagger UI | `http://127.0.0.1:8000/api/docs/` | | Redoc | `http://127.0.0.1:8000/api/redoc/` | +| OpenAPI schema | `http://127.0.0.1:8000/api/schema/` | | پنل ادمین | `http://127.0.0.1:8000/admin/` | **شروع سریع در Swagger:** 1. `POST /api/auth/session` با `{"type":"guest","deviceId":"d_test1"}` → یک `token` بگیرید. 2. روی دکمه‌ی **Authorize** بزنید و فقط خودِ `token` رو (بدون کلمه‌ی Bearer) وارد کنید. -3. برای تست لاگین با شماره: `POST /api/auth/otp/request` بزنید، یا در محیط توسعه کد **0000** رو با هر شماره‌ای در `/api/auth/login` امتحان کنید. +3. برای تست لاگین با شماره: `POST /api/auth/otp/request` بزنید، یا در محیط توسعه کد **0000** + رو با هر شماره‌ای در `POST /api/auth/login` امتحان کنید (dev-only، نیاز به `DEBUG=True`). -## ساختار پروژه +### دستورهای پرکاربرد +```bash +python manage.py createsuperuser # اختیاری، برای /admin +python manage.py test # اجرای کل تست‌سوییت +``` -| اپ | مسئولیت | مدل‌های اصلی | +--- + +## ۲. راهنمای دیپلوی پروداکشن + +### متغیرهای محیطی موردنیاز +| متغیر | لازم در پروداکشن | توضیح | |---|---|---| -| `accounts` | هویت کاربر و session | `User`, `Profile`, `Session`, `OTPCode` | -| `content` | محتوای درسی و پیشرفت روی درس | `Grade`, `Unit`, `Node`, `Lesson`, `Question`, `Choice`, `NodeProgress` | -| `progress` | آمار، لیدربرد، streak | `UserStats`, `LeaderboardEntry`, `StreakRecord`, `UserHearts` | -| `ai_tutor` | دستیار هوش مصنوعی | `Conversation`, `Message`, `MessageFeedback` | -| `subscription` | پلن و اشتراک | `Plan`, `Subscription` | -| `payment` | سفارش و تراکنش | `Order`, `Transaction` | -| `discount` | کد تخفیف و آفر | `DiscountCode`, `Redemption`, `Offer`, `UserOffer` | -| `notifications` | اعلان‌ها | `Notification` | -| `affiliate` | همکاری در فروش | `AffiliateAccount`, `Conversion` | -| `support` | تیکت و گزارش باگ | `BugReport`, `Ticket`, `TicketMessage` | -| `family` | حساب خانواده / کنترل والدین | `FamilyLink`, `ParentalSettings`, `WeeklyReportLog` | -| `statuslog` | لاگ عمومی تغییر status، مستقل از بقیه‌ی اپ‌ها | `StatusChangeLog` | -| `config` | تنظیمات و url های اصلی پروژه | — | +| `DJANGO_DEBUG` | باید ست‌نشده یا `0` باشه | دیفالت‌های امنیتیِ پروداکشن رو فعال می‌کنه | +| `DJANGO_SECRET_KEY` | **بله** | بدونش وقتی `DEBUG=False` است اپ اصلاً بالا نمیاد | +| `DJANGO_ALLOWED_HOSTS` | **بله** | لیستِ host ها، کاما-جدا | +| `CACHE_URL` | **بله** | آدرسِ Redis، مثلاً `redis://host:6379/0` — برای rate limitِ مشترک وقتی `DEBUG=False` لازمه | +| `KAVENEGAR_API_KEY` | **بله** | ارسال واقعیِ OTP از طریق کاوه‌نگار | +| `GRAFANA_ADMIN_PASSWORD` | فقط اگه از استکِ Grafana استفاده می‌کنید | بخشِ Docker پایین رو ببینید | -## نکات فنی مهم +### تنظیمات پروداکشن +- `DEBUG` باید ست **نشه** (یا `0` باشه). با `DEBUG=False`: + - `SECRET_KEY` اجباریه — هیچ fallback ناامنی وجود نداره. + - `CACHE_URL` (Redis) اجباریه — `LocMemCache` فقط برای dev هست. + - `DEV_MASTER_OTP` و فیلدِ `devOtp` توی پاسخ‌ها غیرفعال می‌شن. +- برای ترافیک واقعی به‌جای SQLیت پیش‌فرض، از یک دیتابیسِ پروداکشن‌گرید استفاده کنید + (پروژه با همون SQLite شیپ می‌شه؛ عوض‌کردنِ engine یک تصمیمِ زیرساختیه، خارج از این ریپو). +- از طریق یک سرورِ WSGI/ASGI (مثلاً gunicorn) جلوی اپ سرو کنید — تنظیماتِ Docker موجود + همچنان `runserver` اجرا می‌کنه و فقط برای dev/local مستند شده (پایین رو ببینید). -- **احراز هویت**: Bearer token امضاشده و stateless (`django.core.signing`، بدون کتابخانه‌ی JWT خارجی)، هماهنگ با مدل `Session`. -- **`accounts.User`** عمداً `AUTH_USER_MODEL` جنگو نیست؛ یک مدل بیزنسی مستقل است. -- بین اپ‌ها به‌جای FK واقعی از ارجاع سبک (`user_id`, `object_id` + `label`) استفاده شده تا هر اپ در آینده قابل جدا شدن باشد. -- `POST /api/ai/ask` فعلاً پاسخ stub برمی‌گرداند؛ محل اتصال مدل واقعی در `ai_tutor/views.py` (تابع `generate_tutor_answer`) مشخص شده. -- `DEV_MASTER_OTP` (کد `0000`) فقط برای تست در توسعه است — **پیش از دیپلوی پروداکشن باید حذف یا به `DEBUG` مقیّد شود.** -- **لاگِ ساختاریافته**: همه‌ی درخواست‌های `/api/` به‌صورتِ JSON لاگ می‌شن (متد/مسیر/وضعیت/مدت/کاربر، بدونِ بدنه/هدر/سکرت)؛ قابلِ مشاهده و جست‌وجو در Grafana (بخشِ Docker بالا). +### تنظیمات Docker/دیپلوی +`docker-compose.yml` موجود، اپ رو **به‌همراه** یک استکِ لاگِ متمرکز +(Loki + Grafana Alloy + Grafana) اجرا می‌کنه. این یک تنظیمِ **dev/local** هست، نه دیپلویِ +پروداکشن (سرویسِ `django` همچنان `runserver` اجرا می‌کنه، نه یک سرورِ پروداکشن). -## قبل از دیپلوی پروداکشن +```bash +cp .env.example .env # اختیاری — بدونش compose از دیفالت‌های dev استفاده می‌کنه +docker compose up --build +``` -- [x] `DEBUG` و `SECRET_KEY` و `ALLOWED_HOSTS` از env خونده می‌شن (دیفالتِ - `DEBUG` روی `False` است) -- [x] `DEV_MASTER_OTP` و برگردوندنِ `devOtp` توی پاسخ، فقط وقتی `DEBUG=True` - فعالن؛ در پروداکشن غیرفعالن -- [x] rate limit (throttle) روی endpointهای احرازهویت و بقیه‌ی endpointهای - حساسِ پروژه (پرداخت، خرید اشتراک، تخفیف، AI، محتوا، خانواده، - پشتیبانی، affiliate، …) پیاده‌سازی شده؛ برای کاربرهای مهمان بر - اساسِ IP کلید می‌خوره تا چرخشِ حساب مهمان دورش نزنه -- [x] IDOR در `GET /api/home` و `GET /api/progress/nodes` اصلاح شد: قبلاً - بدون هیچ توکنی، با فرستادنِ `userId` دلخواه می‌شد آمار/پیشرفتِ هر - کاربرِ دیگه‌ای رو خوند؛ حالا auth اجباریه و فقط خودِ کاربر یا والدِ - تأییدشده‌اش (`FamilyLink` با `status='accepted'`) اجازه‌ی دسترسی دارن -- [ ] دیتابیس Postgres به‌جای SQLite -- [x] اتصال واقعی به سرویسِ پیامک برای ارسالِ OTP: دیگه استاب نیست؛ از طریق - کاوه‌نگار (kavenegar.com) ارسال می‌شه (کلید از `KAVENEGAR_API_KEY`)، - و هر شکست (شبکه/کلید/رد اپلیکیشنی) به‌جای موفقیتِ ساختگی، خطای صریح - ۵۰۳ برمی‌گردونه -- [ ] بررسیِ دوره‌ای throttle rate ها با توجه به ترافیکِ واقعی +| سرویس | آدرس | توضیح | +|---|---|---| +| اپ Django | `http://localhost:8000` | | +| Grafana | `http://localhost:3000` | دسترسیِ anonymous admin، فقط محیطِ محلی | +| Loki | `http://localhost:3100` | ذخیره‌سازیِ لاگ، در معرضِ کاربر نیست | +| Alloy | `http://localhost:12345` | رابطِ دیباگِ اختیاری | + +### ترتیبِ لازمِ بالا آمدن +Compose خودش این رو مدیریت می‌کنه: +1. اول `loki` بالا میاد. +2. `alloy` و `grafana` بعد از `loki` بالا میان (`depends_on`). +3. `django` هیچ وابستگی‌ای به استکِ لاگ نداره — خودش `migrate` بعد `runserver` رو + اجرا می‌کنه و منتظرِ Loki/Alloy/Grafana نمی‌مونه. + +### مایگریشن‌ها +```bash +python manage.py migrate --noinput +``` +در Docker، این به‌صورت خودکار به‌عنوان بخشی از دستورِ سرویسِ `django`، قبل از `runserver` اجرا می‌شه. + +### راهنمای seed برای staging/تست +- هرگز `seed_test_data` رو روی یک دیتابیسِ پروداکشنِ واقعی اجرا نکنید. +- روی staging (`DEBUG=False`)، صریحاً با `--force` اجرا کنید: + ```bash + python manage.py seed_test_data --force + ``` +- برای پاک‌کردن و ساختِ دوباره‌ی دیتای seed بین چرخه‌های تست، از `--reset` استفاده کنید. + همه‌ی رکوردهای seed‌شده در فیلدهای متنیِ قابل‌مشاهده با تگِ `[SEED]` مشخص شدن، برای + شناساییِ راحت‌تر و پاک‌سازی. + +### چطور API دیپلوی‌شده رو verify کنیم +```bash +curl -i https:///api/schema/ # باید 200 برگردونه (OpenAPI schema) +curl -i https:///api/docs/ # باید 200 برگردونه (Swagger UI) +curl -i -X POST https:///api/auth/session \ + -H "Content-Type: application/json" \ + -d '{"type":"guest","deviceId":"verify-1"}' # باید یک token برگردونه +``` + +### نکات مهمِ پروداکشن +- `DEV_MASTER_OTP` (کد `0000`) و فیلدِ `devOtp` توی پاسخ‌ها فقط وقتی `DEBUG=True` وجود + دارن — قبل از رفتن روی محیطِ واقعی مطمئن بشید `DEBUG` خاموشه. +- احراز هویت با Bearer token امضاشده و stateless (`django.core.signing`)، هماهنگ با مدل + `Session` — بدون کتابخانه‌ی JWT خارجی. +- `accounts.User` یک مدل بیزنسیِ مستقله، **نه** `AUTH_USER_MODEL` جنگو. +- همه‌ی لاگ‌های ساختاریافته‌ی درخواست (`method`/`path`/`status`/`duration`/`user`، بدونِ + بدنه/هدر/سکرت) روی stdout و — وقتی `LOG_DIR` ست باشه (مثلِ داخلِ Docker) — در + `/var/log/django/app.log` نوشته می‌شن؛ قابلِ مشاهده در Grafana از طریقِ استکِ بالا. +- `POST /api/ai/ask` فعلاً پاسخ stub برمی‌گردونه؛ محلِ اتصالِ مدلِ واقعی + `generate_tutor_answer` در `ai_tutor/views.py` است. +- ارسالِ OTP از طریقِ کاوه‌نگار (`KAVENEGAR_API_KEY`) انجام می‌شه؛ شکستِ ارسال به‌جای + موفقیتِ ساختگی، خطای صریحِ `503` برمی‌گردونه.