From 54152336e96a6a9a7f8d070006acf0ce17afbecd Mon Sep 17 00:00:00 2001 From: RissRIce Date: Thu, 30 Jul 2026 17:50:22 -0600 Subject: [PATCH] fix(files): authorize metadata with internal user ids --- src/app/api/files/[fileId]/route.js | 45 +++++++++++++++++++----- src/app/api/files/[fileId]/route.test.js | 23 +++++++++++- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/app/api/files/[fileId]/route.js b/src/app/api/files/[fileId]/route.js index f8dc6b7..b0bdef9 100644 --- a/src/app/api/files/[fileId]/route.js +++ b/src/app/api/files/[fileId]/route.js @@ -173,13 +173,24 @@ export async function HEAD(request, { params } = {}) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const userId = user.id; const { fileId } = await resolveRouteParams(params); const normalizedFileId = normalizeFileId(fileId); if (!normalizedFileId) { return missingFileIdResponse(); } + const { data: internalUser, error: userError } = await supabase + .from('users') + .select('id') + .eq('auth_user_id', user.id) + .single(); + + if (userError || !internalUser) { + return NextResponse.json({ error: 'User profile not found' }, { status: 404 }); + } + + const userId = internalUser.id; + // Get file metadata from database const { data: fileData, error: fileError } = await supabase .from('encrypted_files') @@ -189,13 +200,15 @@ export async function HEAD(request, { params } = {}) { created_at, messages!inner( conversation_id, - conversation_participants!inner( - user_id + conversations!inner( + conversation_participants!inner( + user_id + ) ) ) `) .eq('id', normalizedFileId) - .eq('messages.conversation_participants.user_id', userId) + .eq('messages.conversations.conversation_participants.user_id', userId) .single(); if (fileError || !fileData) { @@ -231,13 +244,25 @@ export async function POST(request, { params } = {}) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const userId = user.id; const { fileId } = await resolveRouteParams(params); const normalizedFileId = normalizeFileId(fileId); if (!normalizedFileId) { return missingFileIdResponse(); } + const authUserId = user.id; + const { data: internalUser, error: userError } = await supabase + .from('users') + .select('id') + .eq('auth_user_id', authUserId) + .single(); + + if (userError || !internalUser) { + return NextResponse.json({ error: 'User profile not found' }, { status: 404 }); + } + + const userId = internalUser.id; + console.log(`📁 [FILE-INFO] Info request from user: ${userId} for file: ${fileId}`); // Get file metadata from database @@ -252,13 +277,15 @@ export async function POST(request, { params } = {}) { messages!inner( id, conversation_id, - conversation_participants!inner( - user_id + conversations!inner( + conversation_participants!inner( + user_id + ) ) ) `) .eq('id', normalizedFileId) - .eq('messages.conversation_participants.user_id', userId) + .eq('messages.conversations.conversation_participants.user_id', userId) .single(); if (fileError || !fileData) { @@ -274,7 +301,7 @@ export async function POST(request, { params } = {}) { encryptedMetadata: fileData.encrypted_metadata, // Client will decrypt createdAt: fileData.created_at, createdBy: fileData.created_by, - isOwner: fileData.created_by === userId + isOwner: fileData.created_by === authUserId || fileData.created_by === userId }); } catch (err) { diff --git a/src/app/api/files/[fileId]/route.test.js b/src/app/api/files/[fileId]/route.test.js index f08feb7..93befac 100644 --- a/src/app/api/files/[fileId]/route.test.js +++ b/src/app/api/files/[fileId]/route.test.js @@ -42,16 +42,29 @@ function createFileQuery() { return query; } +function createUserQuery() { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + single: vi.fn().mockResolvedValue({ + data: { id: 'user-1' }, + error: null + }) + }; + return query; +} + describe('/api/files/[fileId]', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); mocks.authGetUser.mockResolvedValue({ - data: { user: { id: 'user-1' } }, + data: { user: { id: 'auth-user-1' } }, error: null }); mocks.from.mockImplementation((table) => { + if (table === 'users') return createUserQuery(); if (table === 'encrypted_files') return createFileQuery(); throw new Error(`Unexpected table: ${table}`); }); @@ -79,6 +92,10 @@ describe('/api/files/[fileId]', () => { expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toBe('application/octet-stream'); expect(mocks.eq).toHaveBeenCalledWith('id', 'file-1'); + expect(mocks.eq).toHaveBeenCalledWith( + 'messages.conversations.conversation_participants.user_id', + 'user-1' + ); }); it('resolves async route params for POST metadata requests', async () => { @@ -92,5 +109,9 @@ describe('/api/files/[fileId]', () => { expect(body.id).toBe('file-1'); expect(body.conversationId).toBe('conversation-1'); expect(mocks.eq).toHaveBeenCalledWith('id', 'file-1'); + expect(mocks.eq).toHaveBeenCalledWith( + 'messages.conversations.conversation_participants.user_id', + 'user-1' + ); }); });