parity(apps+api): complete_profile in both apps [ref: henza_server Major/CustomerController::complete]

Ports "تکمیل پروفایل" end to end (D22).

API
- POST customer/me/complete-profile — three required document ids, the
  customer taken from the token, verification step set to the reference's
  LIVE value (approved outright; its `= 1` line is commented out at :83 in
  favour of `= 2` at :85). Both SMS messages byte-identical and non-blocking
  per D20.
- POST customer/me/documents — the reference let the app upload through the
  same file/create the admin used, inside its auth:api-customer group
  (routes/api.php:95); the target's copy is admin-guarded, so the page could
  not have uploaded at all. Reuses AdminUploadsService unchanged, so both
  paths share one size limit, MIME whitelist and naming scheme (UPL-01).
- PROFILE_ADMIN_MOBILES replaces the reference's four hardcoded personal
  numbers; blank means nobody is notified and the upload still succeeds.

Three reference behaviours deliberately NOT ported, all in ISSUES.md:
- SEC-14: `$customer->update($request->all())` mass-assigned the whole request
  body, letting the app write its own credit limit, making charge, gold debt,
  active flag or mobile. Replaced with a three-field whitelist.
- SEC-15: four real personal mobiles hardcoded in the source.
- APP-03: refresh()'s unterminated setInterval page reload.

Apps (both, template+script byte-identical, per-app styling only)
- CompleteProfileView + SingleImageUploader
- route /complete_profile, redirecting home once the documents are in, as the
  reference's guard did (router/index.ts:266)
- the entry point on the profile screen, shown only while they are missing
  (profile.vue:96-102)

Also (D23): JWT_CUSTOMER_SECRET now warns loudly at boot when unset. It falls
back to JWT_SECRET, which signs app and admin tokens with the same key and
leaves the `typ` check as the only barrier — and an expert's app token carries
their AdminUser id in `sub`. Warn rather than refuse to boot (D19, lowest
risk); DELIVERY.md lists it as the first pre-release step (REL-03).

Verified: build 5/5 · jest 45/45 · complete-profile 38/38 (new) ·
expert login 36/36 · apps smoke 43/43 · admin smoke 43/43 ·
B2B scan 34 ok/0 broken · typecheck clean on all four apps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-23 01:42:07 +03:30
co-authored by Claude Opus 5
parent f71fd5bfe1
commit 5ad1083aee
115 changed files with 1746 additions and 99 deletions
+46
View File
@@ -1255,3 +1255,49 @@ If one mobile ever belonged to both an expert and a customer, the owner ruled
the **expert wins**. Since `Customer.mobile` is unique, the code adopts the
existing customer row as the expert's trading account rather than failing the
login or creating a duplicate, and logs a warning.
---
## D22 — «تکمیل پروفایل» (complete_profile), 2026-09-23
Ports `POST /major/complete`
(henza_server `Major/CustomerController::complete`, reached from
`henza_pwa/src/views/user/src/completeProfile.vue`) as
`POST customer/me/complete-profile`.
**Reference behaviour kept**
- all three document ids required; a miss is rejected before anything is written
- the customer comes from the session, never from the body (D21)
- the verification step is set to the reference's **live** value 2 — approved
outright, not "waiting for an expert". The reference's own
`sts_verify_step_two = 1` line is commented out at `:83` in favour of `= 2`
at `:85`. That is `VerificationStep.VERIFIED` here.
- both SMS messages byte-identical, and non-blocking per D20
- the entry point is the **profile screen**, shown only while the documents are
missing (`profile.vue:96-102`), and the route redirects home once they are in
(`router/index.ts:266`)
**Reference behaviour deliberately NOT ported** (all in ISSUES.md)
- **SEC-14** — `$customer->update($request->all())` mass-assigned the whole
request body, letting the app write its own credit limit, making charge, gold
debt, active flag or mobile. Replaced with a three-field whitelist.
- **SEC-15** — four real personal mobiles hardcoded in the source. They come
from `PROFILE_ADMIN_MOBILES` instead; blank means nobody is notified.
- **APP-03** — `refresh()`'s unterminated `setInterval` page reload.
**One gap had to be closed first (UPL-01).** The reference let the app upload
through the same `file/create` the admin used, inside its `auth:api-customer`
group; the target's copy is admin-guarded. Added `POST customer/me/documents`,
reusing `AdminUploadsService` unchanged so both paths share one size limit,
MIME whitelist and naming scheme.
## D23 — `JWT_CUSTOMER_SECRET` warns loudly, 2026-09-23
`customerJwtSecret()` falls back to `JWT_SECRET`. When it does, app and admin
tokens are signed with the **same key**, leaving the `typ` check in
`jwt.strategy.ts` as the only barrier — and an expert's app token carries their
AdminUser id in `sub`. The owner asked for a refusal to boot **or** a very
clear warning; the lower-risk option was chosen (D19), since failing the boot
could take down a running deployment that has not set the variable yet. A
banner as loud as the master-OTP one now fires at boot, and DELIVERY.md lists
it as the **first** pre-release step (REL-03).