First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
13 KiB
طرح — بازگرداندن «ورود کارشناس به حساب مشتری» (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 |
وضعیت در حافظه نشست | بستن اپ = پایان |
| مالکیت یکبار بررسی میشد | هر درخواست | انتقال مشتری فوراً اثر میکند |
| اجرا نشد — مالک دامنه کامل مرجع را خواست؛ کارشناس همهکاره است | ||
| هیچ ردی نبود | لاگ + salesAgentId |
حسابرسی مالی |
| هیچ نشانهای در UI نبود | نوار همیشگی | سفارش روی حساب اشتباه |
۶. برآورد
| بخش | کار |
|---|---|
| سرور | app-principal.ts (+actingAsCustomerId)، customer-jwt.strategy.ts (منطق)، یک گارد برای مسیرهای ممنوع، لاگ در orders.service |
| اپها (×۲) | sessionStore، api/client.ts (هدر)، CustomerChoiceView (changeUser)، نوار act-as، پاک کردن سبد |
| تست | ۸ مورد بالا در apps/api |
ترتیب اجرا: سرور + تستها اول (قابل اثبات)، بعد اپها.
منتظر تأیید. اگر با مرزهای بند ۴ (ممنوع بودن آپلود مدارک و logout) موافق نیستی بگو — بقیه طرح تصمیم فنی است و خودم جلو میبرم.