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:
co-authored by
Claude Opus 5
parent
f71fd5bfe1
commit
5ad1083aee
@@ -264,4 +264,51 @@ export const api = {
|
||||
'/expert/child-orders',
|
||||
{ method: 'POST', body: JSON.stringify({ page, search: search ?? null }) },
|
||||
),
|
||||
|
||||
// ── complete_profile ─────────────────────────────────────────────────
|
||||
// Ports POST /major/complete (henza_server Major/CustomerController::complete)
|
||||
// and the PWA's document uploads, which went through the same file/create the
|
||||
// admin used but behind the customer guard (routes/api.php:95).
|
||||
/**
|
||||
* Uploads one document and resolves its stored id, which is what
|
||||
* completeProfile sends. multipart/form-data, so it bypasses `request()` —
|
||||
* that helper forces a JSON content-type, and setting it here would break
|
||||
* the multipart boundary.
|
||||
*/
|
||||
uploadDocument: async (file: File, purpose: string): Promise<{ id: string; url: string }> => {
|
||||
const body = new FormData();
|
||||
body.append('file', file);
|
||||
body.append('purpose', purpose);
|
||||
|
||||
const token = readToken();
|
||||
const res = await fetch(`${BASE}/customer/me/documents`, {
|
||||
method: 'POST',
|
||||
// No Content-Type: the browser sets it with the boundary.
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body,
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
onUnauthorized?.();
|
||||
throw new UnauthorizedError('/customer/me/documents');
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`آپلود فایل ناموفق بود (${res.status})`);
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* The three ids keep the reference's snake_case keys so the payload matches
|
||||
* what henza_pwa sent (completeProfile.vue:136-140).
|
||||
*/
|
||||
completeProfile: (payload: {
|
||||
business_license_image_id: string;
|
||||
shop_image_id: string;
|
||||
ncode_image_id: string;
|
||||
}) =>
|
||||
request<{ ok: boolean }>('/customer/me/complete-profile', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user