# Finzone v2 — REST API v1

API "WordPress-style" cho agent đăng bài tự động.

**Base URL:** `https://v2.finzone.io.vn/api/v1`
**Auth:** `Authorization: Bearer fz_<env>_<24-rand>`
**Sinh token:** Admin → `/admin/api-tokens` → chọn scopes → tạo (token chỉ hiển thị 1 lần).

## Scopes

| scope             | quyền                                        |
| ----------------- | -------------------------------------------- |
| `post:read`       | GET /articles, /articles/{id}, /tags         |
| `post:write`      | POST/PATCH /articles, POST /tags             |
| `post:publish`    | tạo bài `status=published`, /publish, /unpublish |
| `post:delete`     | DELETE /articles/{id}                        |
| `category:read`   | GET /categories                              |
| `category:write`  | POST /categories                             |
| `media:upload`    | POST /media (multipart)                      |
| `seo:audit`       | (planned) /seo/audit                         |

## Endpoints

### `GET /healthz`
Open, không cần token. Health check.

### `GET /articles`
Query: `status` `category` `q` `page` `pageSize` (≤100) `sort=newest|oldest|viewed`.

### `POST /articles`
Body (JSON):
```json
{
  "title": "string (1..200)",
  "slug": "auto từ title nếu thiếu",
  "content": "markdown (min 1)",
  "excerpt": "auto từ 160 ký tự đầu nếu thiếu",
  "coverImage": "https://...",
  "status": "draft | published | scheduled",
  "publishedAt": "ISO date",
  "scheduledFor": "ISO date",
  "metaTitle": "50–60 ký tự lý tưởng",
  "metaDesc": "130–160 ký tự lý tưởng",
  "focusKeyword": "string",
  "categorySlug": "crypto | chung-khoan | ...",
  "tags": ["Bitcoin", "Fed"]
}
```
Trả: `{ id, slug, status, seoScore, url }`.
Tự tính `seoScore` (0..100), `seoChecks` (13 rules), `readabilityScore`, `jsonLd` (NewsArticle schema).

### `GET /articles/{idOrSlug}`
Trả article đầy đủ (kèm `category`, `author`, `tags`, `seoChecks`, `jsonLd`).

### `PATCH /articles/{idOrSlug}`
Cập nhật một phần. Recompute SEO + JSON-LD. Đổi `status=published` cần scope `post:publish`.

### `DELETE /articles/{idOrSlug}`
Xóa hẳn.

### `POST /articles/{idOrSlug}/publish`
Chuyển `published`, set `publishedAt` nếu null.

### `POST /articles/{idOrSlug}/unpublish`
Quay về `draft`.

### `GET /categories` · `POST /categories`
List + tạo. Slug auto-normalize Việt → ASCII.

### `GET /tags?q=` · `POST /tags`
List + upsert by slug.

### `POST /media`  (multipart)
Field `file` (image/* ≤10MB) + `alt`. Lưu `public/uploads/<yymm>/<rand>-<name>`. Trả `{ id, url, size, mime }`.

## SEO scoring rules (13)

`title_len`, `meta_title_len`, `meta_desc_len`, `focus_in_title`, `focus_in_h1_first` (100 từ đầu), `focus_in_meta_desc`, `focus_density` (0.5–2.5%), `word_count` (≥600 lý tưởng), `has_image`, `alt_on_images`, `has_links`, `slug_short`, `has_subheads`. Mỗi rule có weight 4–12, kết quả `pass=full | warn=½ | fail=0`.

## Audit log

Mọi action qua API ghi `AuditLog` với `source=api`, `tokenPrefix`, `ipAddress`, `actorEmail`. Xem ở `/admin/audit`.

## Error format

```json
{ "error": { "code": "missing_token", "message": "...", "details": {} } }
```

Codes: `missing_token`, `invalid_token`, `expired_token`, `ip_not_allowed`, `insufficient_scope`, `bad_json`, `bad_body`, `bad_query`, `not_found`, `slug_taken`, `too_large`, `bad_mime`, `create_failed`, `update_failed`.

## Quick test

```bash
TOKEN=fz_live_xxxxxxxxxxxxxxxxxxxxxxxx
curl -H "Authorization: Bearer $TOKEN" \
  https://v2.finzone.io.vn/api/v1/healthz

curl -X POST https://v2.finzone.io.vn/api/v1/articles \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Demo","content":"# Hello","categorySlug":"crypto","status":"draft"}'
```
