feat(05-09): authenticated document preview via fetch + Blob URL
- Add fetchDocumentContent() to client.js: fetch with Bearer auth, 401 refresh retry pattern, returns raw Response (not parsed JSON) for blob() calls - Replace iframe :src=proxyUrl (unauthenticated) in DocumentPreviewModal.vue with authenticated fetch → blob → URL.createObjectURL; loading/error states; URL.revokeObjectURL on unmount to prevent memory leaks - Replace window.open(rawUrl) in DocumentView.vue openPdf() with fetchDocumentContent → blob → objectUrl → window.open; 60s auto-revoke - Frontend build exits 0 with zero errors - Closes T-05-09-04: no persistent unauthenticated content exposure
This commit is contained in:
@@ -365,6 +365,49 @@ export function getDocumentContentUrl(docId) {
|
||||
return `/api/documents/${docId}/content`
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch document content bytes with authentication, returning the raw Response.
|
||||
*
|
||||
* Unlike request(), this function does NOT call res.json() — it returns the raw
|
||||
* Response so callers can call .blob() to build an object URL for iframe preview
|
||||
* or window.open() without an unauthenticated src= attribute.
|
||||
*
|
||||
* On 401: attempts one token refresh via authStore.refresh() then retries.
|
||||
* On refresh failure: clears auth state and throws 'Session expired'.
|
||||
*
|
||||
* Security: closes the unauthenticated content-access gap where an iframe src=
|
||||
* or window.open() with a raw /content URL would bypass the Bearer auth check
|
||||
* in cases where the browser does not send the cookie (cross-origin, incognito).
|
||||
* See plan 05-09 trust boundary: frontend→/api/documents/{id}/content.
|
||||
*/
|
||||
export async function fetchDocumentContent(docId, options = {}) {
|
||||
const { useAuthStore } = await import('../stores/auth.js')
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const headers = {}
|
||||
if (authStore.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${authStore.accessToken}`
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/documents/${docId}/content`, {
|
||||
headers,
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (res.status === 401 && !options._retry) {
|
||||
try {
|
||||
await authStore.refresh()
|
||||
return fetchDocumentContent(docId, { _retry: true })
|
||||
} catch {
|
||||
authStore.accessToken = null
|
||||
authStore.user = null
|
||||
throw new Error('Session expired')
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// ── Cloud Storage ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function listCloudConnections() {
|
||||
|
||||
Reference in New Issue
Block a user