{
  "title": "عیب‌یابی و حل مشکلات (Troubleshooting)",
  "slug": "team/backend/troubleshooting",
  "url": "/docs/team/backend/troubleshooting",
  "frontmatter": {
    "layout": "doc",
    "title": "عیب‌یابی و حل مشکلات (Troubleshooting)",
    "description": "راهنمای حل مشکلات رایج توسعه محلی پلتفرم NONS — احراز هویت، گیت‌وی، کلاستر و یکپارچه‌سازی پنل ادمین",
    "version": "1.2.0",
    "status": "Active",
    "author": "Antigravity",
    "owner": "Backend Team",
    "created_at": "2026-07-15",
    "updated_at": "2026-07-23"
  },
  "sections": [
    {
      "level": 1,
      "heading": "عیب‌یابی و حل مشکلات (Troubleshooting)",
      "content": "در این راهنما، مشکلات رایجی که ممکن است در طول توسعه محلی بک‌اند پلتفرم NONS (مخصوصاً در ارتباط با کلاستر K3d، گیت‌وی Traefik و ابزارهای Ory) رخ دهند، به همراه راه‌حل‌های تست‌شده و قطعی آن‌ها مستند شده است.\n\n---"
    },
    {
      "level": 2,
      "heading": "۱. اختلال در ارتباط با دامنه‌های لوکال (خطای ۵۰۲ یا Timeout)",
      "content": "> [!WARNING]\n> **علت اصلی:** روشن بودن ابزارهای تغییر آی‌پی (VPN / Proxy) روی سیستم میزبان."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "هنگام تلاش برای دسترسی به آدرس‌های اینگرس محلی (مثل `http://nons.local/v1/auth/hydra/...`) با خطای `HTTP ERROR 502` یا عدم اتصال مواجه می‌شوید، در حالی که آدرس آی‌پی در ابزار `ping nons.local` به درستی به `127.0.0.1` اشاره می‌کند."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "1. **خاموش کردن VPN:** قبل از شروع تست عملی جریان‌ها، فیلترشکن یا پروکسی خود را خاموش کنید.\n2. **تنظیم Split Tunneling:** در صورتی که نیاز مبرم به VPN دارید، آدرس‌های زیر را در بخش Bypass یا استثناهای برنامه پروکسی خود وارد کنید:\n   * `127.0.0.1`\n   * `localhost`\n   * `nons.local`\n\n---"
    },
    {
      "level": 2,
      "heading": "۲. خطای ۵۰۲ در مرحله تبادل توکن (Token Exchange)",
      "content": "> [!IMPORTANT]\n> **علت اصلی:** عدم دسترسی پاد درون کلاستر به سرویس لوکال خارج از کلاستر."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "مرورگر با موفقیت کد احراز هویت را دریافت کرده و به آدرس بازگشت منتقل می‌شود، اما هنگام ارسال درخواست `POST` به `/oauth2/token` جهت تبادل کد با توکن، با خطای `502 Bad Gateway` مواجه می‌شوید. لاگ‌های Hydra عدم دسترسی به `token-hook` را نشان می‌دهند."
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "ابزار Hydra در کلاستر در حال اجراست و طبق تنظیمات برای تزریق نقش‌ها، قلاب توکن را روی `http://login-consent-app:3002/token-hook` صدا می‌زند. اما از آنجا که `login-consent-app` روی سیستم لوکال شما (خارج از کلاستر) بالا آمده است، پادِ Hydra نمی‌تواند این دامنه را در شبکه داخلی کوبرنتیز پیدا کند."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "یک سرویس از نوع `ExternalName` در کلاستر بسازید تا ترافیک‌های دامنه `login-consent-app` را به کامپیوتر میزبان هدایت کند:\n\n۱. فایل `deploy-bridge.yaml` را ایجاد کنید:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: login-consent-app\n  namespace: nons-platform\nspec:\n  type: ExternalName\n  externalName: host.docker.internal\n```\n۲. آن را به کلاستر اعمال کنید:\n```bash\nkubectl apply -f deploy-bridge.yaml\n```\n\n---"
    },
    {
      "level": 2,
      "heading": "۳. عدم ریدایرکت پس از احراز هویت (گیر کردن در داشبورد دمو)",
      "content": "> [!CAUTION]\n> **علت اصلی:** عدم تعریف دامنه بازگشت در لیست سفید (Whitelist) کراتوس."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "پس از وارد کردن ایمیل و ثبت کد OTP در صفحه ورود دمو، کاربر به جریان رضایت (Consent) در Hydra بازگردانده نمی‌شود و در صفحه داشبورد دمو (`/v1/auth/dashboard`) متوقف می‌شود."
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "ابزار Ory Kratos پارامتر `return_to` را جهت امنیت بیشتر بررسی می‌کند. اگر دامنه بازگشت (در اینجا `http://localhost:3002` یا `http://nons.local`) در لیست سفید کراتوس نباشد، کراتوس ریدایرکت را بلاک کرده و کاربر را به آدرس پیش‌فرض داشبورد می‌فرستد."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "۱. فایل کانفیگ کراتوس را در مسیر [auth-ui/.kratos/kratos.yml](file:///C:/Users/ASUS/Documents/GitHub/nons/auth-ui/.kratos/kratos.yml) باز کنید.\n۲. دامنه‌های مورد نظر را به آرایه `allowed_return_urls` اضافه کنید:\n```yaml\nselfservice:\n  allowed_return_urls:\n    - http://localhost:3000\n    - http://localhost:3001\n    - http://localhost:3002\n    - http://nons.local\n```\n۳. کانتینر کراتوس را مجدداً راه‌اندازی کنید تا تغییرات اعمال شوند:\n```bash\ndocker rm -f nons-kratos"
    },
    {
      "level": 1,
      "heading": "اجرای مجدد کانتینر با والیوم متصل‌شده",
      "content": "docker run -d --name nons-kratos -p 4433:4433 -p 4434:4434 -v <مسیر_کامل_پروژه>/auth-ui/.kratos:/etc/config/kratos oryd/kratos:v1.3.1 serve -c /etc/config/kratos/kratos.yml --dev --watch-courier\n```\n\n---"
    },
    {
      "level": 2,
      "heading": "۴. خطای اجباری بودن S256 در PKCE",
      "content": ""
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "هنگام آغاز درخواست احراز هویت، خطا یا ریدایرکتی با این پیغام در مرورگر دریافت می‌کنید:\n> `Clients must use code_challenge_method=S256, plain is not allowed.`"
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "پلتفرم NONS برای امنیت بیشتر، استفاده از متد ساده (`plain`) را غیرفعال کرده و تمامی کلاینت‌ها را مجبور به رمزنگاری با الگوریتم SHA-256 می‌سازد."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "هنگام ایجاد چالش PKCE در سمت فرانت‌اند یا اسکریپت‌های تست:\n۱. حتماً پارامتر `code_challenge_method` را برابر با `S256` قرار دهید.\n۲. چالش را از طریق هش SHA-256 و انکودِ Base64URL (بدون padding) بر روی `verifier` تولید کنید.\n\n---"
    },
    {
      "level": 2,
      "heading": "۵. خطای عدم یافتن داده‌های PKCE برای درخواست (Unable to find initial PKCE data)",
      "content": ""
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "در مرحله تبادل توکن خطای زیر را دریافت می‌کنید:\n> `The provided authorization grant... is invalid, expired, revoked... Unable to find initial PKCE data tied to this request`"
    },
    {
      "level": 3,
      "heading": "علت‌های احتمالی:",
      "content": "1. **کد احراز هویت منقضی شده:** کدهای صادر شده (`ory_ac_...`) عمر کوتاهی (معمولاً ۶۰ ثانیه) دارند. عملیات تبادل را سریع‌تر انجام دهید.\n2. **یک‌بار مصرف بودن کد:** هر کد پس از اولین استفاده (حتی تلاش ناموفق)، به صورت خودکار باطل می‌شود. باید جریان را از نو شروع کرده و کد جدیدی بگیرید.\n3. **عدم تطابق Verifier با Challenge:** اطمینان حاصل کنید رشته ارسالی در `code_verifier` دقیقاً همان مقداری است که هش چالش از روی آن ساخته شده است. هرگونه عدم تطابق به دلایل امنیتی منجر به حذف اطلاعات جلسه و باطل شدن کد می‌شود.\n\n---"
    },
    {
      "level": 2,
      "heading": "۶. تداخل پروکسی/VPN در ارتباطات محلی پنل ادمین (خطای ۵۰۲ یا ۵۰۴)",
      "content": "> [!WARNING]\n> **علت اصلی:** هدایت ترافیک دامنه‌های لوکال (localhost, nons.local) به پروکسی خارجی VPN در محیط اجرای Node.js و Go."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "هنگام اجرای برنامه پنل ادمین (`npm run dev`) یا استارت میکروسرویس‌های بک‌اند، فراخوانی به آدرس‌های محلی مثل `localhost:3010/v1/auth/session` با خطای `502 Bad Gateway` مواجه می‌شود در حالی که به صورت مستقیم درخواست کار می‌کند."
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "ابزارهای فعال پروکسی یا VPN، متغیرهای سیستم‌عامل مانند `HTTP_PROXY` و `HTTPS_PROXY` را مقداردهی می‌کنند. موتور Node.js و کتابخانه HTTP زبان Go به صورت پیش‌فرض تمام ترافیک‌های ارسالی را از این پروکسی عبور می‌دهند؛ در نتیجه دامنه‌های محلی پلتفرم به جای مسیریابی داخلی، به سرور خارجی VPN هدایت شده و مسدود می‌شوند."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "باید آدرس‌های لوکال را از پروکسی مستثنی کنیم. این کار با تنظیم متغیر `NO_PROXY` در کدهای راه‌انداز انجام می‌شود:\n\n۱. **در سمت پنل ادمین (Vite)**: در فایل [vite.config.ts](file:///C:/Users/ASUS/Documents/GitHub/nons/admin-panel/vite.config.ts) قبل از فراخوانی تنظیمات، کد زیر را قرار دهید:\n```typescript\nif (typeof process !== 'undefined') {\n  process.env.NO_PROXY = 'localhost,127.0.0.1,nons.local,' + (process.env.NO_PROXY || '')\n}\n```\n\n۲. **در سمت میکروسرویس‌های Go**: در بدو ورود متد `main()` متغیر محیطی را به صورت دستی ست کنید:\n```go\nos.Setenv(\"NO_PROXY\", \"localhost,127.0.0.1,nons.local,host.docker.internal\")\n```\n\n---"
    },
    {
      "level": 2,
      "heading": "۷. تداخل پورت ۳۰۱۰ پنل ادمین و مسدود شدن جریان احراز هویت",
      "content": "> [!IMPORTANT]\n> **علت اصلی:** باز ماندن فرآیندهای قدیمی Node.js روی پورت ۳۰۱۰ سیستم میزبان."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "سیستم ورود با موفقیت انجام می‌شود اما پس از ریدایرکت کاربر، پنل ادمین خطای احراز هویت داده یا کلاً لود نمی‌شود؛ و یا سرور توسعه به طور خودکار روی پورت ۳۰۱۱ بالا می‌آید."
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "پروژه `auth-ui` و کانفیگ‌های CORS در سرور احراز هویت، دامنه مجاز بازگشت را روی پورت ۳۰۱۰ ست کرده‌اند (`http://localhost:3010`). اگر یک فرآیند لوکال قدیمی پورت ۳۰۱۰ را اشغال کرده باشد، برنامه جدید روی ۳۰۱۱ استارت می‌خورد که منجر به بروز خطاهای CORS یا عدم انطباق با ریدایرکت‌های Ory Kratos می‌شود."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "۱. فرآیند قدیمی مسدودکننده پورت ۳۰۱۰ را در سیستم‌عامل پیدا کرده و ببندید:\n   * **در ویندوز (PowerShell)**:\n     ```powershell\n     # پیدا کردن شناسه فرآیند (PID)\n     Get-NetTCPConnection -LocalPort 3010\n     # کشتن فرآیند (مثلاً PID = 3616)\n     Stop-Process -Id 3616 -Force\n     ```\n۲. مجدداً پروژه پنل ادمین را اجرا کنید تا پورت اصلی ۳۰۱۰ را تصاحب کند.\n\n---"
    },
    {
      "level": 2,
      "heading": "۸. خطای ۵۰۲ روی آدرس `/v1/users/me` یا کاربران در پنل مدیریت",
      "content": "> [!CAUTION]\n> **علت اصلی:** عدم اجرای میکروسرویس `user-service` روی سیستم میزبان یا عدم ثبت پروفایل به علت خاموش بودن NATS."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "کاربر با موفقیت لاگین می‌کند، اما در صفحه کاربری یا درخواست به آدرس `/v1/users/me` با خطای `502 Bad Gateway` روبه‌رو می‌شود."
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "۱. گیت‌وی کلاستر (Traefik) درخواست‌های `/v1/users` را به صورت `ExternalName` به پورت **۳۰۰۳** سیستم میزبان هدایت می‌کند. اگر سرویس `user-service` روی سیستم شما فعال نباشد، گیت‌وی خطای ۵۰۲ می‌دهد.\n۲. سرویس `user-service` در حالت پیش‌فرض فایل محلی `.env` را برای دیتابیس لود نمی‌کند که باعث خطای عدم دسترسی به `DATABASE_URL` می‌شود.\n۳. در صورت خاموش بودن کلاستر NATS محلی، رویدادهای عضویت به دیتابیس کاربران منتقل نشده و جدول دیتابیس فاقد ردیف کاربری مربوطه است (خطای ۴۰۴)."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "۱. **پیاده‌سازی لودر محیطی در Go**: در فایل اصلی میکروسرویس کاربران، متد خوانش فایل `.env` را پیاده‌سازی کنید تا متغیرها به صورت خودکار لود شوند.\n۲. **اجرای سرویس**: مطمئن شوید سرویس کاربران روی پورت ۳۰۰۳ در حال اجراست:\n   ```bash\n   cd services/user-service\n   go build -o user-service.exe cmd/main.go\n   ./user-service.exe\n   ```\n۳. **تزریق کاربر (Seeding) در نبود NATS**: اگر NATS غیرفعال است، ردیف کاربر را به صورت مستقیم در دیتابیس محلی `user_db` درج کنید:\n   ```sql\n   INSERT INTO users (id, public_id, username, display_name, avatar_id, preferences, status) \n   VALUES ('<USER_UUID>', 'pub_<USER_UUID>', 'admin', 'Admin User', 'default_01', '{\"language\": \"fa\", \"theme\": \"dark\", \"currency\": \"IRT\"}', 'ACTIVE') \n    ON CONFLICT (id) DO NOTHING;\n    ```\n\n---"
    },
    {
      "level": 2,
      "heading": "۹. خطای ۵۰۰ در auth-service هنگام SubmitEntry — Kratos در CrashLoopBackOff",
      "content": "> [!CAUTION]\n> **علت اصلی:** عدم اجرای migration دیتابیس Kratos پس از آپگرید نسخه."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "مرورگر خطای `500 Internal Server Error` نمایش می‌دهد. در console مرورگر:\n```\nRegistry Error (SubmitEntry): Error: {\"error\":\"Internal Server Error\"}\n```\nدر لاگ auth-service:\n```\nhandleEntry: identity check failed\nfailed to list identities: Get \"http://kratos:4434/admin/identities\": dial tcp ...:4434: connect: connection refused\n```\nپاد Kratos در وضعیت `CrashLoopBackOff` با لاگ:\n```\nUnable to locate the table\n```"
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "Kratos پس از آپگرید به نسخه `v26.2.0` نیاز به migration دیتابیس دارد. اگر دیتابیس `kratos` خالی باشد (بدون جدول)، Kratos هنگام استارت fail کرده و وارد `CrashLoopBackOff` می‌شود. auth-service نیز که به Kratos وابسته است، با خطای `connection refused` مواجه می‌شود."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "۱. اجرای migration با دستور زیر (یک بار کافیست):\n```bash\nkubectl run -n nons-platform kratos-migrate --image=oryd/kratos:v26.2.0 --restart=Never --rm -it --command -- kratos migrate sql \"postgres://nons:nons@postgres:5432/kratos?sslmode=disable\" -y\n```\n\n۲. ری‌استارت Kratos (اختیاری — بعد از migration پاد قبلی restart می‌شود):\n```bash\nkubectl rollout restart -n nons-platform deploy/kratos\n```\n\n۳. ری‌استارت auth-service:\n```bash\nkubectl rollout restart -n nons-platform deploy/auth-service\n```\n\n---"
    },
    {
      "level": 2,
      "heading": "۱۰. عدم اتصال user-service لوکال به PostgreSQL داخل کلاستر K3d",
      "content": "> [!IMPORTANT]\n> **علت اصلی:** user-service روی سیستم میزبان اجرا می‌شود ولی PostgreSQL داخل کلاستر K3d است."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "لاگ user-service هنگام استارت:\n```\nFailed to ping PostgreSQL\nfailed to connect to 'user=nons database=user_db':\n  [::1]:5432 (localhost): dial error: dial tcp ...:5432: connectex: No connection could be made\n```"
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "PostgreSQL داخل کلاستر K3d (`nons-platform`) در حال اجراست و از طریق سرویس داخلی `postgres:5432` در دسترس است. user-service که روی سیستم میزبان (لوکال) اجرا می‌شود، به `localhost:5432` متصل می‌شود که PostgreSQLای روی آن listening نیست."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "۱. پورت PostgreSQL کلاستر را به سیستم لوکال فوروارد کنید (در یک ترمینال جدا):\n```bash\nkubectl port-forward -n nons-platform svc/postgres 5432:5432\n```\n\n۲. اطمینان حاصل کنید دیتابیس `user_db` در PostgreSQL کلاستر وجود دارد:\n```bash\nkubectl exec -n nons-platform deploy/postgres -- psql -U nons -c \"CREATE DATABASE user_db;\"\n```\n\n۳. در صورت فعال بودن SSL در PostgreSQL، پارامتر `?sslmode=disable` را به کانکشن استرینگ اضافه کنید:\n```\nDATABASE_URL=postgresql://nons:nons@localhost:5432/user_db?sslmode=disable\n```\n\n۴. برای Redis نیز (در صورت نیاز):\n```bash\nkubectl port-forward -n nons-platform svc/redis 6379:6379\n```\n\n---"
    },
    {
      "level": 2,
      "heading": "۸. خطای ۴۰۱ در لیست کاربران پنل ادمین (Admin Panel — `GET /v1/users` → 401)",
      "content": "> [!CAUTION]\n> **علت اصلی:** استفاده از generated SDK client که `Authorization: Bearer` می‌فرستد، در حالی که `user-service` فقط `X-User-Id` header را می‌شناسد."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "در مرورگر، درخواست `GET /v1/users` با خطای زیر مواجه می‌شود:\n\n```\nFailed to load resource: the server responded with a status of 401 (Unauthorized)\n```\n\nدر حالی که کاربر لاگین بوده و session فعال است."
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "سه لایه احراز هویت جداگانه در پلتفرم NONS وجود دارد و هر سرویس از header متفاوتی استفاده می‌کند:\n\n| سرویس | Header احراز هویت | روش |\n|---|---|---|\n| `user-service` | `X-User-Id: <uuid>` | خواندن مستقیم در `getAuthUserID()` |\n| `iam-service` | `X-User-ID: <uuid>` یا `Authorization: Bearer <uuid>` | middleware |\n| `auth-service` | Cookie `session` (Kratos) | کوکی |\n\nfrontend admin panel از یک **generated SDK client** (در `.nons/sdk/generated/api-client/user-service.ts`) برای `listUsers` استفاده می‌کرد. این client به شکل زیر request می‌فرستاد:\n\n```ts\n// ❌ اشتباه — generated client\n'Authorization': `Bearer ${getToken()}`\n// getToken() همیشه '' بود چون setTokenGetter هرگز صدا زده نشده بود\n```\n\nاما `user-service` در handler.go:\n\n```go\n// handler.go\nfunc getAuthUserID(r *http.Request) string {\n    return r.Header.Get(\"X-User-Id\") // نه Authorization!\n}\n```\n\nنتیجه: bearer خالی → user-service نمی‌تواند user ID را شناسایی کند → 401."
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "به جای generated client، از SDK wrapper با `X-User-Id` header استفاده کنید:\n\n```ts\n// ✅ درست — src/registry/user-service.ts\nfunction getUserIdHeader(): Record<string, string> {\n  const auth = useAuthStore()\n  const userId = auth.state.user?.id\n  if (!userId) return {}\n  return { 'X-User-Id': userId }  // همان چیزی که user-service می‌خواند\n}\n\n// در registry:\nlistUsers: (cursor?, limit?, status?, search?) =>\n  userService.listUsers(cursor, limit, status, search, getUserIdHeader())\n```\n\nو در SDK client، `credentials: 'include'` اضافه شود تا کوکی Kratos هم ارسال شود:\n\n```ts\n// src/sdk/client.ts\nconst response = await fetch(url, {\n  ...options,\n  credentials: 'include', // ✅ کوکی session Kratos ارسال می‌شود\n  headers: { ...headers, ...(options?.headers as Record<string, string> | undefined) },\n})\n```"
    },
    {
      "level": 3,
      "heading": "قانون کلی:",
      "content": "> [!IMPORTANT]\n> هرگز از generated client در `.nons/sdk/generated/` برای endpointهایی که نیاز به احراز هویت دارند استفاده نکنید. این فایل‌ها **فقط برای type reference** هستند. همیشه از `src/sdk/` wrapper که header را صحیح set می‌کند استفاده کنید.\n\n---"
    },
    {
      "level": 2,
      "heading": "۹. یکپارچه‌سازی permission‌ها با IAM واقعی (Admin Panel — «دسترسی غیرمجاز» روی همه صفحات)",
      "content": "> [!WARNING]\n> **علت اصلی:** permission keyهایی در frontend تعریف شده بودند که IAM backend هرگز آن‌ها را نمی‌شناسد."
    },
    {
      "level": 3,
      "heading": "نشانه:",
      "content": "- منوهای ناوبری (Users، Roles، Permissions، ...) در sidebar نمایش داده نمی‌شوند.\n- صفحات IAM با پیغام «دسترسی غیرمجاز» بلاک می‌شوند.\n- کاربر ادمین login کرده ولی پنل خالی است."
    },
    {
      "level": 3,
      "heading": "علت فنی:",
      "content": "frontend permission keyهای اختراعی داشت که IAM backend هرگز آن‌ها را emit نمی‌کند:\n\n```ts\n// ❌ اشتباه — constants.ts قبلی\nUSERS: { VIEW: 'users.view' }      // IAM این key را نمی‌شناسد\nROLES: { VIEW: 'roles.view' }      // IAM این key را نمی‌شناسد\nPERMISSIONS: { VIEW: 'permissions.view' } // IAM این key را نمی‌شناسد\n```\n\nPermission keyهای واقعی که IAM service از طریق `GET /v1/iam/me/context` برمی‌گرداند (مطابق `002_seed_defaults.sql`):\n\n```\nadmin.access        ← هر کاربر با role ADMIN این را دارد\nproduct.create / product.edit / product.delete / product.publish\norder.create / order.cancel\nwallet.withdraw / wallet.view\nticket.view / ticket.resolve\n```"
    },
    {
      "level": 3,
      "heading": "راه‌حل:",
      "content": "**۱. constants.ts** — فقط keyهای واقعی IAM:\n\n```ts\n// ✅ درست — src/permissions/constants.ts\nexport const PERMISSIONS = {\n  ADMIN: { ACCESS: 'admin.access' }, // تنها key ادمین پنل\n  PRODUCT: { CREATE: 'product.create', ... },\n  ORDER: { ... },\n  WALLET: { ... },\n  TICKET: { ... },\n} as const\n```\n\n**۲. menu.ts و schema‌ها** — همه به `admin.access` متصل:\n\n```ts\n// هر کاربر با role ADMIN این permission را دارد\npermission: PERMISSIONS.ADMIN.ACCESS  // 'admin.access'\n```\n\n**۳. store.ts** — parse صحیح پاسخ IAM:\n\n```ts\n// IAM context response: { permissions: { \"admin.access\": true, ... } }\nconst ctx: { permissions?: Record<string, boolean> } = await response.json()\nreturn Object.entries(ctx.permissions ?? {})\n  .filter(([, allowed]) => allowed === true)\n  .map(([key]) => key)\n// نتیجه: ['admin.access', 'product.create', ...]\n```"
    },
    {
      "level": 3,
      "heading": "نقشه Role → Permission در IAM:",
      "content": "| Role | Capability | Permissions دریافتی |\n|---|---|---|\n| `ADMIN` | `ADMIN_ACCESS` | `admin.access` |\n| `SELLER` | `SELLING` + `BUYING` | `product.*` + `order.*` |\n| `BUYER` | `BUYING` | `order.create`, `order.cancel` |\n| `FINANCE_AGENT` | `FINANCE` | `wallet.withdraw`, `wallet.view` |\n| `SUPPORT_AGENT` | `SUPPORT` | `ticket.view`, `ticket.resolve` |\n\n> [!TIP]\n> برای اضافه کردن permission جدید به پنل ادمین، ابتدا در migration IAM تعریف کنید (`INSERT INTO permissions`), سپس به یک capability وصل کنید، و در آخر در `constants.ts` frontend اضافه کنید. ترتیب مهم است."
    }
  ]
}