fix(documents): read attachments with service client so colleague uploads open (#1207)
The documents bucket SELECT policy only covers the uploader's own folder
(documents/{uid}/...), but document_attachments rows are company-scoped.
Every surface that touched storage with the user-bound client therefore
failed for attachments uploaded by another member of the same company
(colleague uploads, email-inbox ingest attributed to the company creator):
- GET /api/documents/:id 500ed with "Failed to create download URL", so
viewing a bilaga on a verifikat or supplier invoice was broken for
every member except the uploader (support case: Odin Aero, where all
40 documents live in the owner's folder and the second member could
open none of them).
- GET /api/documents/:id/integrity 500ed the same way.
- POST /api/documents/:id/verify failed the storage download.
- invoice-inbox retry-extraction could not download the attachment.
- cloud-backup user-triggered syncs silently dropped colleague-uploaded
documents from the Drive archive (manifest rows flipped to 'error').
Fix: authorize on the user client (RLS + explicit company filter, plus
the membership check where present), then do the storage read with the
service-role client. This is the pattern the inline proxy route and the
v1 download route already use; these five call sites were left behind.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6d9846b1e7
commit
968161b42b
@@ -12,6 +12,14 @@ vi.mock('@/lib/reports/archive-readme', () => ({
|
||||
buildDriveFolderReadme: vi.fn().mockReturnValue('README TEXT'),
|
||||
}))
|
||||
|
||||
// performSync builds archives with a service-role client (the documents
|
||||
// bucket SELECT policy is per-uploader-folder, so a user-bound client would
|
||||
// drop colleague-uploaded files from the backup). The archive generators are
|
||||
// mocked above, so a bare stub is enough here.
|
||||
vi.mock('@/lib/supabase/server', () => ({
|
||||
createServiceClient: vi.fn(() => ({ storage: { from: vi.fn() } })),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/branding/service', () => ({
|
||||
getBranding: () => ({ appName: 'Accounted', appUrl: 'https://app.test' }),
|
||||
}))
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@/lib/reports/full-archive-export'
|
||||
import { buildDriveFolderReadme } from '@/lib/reports/archive-readme'
|
||||
import { getBranding } from '@/lib/branding/service'
|
||||
import { createServiceClient } from '@/lib/supabase/server'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import {
|
||||
getOAuthEnv,
|
||||
@@ -126,6 +127,15 @@ interface PeriodRow {
|
||||
export async function performSync(params: PerformSyncParams): Promise<PerformSyncResult> {
|
||||
const { supabase, companyId, userId, origin } = params
|
||||
|
||||
// Archive generation reads the private `documents` bucket, whose SELECT
|
||||
// policy only covers the uploader's own folder. User-triggered syncs pass
|
||||
// a user-bound client, which would silently drop every colleague-uploaded
|
||||
// document from the backup (manifest rows flip to 'error'). The service
|
||||
// client is used for archive generation only; authorization happened at
|
||||
// the extension dispatcher (or the cron), and every archive query filters
|
||||
// by the explicit companyId.
|
||||
const archiveClient = createServiceClient()
|
||||
|
||||
const connection = await loadExtensionData<GoogleDriveConnection>(
|
||||
supabase,
|
||||
companyId,
|
||||
@@ -322,7 +332,7 @@ export async function performSync(params: PerformSyncParams): Promise<PerformSyn
|
||||
].join('|'),
|
||||
includeDocuments,
|
||||
generate: () =>
|
||||
generateFullArchive(supabase, companyId, {
|
||||
generateFullArchive(archiveClient, companyId, {
|
||||
scope: 'period',
|
||||
period_id: period.id,
|
||||
include_documents: includeDocuments,
|
||||
@@ -355,7 +365,7 @@ export async function performSync(params: PerformSyncParams): Promise<PerformSyn
|
||||
].join('|'),
|
||||
includeDocuments: baseDecision.includeDocuments,
|
||||
generate: () =>
|
||||
generateBaseDataArchive(supabase, companyId, {
|
||||
generateBaseDataArchive(archiveClient, companyId, {
|
||||
include_documents: baseDecision.includeDocuments,
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user