From e6401e3975ec6aeb0c4da30cc338345778975800 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sun, 12 Jul 2026 16:09:49 -0600 Subject: [PATCH] Normalize invite bearer token parsing --- src/app/api/auth/invite-anon/route.js | 14 ++- src/app/api/auth/invite-anon/route.test.js | 111 +++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/app/api/auth/invite-anon/route.test.js diff --git a/src/app/api/auth/invite-anon/route.js b/src/app/api/auth/invite-anon/route.js index be62bb68..7fb399cd 100644 --- a/src/app/api/auth/invite-anon/route.js +++ b/src/app/api/auth/invite-anon/route.js @@ -29,6 +29,15 @@ function createServiceClient() { }); } +function getBearerToken(authHeader) { + if (typeof authHeader !== 'string') return null; + + const match = authHeader.match(/^Bearer\s+(.+)$/i); + const token = match?.[1]?.trim(); + + return token || null; +} + /** * GET /api/auth/invite-anon — report remaining quota for the caller. * @param {import('next/server').NextRequest} request @@ -134,10 +143,11 @@ export async function POST(request) { */ async function authenticate(request) { const authHeader = request.headers.get('authorization'); - if (!authHeader) { + const token = getBearerToken(authHeader); + + if (!token) { return { error: NextResponse.json({ error: 'Missing authorization header' }, { status: 401 }) }; } - const token = authHeader.replace('Bearer ', ''); const supabase = await createSupabaseServerClient(); const { data: { user: authUser }, diff --git a/src/app/api/auth/invite-anon/route.test.js b/src/app/api/auth/invite-anon/route.test.js new file mode 100644 index 00000000..20341159 --- /dev/null +++ b/src/app/api/auth/invite-anon/route.test.js @@ -0,0 +1,111 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + createSupabaseServerClient: vi.fn(), + createClient: vi.fn(), + from: vi.fn() +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: mocks.createSupabaseServerClient +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: mocks.createClient +})); + +vi.mock('@/lib/auth/coinpay.js', () => ({ + getAppOrigin: (origin) => origin +})); + +vi.mock('@/lib/invites/mint.js', () => ({ + mintInviteToken: () => ({ + token: 'qci1.test-token', + payload: { jti: 'invite-jti', exp: 1893456000 } + }) +})); + +function inviteRequest(authorization) { + return new Request('https://qrypt.chat/api/auth/invite-anon', { + method: 'GET', + headers: authorization ? { authorization } : {} + }); +} + +function createSingleQuery(result) { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + single: vi.fn().mockResolvedValue(result) + }; + return query; +} + +function createCountQuery(result) { + const query = { + select: vi.fn(() => query), + eq: vi.fn().mockResolvedValue(result) + }; + return query; +} + +describe('GET /api/auth/invite-anon authentication', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + mocks.createSupabaseServerClient.mockResolvedValue({ + auth: { + getUser: mocks.authGetUser + } + }); + mocks.createClient.mockReturnValue({ + from: mocks.from + }); + mocks.from.mockImplementation((table) => { + if (table === 'invite_issuers') { + return createSingleQuery({ + data: { default_quota: 5, disabled: false }, + error: null + }); + } + + if (table === 'issued_invites') { + return createCountQuery({ + count: 1, + error: null + }); + } + + throw new Error(`Unexpected table: ${table}`); + }); + }); + + it('normalizes bearer scheme casing and extra spaces before validating the token', async () => { + const { GET } = await import('./route.js'); + + const response = await GET(inviteRequest('bearer access-token-123 ')); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ remaining: 4, quota: 5, used: 1 }); + expect(mocks.authGetUser).toHaveBeenCalledWith('access-token-123'); + }); + + it('rejects empty bearer headers before Supabase token validation', async () => { + const { GET } = await import('./route.js'); + + const response = await GET(inviteRequest('Bearer ')); + const body = await response.json(); + + expect(response.status).toBe(401); + expect(body).toEqual({ error: 'Missing authorization header' }); + expect(mocks.authGetUser).not.toHaveBeenCalled(); + expect(mocks.createClient).not.toHaveBeenCalled(); + }); +});