Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d4307bdaa |
519
API_DOCS_FA.md
Normal file
519
API_DOCS_FA.md
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
<div dir="rtl" lang="fa">
|
||||||
|
|
||||||
|
# مستندات API (Zoneco ORG)
|
||||||
|
|
||||||
|
این سند نحوهٔ کارکرد APIهای بکاند پروژه Zoneco ORG را به زبان فارسی توضیح میدهد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## وضعیت تستها
|
||||||
|
|
||||||
|
```
|
||||||
|
Ran 24 tests — OK (0 failures)
|
||||||
|
```
|
||||||
|
|
||||||
|
| تعداد تست | گروه |
|
||||||
|
|-----------|------|
|
||||||
|
| ۶ تست | Contact Us |
|
||||||
|
| ۱۱ تست | Compositions |
|
||||||
|
| ۷ تست | Campaigns |
|
||||||
|
|
||||||
|
اجرای تستها:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python manage.py test api -v 2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## اطلاعات پایه
|
||||||
|
|
||||||
|
| مقدار | مورد |
|
||||||
|
|--------|------|
|
||||||
|
| `{{base_url}}` | آدرس پایه |
|
||||||
|
| `/api/` | پیشوند API |
|
||||||
|
| JSON | فرمت پاسخ |
|
||||||
|
| ۲۰ آیتم در هر صفحه | صفحهبندی |
|
||||||
|
| `/admin/` | پنل ادمین |
|
||||||
|
|
||||||
|
### متغیر Postman
|
||||||
|
|
||||||
|
در فایل Postman از متغیر `{{base_url}}` استفاده شده است.
|
||||||
|
مقدار آن را مطابق محیط اجرا (لوکال، سرور تست یا پروداکشن) تنظیم کنید.
|
||||||
|
|
||||||
|
**نمونهٔ آدرس کامل یک endpoint:**
|
||||||
|
|
||||||
|
```
|
||||||
|
{{base_url}}/api/contact-us/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## سطح دسترسی
|
||||||
|
|
||||||
|
| دسترسی | عملیات |
|
||||||
|
|--------|--------|
|
||||||
|
| عمومی — بدون نیاز به ورود | لیست و جزئیات (متد `GET`) |
|
||||||
|
| عمومی — بدون نیاز به ورود | ایجاد (متد `POST`) |
|
||||||
|
| فقط ادمین — نیاز به احراز هویت | ویرایش و حذف (متدهای `PUT`، `PATCH`، `DELETE`) |
|
||||||
|
|
||||||
|
برای درخواستهای ادمین در Postman میتوانید از **Basic Auth** با نام کاربری و رمز ادمین استفاده کنید.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱. تماس با ما (Contact Us)
|
||||||
|
|
||||||
|
**مسیر پایه:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/contact-us/
|
||||||
|
```
|
||||||
|
|
||||||
|
### فیلدها
|
||||||
|
|
||||||
|
| توضیح | الزامی | نوع | فیلد |
|
||||||
|
|-------|--------|-----|------|
|
||||||
|
| نام و نام خانوادگی | بله | string | `name` |
|
||||||
|
| ایمیل یا شماره تماس | بله | string | `email_or_phone` |
|
||||||
|
| متن پیام | بله | string | `description` |
|
||||||
|
| دستهبندی | بله | string | `category` |
|
||||||
|
|
||||||
|
**مقادیر مجاز برای `category`:**
|
||||||
|
|
||||||
|
- همکاری
|
||||||
|
- فروش
|
||||||
|
- پشتیبانی
|
||||||
|
- درخواست مشاور
|
||||||
|
- سایر
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### دریافت لیست تماسها
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/contact-us/
|
||||||
|
```
|
||||||
|
|
||||||
|
**پاسخ نمونه:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "احمد محمدی",
|
||||||
|
"email_or_phone": "ahmad@example.com",
|
||||||
|
"description": "سلام، سوالی دارم.",
|
||||||
|
"category": "پشتیبانی",
|
||||||
|
"created_at": "2025-12-17T12:00:00Z",
|
||||||
|
"updated_at": "2025-12-17T12:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### دریافت یک تماس
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/contact-us/{id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ثبت تماس جدید
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {{base_url}}/api/contact-us/
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**بدنه درخواست:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "احمد محمدی",
|
||||||
|
"email_or_phone": "09121234567",
|
||||||
|
"description": "میخواهم در مورد محصولات اطلاعات بگیرم.",
|
||||||
|
"category": "فروش"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**کد پاسخ:** `201 Created`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### فیلتر بر اساس دستهبندی
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/contact-us/by_category/?category=پشتیبانی
|
||||||
|
```
|
||||||
|
|
||||||
|
| توضیح | الزامی | پارامتر |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| یکی از دستههای مجاز | بله | `category` |
|
||||||
|
|
||||||
|
اگر پارامتر `category` ارسال نشود، خطای `400` برمیگردد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ویرایش و حذف (ادمین)
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT {{base_url}}/api/contact-us/{id}/
|
||||||
|
PATCH {{base_url}}/api/contact-us/{id}/
|
||||||
|
DELETE {{base_url}}/api/contact-us/{id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
نیاز به احراز هویت ادمین دارد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲. ترکیبات (Compositions)
|
||||||
|
|
||||||
|
**مسیر پایه:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/compositions/
|
||||||
|
```
|
||||||
|
|
||||||
|
هر ترکیب میتواند **چند تصویر** داشته باشد.
|
||||||
|
یکی از تصاویر با فیلد `is_main: true` بهعنوان **تصویر اصلی** مشخص میشود.
|
||||||
|
|
||||||
|
### فیلدها
|
||||||
|
|
||||||
|
| توضیح | الزامی | نوع | فیلد |
|
||||||
|
|-------|--------|-----|------|
|
||||||
|
| نام ترکیب | بله | string | `name` |
|
||||||
|
| توضیحات | بله | string | `description` |
|
||||||
|
| یک یا چند فایل تصویر (فقط در ایجاد/ویرایش) | خیر | file[] | `uploaded_images` |
|
||||||
|
| ایندکس تصویر اصلی (شمارش از ۰) | خیر | integer | `main_image_index` |
|
||||||
|
|
||||||
|
### محدودیت تصاویر
|
||||||
|
|
||||||
|
- فرمتهای مجاز: JPEG، PNG، WebP، GIF
|
||||||
|
- حداکثر حجم هر فایل: **۵ مگابایت**
|
||||||
|
- مسیر ذخیره: `media/compositions/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### دریافت لیست ترکیبات
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/compositions/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### دریافت یک ترکیب
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/compositions/{id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
**پاسخ نمونه:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "ترکیب A",
|
||||||
|
"description": "توضیحات ترکیب",
|
||||||
|
"image": null,
|
||||||
|
"image_url": null,
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"image": "/media/compositions/img2.jpg",
|
||||||
|
"image_url": "{{base_url}}/media/compositions/img2.jpg",
|
||||||
|
"is_main": true,
|
||||||
|
"created_at": "2025-12-17T12:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"image": "/media/compositions/img1.jpg",
|
||||||
|
"image_url": "{{base_url}}/media/compositions/img1.jpg",
|
||||||
|
"is_main": false,
|
||||||
|
"created_at": "2025-12-17T11:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"main_image": {
|
||||||
|
"id": 2,
|
||||||
|
"image_url": "{{base_url}}/media/compositions/img2.jpg",
|
||||||
|
"is_main": true,
|
||||||
|
"created_at": "2025-12-17T12:00:00Z"
|
||||||
|
},
|
||||||
|
"created_at": "2025-12-17T10:00:00Z",
|
||||||
|
"updated_at": "2025-12-17T12:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### فیلتر بر اساس تاریخ ایجاد
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/compositions/by-created-at/?from=2026-06-01T00:00:00Z&to=2026-06-30T23:59:59Z
|
||||||
|
```
|
||||||
|
|
||||||
|
| توضیح | الزامی | پارامتر |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| برگشت ترکیبهایی که `created_at >= from` | حداقل یکی | `from` |
|
||||||
|
| برگشت ترکیبهایی که `created_at <= to` | حداقل یکی | `to` |
|
||||||
|
|
||||||
|
فرمت تاریخ: ISO 8601 — مثال: `2026-06-01T00:00:00Z`
|
||||||
|
|
||||||
|
اگر هیچ پارامتری ارسال نشود، خطای `400` برمیگردد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ایجاد ترکیب (بدون تصویر)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {{base_url}}/api/compositions/
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "ترکیب تست",
|
||||||
|
"description": "توضیحات ترکیب"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ایجاد ترکیب با چند تصویر
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {{base_url}}/api/compositions/
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
| مقدار نمونه | نوع | فیلد |
|
||||||
|
|-------------|-----|------|
|
||||||
|
| ترکیب A | text | `name` |
|
||||||
|
| توضیحات | text | `description` |
|
||||||
|
| image1.jpg | file | `uploaded_images` |
|
||||||
|
| image2.jpg | file | `uploaded_images` |
|
||||||
|
| 1 | text | `main_image_index` |
|
||||||
|
|
||||||
|
**نکته:** فیلد `uploaded_images` را برای هر تصویر یکبار تکرار کنید.
|
||||||
|
|
||||||
|
**نکته:** اگر `main_image_index` ارسال نشود، **اولین تصویر** بهعنوان تصویر اصلی انتخاب میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### افزودن تصویر به ترکیب موجود (ادمین)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {{base_url}}/api/compositions/{id}/add-images/
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
| نوع | فیلد |
|
||||||
|
|-----|------|
|
||||||
|
| file (یک یا چند فایل) | `uploaded_images` |
|
||||||
|
| text (اختیاری) | `main_image_index` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### تنظیم تصویر اصلی (ادمین)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {{base_url}}/api/compositions/{id}/set-main-image/
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"image_id": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### حذف یک تصویر (ادمین)
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE {{base_url}}/api/compositions/{id}/images/{image_id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
**کد پاسخ:** `204 No Content`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ویرایش و حذف ترکیب (ادمین)
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT {{base_url}}/api/compositions/{id}/
|
||||||
|
PATCH {{base_url}}/api/compositions/{id}/
|
||||||
|
DELETE {{base_url}}/api/compositions/{id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۳. کمپینها (Campaigns)
|
||||||
|
|
||||||
|
**مسیر پایه:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/campaigns/
|
||||||
|
```
|
||||||
|
|
||||||
|
### فیلدها
|
||||||
|
|
||||||
|
| توضیح | الزامی | نوع | فیلد |
|
||||||
|
|-------|--------|-----|------|
|
||||||
|
| نام کمپین | بله | string | `name` |
|
||||||
|
| توضیحات | بله | string | `description` |
|
||||||
|
| زمان شروع (ISO 8601) | بله | datetime | `start_time` |
|
||||||
|
| زمان پایان (باید بعد از `start_time` باشد) | بله | datetime | `end_time` |
|
||||||
|
| تصویر کمپین | خیر | file | `image` |
|
||||||
|
|
||||||
|
### فیلدهای محاسباتی (فقط خواندنی)
|
||||||
|
|
||||||
|
| توضیح | فیلد |
|
||||||
|
|-------|------|
|
||||||
|
| آیا کمپین الان فعال است | `is_active` |
|
||||||
|
| آیا کمپین هنوز شروع نشده | `is_upcoming` |
|
||||||
|
| آیا کمپین تمام شده | `is_ended` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### دریافت لیست کمپینها
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/campaigns/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### دریافت یک کمپین
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/campaigns/{id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ایجاد کمپین
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {{base_url}}/api/campaigns/
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "کمپین نوروز",
|
||||||
|
"description": "تخفیف ویژه نوروز",
|
||||||
|
"start_time": "2026-03-01T00:00:00Z",
|
||||||
|
"end_time": "2026-03-31T23:59:59Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### کمپینهای فعال
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/campaigns/active/
|
||||||
|
```
|
||||||
|
|
||||||
|
کمپینهایی که زمان فعلی بین `start_time` و `end_time` قرار دارد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### کمپینهای آینده
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/campaigns/upcoming/
|
||||||
|
```
|
||||||
|
|
||||||
|
کمپینهایی که هنوز شروع نشدهاند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### کمپینهای پایانیافته
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {{base_url}}/api/campaigns/ended/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ویرایش و حذف (ادمین)
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT {{base_url}}/api/campaigns/{id}/
|
||||||
|
PATCH {{base_url}}/api/campaigns/{id}/
|
||||||
|
DELETE {{base_url}}/api/campaigns/{id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## کدهای وضعیت HTTP
|
||||||
|
|
||||||
|
| معنی | کد |
|
||||||
|
|------|-----|
|
||||||
|
| موفق | `200` |
|
||||||
|
| ایجاد شد | `201` |
|
||||||
|
| حذف شد (بدون بدنه) | `204` |
|
||||||
|
| داده نامعتبر | `400` |
|
||||||
|
| دسترسی ندارید | `403` |
|
||||||
|
| یافت نشد | `404` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## فایل Postman
|
||||||
|
|
||||||
|
مجموعه Postman در مسیر زیر قرار دارد:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/Zoneco_ORG_API.postman_collection.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### نحوه import
|
||||||
|
|
||||||
|
1. Postman را باز کنید
|
||||||
|
2. گزینه **Import** را بزنید
|
||||||
|
3. فایل `Zoneco_ORG_API.postman_collection.json` را انتخاب کنید
|
||||||
|
4. متغیر `{{base_url}}` را مطابق محیط خود تنظیم کنید
|
||||||
|
5. برای درخواستهای ادمین، در تب **Authorization** گزینه **Basic Auth** را فعال کنید
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## خلاصه endpointها
|
||||||
|
|
||||||
|
| دسترسی | Endpoint | Method | # |
|
||||||
|
|--------|----------|--------|---|
|
||||||
|
| عمومی | `/api/contact-us/` | GET | 1 |
|
||||||
|
| عمومی | `/api/contact-us/` | POST | 2 |
|
||||||
|
| عمومی | `/api/contact-us/{id}/` | GET | 3 |
|
||||||
|
| عمومی | `/api/contact-us/by_category/?category=...` | GET | 4 |
|
||||||
|
| ادمین | `/api/contact-us/{id}/` | PUT/PATCH/DELETE | 5 |
|
||||||
|
| عمومی | `/api/compositions/` | GET | 6 |
|
||||||
|
| عمومی | `/api/compositions/` | POST | 7 |
|
||||||
|
| عمومی | `/api/compositions/{id}/` | GET | 8 |
|
||||||
|
| عمومی | `/api/compositions/by-created-at/?from=...&to=...` | GET | 9 |
|
||||||
|
| ادمین | `/api/compositions/{id}/add-images/` | POST | 10 |
|
||||||
|
| ادمین | `/api/compositions/{id}/set-main-image/` | POST | 11 |
|
||||||
|
| ادمین | `/api/compositions/{id}/images/{image_id}/` | DELETE | 12 |
|
||||||
|
| ادمین | `/api/compositions/{id}/` | PUT/PATCH/DELETE | 13 |
|
||||||
|
| عمومی | `/api/campaigns/` | GET | 14 |
|
||||||
|
| عمومی | `/api/campaigns/` | POST | 15 |
|
||||||
|
| عمومی | `/api/campaigns/{id}/` | GET | 16 |
|
||||||
|
| عمومی | `/api/campaigns/active/` | GET | 17 |
|
||||||
|
| عمومی | `/api/campaigns/upcoming/` | GET | 18 |
|
||||||
|
| عمومی | `/api/campaigns/ended/` | GET | 19 |
|
||||||
|
| ادمین | `/api/campaigns/{id}/` | PUT/PATCH/DELETE | 20 |
|
||||||
|
|
||||||
|
> **توجه:** در Postman قبل از هر مسیر، مقدار `{{base_url}}` را قرار دهید.
|
||||||
|
> مثال: `{{base_url}}/api/campaigns/active/`
|
||||||
|
|
||||||
|
</div>
|
||||||
219
DOCKER_README.md
Normal file
219
DOCKER_README.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# Docker Setup for Zoneco ORG Backend
|
||||||
|
|
||||||
|
This document provides instructions for running the Zoneco ORG backend using Docker and Docker Compose.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker Engine 20.10 or higher
|
||||||
|
- Docker Compose 2.0 or higher
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Navigate to the backend directory:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create a `.env` file** (copy from the template below):
|
||||||
|
```bash
|
||||||
|
# Copy and modify as needed
|
||||||
|
SECRET_KEY=your-secret-key-here-change-in-production
|
||||||
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,185.208.172.158
|
||||||
|
POSTGRES_DB=Zoneco_ORG
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
POSTGRES_HOST=db
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
DJANGO_PORT=8000
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173,http://127.0.0.1:3000,http://185.208.172.158:9123,http://185.208.172.158
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build and start the containers:**
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **The application will be available at:**
|
||||||
|
- API: `http://localhost:8000/api/`
|
||||||
|
- Admin: `http://localhost:8000/admin/`
|
||||||
|
|
||||||
|
## Docker Commands
|
||||||
|
|
||||||
|
### Start services
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start services in detached mode
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop services
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop services and remove volumes (⚠️ deletes database data)
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs for specific service
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f web
|
||||||
|
docker-compose logs -f db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild containers
|
||||||
|
```bash
|
||||||
|
docker-compose build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execute commands in running container
|
||||||
|
```bash
|
||||||
|
# Django shell
|
||||||
|
docker-compose exec web python manage.py shell
|
||||||
|
|
||||||
|
# Create superuser
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Run migrations manually
|
||||||
|
docker-compose exec web python manage.py migrate
|
||||||
|
|
||||||
|
# Collect static files
|
||||||
|
docker-compose exec web python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The Docker setup consists of:
|
||||||
|
|
||||||
|
1. **Web Service** (`web`):
|
||||||
|
- Django application running with Gunicorn
|
||||||
|
- Multi-stage Dockerfile for optimized image size
|
||||||
|
- Non-root user for security
|
||||||
|
- Automatic migrations and static file collection on startup
|
||||||
|
- Health checks enabled
|
||||||
|
|
||||||
|
2. **Database Service** (`db`):
|
||||||
|
- PostgreSQL 15 Alpine (lightweight)
|
||||||
|
- Persistent data volume
|
||||||
|
- Health checks to ensure readiness
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
- ✅ Multi-stage build for smaller image size
|
||||||
|
- ✅ Non-root user execution
|
||||||
|
- ✅ Environment variable configuration
|
||||||
|
- ✅ No hardcoded secrets
|
||||||
|
- ✅ Health checks for both services
|
||||||
|
|
||||||
|
### Production Ready
|
||||||
|
- ✅ Gunicorn WSGI server
|
||||||
|
- ✅ Automatic database migrations
|
||||||
|
- ✅ Static file collection
|
||||||
|
- ✅ Database connection retry logic
|
||||||
|
- ✅ Proper logging
|
||||||
|
|
||||||
|
### Development Friendly
|
||||||
|
- ✅ Volume mounts for media and static files
|
||||||
|
- ✅ Hot-reload capability (with proper setup)
|
||||||
|
- ✅ Easy access to Django management commands
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `SECRET_KEY` | Django secret key | (required in production) |
|
||||||
|
| `DEBUG` | Enable debug mode | `False` |
|
||||||
|
| `ALLOWED_HOSTS` | Comma-separated allowed hosts | `localhost,127.0.0.1` |
|
||||||
|
| `POSTGRES_DB` | Database name | `Zoneco_ORG` |
|
||||||
|
| `POSTGRES_USER` | Database user | `postgres` |
|
||||||
|
| `POSTGRES_PASSWORD` | Database password | `postgres` |
|
||||||
|
| `POSTGRES_HOST` | Database host | `db` |
|
||||||
|
| `POSTGRES_PORT` | Database port | `5432` |
|
||||||
|
| `DJANGO_PORT` | Django application port | `8000` |
|
||||||
|
| `CORS_ALLOWED_ORIGINS` | Comma-separated CORS origins | (see .env.example) |
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
- `postgres_data`: Persistent PostgreSQL data
|
||||||
|
- `./media`: Media files (images, uploads)
|
||||||
|
- `./staticfiles`: Collected static files
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Database connection errors
|
||||||
|
```bash
|
||||||
|
# Check if database is healthy
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check database logs
|
||||||
|
docker-compose logs db
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission issues
|
||||||
|
```bash
|
||||||
|
# Fix media/staticfiles permissions
|
||||||
|
sudo chown -R $USER:$USER media staticfiles
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
```bash
|
||||||
|
# Change port in .env file
|
||||||
|
DJANGO_PORT=8001
|
||||||
|
# Then update docker-compose.yml or restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear everything and start fresh
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
For production deployment:
|
||||||
|
|
||||||
|
1. **Set strong SECRET_KEY:**
|
||||||
|
```bash
|
||||||
|
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set DEBUG=False** in `.env`
|
||||||
|
|
||||||
|
3. **Configure proper ALLOWED_HOSTS**
|
||||||
|
|
||||||
|
4. **Use strong database passwords**
|
||||||
|
|
||||||
|
5. **Consider using:**
|
||||||
|
- Reverse proxy (nginx)
|
||||||
|
- SSL/TLS certificates
|
||||||
|
- Separate database server
|
||||||
|
- Backup strategy
|
||||||
|
- Monitoring and logging
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── Dockerfile # Multi-stage production Dockerfile
|
||||||
|
├── docker-compose.yml # Service orchestration
|
||||||
|
├── .dockerignore # Files to exclude from build
|
||||||
|
├── entrypoint.sh # Startup script
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
└── DOCKER_README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
40
Dockerfile
40
Dockerfile
@@ -1,78 +1,62 @@
|
|||||||
# Multi-stage Dockerfile for Django application
|
# Multi-stage Dockerfile for Django backend (zonco site API)
|
||||||
# Stage 1: Builder stage - Install dependencies
|
# - funzone-site/docker-compose.yml: dev (runserver + bind mount)
|
||||||
FROM python:3.11-slim as builder
|
# - site_backend/docker-compose.yml: prod (gunicorn)
|
||||||
|
|
||||||
|
FROM python:3.11-slim AS builder
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PIP_NO_CACHE_DIR=1 \
|
PIP_NO_CACHE_DIR=1 \
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
# Install system dependencies required for building Python packages
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create and set working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy requirements file
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
RUN pip install --upgrade pip && \
|
RUN pip install --upgrade pip && \
|
||||||
pip install --user -r requirements.txt
|
pip install --user -r requirements.txt
|
||||||
|
|
||||||
# Stage 2: Production stage - Minimal runtime image
|
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH="/home/django/.local/bin:$PATH"
|
PATH="/home/django/.local/bin:$PATH" \
|
||||||
|
POSTGRES_HOST=db \
|
||||||
|
POSTGRES_PORT=5432 \
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,panel.zoneco.org
|
||||||
|
|
||||||
# Install runtime dependencies only
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
bash \
|
bash \
|
||||||
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create non-root user for security
|
|
||||||
RUN groupadd -r django && useradd -r -g django django
|
RUN groupadd -r django && useradd -r -g django django
|
||||||
|
|
||||||
# Create necessary directories
|
|
||||||
RUN mkdir -p /app/staticfiles /app/media && \
|
RUN mkdir -p /app/staticfiles /app/media && \
|
||||||
chown -R django:django /app
|
chown -R django:django /app
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy Python dependencies from builder stage
|
|
||||||
COPY --from=builder /root/.local /home/django/.local
|
COPY --from=builder /root/.local /home/django/.local
|
||||||
|
RUN chown -R django:django /home/django/.local
|
||||||
|
|
||||||
# Copy project files
|
|
||||||
COPY --chown=django:django . .
|
COPY --chown=django:django . .
|
||||||
|
|
||||||
# Copy and set permissions for entrypoint script
|
|
||||||
COPY --chown=django:django entrypoint.sh /app/entrypoint.sh
|
COPY --chown=django:django entrypoint.sh /app/entrypoint.sh
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
# Switch to non-root user
|
|
||||||
USER django
|
USER django
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/')" || exit 1
|
CMD curl -f http://localhost:8000/api/ || exit 1
|
||||||
|
|
||||||
# Use entrypoint script
|
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
|
||||||
# Default command (can be overridden in docker-compose)
|
|
||||||
CMD ["gunicorn", "zonco_backend.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120"]
|
CMD ["gunicorn", "zonco_backend.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120"]
|
||||||
|
|
||||||
|
|||||||
@@ -4,34 +4,25 @@
|
|||||||
|
|
||||||
# Django Settings
|
# Django Settings
|
||||||
SECRET_KEY=your-secret-key-here-change-in-production
|
SECRET_KEY=your-secret-key-here-change-in-production
|
||||||
# Generate a secret key with:
|
# Generate a secret key with: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||||
# python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,185.208.172.158
|
||||||
# Comma-separated hostnames/IPs for this server (no hardcoded defaults in code)
|
|
||||||
# Example: ALLOWED_HOSTS=YOUR_SERVER_IP,your-domain.com
|
|
||||||
ALLOWED_HOSTS=127.0.0.1,localhost
|
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
POSTGRES_DB=Zoneco_ORG
|
POSTGRES_DB=Zoneco_ORG
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=change-me
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_HOST=db
|
POSTGRES_HOST=db
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
DATABASE_URL=postgresql://postgres:change-me@db:5432/Zoneco_ORG
|
DATABASE_URL=postgresql://postgres:postgres@db:5432/Zoneco_ORG
|
||||||
|
|
||||||
# Django Port
|
# Django Port
|
||||||
DJANGO_PORT=8000
|
DJANGO_PORT=8000
|
||||||
|
|
||||||
# CORS Settings (comma-separated origins)
|
# CORS Settings (comma-separated)
|
||||||
# Example: CORS_ALLOWED_ORIGINS=http://YOUR_SERVER_IP:9123,https://your-frontend.com
|
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173,http://127.0.0.1:3000,http://185.208.172.158:9123,http://185.208.172.158
|
||||||
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
|
||||||
|
|
||||||
# Admin bootstrap (used by Docker entrypoint on server)
|
|
||||||
# Leave empty locally if you create the user manually
|
|
||||||
ADMIN_USERNAME=
|
|
||||||
ADMIN_PASSWORD=
|
|
||||||
|
|
||||||
# Gunicorn Settings (optional)
|
# Gunicorn Settings (optional)
|
||||||
GUNICORN_WORKERS=3
|
GUNICORN_WORKERS=3
|
||||||
GUNICORN_TIMEOUT=120
|
GUNICORN_TIMEOUT=120
|
||||||
|
|
||||||
|
|||||||
86
README.md
86
README.md
@@ -7,12 +7,11 @@ A Django REST Framework backend for Zoneco ORG with PostgreSQL database.
|
|||||||
## Test Status
|
## Test Status
|
||||||
|
|
||||||
```
|
```
|
||||||
Ran 31 tests — OK (0 failures)
|
Ran 24 tests in ~12s — OK (0 failures)
|
||||||
```
|
```
|
||||||
|
|
||||||
| Group | Tests | Status |
|
| Group | Tests | Status |
|
||||||
|-------|-------|--------|
|
|-------|-------|--------|
|
||||||
| Admin Login | 7 | ✅ All pass |
|
|
||||||
| Contact Us | 6 | ✅ All pass |
|
| Contact Us | 6 | ✅ All pass |
|
||||||
| Compositions | 11 | ✅ All pass |
|
| Compositions | 11 | ✅ All pass |
|
||||||
| Campaigns | 7 | ✅ All pass |
|
| Campaigns | 7 | ✅ All pass |
|
||||||
@@ -27,7 +26,6 @@ python manage.py test api -v 2
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Admin Login API** — Token-based login with username/password
|
|
||||||
- **Contact Us API** — Contact form submissions with category filter
|
- **Contact Us API** — Contact form submissions with category filter
|
||||||
- **Composition API** — Multi-image upload, main image flag, date-range filter
|
- **Composition API** — Multi-image upload, main image flag, date-range filter
|
||||||
- **Campaign API** — Campaigns with active / upcoming / ended filters
|
- **Campaign API** — Campaigns with active / upcoming / ended filters
|
||||||
@@ -70,70 +68,11 @@ API available at `{{base_url}}/api/`
|
|||||||
|
|
||||||
## Admin Credentials
|
## Admin Credentials
|
||||||
|
|
||||||
Set these in the server `.env` (never hardcode in code):
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
| Env var | Purpose |
|
| URL | `{{base_url}}/admin/` |
|
||||||
|---------|---------|
|
| Username | `Zoneco_@1405` |
|
||||||
| `ADMIN_USERNAME` | Admin username for Docker bootstrap |
|
| Password | `Fun_@_zone2026` |
|
||||||
| `ADMIN_PASSWORD` | Admin password for Docker bootstrap |
|
|
||||||
|
|
||||||
| Endpoint | Path |
|
|
||||||
|----------|------|
|
|
||||||
| Django Admin Panel | `{{base_url}}/admin/` |
|
|
||||||
| API Login | `POST {{base_url}}/api/admin/login/` |
|
|
||||||
|
|
||||||
On Docker deploy, `entrypoint.sh` creates/updates the admin from `ADMIN_USERNAME` / `ADMIN_PASSWORD`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Admin Login API
|
|
||||||
|
|
||||||
### Login
|
|
||||||
|
|
||||||
```
|
|
||||||
POST {{base_url}}/api/admin/login/
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "{{admin_username}}",
|
|
||||||
"password": "{{admin_password}}"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"token": "<token>",
|
|
||||||
"user": {
|
|
||||||
"id": 1,
|
|
||||||
"username": "<admin_username>",
|
|
||||||
"is_staff": true,
|
|
||||||
"is_superuser": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Use the token on protected endpoints
|
|
||||||
|
|
||||||
```
|
|
||||||
Authorization: Token <your_token_here>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Current admin user
|
|
||||||
|
|
||||||
```
|
|
||||||
GET {{base_url}}/api/admin/me/
|
|
||||||
Authorization: Token <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logout (deletes the token)
|
|
||||||
|
|
||||||
```
|
|
||||||
POST {{base_url}}/api/admin/logout/
|
|
||||||
Authorization: Token <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -145,8 +84,7 @@ Authorization: Token <token>
|
|||||||
|--------|--------|
|
|--------|--------|
|
||||||
| GET (list, retrieve, custom filters) | Public — no login required |
|
| GET (list, retrieve, custom filters) | Public — no login required |
|
||||||
| POST (create) | Public — no login required |
|
| POST (create) | Public — no login required |
|
||||||
| PUT / PATCH / DELETE | Admin only — Token required |
|
| PUT / PATCH / DELETE | Admin only — Basic Auth required |
|
||||||
| Admin login / logout / me | See Admin Login API above |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -299,9 +237,15 @@ DB constraint: only one `is_main=True` per composition.
|
|||||||
|
|
||||||
## Postman Collection
|
## Postman Collection
|
||||||
|
|
||||||
Import `Zoneco_ORG_API.postman_collection.json` into Postman.
|
Import `backend/Zoneco_ORG_API.postman_collection.json` into Postman.
|
||||||
|
|
||||||
Set collection variables: `base_url`, `admin_username`, `admin_password`.
|
Set the `base_url` collection variable to your server address.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Persian Documentation
|
||||||
|
|
||||||
|
Full Persian API documentation: `backend/API_DOCS_FA.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,106 +2,10 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"_postman_id": "zoneco-org-api-collection",
|
"_postman_id": "zoneco-org-api-collection",
|
||||||
"name": "Zoneco ORG API",
|
"name": "Zoneco ORG API",
|
||||||
"description": "مجموعه کامل APIهای بکاند Zoneco ORG.\n\n1) متغیرهای collection را تنظیم کنید: base_url, admin_username, admin_password\n2) Admin Auth > POST Admin Login را بزنید\n3) توکن در admin_token ذخیره میشود\n4) درخواستهای ادمین با Authorization: Token {{admin_token}} ارسال میشوند\n\nCredentials را در متغیرها بگذارید — در کد هاردکد نکنید.",
|
"description": "مجموعه کامل APIهای بکاند Zoneco ORG — شامل Contact Us، Compositions (با آپلود چند تصویر) و Campaigns.\n\nمتغیرها:\n- base_url: آدرس سرور (پیشفرض http://localhost:8000)\n- composition_id: شناسه ترکیب برای تست\n- image_id: شناسه تصویر ترکیب\n\nبرای درخواستهای ادمین از Basic Auth استفاده کنید.",
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
},
|
},
|
||||||
"item": [
|
"item": [
|
||||||
{
|
|
||||||
"name": "Admin Auth",
|
|
||||||
"description": "ورود ادمین با username/password و دریافت توکن",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "POST Admin Login",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"type": "text/javascript",
|
|
||||||
"exec": [
|
|
||||||
"if (pm.response.code === 200) {",
|
|
||||||
" var json = pm.response.json();",
|
|
||||||
" if (json.token) {",
|
|
||||||
" pm.collectionVariables.set('admin_token', json.token);",
|
|
||||||
" }",
|
|
||||||
"}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"username\": \"{{admin_username}}\",\n \"password\": \"{{admin_password}}\"\n}"
|
|
||||||
},
|
|
||||||
"url": "{{base_url}}/api/admin/login/",
|
|
||||||
"description": "Login with admin credentials. Saves token to admin_token automatically."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GET Admin Me",
|
|
||||||
"request": {
|
|
||||||
"auth": {
|
|
||||||
"type": "apikey",
|
|
||||||
"apikey": [
|
|
||||||
{
|
|
||||||
"key": "key",
|
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": "{{base_url}}/api/admin/me/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "POST Admin Logout",
|
|
||||||
"request": {
|
|
||||||
"auth": {
|
|
||||||
"type": "apikey",
|
|
||||||
"apikey": [
|
|
||||||
{
|
|
||||||
"key": "key",
|
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"method": "POST",
|
|
||||||
"header": [],
|
|
||||||
"url": "{{base_url}}/api/admin/logout/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Contact Us",
|
"name": "Contact Us",
|
||||||
"description": "APIهای فرم تماس با ما",
|
"description": "APIهای فرم تماس با ما",
|
||||||
@@ -146,15 +50,8 @@
|
|||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{base_url}}/api/contact-us/by_category/?category=پشتیبانی",
|
"raw": "{{base_url}}/api/contact-us/by_category/?category=پشتیبانی",
|
||||||
"host": [
|
"host": ["{{base_url}}"],
|
||||||
"{{base_url}}"
|
"path": ["api", "contact-us", "by_category", ""],
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"contact-us",
|
|
||||||
"by_category",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"query": [
|
"query": [
|
||||||
{
|
{
|
||||||
"key": "category",
|
"key": "category",
|
||||||
@@ -169,31 +66,15 @@
|
|||||||
"name": "PATCH Update Contact Us (Admin)",
|
"name": "PATCH Update Contact Us (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "PATCH",
|
"method": "PATCH",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{"key": "Content-Type", "value": "application/json"}
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
@@ -206,23 +87,10 @@
|
|||||||
"name": "DELETE Contact Us (Admin)",
|
"name": "DELETE Contact Us (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
@@ -259,15 +127,8 @@
|
|||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{base_url}}/api/compositions/by-created-at/?from=2026-01-01T00:00:00Z&to=2026-12-31T23:59:59Z",
|
"raw": "{{base_url}}/api/compositions/by-created-at/?from=2026-01-01T00:00:00Z&to=2026-12-31T23:59:59Z",
|
||||||
"host": [
|
"host": ["{{base_url}}"],
|
||||||
"{{base_url}}"
|
"path": ["api", "compositions", "by-created-at", ""],
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"compositions",
|
|
||||||
"by-created-at",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"query": [
|
"query": [
|
||||||
{
|
{
|
||||||
"key": "from",
|
"key": "from",
|
||||||
@@ -288,10 +149,7 @@
|
|||||||
"request": {
|
"request": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{"key": "Content-Type", "value": "application/json"}
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
@@ -308,32 +166,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"mode": "formdata",
|
"mode": "formdata",
|
||||||
"formdata": [
|
"formdata": [
|
||||||
{
|
{"key": "name", "value": "ترکیب با تصویر", "type": "text"},
|
||||||
"key": "name",
|
{"key": "description", "value": "ترکیب با چند تصویر", "type": "text"},
|
||||||
"value": "ترکیب با تصویر",
|
{"key": "uploaded_images", "type": "file", "src": []},
|
||||||
"type": "text"
|
{"key": "uploaded_images", "type": "file", "src": []},
|
||||||
},
|
{"key": "main_image_index", "value": "0", "type": "text", "description": "ایندکس تصویر اصلی (از 0)"}
|
||||||
{
|
|
||||||
"key": "description",
|
|
||||||
"value": "ترکیب با چند تصویر",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "uploaded_images",
|
|
||||||
"type": "file",
|
|
||||||
"src": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "uploaded_images",
|
|
||||||
"type": "file",
|
|
||||||
"src": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "main_image_index",
|
|
||||||
"value": "0",
|
|
||||||
"type": "text",
|
|
||||||
"description": "ایندکس تصویر اصلی (از 0)"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"url": "{{base_url}}/api/compositions/"
|
"url": "{{base_url}}/api/compositions/"
|
||||||
@@ -343,23 +180,10 @@
|
|||||||
"name": "POST Add Images to Composition (Admin)",
|
"name": "POST Add Images to Composition (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
@@ -367,16 +191,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"mode": "formdata",
|
"mode": "formdata",
|
||||||
"formdata": [
|
"formdata": [
|
||||||
{
|
{"key": "uploaded_images", "type": "file", "src": []},
|
||||||
"key": "uploaded_images",
|
{"key": "main_image_index", "value": "0", "type": "text"}
|
||||||
"type": "file",
|
|
||||||
"src": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "main_image_index",
|
|
||||||
"value": "0",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"url": "{{base_url}}/api/compositions/{{composition_id}}/add-images/"
|
"url": "{{base_url}}/api/compositions/{{composition_id}}/add-images/"
|
||||||
@@ -386,31 +202,15 @@
|
|||||||
"name": "POST Set Main Image (Admin)",
|
"name": "POST Set Main Image (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{"key": "Content-Type", "value": "application/json"}
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
@@ -423,23 +223,10 @@
|
|||||||
"name": "DELETE Composition Image (Admin)",
|
"name": "DELETE Composition Image (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
@@ -451,31 +238,15 @@
|
|||||||
"name": "PATCH Update Composition (Admin)",
|
"name": "PATCH Update Composition (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "PATCH",
|
"method": "PATCH",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{"key": "Content-Type", "value": "application/json"}
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
@@ -488,23 +259,10 @@
|
|||||||
"name": "DELETE Composition (Admin)",
|
"name": "DELETE Composition (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
@@ -539,10 +297,7 @@
|
|||||||
"request": {
|
"request": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{"key": "Content-Type", "value": "application/json"}
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
@@ -559,31 +314,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"mode": "formdata",
|
"mode": "formdata",
|
||||||
"formdata": [
|
"formdata": [
|
||||||
{
|
{"key": "name", "value": "کمپین با تصویر", "type": "text"},
|
||||||
"key": "name",
|
{"key": "description", "value": "توضیحات کمپین", "type": "text"},
|
||||||
"value": "کمپین با تصویر",
|
{"key": "start_time", "value": "2026-06-01T00:00:00Z", "type": "text"},
|
||||||
"type": "text"
|
{"key": "end_time", "value": "2026-12-31T23:59:59Z", "type": "text"},
|
||||||
},
|
{"key": "image", "type": "file", "src": []}
|
||||||
{
|
|
||||||
"key": "description",
|
|
||||||
"value": "توضیحات کمپین",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "start_time",
|
|
||||||
"value": "2026-06-01T00:00:00Z",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "end_time",
|
|
||||||
"value": "2026-12-31T23:59:59Z",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "image",
|
|
||||||
"type": "file",
|
|
||||||
"src": []
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"url": "{{base_url}}/api/campaigns/"
|
"url": "{{base_url}}/api/campaigns/"
|
||||||
@@ -617,31 +352,15 @@
|
|||||||
"name": "PATCH Update Campaign (Admin)",
|
"name": "PATCH Update Campaign (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "PATCH",
|
"method": "PATCH",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{"key": "Content-Type", "value": "application/json"}
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
@@ -654,23 +373,10 @@
|
|||||||
"name": "DELETE Campaign (Admin)",
|
"name": "DELETE Campaign (Admin)",
|
||||||
"request": {
|
"request": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "apikey",
|
"type": "basic",
|
||||||
"apikey": [
|
"basic": [
|
||||||
{
|
{"key": "username", "value": "{{admin_username}}", "type": "string"},
|
||||||
"key": "key",
|
{"key": "password", "value": "{{admin_password}}", "type": "string"}
|
||||||
"value": "Authorization",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"value": "Token {{admin_token}}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "in",
|
|
||||||
"value": "header",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
@@ -684,7 +390,7 @@
|
|||||||
"variable": [
|
"variable": [
|
||||||
{
|
{
|
||||||
"key": "base_url",
|
"key": "base_url",
|
||||||
"value": "http://127.0.0.1:8000",
|
"value": "http://localhost:8000",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -699,17 +405,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "admin_username",
|
"key": "admin_username",
|
||||||
"value": "",
|
"value": "Zoneco_@1405",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "admin_password",
|
"key": "admin_password",
|
||||||
"value": "",
|
"value": "Fun_@_zone2026",
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "admin_token",
|
|
||||||
"value": "",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,34 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.contrib.auth import authenticate
|
|
||||||
from .models import ContactUs, Composition, Campaign, CompositionImage
|
from .models import ContactUs, Composition, Campaign, CompositionImage
|
||||||
|
|
||||||
|
|
||||||
class AdminLoginSerializer(serializers.Serializer):
|
|
||||||
"""Serializer for admin username/password login."""
|
|
||||||
username = serializers.CharField(required=True)
|
|
||||||
password = serializers.CharField(required=True, write_only=True)
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
username = data.get('username', '').strip()
|
|
||||||
password = data.get('password', '')
|
|
||||||
|
|
||||||
if not username or not password:
|
|
||||||
raise serializers.ValidationError('Username and password are required.')
|
|
||||||
|
|
||||||
user = authenticate(username=username, password=password)
|
|
||||||
if user is None:
|
|
||||||
raise serializers.ValidationError('Invalid username or password.')
|
|
||||||
if not user.is_active:
|
|
||||||
raise serializers.ValidationError('This account is disabled.')
|
|
||||||
if not (user.is_staff or user.is_superuser):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
'This account does not have admin access.'
|
|
||||||
)
|
|
||||||
|
|
||||||
data['user'] = user
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ContactUsSerializer(serializers.ModelSerializer):
|
class ContactUsSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for ContactUs model.
|
Serializer for ContactUs model.
|
||||||
|
|||||||
113
api/tests.py
113
api/tests.py
@@ -64,10 +64,7 @@ class ContactUsAPITests(APITestCase):
|
|||||||
{'name': 'Updated'},
|
{'name': 'Updated'},
|
||||||
format='json',
|
format='json',
|
||||||
)
|
)
|
||||||
self.assertIn(response.status_code, (
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
status.HTTP_401_UNAUTHORIZED,
|
|
||||||
status.HTTP_403_FORBIDDEN,
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
class CompositionAPITests(APITestCase):
|
class CompositionAPITests(APITestCase):
|
||||||
@@ -203,10 +200,7 @@ class CompositionAPITests(APITestCase):
|
|||||||
{'name': 'Updated'},
|
{'name': 'Updated'},
|
||||||
format='json',
|
format='json',
|
||||||
)
|
)
|
||||||
self.assertIn(response.status_code, (
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
status.HTTP_401_UNAUTHORIZED,
|
|
||||||
status.HTTP_403_FORBIDDEN,
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
class CampaignAPITests(APITestCase):
|
class CampaignAPITests(APITestCase):
|
||||||
@@ -283,106 +277,3 @@ class CampaignAPITests(APITestCase):
|
|||||||
}
|
}
|
||||||
response = self.client.post('/api/campaigns/', payload, format='json')
|
response = self.client.post('/api/campaigns/', payload, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class AdminLoginAPITests(APITestCase):
|
|
||||||
def setUp(self):
|
|
||||||
User = get_user_model()
|
|
||||||
self.admin_username = 'admin_test'
|
|
||||||
self.admin_password = 'test_admin_pass_123'
|
|
||||||
self.admin = User.objects.create_user(
|
|
||||||
username=self.admin_username,
|
|
||||||
password=self.admin_password,
|
|
||||||
is_staff=True,
|
|
||||||
is_superuser=True,
|
|
||||||
)
|
|
||||||
self.regular = User.objects.create_user(
|
|
||||||
username='normaluser',
|
|
||||||
password='normalpass123',
|
|
||||||
is_staff=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_admin_login_success(self):
|
|
||||||
response = self.client.post(
|
|
||||||
'/api/admin/login/',
|
|
||||||
{'username': self.admin_username, 'password': self.admin_password},
|
|
||||||
format='json',
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertIn('token', response.data)
|
|
||||||
self.assertEqual(response.data['user']['username'], self.admin_username)
|
|
||||||
self.assertTrue(response.data['user']['is_staff'])
|
|
||||||
|
|
||||||
def test_admin_login_wrong_password(self):
|
|
||||||
response = self.client.post(
|
|
||||||
'/api/admin/login/',
|
|
||||||
{'username': self.admin_username, 'password': 'wrong'},
|
|
||||||
format='json',
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def test_non_staff_cannot_login(self):
|
|
||||||
response = self.client.post(
|
|
||||||
'/api/admin/login/',
|
|
||||||
{'username': 'normaluser', 'password': 'normalpass123'},
|
|
||||||
format='json',
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def test_admin_me_with_token(self):
|
|
||||||
login = self.client.post(
|
|
||||||
'/api/admin/login/',
|
|
||||||
{'username': self.admin_username, 'password': self.admin_password},
|
|
||||||
format='json',
|
|
||||||
)
|
|
||||||
token = login.data['token']
|
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token}')
|
|
||||||
response = self.client.get('/api/admin/me/')
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(response.data['username'], self.admin_username)
|
|
||||||
|
|
||||||
def test_admin_me_requires_auth(self):
|
|
||||||
response = self.client.get('/api/admin/me/')
|
|
||||||
self.assertIn(response.status_code, (
|
|
||||||
status.HTTP_401_UNAUTHORIZED,
|
|
||||||
status.HTTP_403_FORBIDDEN,
|
|
||||||
))
|
|
||||||
|
|
||||||
def test_token_allows_protected_update(self):
|
|
||||||
contact = ContactUs.objects.create(
|
|
||||||
name='Old Name',
|
|
||||||
email_or_phone='a@b.com',
|
|
||||||
description='msg',
|
|
||||||
category='سایر',
|
|
||||||
)
|
|
||||||
login = self.client.post(
|
|
||||||
'/api/admin/login/',
|
|
||||||
{'username': self.admin_username, 'password': self.admin_password},
|
|
||||||
format='json',
|
|
||||||
)
|
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f'Token {login.data["token"]}')
|
|
||||||
response = self.client.patch(
|
|
||||||
f'/api/contact-us/{contact.id}/',
|
|
||||||
{'name': 'New Name'},
|
|
||||||
format='json',
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(response.data['name'], 'New Name')
|
|
||||||
|
|
||||||
def test_admin_logout(self):
|
|
||||||
login = self.client.post(
|
|
||||||
'/api/admin/login/',
|
|
||||||
{'username': self.admin_username, 'password': self.admin_password},
|
|
||||||
format='json',
|
|
||||||
)
|
|
||||||
token = login.data['token']
|
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token}')
|
|
||||||
response = self.client.post('/api/admin/logout/')
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
# Token should no longer work
|
|
||||||
response = self.client.get('/api/admin/me/')
|
|
||||||
self.assertIn(response.status_code, (
|
|
||||||
status.HTTP_401_UNAUTHORIZED,
|
|
||||||
status.HTTP_403_FORBIDDEN,
|
|
||||||
))
|
|
||||||
|
|||||||
13
api/urls.py
13
api/urls.py
@@ -1,13 +1,6 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import (
|
from .views import ContactUsViewSet, CompositionViewSet, CampaignViewSet
|
||||||
ContactUsViewSet,
|
|
||||||
CompositionViewSet,
|
|
||||||
CampaignViewSet,
|
|
||||||
admin_login,
|
|
||||||
admin_logout,
|
|
||||||
admin_me,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'contact-us', ContactUsViewSet, basename='contact-us')
|
router.register(r'contact-us', ContactUsViewSet, basename='contact-us')
|
||||||
@@ -15,8 +8,6 @@ router.register(r'compositions', CompositionViewSet, basename='composition')
|
|||||||
router.register(r'campaigns', CampaignViewSet, basename='campaign')
|
router.register(r'campaigns', CampaignViewSet, basename='campaign')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/login/', admin_login, name='admin-login'),
|
|
||||||
path('admin/logout/', admin_logout, name='admin-logout'),
|
|
||||||
path('admin/me/', admin_me, name='admin-me'),
|
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
50
api/views.py
50
api/views.py
@@ -1,9 +1,8 @@
|
|||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.decorators import action, api_view, permission_classes
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
from rest_framework.authtoken.models import Token
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -14,56 +13,9 @@ from .serializers import (
|
|||||||
CompositionSerializer,
|
CompositionSerializer,
|
||||||
CampaignSerializer,
|
CampaignSerializer,
|
||||||
CompositionImageSerializer,
|
CompositionImageSerializer,
|
||||||
AdminLoginSerializer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
|
||||||
@permission_classes([AllowAny])
|
|
||||||
def admin_login(request):
|
|
||||||
"""
|
|
||||||
Admin login with username and password.
|
|
||||||
|
|
||||||
Returns an auth token to use as:
|
|
||||||
Authorization: Token <token>
|
|
||||||
"""
|
|
||||||
serializer = AdminLoginSerializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
user = serializer.validated_data['user']
|
|
||||||
token, _ = Token.objects.get_or_create(user=user)
|
|
||||||
return Response({
|
|
||||||
'token': token.key,
|
|
||||||
'user': {
|
|
||||||
'id': user.id,
|
|
||||||
'username': user.username,
|
|
||||||
'is_staff': user.is_staff,
|
|
||||||
'is_superuser': user.is_superuser,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def admin_logout(request):
|
|
||||||
"""Delete the current admin auth token (logout)."""
|
|
||||||
Token.objects.filter(user=request.user).delete()
|
|
||||||
return Response({'detail': 'Logged out successfully.'})
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def admin_me(request):
|
|
||||||
"""Return the currently authenticated admin user."""
|
|
||||||
user = request.user
|
|
||||||
return Response({
|
|
||||||
'id': user.id,
|
|
||||||
'username': user.username,
|
|
||||||
'is_staff': user.is_staff,
|
|
||||||
'is_superuser': user.is_superuser,
|
|
||||||
'is_active': user.is_active,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class ContactUsViewSet(viewsets.ModelViewSet):
|
class ContactUsViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
ViewSet for ContactUs model.
|
ViewSet for ContactUs model.
|
||||||
|
|||||||
@@ -28,16 +28,14 @@ if [ ! -f .env ]; then
|
|||||||
cat > .env << EOF
|
cat > .env << EOF
|
||||||
SECRET_KEY=$(python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())")
|
SECRET_KEY=$(python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())")
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
ALLOWED_HOSTS=localhost,127.0.0.1,YOUR_SERVER_IP
|
ALLOWED_HOSTS=localhost,127.0.0.1,185.208.172.158
|
||||||
POSTGRES_DB=Zoneco_ORG
|
POSTGRES_DB=Zoneco_ORG
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=change-me
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_HOST=db
|
POSTGRES_HOST=db
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
DJANGO_PORT=8000
|
DJANGO_PORT=8000
|
||||||
ADMIN_USERNAME=
|
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173,http://127.0.0.1:3000,http://185.208.172.158:9123,http://185.208.172.158
|
||||||
ADMIN_PASSWORD=
|
|
||||||
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://YOUR_SERVER_IP:9123
|
|
||||||
EOF
|
EOF
|
||||||
print_info ".env file created with generated SECRET_KEY"
|
print_info ".env file created with generated SECRET_KEY"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-Zoneco_ORG}
|
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-Zoneco_ORG}
|
||||||
- POSTGRES_HOST=db
|
|
||||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-}
|
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -1,47 +1,26 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
DB_HOST="${POSTGRES_HOST:-db}"
|
echo "Waiting for PostgreSQL to be ready..."
|
||||||
DB_USER="${POSTGRES_USER:-postgres}"
|
while ! pg_isready -h db -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-Zoneco_ORG}; do
|
||||||
DB_NAME="${POSTGRES_DB:-Zoneco_ORG}"
|
|
||||||
|
|
||||||
echo "Waiting for PostgreSQL to be ready at ${DB_HOST}..."
|
|
||||||
while ! pg_isready -h "${DB_HOST}" -U "${DB_USER}" -d "${DB_NAME}"; do
|
|
||||||
echo "PostgreSQL is unavailable - sleeping"
|
echo "PostgreSQL is unavailable - sleeping"
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "PostgreSQL is up"
|
echo "PostgreSQL is up - executing command"
|
||||||
|
|
||||||
|
# Collect static files
|
||||||
echo "Collecting static files..."
|
echo "Collecting static files..."
|
||||||
python manage.py collectstatic --noinput
|
python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
echo "Running migrations..."
|
echo "Running migrations..."
|
||||||
python manage.py migrate --noinput
|
python manage.py migrate --noinput
|
||||||
|
|
||||||
# Create/update admin from environment variables (no hardcoded credentials).
|
# Create superuser if it doesn't exist (optional, can be removed in production)
|
||||||
# Set ADMIN_USERNAME and ADMIN_PASSWORD in the server .env / compose env.
|
# Uncomment and modify if needed:
|
||||||
if [ -n "${ADMIN_USERNAME:-}" ] && [ -n "${ADMIN_PASSWORD:-}" ]; then
|
# echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.filter(username='admin').exists() or User.objects.create_superuser('admin', 'admin@example.com', 'admin')" | python manage.py shell
|
||||||
echo "Ensuring admin user from environment variables..."
|
|
||||||
python manage.py shell <<EOF
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
User = get_user_model()
|
|
||||||
username = "${ADMIN_USERNAME}"
|
|
||||||
password = "${ADMIN_PASSWORD}"
|
|
||||||
user, created = User.objects.get_or_create(
|
|
||||||
username=username,
|
|
||||||
defaults={"is_staff": True, "is_superuser": True, "is_active": True},
|
|
||||||
)
|
|
||||||
user.is_staff = True
|
|
||||||
user.is_superuser = True
|
|
||||||
user.is_active = True
|
|
||||||
user.set_password(password)
|
|
||||||
user.save()
|
|
||||||
print("Admin user CREATED" if created else "Admin user UPDATED")
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
echo "ADMIN_USERNAME/ADMIN_PASSWORD not set — skipping admin bootstrap"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting application..."
|
echo "Starting application..."
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ DEBUG = os.environ.get('DEBUG', 'False') == 'True'
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
ALLOWED_HOSTS = ['*'] # Allow all hosts in development only
|
ALLOWED_HOSTS = ['*'] # Allow all hosts in development only
|
||||||
else:
|
else:
|
||||||
allowed_hosts_env = os.environ.get('ALLOWED_HOSTS', '')
|
# Production: require ALLOWED_HOSTS to be set explicitly
|
||||||
ALLOWED_HOSTS = [h.strip() for h in allowed_hosts_env.split(',') if h.strip()]
|
allowed_hosts_env = os.environ.get('ALLOWED_HOSTS')
|
||||||
if not ALLOWED_HOSTS:
|
if allowed_hosts_env:
|
||||||
# Fail safe: empty list rejects all hosts until env is configured
|
ALLOWED_HOSTS = allowed_hosts_env.split(',')
|
||||||
ALLOWED_HOSTS = []
|
else:
|
||||||
|
# Fallback for production (should be overridden by env var)
|
||||||
|
ALLOWED_HOSTS = ['185.208.172.158'] # Your server IP
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@@ -50,7 +52,6 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
# Third party apps
|
# Third party apps
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
# Local apps
|
# Local apps
|
||||||
'api',
|
'api',
|
||||||
@@ -151,11 +152,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
|
|
||||||
# REST Framework configuration
|
# REST Framework configuration
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
|
||||||
],
|
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
'rest_framework.permissions.AllowAny',
|
'rest_framework.permissions.AllowAny',
|
||||||
],
|
],
|
||||||
@@ -177,8 +173,18 @@ REST_FRAMEWORK = {
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
CORS_ALLOW_ALL_ORIGINS = True # Development only - allows all origins
|
CORS_ALLOW_ALL_ORIGINS = True # Development only - allows all origins
|
||||||
else:
|
else:
|
||||||
cors_origins_env = os.environ.get('CORS_ALLOWED_ORIGINS', '')
|
# Production: require CORS_ALLOWED_ORIGINS to be set explicitly
|
||||||
CORS_ALLOWED_ORIGINS = [o.strip() for o in cors_origins_env.split(',') if o.strip()]
|
cors_origins_env = os.environ.get('CORS_ALLOWED_ORIGINS')
|
||||||
|
if cors_origins_env:
|
||||||
|
CORS_ALLOWED_ORIGINS = cors_origins_env.split(',')
|
||||||
|
else:
|
||||||
|
# Fallback for production (should be overridden by env var)
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
'http://185.208.172.158:9123',
|
||||||
|
'http://185.208.172.158',
|
||||||
|
'https://185.208.172.158:9123',
|
||||||
|
'https://185.208.172.158',
|
||||||
|
]
|
||||||
|
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user