实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -510,6 +510,11 @@ const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_bus" };
|
||||
- Reimbursement attachments and invoice PDFs retain `inline`; purchase,
|
||||
employee and payment receipt downloads keep their existing default
|
||||
`attachment` behavior through the same shared builder.
|
||||
- Inside the WeCom self-built app, reimbursement invoice and attachment viewing
|
||||
uses the official `previewFile` JSAPI with accurate persisted file name/size.
|
||||
Because the native layer does not reliably carry OA authentication headers,
|
||||
only the narrow grant defined in `wecom-boundary.md` may use a query token;
|
||||
expiry is at most 120 seconds and ordinary PC downloads remain unchanged.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
@@ -519,6 +524,8 @@ const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_bus" };
|
||||
- Unsupported MIME, empty file or oversized file -> existing stable file
|
||||
validation error; no partial persistence.
|
||||
- Chinese name -> `inline; filename="attachment.<ext>"; filename*=UTF-8''...`.
|
||||
- WeCom preview grant is expired, tampered or bound to another reimbursement or
|
||||
attachment -> `403` with no file bytes.
|
||||
- Invalid disposition value -> `TypeError`; callers may use only `inline` or
|
||||
`attachment`.
|
||||
|
||||
@@ -539,6 +546,10 @@ const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_bus" };
|
||||
- Download an existing Chinese-name reimbursement attachment and invoice PDF;
|
||||
assert `inline`, ASCII `filename`, percent-encoded `filename*`, and no raw
|
||||
Chinese in the header.
|
||||
- Assert the WeCom grant carries the exact original name and file size, expires
|
||||
within 120 seconds, records issue/access audits and rejects tamper, expiry and
|
||||
object mismatch; Playwright verifies PC browser behavior and WeCom
|
||||
`previewFile` behavior with synthetic files.
|
||||
- Cover CR/LF, quote and path cleanup in the shared Content-Disposition helper,
|
||||
while retaining existing purchase `attachment` regression tests.
|
||||
- Run backend full tests and frontend typecheck/build because the limit and
|
||||
|
||||
@@ -134,7 +134,10 @@
|
||||
`buffer`, `extension`, `mimeType`, `fileSize` and `fileSha256`.
|
||||
- Browser downloads of OA-stored attachments must use a currently
|
||||
authenticated request, preferably an API-local `POST` with optional JSON
|
||||
`{ "reason": "..." }`, rather than a query bearer URL.
|
||||
`{ "reason": "..." }`, rather than a query bearer URL. The only approved
|
||||
exception is the at-most-120-second, purpose/object/actor-bound grant used by
|
||||
the official WeCom `previewFile` native layer; follow `wecom-boundary.md` and
|
||||
do not reuse that grant for ordinary browser downloads or other integrations.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
@@ -163,9 +166,12 @@
|
||||
- Successful download audit is part of authorization: write it before setting
|
||||
attachment headers or sending bytes. Audit failure must return a non-success
|
||||
response with no file body.
|
||||
- Successful responses use `application/octet-stream`, `attachment`,
|
||||
`nosniff`, private no-store caching, no-referrer and a route-level sandbox
|
||||
CSP. Do not restore same-origin inline preview or query-token download routes.
|
||||
- Successful ordinary browser downloads use `application/octet-stream`,
|
||||
`attachment`, `nosniff`, private no-store caching, no-referrer and a
|
||||
route-level sandbox CSP. Do not restore general same-origin inline preview or
|
||||
query-token download routes. The WeCom-native reimbursement invoice/attachment
|
||||
exception retains the domain-specific `inline` response and must still audit
|
||||
before bytes, use no-store/no-referrer and reject expired or mismatched grants.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
@@ -220,10 +226,11 @@
|
||||
- Frontend typecheck/build after API type changes.
|
||||
- Focused UI verification for pages that add attachment governance labels or
|
||||
download-reason interactions.
|
||||
- Frontend download tests must assert authenticated JSON request + Blob browser
|
||||
download and absence of popup/inline preview. Use only synthetic in-memory
|
||||
fixtures; production attachments and historical bulk scanning require a
|
||||
separate approval.
|
||||
- Frontend ordinary-download tests must assert authenticated JSON request + Blob
|
||||
browser download. WeCom-native preview tests instead assert the official
|
||||
bridge receives the exact short URL/name/size and that `window.open` is not
|
||||
used. Use only synthetic in-memory fixtures; production attachments and
|
||||
historical bulk scanning require a separate approval.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
|
||||
@@ -41,6 +41,98 @@ or administration workflow has already migrated.
|
||||
rule, applicant-visible mobile submission page and actual post-submission
|
||||
approval chain; API template metadata alone is insufficient.
|
||||
|
||||
## Scenario: Native File Preview With Narrow Short-Lived Grants
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: an authenticated OA page inside the Enterprise WeChat self-built app
|
||||
must preview a reimbursement invoice or supporting file with the official
|
||||
`previewFile` JSAPI.
|
||||
- Purpose: WeCom's native downloader does not reliably carry the WebView cookie
|
||||
or OA bearer header, while ordinary PDF links may download instead of preview.
|
||||
This is a narrow native-preview bridge, not a general query-token download
|
||||
design.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- JS-SDK signature: `POST /api/auth/wecom/js-sdk-signature`, authenticated body
|
||||
`{ url: string }`; response `{ corpId, timestamp, nonceStr, signature }`.
|
||||
- Invoice grant: `POST /api/invoice-pool/:id/signed-url`; response includes
|
||||
`{ signedUrl, expiresAt, originalName, mimeType, fileSize }`.
|
||||
- Reimbursement attachment grant:
|
||||
`POST /api/reimbursements/:id/attachments/:attachmentId/preview-url`, optional
|
||||
body `{ reason }`; response uses the same file metadata shape.
|
||||
- Native call: `previewFile({ url, name, size })`; `name` includes an extension
|
||||
and `size` is the persisted positive byte count.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Fetch and cache the WeCom JSAPI ticket only on the server. Never return or log
|
||||
the ticket, app access token, Agent Secret or raw provider response.
|
||||
- Sign only the first configured OA `APP_ORIGIN`; reject cross-origin URLs,
|
||||
URL credentials and fragments. The signed page URL excludes the hash.
|
||||
- A native file grant expires in at most 120 seconds and binds purpose, exact
|
||||
business object, exact file object and actor. It is signed only after current
|
||||
authentication, business-read permission and download-reason checks pass.
|
||||
- Invoice access may reuse the existing current-pending-task exception. Do not
|
||||
grant the approver invoice-management, edit, unbind or delete permission.
|
||||
- The native GET writes a successful access audit before sending headers or
|
||||
bytes and returns `private, no-store` plus `no-referrer`.
|
||||
- PC keeps its browser/authenticated download behavior. Only a detected WeCom
|
||||
WebView dynamically loads `@wecom/jssdk` and calls `previewFile`; do not send
|
||||
the actual file through a WeCom message or native approval payload.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Missing authentication on signature/grant issue -> `401`.
|
||||
- Cross-origin page URL -> `403`; malformed, credential-bearing or fragment URL
|
||||
-> `400`, before any provider call.
|
||||
- Ticket/token provider failure -> stable `503` detail without provider values.
|
||||
- Missing file metadata, extension or positive integer size -> frontend stops
|
||||
before `previewFile` and shows a stable error.
|
||||
- Missing required download reason -> `400 { reason: "required" }`.
|
||||
- Expired, tampered, wrong-purpose or object-mismatched file grant -> `403`.
|
||||
- Access audit failure -> `5xx` with no file body.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: a pending reimbursement approver opens a 120-second invoice URL through
|
||||
`previewFile`, sees the original in WeCom and receives no edit capability.
|
||||
- Base: a PC user opens the same invoice through the browser and downloads an
|
||||
attachment through the existing authenticated Blob path.
|
||||
- Bad: expose a long-lived generic `?token=`, trust a caller-supplied size/name,
|
||||
put a provider ticket in the response, or use async `window.open(blob:)` in
|
||||
WeCom and assume desktop popup behavior applies.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Route tests cover same-origin signing, provider failure/redaction and rejection
|
||||
of cross-origin, credential-bearing and fragment URLs before provider access.
|
||||
- Grant tests assert exact object/actor binding, at-most-120-second expiry,
|
||||
metadata, reason gate, issue audit, access audit, tamper/expiry/mismatch denial
|
||||
and audit-failure fail-closed behavior using synthetic files only.
|
||||
- Playwright injects a synthetic `WeixinJSBridge`, asserts `previewFile` receives
|
||||
the exact URL/name/size for invoice and attachment, and asserts no
|
||||
`window.open` call in WeCom. PC assertions keep browser URL and Blob behavior.
|
||||
- Production deployment and real-device preview remain separate approvals.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```text
|
||||
await downloadWithBearer();
|
||||
window.open(URL.createObjectURL(blob)); // WeCom may block after await
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```text
|
||||
authenticated grant issue -> <=120s object-bound URL
|
||||
-> ww.previewFile({ url, nameWithExtension, exactSize })
|
||||
-> audit before file bytes
|
||||
```
|
||||
|
||||
## Scenario: WeCom Member Data Is A Read-Only Source For OA
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
在新工单中引用
屏蔽一个用户