From ded5ce2343ebda2bcda48f34457371aa43b7d99f Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Thu, 9 Jul 2026 12:16:11 -0600 Subject: [PATCH 1/2] Fix QryptChat QA route validation issues --- src/app/api/auth/debug-sms/route.js | 10 +-- src/app/api/auth/debug-sms/route.test.js | 74 +++++++++++++++++++ src/app/api/conversations/create/route.js | 15 +++- .../api/conversations/create/route.test.js | 64 ++++++++++++++++ 4 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 src/app/api/auth/debug-sms/route.test.js create mode 100644 src/app/api/conversations/create/route.test.js diff --git a/src/app/api/auth/debug-sms/route.js b/src/app/api/auth/debug-sms/route.js index 700ef36..b415b89 100644 --- a/src/app/api/auth/debug-sms/route.js +++ b/src/app/api/auth/debug-sms/route.js @@ -33,7 +33,7 @@ export async function POST(request, { params } = {}) { if (!isDevelopment()) { return NextResponse.json({ error: 'Debug endpoints are disabled in production' }, { status: 403 }); } - if (!(await verifyAuth(event))) { + if (!(await verifyAuth(request))) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -48,7 +48,7 @@ export async function POST(request, { params } = {}) { ); } - const diagnostics = new SMSAuthDiagnostics(event); + const diagnostics = new SMSAuthDiagnostics(request); switch (action) { case 'diagnose': @@ -109,12 +109,12 @@ export async function GET(request, { params } = {}) { if (!isDevelopment()) { return NextResponse.json({ error: 'Debug endpoints are disabled in production' }, { status: 403 }); } - if (!(await verifyAuth(event))) { + if (!(await verifyAuth(request))) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { - const diagnostics = new SMSAuthDiagnostics(event); + const diagnostics = new SMSAuthDiagnostics(request); // Run basic system checks without phone number const result = await diagnostics.runDiagnostics('+1234567890'); // Dummy number for format check @@ -143,4 +143,4 @@ export async function GET(request, { params } = {}) { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/src/app/api/auth/debug-sms/route.test.js b/src/app/api/auth/debug-sms/route.test.js new file mode 100644 index 0000000..140b8d0 --- /dev/null +++ b/src/app/api/auth/debug-sms/route.test.js @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + getUser: vi.fn(), + runDiagnostics: vi.fn(), + SMSAuthDiagnostics: vi.fn(function SMSAuthDiagnostics(request) { + this.request = request; + this.logger = { getLogs: () => [] }; + this.runDiagnostics = mocks.runDiagnostics; + }) +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(() => ({ + auth: { + getUser: mocks.getUser + } + })) +})); + +vi.mock('@/lib/utils/sms-debug.js', () => ({ + SMSAuthDiagnostics: mocks.SMSAuthDiagnostics +})); + +function debugRequest(method, body) { + return new Request('https://example.com/api/auth/debug-sms', { + method, + headers: { 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : undefined + }); +} + +describe('SMS debug endpoint', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + vi.stubEnv('NODE_ENV', 'development'); + mocks.getUser.mockResolvedValue({ + data: { user: { id: 'user-1' } }, + error: null + }); + mocks.runDiagnostics.mockResolvedValue({ + success: true, + issues: [], + logs: [] + }); + }); + + it('uses the incoming request for authenticated POST diagnostics', async () => { + const { POST } = await import('./route.js'); + const request = debugRequest('POST', { phoneNumber: '+1234567890' }); + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.success).toBe(true); + expect(mocks.SMSAuthDiagnostics).toHaveBeenCalledWith(request); + expect(mocks.runDiagnostics).toHaveBeenCalledWith('+1234567890'); + }); + + it('uses the incoming request for authenticated GET diagnostics', async () => { + const { GET } = await import('./route.js'); + const request = debugRequest('GET'); + + const response = await GET(request); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.systemStatus).toBe('healthy'); + expect(mocks.SMSAuthDiagnostics).toHaveBeenCalledWith(request); + expect(mocks.runDiagnostics).toHaveBeenCalledWith('+1234567890'); + }); +}); diff --git a/src/app/api/conversations/create/route.js b/src/app/api/conversations/create/route.js index 5b9af0d..3d3dbcf 100644 --- a/src/app/api/conversations/create/route.js +++ b/src/app/api/conversations/create/route.js @@ -10,7 +10,18 @@ import { MESSAGE_TYPES } from '@/lib/api/protocol.js'; export const POST = withAuth(async ({ request, locals }) => { try { - const { participantIds, name, isGroup } = await request.json(); + let body; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 }); + } + + const { participantIds, name, isGroup } = body; if (!participantIds || !Array.isArray(participantIds)) { return NextResponse.json({ error: 'Missing or invalid participantIds' }, { status: 400 }); @@ -86,4 +97,4 @@ export const POST = withAuth(async ({ request, locals }) => { console.error('Create conversation error:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -}); \ No newline at end of file +}); diff --git a/src/app/api/conversations/create/route.test.js b/src/app/api/conversations/create/route.test.js new file mode 100644 index 0000000..63cd19a --- /dev/null +++ b/src/app/api/conversations/create/route.test.js @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + mockSupabase: { + from: vi.fn() + }, + mockJoinRoom: vi.fn(), + mockSendToUser: vi.fn() +})); + +vi.mock('@/lib/api/middleware/auth.js', () => ({ + withAuth: (handler) => (request, context) => + handler({ + request, + locals: { + supabase: mocks.mockSupabase, + user: { id: 'auth-user-id' } + }, + context + }) +})); + +vi.mock('@/lib/api/sse-manager.js', () => ({ + sseManager: { + joinRoom: mocks.mockJoinRoom, + sendToUser: mocks.mockSendToUser + } +})); + +vi.mock('@/lib/api/protocol.js', () => ({ + MESSAGE_TYPES: { + CONVERSATION_CREATED: 'conversation.created' + } +})); + +describe('POST /api/conversations/create validation', () => { + it('returns 400 for malformed JSON instead of a generic 500', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockRejectedValue(new SyntaxError('Unexpected token')) + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('Invalid JSON body'); + expect(mocks.mockSupabase.from).not.toHaveBeenCalled(); + }); + + it('rejects non-object JSON before database work', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockResolvedValue(['user-1']) + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('Request body must be a JSON object'); + expect(mocks.mockSupabase.from).not.toHaveBeenCalled(); + }); +}); From 2de265de2b2dd68abddc0f2eb80b18cc5006338e Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Thu, 9 Jul 2026 12:29:56 -0600 Subject: [PATCH 2/2] Harden file upload JSON validation --- src/app/api/files/upload-complete/route.js | 25 ++++++- .../api/files/upload-complete/route.test.js | 69 +++++++++++++++++++ src/app/api/files/upload-url/route.js | 35 +++++++--- src/app/api/files/upload-url/route.test.js | 54 +++++++++++++++ 4 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 src/app/api/files/upload-complete/route.test.js create mode 100644 src/app/api/files/upload-url/route.test.js diff --git a/src/app/api/files/upload-complete/route.js b/src/app/api/files/upload-complete/route.js index c2ea4c6..f17cf6d 100644 --- a/src/app/api/files/upload-complete/route.js +++ b/src/app/api/files/upload-complete/route.js @@ -4,6 +4,21 @@ import { createSupabaseServerClient } from '@/lib/supabase.js'; import { sseManager } from '@/lib/api/sse-manager.js'; import { MESSAGE_TYPES } from '@/lib/api/protocol.js'; +async function parseJsonObject(request) { + let body; + try { + body = await request.json(); + } catch { + return { error: NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) }; + } + + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return { error: NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 }) }; + } + + return { body }; +} + export async function POST(request, { params } = {}) { try { // Create Supabase server client @@ -17,12 +32,16 @@ export async function POST(request, { params } = {}) { console.log(`📁 [UPLOAD-COMPLETE] Upload completion from auth user: ${user.id}`); - // Parse the JSON request body + const parsedBody = await parseJsonObject(request); + if (parsedBody.error) { + return parsedBody.error; + } + const { storagePath, fileId, metadata - } = await request.json(); + } = parsedBody.body; // Validate inputs if (!storagePath || !fileId || !metadata) { @@ -169,4 +188,4 @@ function formatFileSize(bytes) { const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} \ No newline at end of file +} diff --git a/src/app/api/files/upload-complete/route.test.js b/src/app/api/files/upload-complete/route.test.js new file mode 100644 index 0000000..24429cf --- /dev/null +++ b/src/app/api/files/upload-complete/route.test.js @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + getUser: vi.fn(), + from: vi.fn(), + broadcastToRoom: vi.fn() +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(() => ({ + auth: { + getUser: mocks.getUser + }, + from: mocks.from + })) +})); + +vi.mock('@/lib/api/sse-manager.js', () => ({ + sseManager: { + broadcastToRoom: mocks.broadcastToRoom + } +})); + +vi.mock('@/lib/api/protocol.js', () => ({ + MESSAGE_TYPES: { + NEW_MESSAGE: 'message.new' + } +})); + +describe('POST /api/files/upload-complete validation', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + mocks.getUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + }); + + it('returns 400 for malformed JSON before database work', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockRejectedValue(new SyntaxError('Unexpected token')) + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('Invalid JSON body'); + expect(mocks.from).not.toHaveBeenCalled(); + expect(mocks.broadcastToRoom).not.toHaveBeenCalled(); + }); + + it('rejects non-object JSON before database work', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockResolvedValue('storage-path') + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('Request body must be a JSON object'); + expect(mocks.from).not.toHaveBeenCalled(); + expect(mocks.broadcastToRoom).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/api/files/upload-url/route.js b/src/app/api/files/upload-url/route.js index 68e9633..1783799 100644 --- a/src/app/api/files/upload-url/route.js +++ b/src/app/api/files/upload-url/route.js @@ -2,6 +2,21 @@ import { NextResponse } from 'next/server'; import { createSupabaseServerClient } from '@/lib/supabase.js'; +async function parseJsonObject(request) { + let body; + try { + body = await request.json(); + } catch { + return { error: NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) }; + } + + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return { error: NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 }) }; + } + + return { body }; +} + export async function POST(request, { params } = {}) { try { // Create Supabase server client @@ -15,6 +30,17 @@ export async function POST(request, { params } = {}) { console.log(`📁 [UPLOAD-URL] Signed URL request from auth user: ${user.id}`); + const parsedBody = await parseJsonObject(request); + if (parsedBody.error) { + return parsedBody.error; + } + + const { + conversationId, + messageId, + encryptedMetadata + } = parsedBody.body; + // Get the internal user ID from the users table using auth_user_id const { data: internalUser, error: userError } = await supabase .from('users') @@ -30,13 +56,6 @@ export async function POST(request, { params } = {}) { const userId = internalUser.id; console.log(`📁 [UPLOAD-URL] Using internal user ID: ${userId}`); - // Parse the JSON request body (should be small - just encrypted metadata) - const { - conversationId, - messageId, - encryptedMetadata - } = await request.json(); - // Validate inputs if (!conversationId || !messageId || !encryptedMetadata) { console.error( '📁 [UPLOAD-URL] Missing required fields'); @@ -116,4 +135,4 @@ export async function POST(request, { params } = {}) { console.error( '📁 [UPLOAD-URL] ❌ Unexpected error:', err); return NextResponse.json({ error: 'Internal server error during signed URL generation' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/api/files/upload-url/route.test.js b/src/app/api/files/upload-url/route.test.js new file mode 100644 index 0000000..725dbe5 --- /dev/null +++ b/src/app/api/files/upload-url/route.test.js @@ -0,0 +1,54 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + getUser: vi.fn(), + from: vi.fn() +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(() => ({ + auth: { + getUser: mocks.getUser + }, + from: mocks.from + })) +})); + +describe('POST /api/files/upload-url validation', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + mocks.getUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + }); + + it('returns 400 for malformed JSON before database work', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockRejectedValue(new SyntaxError('Unexpected token')) + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('Invalid JSON body'); + expect(mocks.from).not.toHaveBeenCalled(); + }); + + it('rejects non-object JSON before database work', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockResolvedValue(['conversation-1']) + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('Request body must be a JSON object'); + expect(mocks.from).not.toHaveBeenCalled(); + }); +});