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
103 changes: 27 additions & 76 deletions src/app/api/auth/upload-avatar/route.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,29 @@
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

const supabaseAuth = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);

async function authenticateBearerToken(request) {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return { error: 'Authentication required' };
}

const token = authHeader.substring(7);
const { data: { user }, error } = await supabaseAuth.auth.getUser(token);

if (error || !user?.id) {
return { error: 'Invalid authentication token' };
}

return { user };
}

export async function POST(request) {
try {
// Get the authorization header
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}

const token = authHeader.replace('Bearer ', '');

// Decode JWT token to get user ID (simple validation)
let user;
try {
// JWT tokens have 3 parts separated by dots: header.payload.signature
const tokenParts = token.split('.');
if (tokenParts.length !== 3) {
throw new Error('Invalid token format');
}

// Decode the payload (second part)
const payload = JSON.parse(atob(tokenParts[1]));

// Check if token is expired
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('Token expired');
}

// Extract user info from payload
user = {
id: payload.sub,
email: payload.email
};

if (!user.id) {
throw new Error('Invalid token payload');
}
} catch (error) {
console.error('Token validation error:', error);
return NextResponse.json({ error: 'Invalid authentication token' }, { status: 401 });
const { user, error: authError } = await authenticateBearerToken(request);
if (authError || !user) {
return NextResponse.json({ error: authError }, { status: 401 });
}

// Create service role client for database operations
Expand Down Expand Up @@ -74,7 +59,7 @@ export async function POST(request) {
const fileBuffer = await file.arrayBuffer();

// Upload to Supabase Storage
const { data: uploadData, error: uploadError } = await supabase.storage
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(fileName, fileBuffer, {
contentType: file.type,
Expand Down Expand Up @@ -119,43 +104,9 @@ export async function POST(request) {

export async function DELETE(request) {
try {
// Get the authorization header
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}

const token = authHeader.replace('Bearer ', '');

// Decode JWT token to get user ID (simple validation)
let user;
try {
// JWT tokens have 3 parts separated by dots: header.payload.signature
const tokenParts = token.split('.');
if (tokenParts.length !== 3) {
throw new Error('Invalid token format');
}

// Decode the payload (second part)
const payload = JSON.parse(atob(tokenParts[1]));

// Check if token is expired
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('Token expired');
}

// Extract user info from payload
user = {
id: payload.sub,
email: payload.email
};

if (!user.id) {
throw new Error('Invalid token payload');
}
} catch (error) {
console.error('Token validation error:', error);
return NextResponse.json({ error: 'Invalid authentication token' }, { status: 401 });
const { user, error: authError } = await authenticateBearerToken(request);
if (authError || !user) {
return NextResponse.json({ error: authError }, { status: 401 });
}

// Create service role client for storage and database operations
Expand Down Expand Up @@ -184,4 +135,4 @@ export async function DELETE(request) {
console.error('Avatar removal error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
}
65 changes: 65 additions & 0 deletions src/app/api/auth/upload-avatar/route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
authGetUser: vi.fn(),
createClient: vi.fn(() => ({
auth: {
getUser: mocks.authGetUser
}
}))
}));

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

const forgedToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ2aWN0aW0tdXNlciJ9.fake-signature';

function avatarRequest(method) {
return new Request('https://example.com/api/auth/upload-avatar', {
method,
headers: {
authorization: `Bearer ${forgedToken}`
}
});
}

describe('upload-avatar 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';
process.env.SUPABASE_SERVICE_ROLE_KEY = 'service-role-key';
mocks.authGetUser.mockResolvedValue({
data: { user: null },
error: { message: 'invalid signature' }
});
});

it('rejects forged bearer tokens before POST storage/database work', async () => {
const { POST } = await import('./route.js');

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

expect(response.status).toBe(401);
expect(body.error).toBe('Invalid authentication token');
expect(mocks.authGetUser).toHaveBeenCalledWith(forgedToken);
expect(mocks.createClient).toHaveBeenCalledTimes(1);
expect(mocks.createClient).toHaveBeenCalledWith('https://example.supabase.co', 'anon-key');
});

it('rejects forged bearer tokens before DELETE database work', async () => {
const { DELETE } = await import('./route.js');

const response = await DELETE(avatarRequest('DELETE'));
const body = await response.json();

expect(response.status).toBe(401);
expect(body.error).toBe('Invalid authentication token');
expect(mocks.authGetUser).toHaveBeenCalledWith(forgedToken);
expect(mocks.createClient).toHaveBeenCalledTimes(1);
expect(mocks.createClient).toHaveBeenCalledWith('https://example.supabase.co', 'anon-key');
});
});
Loading