From 3899239f6baac801b4e3d2c60b3d8943b46b7358 Mon Sep 17 00:00:00 2001 From: schererleander Date: Fri, 26 Dec 2025 14:57:38 +0100 Subject: refactor(api): enhance profile image upload security and validation --- src/app/api/user/profile-image/route.ts | 189 ++++++-------------------------- 1 file changed, 31 insertions(+), 158 deletions(-) diff --git a/src/app/api/user/profile-image/route.ts b/src/app/api/user/profile-image/route.ts index aabc337..c620bf0 100644 --- a/src/app/api/user/profile-image/route.ts +++ b/src/app/api/user/profile-image/route.ts @@ -6,194 +6,67 @@ import dbConnect from "@/lib/mongodb" import User from "@/model/User" import { uploadToMinio, deleteFromMinio } from "@/lib/minio" -// Configuration -const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB -const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'] -const OUTPUT_WIDTH = 400 -const OUTPUT_HEIGHT = 400 -const OUTPUT_QUALITY = 80 +const MAX_FILE_SIZE = 5 * 1024 * 1024 // Reduced to 5MB +const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'] export async function POST(request: NextRequest) { try { - // Check authentication const session = await getServerSession(authOptions) - if (!session?.user?.id) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ) - } + if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) - // Parse FormData const formData = await request.formData() const file = formData.get('image') as File - // Validate file exists - if (!file) { - return NextResponse.json( - { error: "No file provided" }, - { status: 400 } - ) - } - - // Validate file type - if (!ALLOWED_TYPES.includes(file.type)) { - return NextResponse.json( - { error: `Invalid file type. Allowed: ${ALLOWED_TYPES.join(', ')}` }, - { status: 400 } - ) - } - - // Validate file size - if (file.size > MAX_FILE_SIZE) { - return NextResponse.json( - { error: `File too large. Maximum size: ${MAX_FILE_SIZE / 1024 / 1024}MB` }, - { status: 400 } - ) + if (!file || !ALLOWED_TYPES.includes(file.type) || file.size > MAX_FILE_SIZE) { + return NextResponse.json({ error: "Invalid file" }, { status: 400 }) } - // Convert File to Buffer const buffer = Buffer.from(await file.arrayBuffer()) + + try { await sharp(buffer).metadata() } + catch { return NextResponse.json({ error: "Invalid image file" }, { status: 400 }) } - // Validate image and get metadata - let imageMetadata - try { - imageMetadata = await sharp(buffer).metadata() - } catch (error) { - return NextResponse.json( - { error: "Invalid image file" }, - { status: 400 } - ) - } - - // Additional validation - if (!imageMetadata.width || !imageMetadata.height) { - return NextResponse.json( - { error: "Unable to read image dimensions" }, - { status: 400 } - ) - } - - // Process image: resize and convert to WebP const processedBuffer = await sharp(buffer) - .resize(OUTPUT_WIDTH, OUTPUT_HEIGHT, { - fit: 'cover', - position: 'center' - }) - .webp({ quality: OUTPUT_QUALITY }) + .resize(400, 400, { fit: 'cover' }) + .webp({ quality: 80 }) .toBuffer() - // Generate unique filename - const timestamp = Date.now() - const filename = `avatar_${timestamp}.webp` - const key = `users/${session.user.id}/profile/${filename}` - - // Connect to database + const key = `users/${session.user.id}/profile/avatar.webp` + await dbConnect() - - // Get current user to check for existing profile image - const currentUser = await User.findById(session.user.id) - if (!currentUser) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ) - } - - // Delete old profile image from MinIO if it exists - if (currentUser.profileImage?.key) { - try { - await deleteFromMinio(currentUser.profileImage.key) - } catch (error) { - console.warn("Failed to delete old profile image:", error) - // Continue with upload even if deletion fails - } - } - - // Upload to MinIO const minioUrl = await uploadToMinio(key, processedBuffer, 'image/webp') - // Update user with new profile image - const updatedUser = await User.findByIdAndUpdate( + await User.findByIdAndUpdate( session.user.id, - { - profileImage: { - url: minioUrl, - key: key, - uploadedAt: new Date() - } - }, - { new: true } + { profileImage: { url: minioUrl, key, uploadedAt: new Date() } } ) - // Return success response - return NextResponse.json({ - message: "Profile image uploaded successfully", - profileImage: { - url: minioUrl, - uploadedAt: new Date() - } - }, { status: 200 }) - + return NextResponse.json({ + message: "Profile image uploaded successfully", + profileImage: { url: minioUrl } + }) } catch (error) { - console.error("Profile image upload error:", error) - - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ) + console.error("Upload error:", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) } } -export async function DELETE(request: NextRequest) { +export async function DELETE() { try { - // Check authentication const session = await getServerSession(authOptions) - if (!session?.user?.id) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ) - } + if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) await dbConnect() - - // Get current user - const currentUser = await User.findById(session.user.id) - if (!currentUser) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ) - } - - // Check if user has profile image - if (!currentUser.profileImage?.key) { - return NextResponse.json( - { error: "No profile image to delete" }, - { status: 400 } - ) + const user = await User.findById(session.user.id) + + if (user?.profileImage?.key) { + await deleteFromMinio(user.profileImage.key) + await User.findByIdAndUpdate(session.user.id, { $unset: { profileImage: 1 } }) } - // Delete from MinIO - await deleteFromMinio(currentUser.profileImage.key) - - // Remove profile image from user document - await User.findByIdAndUpdate( - session.user.id, - { $unset: { profileImage: 1 } } - ) - - return NextResponse.json({ - message: "Profile image deleted successfully" - }, { status: 200 }) - + return NextResponse.json({ message: "Profile image deleted" }) } catch (error) { - console.error("Profile image deletion error:", error) - - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ) + console.error("Delete error:", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) } -} \ No newline at end of file +} -- cgit v1.3.1