Skip to content
Open
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
131 changes: 45 additions & 86 deletions src/app/api/auth/upload-avatar/route.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,40 @@
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

/**
* Validate a Bearer token by verifying its signature via Supabase Auth,
* not just decoding the payload.
*/
async function getAuthenticatedUser(request) {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return { user: null, error: 'Authentication required' };
}

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

// Use a Supabase client with the anon key to verify the JWT signature
const authClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
const { data: { user }, error } = await authClient.auth.getUser(token);

if (error || !user) {
return { user: null, error: 'Invalid authentication token' };
}

return { user, error: null };
}

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 { user, error: authError } = await getAuthenticatedUser(request);
if (authError) {
return NextResponse.json({ error: authError }, { 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 });
}

// Create service role client for database operations
// Create service role client for storage and database operations
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);

const formData = await request.formData();
Expand All @@ -54,15 +47,15 @@ export async function POST(request) {
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
return NextResponse.json({
error: 'Invalid file type. Please upload JPEG, PNG, WebP, or GIF images only.'
return NextResponse.json({
error: 'Invalid file type. Please upload JPEG, PNG, WebP, or GIF images only.'
}, { status: 400 });
}

// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
return NextResponse.json({
error: 'File size too large. Please upload files smaller than 5MB.'
return NextResponse.json({
error: 'File size too large. Please upload files smaller than 5MB.'
}, { status: 400 });
}

Expand Down Expand Up @@ -92,7 +85,7 @@ export async function POST(request) {
.from('avatars')
.getPublicUrl(fileName);

// Update user's avatar_url in database using authenticated client
// Update user's avatar_url in database
const { error: updateError } = await supabase
.from('users')
.update({ avatar_url: publicUrl })
Expand All @@ -105,8 +98,8 @@ export async function POST(request) {
return NextResponse.json({ error: 'Failed to update user profile' }, { status: 500 });
}

return NextResponse.json({
success: true,
return NextResponse.json({
success: true,
avatarUrl: publicUrl,
message: 'Avatar uploaded successfully'
});
Expand All @@ -119,49 +112,15 @@ 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 { user, error: authError } = await getAuthenticatedUser(request);
if (authError) {
return NextResponse.json({ error: authError }, { 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 });
}

// Create service role client for storage and database operations
// Create service role client for database operations
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);

// Update user's avatar_url to null in database using authenticated client
// Update user's avatar_url to null in database
const { error: updateError } = await supabase
.from('users')
.update({ avatar_url: null })
Expand All @@ -175,7 +134,7 @@ export async function DELETE(request) {
// Note: We're not deleting the file from storage to avoid issues with caching
// The file will remain in storage but won't be referenced by the user

return NextResponse.json({
return NextResponse.json({
success: true,
message: 'Avatar removed successfully'
});
Expand All @@ -184,4 +143,4 @@ export async function DELETE(request) {
console.error('Avatar removal error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
}
Loading