Image Upload

This guide explains how to use the image upload system in our application using AWS S3 for storage and pre-signed URLs for secure uploads.

Setup

Prerequisites

  • AWS Account with S3 access
  • AWS S3 bucket created
  • AWS credentials configured

Environment Variables

# AWS Configuration
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AWS_REGION=your_region
S3_UPLOAD_BUCKET=your_bucket_name

Components

Image Upload Component

A reusable component for handling image uploads with preview:

type ImageUploadProps = {
  value: string | null | undefined
  onChange: (url?: string, name?: string) => void
  type?: "profile" | "logo"
  disabled?: boolean
  disableActions?: boolean
}

export function ImageUpload({
  value,
  onChange,
  type = "profile",
  disabled = false,
  disableActions = false,
}: ImageUploadProps) {
  const [isUploading, setIsUploading] = useState<boolean>(false)

  const handleFileDrop = async (file: File) => {
    try {
      setIsUploading(true)
      const data = await getPreSignedUrl(file)
      
      if (!data?.data) {
        return toast.error("Too many requests")
      }

      const uploadSuccessful = await uploadFileToS3(
        data.data.preSignedUrl, 
        file
      )

      if (uploadSuccessful) {
        onChange(data.data.imageUrl, file.name)
      }
    } catch (error) {
      toast.error(error)
    } finally {
      setIsUploading(false)
    }
  }
  
  // ... rest of component
}

Features

Drag & Drop

Support for drag and drop file uploads using react-dropzone

Image Preview

Real-time preview of uploaded images with Next.js Image component

File Validation

Validates file types and shows appropriate error messages

Loading States

Visual feedback during upload process with loading indicators

Upload Process

1. Get Pre-signed URL

Request a pre-signed URL from the server:

async function getPreSignedUrl(file: File) {
  try {
    const res = await fetch("/api/image-upload", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        filename: file.name,
        filetype: file.type,
      }),
    })

    if (!res.ok) {
      throw new Error((await res.json()).message)
    }

    return await res.json()
  } catch (error) {
    throw Error(error?.message)
  }
}

2. Upload to S3

Upload the file directly to S3 using the pre-signed URL:

export const uploadFileToS3 = async (url: string, file: File) => {
  try {
    const buffer = await file.arrayBuffer()

    await new Promise<void>((resolve, reject) => {
      const xhr = new XMLHttpRequest()
      xhr.open("PUT", url, true)
      xhr.setRequestHeader("Content-Type", file.type)
      xhr.setRequestHeader("Cache-Control", "max-age=63072000")

      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          xhr.status >= 200 && xhr.status < 300
            ? resolve()
            : reject()
        }
      }

      xhr.send(buffer)
    })

    return true
  } catch (error) {
    return false
  }
}

API Route

The /api/image-upload route handles the server-side logic for generating pre-signed URLs and managing S3 uploads.

API Route Handler

Server-side implementation for secure image uploads:

export async function POST(req: NextRequest) {
  try {
    // 1. Get request data and authenticate user
    const { filename, filetype } = await req.json()
    const { user } = await getCurrentUser()

    if (!user) {
      return responses.notAuthenticatedResponse()
    }

    // 2. Check rate limit
    const identifier = `ratelimit:image-upload:${user.id}`
    const { success } = await ratelimit.limit(identifier)

    if (!success) {
      return responses.tooManyRequestsResponse()
    }

    // 3. Generate unique filename
    const generatedFilename = getFilename(filename)

    // 4. Configure S3 parameters
    const params = {
      Bucket: process.env.S3_UPLOAD_BUCKET,
      Key: `${user.id}/${generatedFilename}`,
      ContentType: filetype,
      CacheControl: "max-age=63072000",
    }

    // 5. Generate pre-signed URL
    const preSignedUrl = await getSignedUrl(
      awsClient,
      new PutObjectCommand(params),
      { expiresIn: 60 * 60 }
    )

    // 6. Create the final image URL
    const imageUrl = `https://${process.env.S3_UPLOAD_BUCKET}.s3.amazonaws.com/${user.id}/${generatedFilename}`

    return responses.successResponse(
      { preSignedUrl, imageUrl },
      "Image uploaded successfully"
    )
  } catch (error: any) {
    return responses.internalServerErrorResponse(error.message)
  }
}

Key Features

Secure File Names

Generates unique file names using MD5 hashing and timestamps

Rate Limiting

Prevents abuse with Upstash Redis-based rate limiting

User Isolation

Stores files in user-specific S3 directories

Caching Strategy

Implements proper cache control headers

Filename Generation

Secure filename generation to prevent conflicts:

const getFilename = (originalName: string) => {
  const originalExtension = path.extname(originalName)
  const currentTime = new Date().getTime().toString()
  const hash = crypto
    .createHash("md5")
    .update(currentTime)
    .digest("hex")
  return `${hash}${originalExtension}`
}

Rate Limiting Setup

Configure rate limiting with Upstash Redis:

const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(
    RATE_LIMIT_5,
    RATE_LIMIT_1_MINUTE
  ),
})

AWS Configuration

AWS Client Setup

Configure the AWS S3 client:

import { S3Client } from "@aws-sdk/client-s3"

export const awsClient = new S3Client({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
  region: process.env.AWS_REGION,
})

Best Practices

Security

  • Use pre-signed URLs for secure uploads
  • Implement proper file validation
  • Set appropriate CORS policies
  • Use environment variables for credentials

User Experience

  • Show upload progress indicators
  • Provide clear error messages
  • Support drag and drop
  • Preview uploaded images