413 lines
14 KiB
Markdown
413 lines
14 KiB
Markdown
|
|
# مرجع 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\"}"
|
|||
|
|
```
|