Skip to content

Conversation

@varshapanda
Copy link
Collaborator

@varshapanda varshapanda commented Nov 22, 2025

User description

Problem

Volunteers were unable to view report images in the verify and history pages. Images failed to load with "Failed to load image" errors because the stored S3 URLs pointed to a private bucket that blocked direct browser access.

Solution

Implemented presigned URL generation for image reading/viewing:

  • Added generateReadPresignedUrl() function to generate temporary signed URLs for GET requests
  • Updated volunteer pending reports API to generate presigned URLs before returning data
  • Updated volunteer history API to generate presigned URLs before returning data
  • Changed image queries to fetch complete image data instead of limiting to first image

Changes Made

Modified Files

  1. src/lib/s3Client.ts

    • Added GetObjectCommand import from AWS SDK
    • Added generateReadPresignedUrl() function to create temporary signed URLs (valid 1 hour)
  2. src/app/api/volunteer/reports/pending/route.ts

    • Imported generateReadPresignedUrl function
    • Changed images: { take: 1 } to images: true
    • Added presigned URL generation for both imageUrl and images array
    • Return signed URLs in API response
  3. src/app/api/volunteer/history/route.ts

    • Same changes as pending route
    • Generate presigned URLs for historical reports

How to Test

Prerequisites

  • Have AWS credentials configured in .env
  • Have at least one pending report with an image in the database
  • Be logged in as a volunteer user

Test Steps

1. Test Pending Reports (Verify Page)

# Start the development server
npm run dev

# Navigate to volunteer verify page
http://localhost:3000/dashboard/volunteer/verify

Expected Results:

  • Report images should display correctly
  • No "Failed to load image" errors
  • Images should be visible in both the list view and modal

To Verify in Console:

  1. Open browser DevTools (F12) → Console tab
  2. Look for logs: Full API Response:
  3. Check that image URLs contain X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature query parameters (presigned URL markers)
  4. No Image failed to load errors should appear

2. Test History Page

# Navigate to volunteer history page
http://localhost:3000/dashboard/volunteer/history

Expected Results:

  • Previously verified/rejected report images should display
  • Switch between "Verified" and "Rejected" tabs - images load in both
  • No image loading errors

Expected Results:

  • Old presigned URLs expire
  • New presigned URLs are generated automatically
  • Images continue to load correctly

4. Browser Network Tab Verification

  1. Open DevTools → Network tab
  2. Filter by "Img" or "Media"
  3. Click on an image request

Expected Results:

  • Status: 200 OK
  • URL contains AWS signature parameters
  • Response headers show S3 origin

Security Considerations

  • Presigned URLs expire after 1 hour for security
  • S3 bucket remains private - no public access configured
  • Each API request generates fresh signed URLs
  • URLs are unique per request and cannot be reused indefinitely

Related Issues

Fixes #43


PR Type

Bug fix, Enhancement


Description

  • Enable image viewing in volunteer dashboard using presigned URLs

  • Add generateReadPresignedUrl() function for secure S3 image access

  • Update pending and history APIs to generate presigned URLs for all images

  • Fetch complete image data instead of limiting to first image

  • Improve report creation to explicitly create image records in database


Diagram Walkthrough

flowchart LR
  A["S3 Private Bucket"] -->|"generateReadPresignedUrl()"| B["Presigned URL<br/>1 hour expiry"]
  B -->|"GET request"| C["Volunteer Dashboard"]
  D["Report Creation"] -->|"Create image record"| E["Database"]
  E -->|"Fetch with images"| F["Pending/History APIs"]
  F -->|"Generate signed URLs"| C
Loading

File Walkthrough

Relevant files
Enhancement
s3Client.ts
Add presigned URL generation for image reading                     

src/lib/s3Client.ts

  • Added GetObjectCommand import from AWS SDK
  • Implemented generateReadPresignedUrl() function for generating
    temporary signed URLs
  • Function extracts S3 key from image URL and creates presigned GET
    request
  • URLs expire in 1 hour (3600 seconds) for security
  • Includes error handling that falls back to original URL on failure
+27/-1   
route.ts
Improve report creation with explicit image records           

src/app/api/reports/create/route.ts

  • Explicitly create image record in database after report creation
  • Fetch complete report with all images and reporter details
  • Return full report object instead of minimal report data
  • Improved reliability by separating image record creation from report
    creation
+26/-2   
Bug fix
route.ts
Generate presigned URLs for pending report images               

src/app/api/volunteer/reports/pending/route.ts

  • Imported generateReadPresignedUrl function from s3Client
  • Changed image query from take: 1 to true to fetch all images
  • Added presigned URL generation for both imageUrl and images array
  • Map over reports to generate signed URLs for each image before
    returning
  • Return reports with presigned URLs in API response
+17/-4   
route.ts
Generate presigned URLs for historical report images         

src/app/api/volunteer/history/route.ts

  • Imported generateReadPresignedUrl function from s3Client
  • Changed image query from take: 1 to true to fetch all images
  • Added presigned URL generation for both imageUrl and images array
  • Map over reports to generate signed URLs for each image before
    returning
  • Return reports with presigned URLs in API response
+17/-4   

- Add generateReadPresignedUrl() function to s3Client for reading images
- Update pending reports API to generate presigned GET URLs for images
- Update history API to generate presigned GET URLs for images
- Change images query from take:1 to true for complete image data

Previously, volunteers couldn't view report images because the stored
S3 URLs required public bucket access. Now, we generate temporary
presigned URLs (valid 1 hour) for all image requests, allowing secure
access to images in private S3 buckets.
@gemini-code-assist
Copy link

Summary of Changes

Hello @varshapanda, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a critical issue where volunteers were unable to view report images due to direct access attempts to a private S3 bucket. By introducing presigned URLs, the system now securely generates temporary access links for images, allowing them to be displayed correctly in the volunteer dashboard. This enhancement significantly improves the user experience by providing complete visual information for reports while maintaining the security of the stored data.

Highlights

  • Image Viewing Enabled: Volunteers can now successfully view report images in both the verify and history pages of the dashboard, resolving previous 'Failed to load image' errors.
  • Secure Image Access with Presigned URLs: Implemented a new utility function, generateReadPresignedUrl(), to create temporary, secure URLs for accessing images stored in a private S3 bucket, ensuring data security.
  • API Integration for Image Retrieval: The volunteer pending reports and history APIs have been updated to utilize the new presigned URL generation, returning secure links for all associated report images.
  • Full Image Data Retrieval: Image queries within the volunteer APIs now fetch all images related to a report, rather than being limited to just the first image, providing comprehensive visual context.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-merge-for-open-source
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Silent error fallback

Description: The generateReadPresignedUrl() function silently falls back to returning the original
private S3 URL when presigned URL generation fails, potentially exposing inaccessible URLs
to clients and masking critical AWS configuration errors.
s3Client.ts [37-57]

Referred Code
export async function generateReadPresignedUrl(
  imageUrl: string,
  expiresIn: number = 3600
): Promise<string> {
  try {
    // Extract key from S3 URL
    const url = new URL(imageUrl);
    const key = url.pathname.substring(1);

    const command = new GetObjectCommand({
      Bucket: process.env.AWS_BUCKET_NAME!,
      Key: key,
    });

    const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
    return signedUrl;
  } catch (error) {
    console.error("Error generating read presigned URL:", error);
    return imageUrl;
  }
}
Ticket Compliance
🟡
🎫 #43
🟢 Fix "Failed to load image" errors when accessing S3 images
Enable secure access to private S3 bucket images
Images must display correctly in volunteer verify page
Images must display correctly in volunteer history page
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Error exposure risk: The console.error at line 54 may expose sensitive S3 configuration details or internal
error information that should only be in secure logs.

Referred Code
console.error("Error generating read presigned URL:", error);
return imageUrl;

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing error handling: The presigned URL generation in Promise.all lacks error handling for individual URL
generation failures, which could cause the entire request to fail.

Referred Code
const reportsWithSignedUrls = await Promise.all(
  reports.map(async (report) => ({
    ...report,
    imageUrl: await generateReadPresignedUrl(report.imageUrl),
    images: await Promise.all(
      report.images.map(async (img) => ({
        ...img,
        url: await generateReadPresignedUrl(img.url),
      }))
    ),
  }))
);

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured error logging: The error logging uses console.error with raw error object which may contain sensitive S3
keys or URLs and is not structured for proper auditing.

Referred Code
console.error("Error generating read presigned URL:", error);
return imageUrl;

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Missing URL validation: The imageUrl parameter in generateReadPresignedUrl is not validated before parsing, which
could lead to URL parsing errors or potential injection if malicious URLs are provided.

Referred Code
export async function generateReadPresignedUrl(
  imageUrl: string,
  expiresIn: number = 3600
): Promise<string> {
  try {
    // Extract key from S3 URL
    const url = new URL(imageUrl);
    const key = url.pathname.substring(1);

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-merge-for-open-source
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Use a transaction for database writes

Wrap the report and image creation operations within a prisma.$transaction to
ensure they either both succeed or both fail, preventing data inconsistency.

src/app/api/reports/create/route.ts [40-61]

-// Create report WITH image record in a transaction
-const report = await prisma.report.create({
-  data: {
-    reporterId: String(user.id),
-    imageUrl: validated.imageUrl,
-    imageHash: validated.imageHash,
-    category: validated.category,
-    note: validated.note || null,
-    lat: validated.lat,
-    lng: validated.lng,
-    status: "PENDING",
-  },
+// Create report and image record in a transaction
+const report = await prisma.$transaction(async (tx) => {
+  const newReport = await tx.report.create({
+    data: {
+      reporterId: String(user.id),
+      imageUrl: validated.imageUrl,
+      imageHash: validated.imageHash,
+      category: validated.category,
+      note: validated.note || null,
+      lat: validated.lat,
+      lng: validated.lng,
+      status: "PENDING",
+    },
+  });
+
+  await tx.image.create({
+    data: {
+      url: validated.imageUrl,
+      reportId: newReport.id,
+      uploadedBy: String(user.id),
+    },
+  });
+
+  return newReport;
 });
 
-// Create the image record separately (more reliable)
-await prisma.image.create({
-  data: {
-    url: validated.imageUrl,
-    reportId: report.id,
-    uploadedBy: String(user.id),
-  },
-});
-
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a data integrity issue where a partial failure could lead to orphaned report records and proposes using a database transaction to ensure atomicity, which is a critical fix.

Medium
General
Add guard clause for invalid URLs

Add a guard clause at the beginning of the generateReadPresignedUrl function to
check for and return early if imageUrl is falsy, preventing unnecessary parsing
attempts and error logging.

src/lib/s3Client.ts [37-57]

 export async function generateReadPresignedUrl(
   imageUrl: string,
   expiresIn: number = 3600
 ): Promise<string> {
+  if (!imageUrl) {
+    return imageUrl;
+  }
+
   try {
     // Extract key from S3 URL
     const url = new URL(imageUrl);
     const key = url.pathname.substring(1);
 
     const command = new GetObjectCommand({
       Bucket: process.env.AWS_BUCKET_NAME!,
       Key: key,
     });
 
     const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
     return signedUrl;
   } catch (error) {
-    console.error("Error generating read presigned URL:", error);
+    console.error("Error generating read presigned URL for:", imageUrl, error);
     return imageUrl;
   }
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion improves the function's robustness by adding a guard clause to handle falsy imageUrl values, preventing unnecessary try-catch block execution and error logging for expected invalid inputs.

Low
Avoid redundant presigned URL generation

To improve efficiency, generate presigned URLs only for the images array and
then reuse the first image's URL for report.imageUrl, thus avoiding a redundant
API call.

src/app/api/volunteer/history/route.ts [59-70]

 const reportsWithSignedUrls = await Promise.all(
-  reports.map(async (report) => ({
-    ...report,
-    imageUrl: await generateReadPresignedUrl(report.imageUrl),
-    images: await Promise.all(
+  reports.map(async (report) => {
+    const presignedImages = await Promise.all(
       report.images.map(async (img) => ({
         ...img,
         url: await generateReadPresignedUrl(img.url),
       }))
-    ),
-  }))
+    );
+
+    return {
+      ...report,
+      // Assign the presigned URL from the first image to the report's imageUrl
+      imageUrl: presignedImages.length > 0 ? presignedImages[0].url : report.imageUrl,
+      images: presignedImages,
+    };
+  })
 );

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies and removes a redundant generateReadPresignedUrl call, which improves performance by avoiding an unnecessary asynchronous operation for each report.

Low
  • More

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully addresses the issue of volunteers being unable to view images by implementing presigned URLs for S3 objects. The changes are well-structured, introducing a new utility function for generating read URLs and integrating it into the pending and history API routes for volunteers. My review focuses on improving robustness, data consistency, and efficiency. I've identified a couple of high-severity issues. In src/app/api/reports/create/route.ts, the report and image creation are not performed in a transaction, which could lead to data inconsistency. I've suggested using a Prisma nested write to make this operation atomic and more efficient. In src/lib/s3Client.ts, the use of a non-null assertion (!) for an environment variable could cause runtime crashes if the variable is not set. I've recommended adding explicit validation. Additionally, I've noted a medium-severity issue in the volunteer API routes where presigned URLs are being generated redundantly for the same image. Overall, this is a solid contribution that fixes a key bug. Addressing the feedback will make the implementation more resilient and correct.

Comment on lines +46 to +49
const command = new GetObjectCommand({
Bucket: process.env.AWS_BUCKET_NAME!,
Key: key,
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using the non-null assertion operator (!) on environment variables like process.env.AWS_BUCKET_NAME! can introduce runtime errors if the variable is not defined. It's safer to validate the environment variable's existence and handle its absence gracefully by throwing an error within the try...catch block.

    const bucketName = process.env.AWS_BUCKET_NAME;
    if (!bucketName) {
      throw new Error("AWS_BUCKET_NAME environment variable is not set.");
    }

    const command = new GetObjectCommand({
      Bucket: bucketName,
      Key: key,
    });

Comment on lines +59 to +69
const reportsWithSignedUrls = await Promise.all(
reports.map(async (report) => ({
...report,
imageUrl: await generateReadPresignedUrl(report.imageUrl),
images: await Promise.all(
report.images.map(async (img) => ({
...img,
url: await generateReadPresignedUrl(img.url),
}))
),
}))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block generates a presigned URL for report.imageUrl and then again for each image in report.images. Since report.imageUrl is also present in the report.images array (based on the creation logic), you are generating a presigned URL for the same image twice for every report. This is redundant. Consider refactoring to avoid the duplicate signing operation, for example by signing all images in the images array and then finding the corresponding new URL for imageUrl from that array.

Comment on lines +56 to +66
const reportsWithSignedUrls = await Promise.all(
reports.map(async (report) => ({
...report,
imageUrl: await generateReadPresignedUrl(report.imageUrl),
images: await Promise.all(
report.images.map(async (img) => ({
...img,
url: await generateReadPresignedUrl(img.url),
}))
),
}))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block generates a presigned URL for report.imageUrl and then again for each image in report.images. Since report.imageUrl is also present in the report.images array (based on the creation logic), you are generating a presigned URL for the same image twice for every report. This is redundant. Consider refactoring to avoid the duplicate signing operation, for example by signing all images in the images array and then finding the corresponding new URL for imageUrl from that array.

@varshapanda varshapanda merged commit 60146e8 into main Nov 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Images not displaying in volunteer verify/history pages

3 participants