- MsGraphClient.getUserPhotoBytes() added (3f6b135) - /api/mobile/engagement/user/[userId]/photo route added (4978780) - Type-check and build both pass - All threat mitigations verified (T-08-01 through T-08-08)
4.8 KiB
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | requirements_addressed | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-engagement-user-profile-new | 01 | mobile-engagement |
|
|
|
|
|
|
|
Phase 8 Plan 01: MS Graph Photo Proxy Summary
One-liner: Server-side photo proxy at /api/mobile/engagement/user/[userId]/photo backed by a new MsGraphClient.getUserPhotoBytes() method — returns JPEG/PNG bytes or neutral 404/503, gated by requireAuth().
Tasks Completed
| Task | Name | Commit | Files |
|---|---|---|---|
| 1 | Add getUserPhotoBytes() to MsGraphClient | 3f6b135 |
lib/services/msgraph-client.ts |
| 2 | Add /api/mobile/engagement/user/[userId]/photo route | 4978780 |
app/api/mobile/engagement/user/[userId]/photo/route.ts |
What Was Built
Task 1 — MsGraphClient.getUserPhotoBytes(userId) (lib/services/msgraph-client.ts)
New public async method inserted as a sibling of the user-scoped methods (before getUserCalendarEvents). It:
- Calls
this.getToken()to reuse the existing OAuth2 client_credentials token cache (no per-request token churn) - Issues a bare
fetchtohttps://graph.microsoft.com/v1.0/users/{encodeURIComponent(userId)}/photo/$valuewith only theAuthorization: Bearerheader (noAccept: application/json— the endpoint returns binary) - Returns
nullon 404 (user has no photo — normal outcome) - Throws
Error(Graph photo error ${status}...)on other non-2xx for the caller to map to 502 - Returns
{ bytes: ArrayBuffer, contentType: string }on 2xx (defaults content-type toimage/jpegif Graph omits the header) - No retry logic — best-effort per D-26
Task 2 — /api/mobile/engagement/user/[userId]/photo route (app/api/mobile/engagement/user/[userId]/photo/route.ts)
New GET handler that:
- Calls
requireAuth()first (mandatory — middleware whitelists/api/mobile/*without pre-gating) - Returns 503 if
isMsgraphConfigured()is false (D-26) - Validates
userIdwith a permissive denylist (rejects/,?,#,.., whitespace, empty, >128 chars) — NOT a strict GUID regex (per migration 041graph_users.id VARCHAR(255)which accepts UPN-form IDs) - Calls
client.getUserPhotoBytes(userId)and returns binary bytes withContent-Type+Cache-Control: private, max-age=3600on success (D-25) - Returns neutral 404 when
getUserPhotoBytesreturns null - Returns neutral 502 on any thrown error (no userId or token echoed in response body)
Deviations from Plan
None — plan executed exactly as written.
Known Stubs
None — this plan delivers a complete, wired endpoint with no placeholder data.
Threat Flags
No new security surface beyond what is catalogued in the plan's <threat_model>. All high-severity threats mitigated:
- T-08-06 (Spoofing/unauthenticated):
requireAuth()is first call in handler — verified at line 10, before any Graph interaction. - T-08-03 (Path traversal): userId denylist +
encodeURIComponentingetUserPhotoBytes. - T-08-02 (DoS/rate amplification):
Cache-Control: private, max-age=3600+ early 400 on invalid userId. - T-08-04 (Info disclosure oracle): 404 and 502 responses use neutral copy with no userId echo.
- T-08-05 (Token leak via logs):
console.errorlogs the Error object (status text only), not the bearer token.
Self-Check: PASSED
Files exist:
- FOUND: lib/services/msgraph-client.ts (modified)
- FOUND: app/api/mobile/engagement/user/[userId]/photo/route.ts (created)
Commits exist:
- FOUND:
3f6b135— feat(08-01): add getUserPhotoBytes() to MsGraphClient - FOUND:
4978780— feat(08-01): add /api/mobile/engagement/user/[userId]/photo proxy route
Type-check: PASSED (npx tsc --noEmit --pretty exits 0) Build: PASSED (npm run build exits 0)