# مرجع API
پایه: `/api/v1/` · نسخه‌بندی از طریق namespace آدرس‌ها · اسکیمای ماشین‌خوان: [`openapi.yaml`](openapi.yaml) مستندات تعاملی (همیشه به‌روز، از روی کد تولید می‌شود): - Swagger UI — - ReDoc —
--- ## ۱. قراردادهای عمومی ### ۱.۱ احراز هویت ```http Authorization: Bearer ```
سیاست پیش‌فرض **deny** است: هر endpoint که صریحاً عمومی اعلام نشده، لاگین می‌خواهد.
### ۱.۲ قالب خطا ```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" } ```
ثبت‌نام همیشه نقش `viewer` می‌دهد؛ ارتقا یک اقدام صریح ادمین است. ایمیل نرمال (lowercase) می‌شود. رمز از اعتبارسنج‌های جنگو رد می‌شود (حداقل ۱۰ کاراکتر، غیرعددی، غیرمعمول، غیرمشابه ایمیل).
### `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": "..." } ```
توکن refresh **چرخشی** است: هر بار یک جفت جدید می‌گیرید و توکن قبلی بلاک‌لیست می‌شود. اگر یک توکن دزدیده و استفاده شود، استفاده بعدی کاربر واقعی رد می‌شود — یعنی نشت قابل تشخیص است.
### `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/` — لاگین لازم
**پارامترها** | نام | نوع | پیش‌فرض | توضیح | |---|---|---|---| | `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`. حداکثر ۲۰ مقدار در هر فیلتر. **پاسخ ۲۰۰**
```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": ["leadership"], "job_title": ["recruiting manager"] } } ], "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 }] } } ```
**نکته‌ها** - `score` فقط با `sort=relevance` معنادار است؛ در مرتب‌سازی‌های دیگر نمایش داده نمی‌شود. - `highlight` فقط وقتی `q` داده شده باشد پر می‌شود و شامل تگ `` است. آن را به‌عنوان HTML خام رندر نکنید — کامپوننت `Highlight` در فرانت‌اند نشان می‌دهد چطور امن رندر شود. - `facets` روی مجموعه فیلترشده فعلی محاسبه می‌شود، نه کل دیتاست. - `backend` برای شفافیت عملیاتی است: مشخص می‌کند کدام موتور جواب داده. **خطاها**
```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", ...] } ```
از فیلد edge n-gram روی نام و مهارت استفاده می‌کند. `limit` بین ۱ تا ۲۰.
### `POST /api/v1/search/reindex/` — فقط نقش admin ```jsonc // 202 { "alias": "linkedin-profiles", "index": "linkedin-profiles-20260903040012", "indexed": 284, "errors": 0, "replaced": ["linkedin-profiles-20260901101500"] } ```
بازسازی بدون قطعی: ایندکس فیزیکی جدید ساخته می‌شود، پر می‌شود، و بعد alias به‌صورت اتمیک جابه‌جا می‌شود. کاربر هیچ‌وقت ایندکس نیمه‌پر نمی‌بیند.
--- ## ۴. پروفایل‌ها ### `GET /api/v1/profiles/` — لاگین لازم
لیست ساده و بدون رتبه‌بندی، با صفحه‌بندی استاندارد. برای مرور و بررسی دیتاست مفید است؛ سرچ رتبه‌بندی‌شده در `/search/` است.
```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 } ```
**`pii_masked`** به فرانت‌اند می‌گوید آیا مقادیر تماس ماسک شده‌اند یا نه، تا بتواند یک پیام روشن نشان دهد به‌جای اینکه کاربر فکر کند داده خراب است. | نقش | `emails` / `phone_numbers` | |---|---| | `viewer` | ماسک‌شده (`j3*******@yahoo.com`) | | `analyst` | کامل | | `admin` | کامل |
### `GET /api/v1/profiles/skills/` — لاگین لازم ``` ?q=py&limit=50 → [{ "id": 12, "name": "python", "slug": "python", "profile_count": 8 }, ...] ```
دیکشنری مهارت‌ها، مرتب بر اساس محبوبیت. فرانت‌اند از آن برای type-ahead فیلتر مهارت استفاده می‌کند بدون اینکه برای هر کلید به Elasticsearch بزند.
--- ## ۵. سلامت ### `GET /api/v1/health/live/` — عمومی ```jsonc { "status": "ok", "service": "linkedin-search-api" } ```
فقط می‌گوید پروسه زنده است. مناسب liveness probe.
### `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." } } } ```
وقتی هر وابستگی‌ای خراب باشد کد ۵۰۳ برمی‌گرداند تا orchestrator ترافیک را به این نمونه نفرستد.
--- ## ۶. یک جریان کامل با 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\"}" ```