189 lines
13 KiB
Markdown
189 lines
13 KiB
Markdown
# طرح — بازگرداندن «ورود کارشناس به حساب مشتری» (act-as)
|
||||
|
|
|
|||
|
|
تاریخ: ۱۴۰۵/۰۷/۲۰ (2026-10-12)
|
|||
|
|
|
|||
|
|
## وضعیت: ✅ **انجام شد** — تصمیم مالک ۲۰۲۶/۱۰/۱۲
|
|||
|
|
|
|||
|
|
> «نه دقیقا همون چیزی که بود در ساختار مرجع همون رو بیار ولی با ساختار درستی که
|
|||
|
|
> خودت میدونی»
|
|||
|
|
|
|||
|
|
یعنی: **دامنه کامل مرجع**، با سازوکار امنِ بند ۳. پس **بند ۴ (مرزها) حذف شد** —
|
|||
|
|
هیچ مسیری در حالت act-as بسته نیست، آپلود مدارک و logout هم باز است، چون مرجع
|
|||
|
|
هم interceptor سراسری داشت و همهچیز از طرف مشتری میرفت.
|
|||
|
|
|
|||
|
|
بقیه طرح عیناً اجرا شد. سه چیز که در طرح نبود و در حین کار پیدا شد (هر سه رفع
|
|||
|
|
شد): پاکسازی `queryCache`/علاقهمندی/اجرت کششده در جابجایی، و هدر در مسیر
|
|||
|
|
آپلود که `request()` را دور میزند. جزئیات: `ISSUES.md` EXP-01/02/03 و D29.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### متن اصلی طرح (برای سابقه)
|
|||
|
|
|
|||
|
|
> **خواسته مالک:** «کارشناس فروش میتونه از طریق اپ خودش وارد صفحه مشتری هاش بشه و
|
|||
|
|
> از جانب اون ها ثبت سفارش کنه و وضعیت اون هارو ببینه و یکجورایی انگار با اکانت
|
|||
|
|
> اون ها لاگین کرده همه چیز رو از دید اون مشتریش میدید… همان قابلیت هارو میخوام،
|
|||
|
|
> حالا هرجور که فکر میکنی از لحاظ امنیت و پرفورمنس بهتره.»
|
|||
|
|
|
|||
|
|
**همان قابلیت، بدون سه عیبِ روش قدیم.** تصمیمهای فنی زیر با من است؛ هرجا چیزی
|
|||
|
|
از مرجع **کم** شده باشد، صریح نوشتهام.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱. مرجع چه میکرد (بررسیشده، نه حدس)
|
|||
|
|
|
|||
|
|
| مرحله | کد مرجع |
|
|||
|
|
|---|---|
|
|||
|
|
| لیست مشتریان | `POST /my-child` → `ApiMobileController.php:185-196`، فیلتر `reagent_id = auth()->id()` |
|
|||
|
|
| **توکن مشتری ساخته میشد** | `:197-201` — اگر `api_token` مشتری خالی بود، `Str::random(60)` میساخت و **ذخیره** میکرد |
|
|||
|
|
| کلیک روی کارت | `customerChoice.vue:155` → `changeUser()` → `myStore.toggleActiveUser(user)` |
|
|||
|
|
| ذخیره توکن | `myDataStore.ts:163` → `localStorage.setItem('activeChildToken', value.api_token)` |
|
|||
|
|
| اعمال روی همه درخواستها | `tokenManage.ts:8` `getUserToken()` = توکن مشتری **یا** توکن خودش → `plugins/axios.ts:18` روی **هر** درخواست |
|
|||
|
|
| خروج از حساب مشتری | `Dashboard.vue:784` → پاک کردن `activeChildToken` |
|
|||
|
|
|
|||
|
|
یعنی دامنه مرجع: **هر کاری که مشتری میتوانست**، چون interceptor سراسری بود.
|
|||
|
|
|
|||
|
|
## ۲. سه عیبی که تکرار نمیکنم
|
|||
|
|
|
|||
|
|
۱. **توکن دائمی مشتری در دست کارشناس.** `api_token` انقضا نداشت و در
|
|||
|
|
`localStorage` میماند. کارشناسی که فردا قطع همکاری میکرد، توکن مشتریانش را
|
|||
|
|
داشت و همچنان کار میکرد.
|
|||
|
|
۲. **هیچ ردی نمیماند.** درخواست عیناً از طرف مشتری میآمد، پس در سرور معلوم
|
|||
|
|
نبود این سفارش را مشتری ثبت کرده یا کارشناس. برای مالی، این یعنی غیرقابل
|
|||
|
|
حسابرسی.
|
|||
|
|
۳. **بررسی مالکیت فقط یکبار.** توکن در لحظه گرفتن صادر میشد؛ بعد از آن اگر
|
|||
|
|
مشتری به کارشناس دیگری منتقل میشد، توکن قدیمی همچنان کار میکرد.
|
|||
|
|
|
|||
|
|
## ۳. طراحی پیشنهادی
|
|||
|
|
|
|||
|
|
**هسته: توکنِ مشتری صادر نمیشود. کارشناس با توکن خودش درخواست میدهد و
|
|||
|
|
میگوید «از طرف مشتری X».**
|
|||
|
|
|
|||
|
|
### چرا این انتخاب (و نه گزینههای دیگر)
|
|||
|
|
|
|||
|
|
`CustomerJwtStrategy.validate()` در تارگت همیشه یک `AuthenticatedCustomer`
|
|||
|
|
میسازد که `id` آن **یک Customer id** است، و **هر ۱۷ مسیر مشتری** از همان
|
|||
|
|
استفاده میکنند. پس اگر جابجایی را همانجا اعمال کنم، همه آن مسیرها **بدون یک خط
|
|||
|
|
تغییر** از دید مشتری کار میکنند — همان «همهچیز را از دید او میبیند» که خواستی،
|
|||
|
|
با کمترین سطح تماس با کد.
|
|||
|
|
|
|||
|
|
| گزینه | نتیجه | رأی |
|
|||
|
|
|---|---|---|
|
|||
|
|
| صدور توکن مشتری (روش مرجع) | ساده، ولی هر سه عیب بالا | ❌ |
|
|||
|
|
| پارامتر `customerId` روی هر مسیر | باید ۱۷ مسیر و همه سرویسها عوض شوند؛ فراموش کردن یکی = حفره | ❌ |
|
|||
|
|
| **هدر `X-Act-As-Customer` + اعمال در strategy** | یک نقطه تغییر، هر ۱۷ مسیر خودکار، قابل حسابرسی | ✅ |
|
|||
|
|
|
|||
|
|
### سمت سرور
|
|||
|
|
|
|||
|
|
**۱. هدر درخواست.** کارشناس توکن خودش را میفرستد بهعلاوه
|
|||
|
|
`X-Act-As-Customer: <customerId>`.
|
|||
|
|
|
|||
|
|
**۲. اعمال در `validateExpert`** (تنها نقطه تغییر منطق دسترسی):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
اگر هدر بود و principal کارشناس بود:
|
|||
|
|
مشتری را بخوان: id = هدر، referredByAgentId = admin.id، isActive، !isArchived
|
|||
|
|
اگر نبود → 403 (مشتری دیگری، یا مشتریِ کارشناس دیگر)
|
|||
|
|
اگر بود → { id: customer.id, ← از دید مشتری
|
|||
|
|
actingAsCustomerId: customer.id,
|
|||
|
|
adminUserId: admin.id, ← هویت واقعی حفظ میشود
|
|||
|
|
type: 'sales-expert' }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
نکات مهم این طراحی:
|
|||
|
|
|
|||
|
|
- **بررسی مالکیت در هر درخواست** انجام میشود، نه یکبار. مشتری که به کارشناس
|
|||
|
|
دیگری منتقل شود، دسترسی **در همان لحظه** قطع میشود. (عیب ۳ رفع شد.)
|
|||
|
|
- کارشناس غیرفعال/تنزلیافته همانجا ۴۰۱ میگیرد، چون `validateExpert` از قبل
|
|||
|
|
`isActive` و `role` را هر بار میخواند. (عیب ۱ رفع شد.)
|
|||
|
|
- `adminUserId` حفظ میشود ⇒ هویت واقعی هرگز گم نمیشود. (عیب ۲ رفع شد.)
|
|||
|
|
- توکن **عوض نمیشود**؛ پس چیزی در `localStorage` نمینشیند که بعداً سوءاستفاده
|
|||
|
|
شود. بستن اپ = پایان حالت act-as.
|
|||
|
|
|
|||
|
|
**۳. هزینه پرفورمنس (خواستهات).** یک `findUnique` اضافه روی `Customer` با
|
|||
|
|
`where: { id }` — کلید اصلی، پس index-only. **فقط** وقتی هدر وجود دارد؛ یعنی
|
|||
|
|
درخواستهای مشتری عادی و کارشناس در حالت معمولی **صفر هزینه اضافه** دارند. در
|
|||
|
|
حالت act-as هم جایگزین کوئریِ `tradingCustomer` میشود، نه اضافه بر آن — پس
|
|||
|
|
تعداد کوئری ثابت میماند.
|
|||
|
|
|
|||
|
|
**۴. ⛔ این بند اجرا نشد.** مالک دامنه کامل مرجع را خواست، پس هیچکدام از این
|
|||
|
|
مرزها گذاشته نشد — همه مسیرها در حالت act-as باز است:
|
|||
|
|
|
|||
|
|
| مسیر | در حالت act-as | چرا |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `POST /orders` | ✅ مجاز | خواسته اصلی |
|
|||
|
|
| مشاهده: `me`، `me/orders`، `me/ledger-entries`، `me/report*`، `accounting-balance`، `wage-quote` | ✅ مجاز | «وضعیت آنها را ببیند» |
|
|||
|
|
| علاقهمندیها | ✅ مجاز | بیخطر، و بخشی از «از دید او» |
|
|||
|
|
| `POST /customer/me/documents` و `complete-profile` | ❌ **۴۰۳** | آپلود مدارک هویتی بهجای مشتری. کارشناس مدرکِ مشتری را ندارد و این کار جعل هویت است، نه خدمت. در پنل ادمین انجام میشود. |
|
|||
|
|
| `POST /auth/logout` | ❌ **۴۰۳** | نشست مشتری را نباید کارشناس ببندد |
|
|||
|
|
| مسیرهای `expert/*` | ❌ **۴۰۳** | در حالت act-as بیمعناست؛ از ابهام جلوگیری میکند |
|
|||
|
|
|
|||
|
|
**۵. ثبت در سفارش.** `OrdersService.createOrder` از قبل `placedBy` دارد. سفارشِ
|
|||
|
|
act-as با `placedBy: 'operator'` ثبت میشود — یعنی قیمت را سرور تحمیل نمیکند و
|
|||
|
|
عدد کارشناس میماند؟ **نه**: برای این حالت `placedBy: 'customer'` میگذارم تا
|
|||
|
|
قیمتگذاری سرور (MON-08) اعمال شود، چون این سفارشِ مشتری است نه اپراتورِ پنل.
|
|||
|
|
علاوه بر آن `salesAgentId` از قبل خودکار از `customer.referredByAgentId` پر
|
|||
|
|
میشود — که همان کارشناس است. **ردّ حسابرسی** هم در لاگ ثبت میشود:
|
|||
|
|
`expert <adminUserId> acting as customer <id>`.
|
|||
|
|
|
|||
|
|
**۶. پیامک.** مشتری پیامک «سفارش شما ثبت شد» را **میگیرد** (مثل مرجع). دلیل:
|
|||
|
|
روی حساب و بدهی طلای او نشسته و باید بداند. کارشناس هم پیامک ارجاعدهنده را
|
|||
|
|
میگیرد، چون `referredByAgentId` خودش است — رفتار موجود، تغییری لازم نیست.
|
|||
|
|
|
|||
|
|
### سمت اپ (هر دو اپ)
|
|||
|
|
|
|||
|
|
**۱. وضعیت act-as** در `sessionStore` (نه `localStorage`): `actingAs = {id, name}`.
|
|||
|
|
چون در `sessionStorage`/حافظه است، بستن تب پایانش میدهد — برعکس مرجع.
|
|||
|
|
|
|||
|
|
**۲. کلاینت API**: اگر `actingAs` ست بود، هدر `X-Act-As-Customer` به همه
|
|||
|
|
درخواستها اضافه شود (یک نقطه، در `request()` کلاینت).
|
|||
|
|
|
|||
|
|
**۳. `CustomerChoiceView`**: زدن روی کارت ⇒ `changeUser` ⇒ ست کردن `actingAs` و
|
|||
|
|
رفتن به خانه، **عیناً مثل مرجع** (`customerChoice.vue:155-167`).
|
|||
|
|
|
|||
|
|
**۴. ⚠️ نوار هشدار همیشگی** — چیزی که مرجع نداشت و از نظر من ضروری است: یک نوار
|
|||
|
|
ثابت بالای صفحه «در حال مشاهده بهعنوان **فلانی** — خروج». بدون این، کارشناس
|
|||
|
|
نمیفهمد در حساب کیست و ممکن است سفارش را روی حساب اشتباهی ثبت کند. این ریسک
|
|||
|
|
واقعیِ روش مرجع بود.
|
|||
|
|
|
|||
|
|
**۵. پاک کردن سبد در جابجایی.** سبد در `localStorage` است؛ اگر پاک نشود، کالاهای
|
|||
|
|
سبدِ کارشناس وارد سفارش مشتری میشود. در جابجایی (ورود و خروج) سبد خالی میشود.
|
|||
|
|
|
|||
|
|
## ۴. تستها
|
|||
|
|
|
|||
|
|
| تست | میسنجد |
|
|||
|
|
|---|---|
|
|||
|
|
| کارشناس + هدر مشتریِ خودش | دادههای آن مشتری برمیگردد |
|
|||
|
|
| کارشناس + هدر **مشتریِ کارشناس دیگر** | ۴۰۳ |
|
|||
|
|
| کارشناس + هدر مشتری غیرفعال/آرشیو | ۴۰۳ |
|
|||
|
|
| **مشتری عادی** + هدر | هدر **نادیده** گرفته میشود (نه ۴۰۳ خاموش؛ اصلاً اثر ندارد) |
|
|||
|
|
| کارشناس غیرفعالشده + هدر معتبر | ۴۰۱ فوری |
|
|||
|
|
| سفارش در حالت act-as | `customerId` = مشتری، `salesAgentId` = کارشناس، قیمت از سرور |
|
|||
|
|
| مسیرهای ممنوع (مدارک، logout) | ۴۰۳ |
|
|||
|
|
| بدون هدر | رفتار امروز، بدون تغییر (regression) |
|
|||
|
|
|
|||
|
|
## ۵. چه چیزی عمداً مثل مرجع نیست
|
|||
|
|
|
|||
|
|
| مرجع | اینجا | دلیل |
|
|||
|
|
|---|---|---|
|
|||
|
|
| توکن دائمی مشتری به کارشناس داده میشد | هیچ توکنی صادر نمیشود | سرقت نشست؛ بعد از قطع همکاری هم کار میکرد |
|
|||
|
|
| `api_token` در `localStorage` | وضعیت در حافظه نشست | بستن اپ = پایان |
|
|||
|
|
| مالکیت یکبار بررسی میشد | هر درخواست | انتقال مشتری فوراً اثر میکند |
|
|||
|
|
| ~~کارشناس همهکاره بود~~ | ~~مدارک/logout ممنوع~~ | **اجرا نشد** — مالک دامنه کامل مرجع را خواست؛ کارشناس همهکاره است |
|
|||
|
|
| هیچ ردی نبود | لاگ + `salesAgentId` | حسابرسی مالی |
|
|||
|
|
| هیچ نشانهای در UI نبود | نوار همیشگی | سفارش روی حساب اشتباه |
|
|||
|
|
|
|||
|
|
## ۶. برآورد
|
|||
|
|
|
|||
|
|
| بخش | کار |
|
|||
|
|
|---|---|
|
|||
|
|
| سرور | `app-principal.ts` (+`actingAsCustomerId`)، `customer-jwt.strategy.ts` (منطق)، یک گارد برای مسیرهای ممنوع، لاگ در `orders.service` |
|
|||
|
|
| اپها (×۲) | `sessionStore`، `api/client.ts` (هدر)، `CustomerChoiceView` (changeUser)، نوار act-as، پاک کردن سبد |
|
|||
|
|
| تست | ۸ مورد بالا در `apps/api` |
|
|||
|
|
|
|||
|
|
ترتیب اجرا: سرور + تستها اول (قابل اثبات)، بعد اپها.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**منتظر تأیید.** اگر با مرزهای بند ۴ (ممنوع بودن آپلود مدارک و logout) موافق
|
|||
|
|
نیستی بگو — بقیه طرح تصمیم فنی است و خودم جلو میبرم.
|