fix(auth): tell the user when the number is not registered (D28)
Reported by the owner: a number that is not in the system produced no error at all. The app advanced to the code box and then nothing happened, and there was no way to tell apart "the SMS is late", "the gateway is down" and "this number was never registered" — the only one of the three the person can actually do something about. Step 1 now says which it is: - unregistered number -> 404 «این شماره در سیستم ثبت نشده است…» - SMS send failed -> 503 «ارسال پیام کوتاه … ممکن نیست…» - sales expert, account unusable -> 403, its own message and both apps surface the server's Persian message for a 404 exactly as they already did for a 429. Without that second half the person would still have seen the generic «خطا در ارسال کد», which is the confusion being removed. This restores the reference's behaviour (firstOrFail() in ApiMobileController::userAuth:43 returns 404); the opaque answer was our deviation, not this. The anti-enumeration protection it was there for is deliberately reduced at step 1 on the owner's instruction — the cost was being paid by real users and by support. Still standing: the per-mobile and per-IP send throttles, which the reference never had, so enumeration is rate-bounded rather than free, and verifyOtp keeps one message for every failure reason. ISSUES.md SEC-03's "no enumeration" claim is corrected rather than left to go stale. Two more instances of the same shape turned up while fixing it: A failed SMS send also returned success, because SmsService never throws (D20), so a dead gateway produced the identical dead end. D20 exists so a failed SMS cannot break an operation — an order must still be placed — but in a login the SMS IS the operation, so it now fails loudly. The local-dev path keys off SmsService's own `sms-not-configured` result rather than re-testing the config: with D26's default relay driver, smsConfigured() is true on any machine with an external IPv4, so a config test would have called a dev box configured and then failed its login. The new test caught precisely that on its first run. Three states return no trading account for a sales expert who does exist. Under the new 404 each would have claimed the number was not registered — false, and it would send support hunting a registration problem that is not there. Also corrects both apps' LoginView header comment, which still described this flow as stubbed: no SMS provider, any 4-digit code accepted, step 1 matching the customer list in the client (that was the login leak, D8). All three had been untrue for weeks and the stale text misled a reader during this session. requestOtp had NO test coverage: the 28-test customer-auth suite stayed green through a change to its response for every unregistered number, so its greenness was not treated as evidence. request-otp.spec.ts adds 9 cases, including that the dev fallback does not apply in production. api: 295/295 tests, 27 suites, typecheck clean; 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
89e2d7937e
commit
c518f9995b
@@ -929,3 +929,65 @@ Noted in the code: with the IAM enabled, silent SSO leaves the page before this
|
||||
guard runs, so the toast is a no-op there by design, not a defect.
|
||||
|
||||
_No data was deleted in this entry._
|
||||
|
||||
---
|
||||
|
||||
## 2026-10-12 — App login — say what went wrong (AUTH-01/02/03, DOC-01)
|
||||
|
||||
- **What:** Step 1 of the app login now reports an unregistered number (404) and
|
||||
a failed SMS send (503) instead of answering success in both cases. A sales
|
||||
expert whose trading account cannot be used gets its own 403 rather than the
|
||||
unknown-number message. Both apps surface the server's Persian message for a
|
||||
404 as they already did for a 429.
|
||||
- **Why:** owner report — a number that was not in the system produced no error:
|
||||
the app moved to the code box and «واقعا نمیشد فهمید خطا از پروژه هست یا نه
|
||||
پیامک نمیاد یا نه واقعا نیست». Three causes looked identical on screen, and
|
||||
only one of them was something the person could act on.
|
||||
- **Reference:** `ApiMobileController::userAuth:43` — `firstOrFail()`, i.e. 404
|
||||
for a number with no active customer. **The opaque answer was the deviation;
|
||||
this restores the reference.**
|
||||
- **Target:** `apps/api/src/modules/customer-auth/customer-auth.service.ts`
|
||||
(`requestOtp`, `deliver`, `resolveExpertTradingAccount`), both apps'
|
||||
`src/views/user/LoginView.vue`.
|
||||
- **Matrix row:** AUTH-01, AUTH-02, AUTH-03, DOC-01 in `ISSUES.md`.
|
||||
- **Commit:** see below.
|
||||
|
||||
**The security trade-off, stated plainly.** The opaque response existed to stop
|
||||
an outsider enumerating which mobile numbers are registered. That protection is
|
||||
now reduced at step 1, deliberately, on the owner's instruction: the cost was
|
||||
being paid by real users and by support on every login problem. What still
|
||||
stands — the per-mobile and per-IP send throttles (`assertSendAllowed`), which
|
||||
the reference did not have, so enumeration is rate-bounded rather than free; and
|
||||
`verifyOtp`, which keeps one message for every failure reason. `ISSUES.md`
|
||||
SEC-03's "no enumeration" claim was corrected rather than left standing.
|
||||
|
||||
**Two further problems found while fixing the reported one.** Both were the same
|
||||
shape — an operation reporting success while doing nothing:
|
||||
|
||||
1. A failed SMS send also returned `{ sent: true }`, because `SmsService` never
|
||||
throws (D20). A dead gateway therefore produced the exact same dead end. D20
|
||||
is about not letting a failed SMS break an *operation*; in a login the SMS is
|
||||
the operation, so it now fails loudly. The local-dev path is preserved by
|
||||
keying off `SmsService`'s own `sms-not-configured` result rather than
|
||||
re-testing the config — with D26's default `relay` driver, `smsConfigured()`
|
||||
is true on any machine with an external IPv4, so a config test would have
|
||||
called a dev box configured and then failed its login. **The test caught
|
||||
exactly this**, on the first run.
|
||||
2. Three states return no trading account for a sales expert who *does* exist.
|
||||
Under the new 404 they would each have claimed the number was not registered
|
||||
— a false message that sends support looking for the wrong thing.
|
||||
|
||||
**A stale comment that actively misled.** Both apps' `LoginView.vue` header still
|
||||
described the login as stubbed: no SMS provider, any 4-digit code accepted, and
|
||||
step 1 matching against the customer list in the client (which was the login
|
||||
leak, D8). All three had been untrue for weeks, and it misled a reader during
|
||||
this very session. Corrected in place, with that noted.
|
||||
|
||||
**Verification.** `requestOtp` had **no tests at all** — the 28-test customer-auth
|
||||
suite stayed green through a change to its response for every unregistered
|
||||
number, which is why the suite's greenness was not treated as evidence.
|
||||
`request-otp.spec.ts` adds 9 cases, including that the dev fallback does *not*
|
||||
apply in production. Full api suite **295/295, 27 suites**; both apps `vue-tsc`
|
||||
clean.
|
||||
|
||||
_No data was deleted in this entry._
|
||||
|
||||
Reference in New Issue
Block a user