Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/app/api/profile/update/route.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { NextResponse } from 'next/server';
import { createSupabaseServerClient } from '@/lib/supabase.js';

/**
* @param {string | null} authHeader
* @returns {string | null}
*/
function getBearerToken(authHeader) {
if (typeof authHeader !== 'string') return null;

const match = authHeader.match(/^Bearer\s+(.+)$/i);
const token = match?.[1]?.trim();

return token || null;
}

export async function POST(request, { params } = {}) {
try {
const { bio, website } = await request.json();

// Get authorization header
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
const token = getBearerToken(authHeader);
if (!token) {
return NextResponse.json({ error: 'Missing or invalid authorization header' }, { status: 401 });
}

// Create Supabase client and set session
const supabase = await createSupabaseServerClient();
const token = authHeader.replace('Bearer ', '');

// Set the session to ensure auth.uid() is available for RLS
const { data: { user }, error: authError } = await supabase.auth.getUser(token);
Expand Down Expand Up @@ -103,4 +115,4 @@ export async function POST(request, { params } = {}) {
console.error('Profile update error:', err);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
}
79 changes: 79 additions & 0 deletions src/app/api/profile/update/route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
getUser: vi.fn(),
setSession: vi.fn(),
createSupabaseServerClient: vi.fn(() => ({
auth: {
getUser: mocks.getUser,
setSession: mocks.setSession
},
from: vi.fn(() => ({
update: vi.fn(() => ({
eq: vi.fn(() => ({
select: vi.fn(() => ({
data: [{
id: 'row-1',
username: 'alice',
display_name: 'Alice',
avatar_url: null,
bio: 'hello',
website: null
}],
error: null
}))
}))
}))
}))
}))
}));

vi.mock('@/lib/supabase.js', () => ({
createSupabaseServerClient: mocks.createSupabaseServerClient
}));

function profileRequest(authorization) {
return new Request('https://example.com/api/profile/update', {
method: 'POST',
headers: authorization ? { authorization } : {},
body: JSON.stringify({ bio: 'hello' })
});
}

describe('profile update bearer authentication', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mocks.getUser.mockResolvedValue({
data: { user: { id: 'user-1', email: 'alice@example.com', phone: null } },
error: null
});
});

it('normalizes bearer scheme casing and extra spaces before validating the token', async () => {
const { POST } = await import('./route.js');

const response = await POST(profileRequest('bearer access-token-123 '));
const body = await response.json();

expect(response.status).toBe(200);
expect(body.success).toBe(true);
expect(mocks.getUser).toHaveBeenCalledWith('access-token-123');
expect(mocks.setSession).toHaveBeenCalledWith({
access_token: 'access-token-123',
refresh_token: ''
});
});

it('rejects an empty bearer header before creating a Supabase client', async () => {
const { POST } = await import('./route.js');

const response = await POST(profileRequest('Bearer '));
const body = await response.json();

expect(response.status).toBe(401);
expect(body.error).toBe('Missing or invalid authorization header');
expect(mocks.createSupabaseServerClient).not.toHaveBeenCalled();
expect(mocks.getUser).not.toHaveBeenCalled();
});
});
Loading