From 708ba498a29e23a4a544819a18a135f5df84d69d Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sun, 12 Jul 2026 17:42:32 -0600 Subject: [PATCH] Normalize nuclear delete bearer tokens --- src/app/api/user/nuclear-delete/route.js | 14 ++- src/app/api/user/nuclear-delete/route.test.js | 105 ++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/app/api/user/nuclear-delete/route.test.js diff --git a/src/app/api/user/nuclear-delete/route.js b/src/app/api/user/nuclear-delete/route.js index f836edfa..d248d0d2 100644 --- a/src/app/api/user/nuclear-delete/route.js +++ b/src/app/api/user/nuclear-delete/route.js @@ -4,6 +4,15 @@ import { createClient } from '@supabase/supabase-js'; // Create regular Supabase client for authentication const supabaseClient = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY); +function getBearerToken(authHeader) { + if (typeof authHeader !== 'string') return null; + + const match = authHeader.match(/^Bearer\s+(.+)$/i); + const token = match?.[1]?.trim(); + + return token || null; +} + /** * Authenticate user from request cookies * @param {Request} request - The request object @@ -15,8 +24,9 @@ async function authenticateUser(request) { // First, check for Authorization header (priority) const authHeader = request.headers.get('authorization'); - if (authHeader?.startsWith('Bearer ')) { - accessToken = authHeader.replace('Bearer ', ''); + const bearerToken = getBearerToken(authHeader); + if (bearerToken) { + accessToken = bearerToken; console.log('🔐 [API] ✅ Using JWT from Authorization header'); console.log('🔐 [API] JWT segments count:', accessToken.split('.').length); console.log('🔐 [API] JWT preview:', accessToken.substring(0, 50) + '...'); diff --git a/src/app/api/user/nuclear-delete/route.test.js b/src/app/api/user/nuclear-delete/route.test.js new file mode 100644 index 00000000..945dea91 --- /dev/null +++ b/src/app/api/user/nuclear-delete/route.test.js @@ -0,0 +1,105 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + serviceFrom: vi.fn(), + serviceRpc: vi.fn() +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(() => ({ + auth: { + getUser: mocks.authGetUser + } + })) +})); + +vi.mock('@/lib/supabase/service-role.js', () => ({ + createServiceRoleClient: vi.fn(() => ({ + from: mocks.serviceFrom, + rpc: mocks.serviceRpc + })) +})); + +function cookieValue(token) { + return `base64-${Buffer.from(JSON.stringify({ access_token: token })).toString('base64')}`; +} + +function deleteRequest(headers) { + return new Request('https://qrypt.chat/api/user/nuclear-delete', { + method: 'DELETE', + headers, + body: JSON.stringify({ confirmation: 'DELETE_ALL_MY_DATA' }) + }); +} + +function createUserQuery() { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + single: vi.fn(() => + Promise.resolve({ + data: { + id: 'internal-user-id', + phone_number: '+15551234567', + username: 'cipher-user' + }, + error: null + }) + ) + }; + return query; +} + +describe('DELETE /api/user/nuclear-delete bearer authentication', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + process.env.NEXT_PUBLIC_SUPABASE_URL = 'https://example.supabase.co'; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = 'anon-key'; + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + mocks.serviceFrom.mockImplementation((table) => { + if (table === 'users') return createUserQuery(); + throw new Error(`Unexpected table: ${table}`); + }); + mocks.serviceRpc.mockResolvedValue({ + data: { deleted_messages: 1, deleted_keys: 1 }, + error: null + }); + }); + + it('normalizes bearer scheme casing and extra spaces', async () => { + const { DELETE } = await import('./route.js'); + const response = await DELETE( + deleteRequest({ + authorization: 'bearer access-token ', + 'content-type': 'application/json' + }) + ); + + expect(response.status).toBe(200); + expect(mocks.authGetUser).toHaveBeenCalledWith('access-token'); + expect(mocks.serviceRpc).toHaveBeenCalledWith('delete_encrypted_data_only', { + authenticated_user_id: 'internal-user-id', + target_user_id: 'internal-user-id' + }); + }); + + it('ignores an empty bearer header and falls back to cookies', async () => { + const { DELETE } = await import('./route.js'); + const response = await DELETE( + deleteRequest({ + authorization: 'Bearer ', + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('cookie-token')}`, + 'content-type': 'application/json' + }) + ); + + expect(response.status).toBe(200); + expect(mocks.authGetUser).toHaveBeenCalledWith('cookie-token'); + }); +});