- Removed the "دکتر" prefix from doctor names in various components and API responses to ensure consistency and clarity. - Updated the AppointmentDetailPage, CommentsPage, DashboardPage, RatingsPage, SecretariesPage, and other relevant files to reflect the changes in doctor name formatting. - Adjusted API documentation to align with the new naming conventions. - Implemented validation to prevent the creation of clinics without a name and restricted users to a single clinic. - Added tests to verify that doctor names are stored without titles and that clinic creation adheres to the new validation rules.
259 lines
7.4 KiB
Markdown
259 lines
7.4 KiB
Markdown
# Blog API
|
|
|
|
> **Prefix:** `/api/v1/blog`, `/api/v1/blogs`
|
|
|
|
---
|
|
|
|
## GET `/api/v1/blogs`
|
|
|
|
List published blog posts.
|
|
|
|
**Permission:** `PUBLIC`
|
|
|
|
### Query Parameters
|
|
| Param | Type | Required | Default | Description |
|
|
|-------|------|----------|---------|-------------|
|
|
| `page` | integer | ❌ | 1 | Page number |
|
|
| `limit` | integer | ❌ | 20 | Items per page |
|
|
| `tag` | string | ❌ | — | Filter by exact tag **name** (e.g. `?tag=دیابت`). Blog tags are stored as a JSON array of names; only blogs whose `tags` array contains this exact name are returned. |
|
|
| `city_id` | integer | ❌ | — | Scope to one city. Returns that city's posts **plus every nationwide post** (`city_id IS NULL`). Omit it to return all published posts regardless of city. |
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"uuid": "...",
|
|
"title": "آشنایی با بیماری دیابت",
|
|
"slug": "ashnayi-ba-bimari-diabat",
|
|
"summary": "خلاصه مطلب...",
|
|
"image": "https://...",
|
|
"author": { "uuid": "...", "real_name": "احمدی" },
|
|
"tags": [{ "id": 1, "name": "دیابت" }],
|
|
"status": "published",
|
|
"city": { "id": "123", "name": "یاسوج" },
|
|
"created_at": 1717000000
|
|
}
|
|
],
|
|
"meta": {
|
|
"totalRecords": 25,
|
|
"totalPages": 2,
|
|
"currentPage": 1,
|
|
"limit": 20
|
|
}
|
|
}
|
|
```
|
|
|
|
### فیلد `city` — شهر پست
|
|
|
|
`city` در پاسخ لیست و جزئیات وجود دارد و دو حالت دارد:
|
|
|
|
| مقدار | معنا |
|
|
|-------|------|
|
|
| `{ "id": "123", "name": "یاسوج" }` | پست **شهری** — به آن شهر تعلق دارد |
|
|
| `null` | پست **سراسری** — حالت دائمی و معتبر، نه «تنظیمنشده» |
|
|
|
|
- `city_id` روی `blogs` **nullable** است و `NULL` معنای دائمی «سراسری» دارد. رکوردهای قبل از این تغییر همگی سراسری شدند.
|
|
- حذف شهر پست را حذف نمیکند (`ON DELETE SET NULL`) — پست سراسری میشود.
|
|
- پست سراسری روی **همهٔ** دامنههای شهری در لیست دیده میشود؛ فقط canonical آن روی دامنهٔ اصلی مینشیند. به همین دلیل `city_id=X` هم پستهای شهر X و هم پستهای سراسری را برمیگرداند — نه فقط شهر X.
|
|
|
|
> 🔗 مصرفکننده: سایت عمومی چند-دامنهای (`nobat724_front`) با همین فیلد تصمیم میگیرد پست را روی دامنهٔ شهر canonical کند یا روی دامنهٔ اصلی، و در کدام sitemap بگذارد. تغییر معنای `null` قرارداد آن را میشکند.
|
|
|
|
---
|
|
|
|
## GET `/api/v1/blog/{slug}`
|
|
|
|
Get a single blog post by slug.
|
|
|
|
**Permission:** `PUBLIC`
|
|
|
|
### Path Parameters
|
|
| Param | Type | Description |
|
|
|-------|------|-------------|
|
|
| `slug` | string | URL slug (e.g. `ashnayi-ba-bimari-diabat`) |
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"uuid": "...",
|
|
"title": "آشنایی با بیماری دیابت",
|
|
"slug": "ashnayi-ba-bimari-diabat",
|
|
"summary": "خلاصه...",
|
|
"body": "<p>محتوای کامل...</p>",
|
|
"image": "https://...",
|
|
"author": { "uuid": "...", "real_name": "احمدی" },
|
|
"tags": [{ "id": 1, "name": "دیابت" }],
|
|
"status": "published",
|
|
"created_at": 1717000000,
|
|
"updated_at": 1717100000
|
|
}
|
|
}
|
|
```
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_NOT_FOUND_001` | 404 | Blog not found or not published |
|
|
|
|
---
|
|
|
|
## POST `/api/v1/blog`
|
|
|
|
Create a new blog post.
|
|
|
|
**Permission:** `ROLE_ADMIN`
|
|
|
|
### Request Body (`application/json`)
|
|
```json
|
|
{
|
|
"title": "آشنایی با بیماری دیابت",
|
|
"body": "<p>محتوای کامل مقاله...</p>",
|
|
"summary": "خلاصه کوتاه از مقاله",
|
|
"tags": [1, 2],
|
|
"status": "draft",
|
|
"image_url": "/uploads/blogs/blog_abc.jpg"
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `title` | string | ✅ | Post title (slug auto-generated) |
|
|
| `body` | string | ✅ | Full HTML body (from the admin CKEditor) |
|
|
| `summary` | string | ❌ | Short excerpt |
|
|
| `tags` | integer[] | ❌ | Array of tag IDs |
|
|
| `status` | string | ❌ | `"draft"` (default) or `"published"` |
|
|
| `image_url` | string | ❌ | Cover image path returned by the upload endpoint |
|
|
| `city_id` | integer\|null | ❌ | City this post belongs to. **Omitting it, or sending `null`/`0`, creates a nationwide post.** An unknown city id is rejected with `422`. |
|
|
|
|
### Response `201`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"uuid": "blog-uuid-...",
|
|
"title": "آشنایی با بیماری دیابت",
|
|
"slug": "ashnayi-ba-bimari-diabat",
|
|
"summary": "...",
|
|
"body": "...",
|
|
"image": null,
|
|
"tags": [],
|
|
"status": "draft",
|
|
"created_at": 1717000000
|
|
}
|
|
}
|
|
```
|
|
|
|
**Blog Status Values:**
|
|
| Value | Description |
|
|
|-------|-------------|
|
|
| `draft` | Not visible to public |
|
|
| `published` | Visible in public listing |
|
|
| `archived` | Hidden from listing |
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not admin |
|
|
| `ERR_VALIDATION_002` | 422 | Missing required fields |
|
|
|
|
---
|
|
|
|
## PATCH `/api/v1/blog/{uuid}`
|
|
|
|
Update a blog post.
|
|
|
|
**Permission:** `ROLE_ADMIN`
|
|
|
|
### Path Parameters
|
|
| Param | Type | Description |
|
|
|-------|------|-------------|
|
|
| `uuid` | string (UUID) | Blog UUID |
|
|
|
|
### Request Body (`application/json`)
|
|
```json
|
|
{
|
|
"title": "عنوان جدید",
|
|
"body": "<p>محتوای جدید</p>",
|
|
"summary": "خلاصه جدید",
|
|
"tags": [1, 3],
|
|
"status": "published",
|
|
"image_url": "/uploads/blogs/blog_abc.jpg"
|
|
}
|
|
```
|
|
|
|
All fields optional. Send `image_url: ""` to clear the cover image.
|
|
|
|
`city_id` follows PATCH semantics: **omit it and the post's city is left untouched**; send `null` (or `0`) to turn the post into a nationwide one; send a city id to move it to that city.
|
|
|
|
### Response `200`
|
|
Updated blog object.
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not admin |
|
|
| `ERR_NOT_FOUND_001` | 404 | Blog not found |
|
|
| `ERR_VALIDATION_002` | 422 | Unknown `city_id` |
|
|
|
|
---
|
|
|
|
## DELETE `/api/v1/blog/{uuid}`
|
|
|
|
Delete a blog post.
|
|
|
|
**Permission:** `ROLE_ADMIN`
|
|
|
|
### Response `200`
|
|
```json
|
|
{ "success": true, "data": { "message": "مقاله حذف شد" } }
|
|
```
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not admin |
|
|
| `ERR_NOT_FOUND_001` | 404 | Blog not found |
|
|
|
|
---
|
|
|
|
## POST `/file/upload/clinic_pro/blog/field_image`
|
|
|
|
Upload blog post header image.
|
|
|
|
**Permission:** `ROLE_ADMIN`
|
|
|
|
### Request
|
|
`Content-Type: multipart/form-data`
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `file` | binary | ✅ | Image (max 5MB) |
|
|
| `blog_uuid` | string (UUID) | ❌ | Auto-associate with blog post |
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"url": "https://clinic-pro.ddev.site/uploads/blog/post_abc.jpg",
|
|
"uuid": "...",
|
|
"filename": "post_abc.jpg",
|
|
"filemime": "image/jpeg",
|
|
"filesize": 204800
|
|
}
|
|
}
|
|
```
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not admin |
|
|
| `ERR_FILE_001` | 422 | Invalid file type |
|