feat(apps): a sales expert can work inside their customer's account (D29)
Reported missing by the owner: in the reference an expert could open any
of their referred customers and use the app AS them — browse, see
balance and debt, place orders — «یکجورایی انگار با اکانت اون ها لاگین
کرده». D21 had deliberately left it out as a session-hijack primitive.
The owner asked for the capability back with the implementation left to
judgement, so the scope is the reference's and the mechanism is not.
Scope: everything, as the reference had it. Its axios interceptor
swapped the bearer token, so every request went as the customer,
document upload included. No route is closed in act-as mode.
Mechanism: the reference minted and handed over the customer's own
permanent api_token. Here the expert keeps their own token and sends
X-Act-As-Customer, resolved in one place — CustomerJwtStrategy
.validateExpert. That single point is why all 17 customer routes work
unchanged: validate() already returns a principal whose `id` is a
Customer id and every customer route reads it. It also fixes four real
defects of the original:
- ownership is re-checked on EVERY request, with referredByAgentId in
the same where clause as the id, so another expert's customer and a
non-existent id both answer 403, and a customer moved to another
expert loses access at once rather than at token expiry;
- nothing durable is handed out — the reference's token sat in
localStorage with no expiry and kept working after the expert left;
- adminUserId survives, so an order placed this way is attributable;
in the reference the request was literally the customer's;
- 403 rather than 401, so the app clears the act-as state instead of
logging a perfectly valid session out.
Performance: one findFirst on the primary key, only when the header is
present, and it replaces the trading-account lookup rather than adding
to it.
Three things the reference never needed, because it swapped the whole
session. queryCache's `me:` keys, favouritesStore and wageQuoteStore all
dropped their data on a TOKEN change, which act-as does not cause — so
an expert would have seen their own profile, orders, balance, ledger and
favourites inside the customer's account. The wage-quote one is the
money case: making charge is per customer, so the figure on screen would
not have been the one the order was priced at. And uploadDocument builds
its own headers, so an expert's upload would have been filed against
their own account, silently.
Added with no reference equivalent: a permanent «در حال مشاهده بهعنوان
…» bar. There, impersonation was invisible once the token had been
swapped, and placing an order on the wrong customer's gold debt was one
tap away. It is also the exit — the reference's only way out was a
logout that ended the expert's own session too. The cart is cleared in
both directions.
CustomerJwtStrategy had no test coverage at all and is now where this
feature's whole authorisation boundary lives: act-as-customer.spec.ts
covers 11 cases, including the header being ignored on a customer token,
the no-header path asserting the extra query does not run, and a revoked
session short-circuiting before the header is read.
api: 308/308 tests, 28 suites; both apps vue-tsc clean.
اپ: کارشناس فروش میتواند وارد حساب مشتریان خود شود، وضعیتشان را ببیند و از طرفشان سفارش ثبت کند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c518f9995b
commit
ddcc8c5649
@@ -1249,6 +1249,12 @@ customer. That is a session-hijack primitive; every session here derives from
|
||||
the caller's own token. Not ported, and recorded here rather than silently
|
||||
dropped.
|
||||
|
||||
> ⚠️ **SUPERSEDED BY D29 (2026-10-12).** The owner asked for the capability
|
||||
> back, so acting as one's own customer now exists — with the reference's full
|
||||
> scope, but not its mechanism: no token is minted, the expert keeps their own,
|
||||
> and ownership is re-checked on every request. The paragraph above still
|
||||
> describes correctly what is **not** done (minting customer tokens). See D29.
|
||||
|
||||
## Ambiguity resolved the low-risk way
|
||||
|
||||
If one mobile ever belonged to both an expert and a customer, the owner ruled
|
||||
@@ -1451,3 +1457,59 @@ status that posts the temporary document (پیشفاکتور) is an admin-cho
|
||||
عملیات** است، پس سکوت از چیزی محافظت نمیکند.
|
||||
|
||||
ثبت شده در `ISSUES.md` بهعنوان AUTH-01/02/03 و DOC-01.
|
||||
|
||||
---
|
||||
|
||||
## D29 — «ورود به حساب مشتری» برای کارشناس فروش (۲۰۲۶/۱۰/۱۲)
|
||||
|
||||
**خواسته مالک:** «کارشناس فروش میتونه از طریق اپ خودش وارد صفحه مشتری هاش بشه و
|
||||
از جانب اون ها ثبت سفارش کنه و وضعیت اون هارو ببینه و یکجورایی انگار با اکانت
|
||||
اون ها لاگین کرده همه چیز رو از دید اون مشتریش میدید و این خیلی کاربردی بود ولی
|
||||
الان نیست» و سپس: «همان قابلیت هارو میخوام حالا هرجور که فکر میکنی از لحاظ
|
||||
امنیت و پرفورمنس بهتره».
|
||||
|
||||
این **D21 را اصلاح میکند**: آنجا `changeUser` «عمداً پورت نشد» و بهعنوان
|
||||
session-hijack رد شد. مالک قابلیت را خواست، پس بازگردانده شد — با همان دامنه
|
||||
کامل مرجع، ولی با سازوکار دیگر.
|
||||
|
||||
### دامنه: کامل، مثل مرجع
|
||||
|
||||
مرجع توکن را در interceptor سراسری axios عوض میکرد
|
||||
(`henza_pwa/plugins/axios.ts:18` ← `getUserToken()`)، پس **هر** درخواست از طرف
|
||||
مشتری میرفت: دیدن، ثبت سفارش، علاقهمندی، و آپلود مدارک. همان دامنه حفظ شد —
|
||||
هیچ مسیری در حالت act-as بسته نشد.
|
||||
|
||||
### سازوکار: هدر، نه توکن
|
||||
|
||||
| | مرجع | اینجا |
|
||||
|---|---|---|
|
||||
| چطور | توکن دائمی مشتری (`api_token`) به کارشناس داده میشد (`ApiMobileController.php:197-201`) | کارشناس توکن **خودش** را میفرستد + هدر `X-Act-As-Customer` |
|
||||
| کجا اعمال میشود | interceptor کلاینت | `CustomerJwtStrategy.validateExpert` — تنها نقطه |
|
||||
| بررسی مالکیت | یکبار، در لحظه گرفتن توکن | **هر درخواست**، با `referredByAgentId` در همان کوئری |
|
||||
| ماندگاری | `localStorage`، بیانقضا | `sessionStorage`؛ بستن تب = پایان |
|
||||
| ردّ حسابرسی | هیچ — درخواست عیناً مشتری بود | `adminUserId` حفظ میشود؛ سفارش لاگ میشود |
|
||||
| بعد از قطع همکاری کارشناس | توکن مشتری همچنان کار میکرد | ۴۰۱ فوری (نقش و `isActive` هر بار خوانده میشود) |
|
||||
|
||||
**چرا این نقطه:** `validate()` همیشه یک principal میسازد که `id` آن یک Customer
|
||||
id است و **هر ۱۷ مسیر مشتری** از آن میخوانند. پس تغییر در یک نقطه، همه مسیرها
|
||||
را از دید مشتری میکند — بدون تغییر در هیچ route.
|
||||
|
||||
**پرفورمنس:** یک `findFirst` روی کلید اصلی، **فقط** وقتی هدر هست؛ و جایگزین
|
||||
کوئریِ `tradingCustomer` میشود نه اضافه بر آن. مشتری عادی و کارشناس در حالت
|
||||
معمولی صفر هزینه اضافه.
|
||||
|
||||
**۴۰۳ و نه ۴۰۱** برای مشتریِ غیرمجاز: نشست خود کارشناس سالم است، پس اپ وضعیت
|
||||
act-as را پاک میکند و او را از سیستم بیرون نمیاندازد.
|
||||
|
||||
### چیزهایی که مرجع نداشت و اضافه شد
|
||||
|
||||
۱. **نوار همیشگی «در حال مشاهده بهعنوان …» + دکمه خروج.** در مرجع، جابجایی
|
||||
نامرئی بود: راه فهمیدن اینکه در حساب کیستی، فقط دیدن دادههای عجیب بود، و
|
||||
ثبت سفارش روی حساب اشتباه یک کلیک فاصله داشت. تنها راه خروج هم logout بود که
|
||||
نشست خود کارشناس را هم میبست (`Dashboard.vue:781-789`).
|
||||
۲. **پاکسازی حالت در جابجایی** (هر دو جهت): سبد، `queryCache` کلیدهای `me:`،
|
||||
علاقهمندیها، و **اجرتهای کششده**. این آخری مالی است: اجرت هر مشتری
|
||||
متفاوت است، و بدون پاکسازی عددِ روی صفحه با عددِ ثبت سفارش یکی نمیبود.
|
||||
(در مرجع این مسئله وجود نداشت چون کل نشست با توکن عوض میشد.)
|
||||
|
||||
ثبت شده در `ISSUES.md` بهعنوان EXP-01.
|
||||
|
||||
Reference in New Issue
Block a user