Files

413 lines
14 KiB
Markdown
Raw Permalink Normal View History

# مرجع API
<div dir="rtl">
پایه: `/api/v1/` · نسخه‌بندی از طریق namespace آدرس‌ها · اسکیمای ماشین‌خوان: [`openapi.yaml`](openapi.yaml)
مستندات تعاملی (همیشه به‌روز، از روی کد تولید می‌شود):
- Swagger UI — <http://localhost:8080/api/docs/>
- ReDoc — <http://localhost:8080/api/redoc/>
</div>
---
## ۱. قراردادهای عمومی
### ۱.۱ احراز هویت
```http
Authorization: Bearer <access_token>
```
<div dir="rtl">
سیاست پیش‌فرض **deny** است: هر endpoint که صریحاً عمومی اعلام نشده، لاگین می‌خواهد.
</div>
### ۱.۲ قالب خطا
```json
{
"error": {
"code": "validation_error",
"message": "The request payload failed validation.",
"details": { "min_experience": ["min_experience cannot be greater than max_experience."] },
"request_id": "9f2c1a7e8b4d4c0e"
}
}
```
| `code` | HTTP | معنی |
|---|---|---|
| `validation_error` | 400 | ورودی نامعتبر (`details` فیلد به فیلد) |
| `authentication_failed` / `not_authenticated` | 401 | توکن نبود یا منقضی شده |
| `permission_denied` | 403 | نقش کافی نیست |
| `not_found` | 404 | منبع وجود ندارد |
| `method_not_allowed` | 405 | متد پشتیبانی نمی‌شود |
| `throttled` | 429 | سقف نرخ رد شده |
| `search_unavailable` | 503 | موتور سرچ در دسترس نیست |
| `internal_error` | 500 | خطای پیش‌بینی‌نشده (جزئیات فقط در لاگ) |
### ۱.۳ هدرها
| هدر | جهت | توضیح |
|---|---|---|
| `X-Request-ID` | ورودی (اختیاری) | اگر بفرستید همان برمی‌گردد، اگر نه تولید می‌شود |
| `X-Request-ID` | خروجی | کلید ردیابی درخواست در Kibana |
### ۱.۴ سقف نرخ
| گروه | پیش‌فرض | کلید |
|---|---|---|
| ناشناس | ۳۰/دقیقه | IP |
| کاربر لاگین‌کرده | ۳۰۰/دقیقه | user id |
| سرچ | ۱۲۰/دقیقه | user id |
| لاگین/ثبت‌نام | ۱۰/دقیقه | **IP** (چون نام کاربری را مهاجم انتخاب می‌کند) |
---
## ۲. احراز هویت
### `POST /api/v1/auth/register/` — عمومی
```jsonc
// درخواست
{ "email": "me@example.com", "full_name": "Me",
"password": "Str0ng-Passw0rd!", "password_confirm": "Str0ng-Passw0rd!" }
// 201
{ "id": 4, "email": "me@example.com", "full_name": "Me",
"role": "viewer", "can_view_pii": false, "date_joined": "2026-09-03T04:00:00Z" }
```
<div dir="rtl">
ثبت‌نام همیشه نقش `viewer` می‌دهد؛ ارتقا یک اقدام صریح ادمین است. ایمیل نرمال (lowercase) می‌شود. رمز از اعتبارسنج‌های جنگو رد می‌شود (حداقل ۱۰ کاراکتر، غیرعددی، غیرمعمول، غیرمشابه ایمیل).
</div>
### `POST /api/v1/auth/login/` — عمومی
```jsonc
// درخواست
{ "email": "analyst@example.com", "password": "Analyst-Passw0rd!" }
// 200
{ "access": "eyJhbGciOi...", // عمر ۱۵ دقیقه
"refresh": "eyJhbGciOi...", // عمر ۷ روز، چرخشی
"user": { "id": 2, "email": "analyst@example.com", "role": "analyst",
"can_view_pii": true, ... } }
```
### `POST /api/v1/auth/refresh/` — عمومی
```jsonc
{ "refresh": "..." } → { "access": "...", "refresh": "..." }
```
<div dir="rtl">
توکن refresh **چرخشی** است: هر بار یک جفت جدید می‌گیرید و توکن قبلی بلاک‌لیست می‌شود. اگر یک توکن دزدیده و استفاده شود، استفاده بعدی کاربر واقعی رد می‌شود — یعنی نشت قابل تشخیص است.
</div>
### `POST /api/v1/auth/logout/` — لاگین لازم
```jsonc
{ "refresh": "..." } → 205 No Content
```
### `GET /api/v1/auth/me/` — لاگین لازم
```jsonc
{ "id": 2, "email": "analyst@example.com", "full_name": "Demo Analyst",
"role": "analyst", "can_view_pii": true, "date_joined": "..." }
```
---
## ۳. سرچ
### `GET /api/v1/search/` — لاگین لازم
<div dir="rtl">
**پارامترها**
| نام | نوع | پیش‌فرض | توضیح |
|---|---|---|---|
| `q` | string | `""` | کلیدواژه؛ حداکثر ۲۰۰ کاراکتر. از عبارت داخل گیومه و `-حذف` پشتیبانی می‌کند |
| `skills` | لیست | — | مهارت‌ها (OR داخلی) |
| `roles` | لیست | — | خانواده شغلی، مثل `engineering` |
| `seniority` | لیست | — | `senior`, `manager`, `director`, `vp`, `cxo`, ... |
| `countries` | لیست | — | کشور محل سکونت |
| `industries` | لیست | — | صنعت کارفرمای فعلی |
| `companies` | لیست | — | نام کارفرمای فعلی |
| `min_experience` | int | — | ۰ تا ۷۰ |
| `max_experience` | int | — | ۰ تا ۷۰ |
| `sort` | enum | `relevance` | `relevance` \| `-experience` \| `experience` \| `name` |
| `page` | int | `1` | |
| `page_size` | int | `20` | حداکثر ۱۰۰ |
| `facets` | bool | `true` | `false` یعنی aggregation اجرا نشود |
لیست‌ها هر دو نگارش را می‌پذیرند: `?skills=a,b` یا `?skills=a&skills=b`. حداکثر ۲۰ مقدار در هر فیلتر.
**پاسخ ۲۰۰**
</div>
```jsonc
{
"count": 41,
"page": 1,
"page_size": 20,
"total_pages": 3,
"took_ms": 12,
"backend": "elasticsearch", // یا "postgres"
"results": [
{
"id": 118,
"score": 14.82,
"full_name": "joseph holland",
"headline": "recruiting manager at garver",
"job_title": "recruiting manager",
"job_title_role": "human_resources",
"seniority_levels": ["manager"],
"company": "garver",
"industry": "civil engineering",
"location_name": "denton, texas, united states",
"location_country": "united states",
"years_experience": 12,
"linkedin_url": "linkedin.com/in/joeyholland",
"skills": ["recruiting", "leadership", "human resources", "training"],
"highlight": {
"skills": ["<mark>leadership</mark>"],
"job_title": ["recruiting <mark>manager</mark>"]
}
}
],
"facets": {
"skills": [{ "key": "leadership", "count": 41 }, { "key": "management", "count": 33 }],
"job_title_role": [{ "key": "human_resources", "count": 12 }],
"seniority": [{ "key": "manager", "count": 29 }],
"country": [{ "key": "united states", "count": 41 }],
"industry": [{ "key": "banking", "count": 6 }],
"company": [{ "key": "garver", "count": 2 }]
}
}
```
<div dir="rtl">
**نکته‌ها**
- `score` فقط با `sort=relevance` معنادار است؛ در مرتب‌سازی‌های دیگر نمایش داده نمی‌شود.
- `highlight` فقط وقتی `q` داده شده باشد پر می‌شود و شامل تگ `<mark>` است. آن را به‌عنوان HTML خام رندر نکنید — کامپوننت `Highlight` در فرانت‌اند نشان می‌دهد چطور امن رندر شود.
- `facets` روی مجموعه فیلترشده فعلی محاسبه می‌شود، نه کل دیتاست.
- `backend` برای شفافیت عملیاتی است: مشخص می‌کند کدام موتور جواب داده.
**خطاها**
</div>
```jsonc
// 400 - بازه نامعتبر
{"error":{"code":"validation_error","message":"The request payload failed validation.",
"details":{"min_experience":["min_experience cannot be greater than max_experience."]},
"request_id":"..."}}
// 400 - صفحه‌بندی عمیق
{"error":{"code":"validation_error","details":{"page":["Cannot page beyond 10000 results."]}, ...}}
// 429
{"error":{"code":"throttled","message":"Request was throttled. Expected available in 42 seconds.", ...}}
// 503 - موتور سرچ در دسترس نیست
{"error":{"code":"search_unavailable","message":"The search backend is currently unavailable.", ...}}
```
### `GET /api/v1/search/suggest/` — لاگین لازم
```
?q=mar&limit=8 → { "suggestions": ["maria frontend", "mark heinrich", ...] }
```
<div dir="rtl">
از فیلد edge n-gram روی نام و مهارت استفاده می‌کند. `limit` بین ۱ تا ۲۰.
</div>
### `POST /api/v1/search/reindex/` — فقط نقش admin
```jsonc
// 202
{ "alias": "linkedin-profiles",
"index": "linkedin-profiles-20260903040012",
"indexed": 284, "errors": 0,
"replaced": ["linkedin-profiles-20260901101500"] }
```
<div dir="rtl">
بازسازی بدون قطعی: ایندکس فیزیکی جدید ساخته می‌شود، پر می‌شود، و بعد alias به‌صورت اتمیک جابه‌جا می‌شود. کاربر هیچ‌وقت ایندکس نیمه‌پر نمی‌بیند.
</div>
---
## ۴. پروفایل‌ها
### `GET /api/v1/profiles/` — لاگین لازم
<div dir="rtl">
لیست ساده و بدون رتبه‌بندی، با صفحه‌بندی استاندارد. برای مرور و بررسی دیتاست مفید است؛ سرچ رتبه‌بندی‌شده در `/search/` است.
</div>
```jsonc
{ "count": 284, "page": 1, "page_size": 20, "total_pages": 15,
"next": "http://.../profiles/?page=2", "previous": null,
"results": [ /* همان شکل کارت نتیجه */ ] }
```
### `GET /api/v1/profiles/{id}/` — لاگین لازم
```jsonc
{
"id": 118,
"full_name": "joseph holland",
"headline": "recruiting manager at garver",
"current_company": { "id": 42, "name": "garver", "industry": "civil engineering",
"size": "501-1000", "founded": 1919, "location_name": "..." },
"summary": "Celebrating its 100th year, Garver is an employee-owned ...",
"years_experience": 12,
"connections": 3761,
"interests": ["guitar", "aviation", "hiking"],
"languages": ["english"],
"skills": [{ "name": "recruiting", "slug": "recruiting" }, ...],
"experiences": [
{ "id": 903, "title": "recruiting manager", "company_name": "garver",
"start_date": "2019-10", "end_date": "", "is_primary": true,
"company": { "id": 42, "name": "garver", ... } }
],
"educations": [
{ "id": 210, "school_name": "university of north texas",
"degrees": ["bachelors", "bachelor of arts"],
"majors": ["politics", "international studies"],
"start_date": "2007", "end_date": "2008" }
],
"emails": [{ "address": "j3*******@yahoo.com", "type": "personal" }],
"phone_numbers": ["+1940*****28"],
"pii_masked": true
}
```
<div dir="rtl">
**`pii_masked`** به فرانت‌اند می‌گوید آیا مقادیر تماس ماسک شده‌اند یا نه، تا بتواند یک پیام روشن نشان دهد به‌جای اینکه کاربر فکر کند داده خراب است.
| نقش | `emails` / `phone_numbers` |
|---|---|
| `viewer` | ماسک‌شده (`j3*******@yahoo.com`) |
| `analyst` | کامل |
| `admin` | کامل |
</div>
### `GET /api/v1/profiles/skills/` — لاگین لازم
```
?q=py&limit=50 → [{ "id": 12, "name": "python", "slug": "python", "profile_count": 8 }, ...]
```
<div dir="rtl">
دیکشنری مهارت‌ها، مرتب بر اساس محبوبیت. فرانت‌اند از آن برای type-ahead فیلتر مهارت استفاده می‌کند بدون اینکه برای هر کلید به Elasticsearch بزند.
</div>
---
## ۵. سلامت
### `GET /api/v1/health/live/` — عمومی
```jsonc
{ "status": "ok", "service": "linkedin-search-api" }
```
<div dir="rtl">
فقط می‌گوید پروسه زنده است. مناسب liveness probe.
</div>
### `GET /api/v1/health/ready/` — عمومی
```jsonc
// 200
{ "status": "ok",
"checks": { "database": { "healthy": true, "detail": "reachable" },
"search": { "healthy": true, "detail": "elasticsearch reachable" } } }
// 503
{ "status": "degraded",
"checks": { "database": { "healthy": true, "detail": "reachable" },
"search": { "healthy": false, "detail": "Elasticsearch did not answer a ping." } } }
```
<div dir="rtl">
وقتی هر وابستگی‌ای خراب باشد کد ۵۰۳ برمی‌گرداند تا orchestrator ترافیک را به این نمونه نفرستد.
</div>
---
## ۶. یک جریان کامل با curl
```bash
BASE=http://localhost:8080/api/v1
# ۱) ثبت‌نام (اختیاری — کاربران نمونه از قبل هستند)
curl -s -X POST $BASE/auth/register/ -H 'Content-Type: application/json' \
-d '{"email":"me@example.com","full_name":"Me",
"password":"Str0ng-Passw0rd!","password_confirm":"Str0ng-Passw0rd!"}' | jq
# ۲) لاگین
TOKENS=$(curl -s -X POST $BASE/auth/login/ -H 'Content-Type: application/json' \
-d '{"email":"analyst@example.com","password":"Analyst-Passw0rd!"}')
ACCESS=$(echo "$TOKENS" | jq -r .access)
REFRESH=$(echo "$TOKENS" | jq -r .refresh)
# ۳) هویت
curl -s $BASE/auth/me/ -H "Authorization: Bearer $ACCESS" | jq
# ۴) سرچ با کلیدواژه و دو فیلتر
curl -s -G $BASE/search/ -H "Authorization: Bearer $ACCESS" \
--data-urlencode 'q=leadership' \
--data-urlencode 'skills=management,training' \
--data-urlencode 'countries=united states' \
--data-urlencode 'sort=-experience' | jq '{count, backend, took_ms,
top: [.results[:3][] | {full_name, headline, years_experience}]}'
# ۵) جزئیات اولین نتیجه
ID=$(curl -s -G $BASE/search/ -H "Authorization: Bearer $ACCESS" \
--data-urlencode 'q=leadership' | jq -r '.results[0].id')
curl -s $BASE/profiles/$ID/ -H "Authorization: Bearer $ACCESS" | jq '{full_name, pii_masked, emails}'
# ۶) چرخش توکن
curl -s -X POST $BASE/auth/refresh/ -H 'Content-Type: application/json' \
-d "{\"refresh\":\"$REFRESH\"}" | jq
# ۷) خروج
curl -s -o /dev/null -w '%{http_code}\n' -X POST $BASE/auth/logout/ \
-H "Authorization: Bearer $ACCESS" -H 'Content-Type: application/json' \
-d "{\"refresh\":\"$REFRESH\"}"
```