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
28 changes: 26 additions & 2 deletions src/app/api/reports/create/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
}
}

// Create report
// Create report WITH image record in a transaction
const report = await prisma.report.create({
data: {
reporterId: String(user.id),
Expand All @@ -51,10 +51,34 @@ export async function POST(req: NextRequest) {
},
});

// Create the image record separately (more reliable)
await prisma.image.create({
data: {
url: validated.imageUrl,
reportId: report.id,
uploadedBy: String(user.id),
},
});

// Fetch the complete report with images
const completeReport = await prisma.report.findUnique({
where: { id: report.id },
include: {
images: true,
reporter: {
select: {
id: true,
name: true,
email: true,
},
},
},
});

// TODO: Enqueue verification job (BullMQ)
// await verificationQueue.add("verify-report", { reportId: report.id });

return sendSuccess(report, "Report created successfully", 201);
return sendSuccess(completeReport, "Report created successfully", 201);
} catch (error: unknown) {
if (error instanceof ZodError) {
return sendError(
Expand Down
21 changes: 17 additions & 4 deletions src/app/api/volunteer/history/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { verifyToken } from "@/lib/auth";
import { pendingReportsQuerySchema } from "@/lib/validation/volunteerSchemas";
import { ERROR_CODES } from "@/lib/errorCodes";
import { ZodError } from "zod";
import { generateReadPresignedUrl } from "@/lib/s3Client";

export async function GET(req: NextRequest) {
try {
Expand Down Expand Up @@ -45,9 +46,7 @@ export async function GET(req: NextRequest) {
name: true,
},
},
images: {
take: 1,
},
images: true, // CHANGED from take: 1
},
orderBy: {
verifiedAt: "desc",
Expand All @@ -56,6 +55,20 @@ export async function GET(req: NextRequest) {
take: validatedQuery.limit,
});

// ADDED: Generate presigned URLs
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),
}))
),
}))
Comment on lines +59 to +69

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.

);

const totalCount = await prisma.report.count({
where: {
verifiedBy: String(user.id),
Expand All @@ -65,7 +78,7 @@ export async function GET(req: NextRequest) {

return sendSuccess(
{
reports,
reports: reportsWithSignedUrls, // CHANGED from reports
pagination: {
page: validatedQuery.page,
limit: validatedQuery.limit,
Expand Down
21 changes: 17 additions & 4 deletions src/app/api/volunteer/reports/pending/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { verifyToken } from "@/lib/auth";
import { pendingReportsQuerySchema } from "@/lib/validation/volunteerSchemas";
import { ERROR_CODES } from "@/lib/errorCodes";
import { ZodError } from "zod";
import { generateReadPresignedUrl } from "@/lib/s3Client"; // ADDED

export async function GET(req: NextRequest) {
try {
Expand Down Expand Up @@ -42,9 +43,7 @@ export async function GET(req: NextRequest) {
name: true,
},
},
images: {
take: 1,
},
images: true, // CHANGED from take: 1
},
orderBy: {
createdAt: "asc",
Expand All @@ -53,13 +52,27 @@ export async function GET(req: NextRequest) {
take: validatedQuery.limit,
});

// ADDED: Generate presigned URLs
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),
}))
),
}))
Comment on lines +56 to +66

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.

);

const totalCount = await prisma.report.count({
where: { status: "PENDING" },
});

return sendSuccess(
{
reports,
reports: reportsWithSignedUrls, // CHANGED from reports
pagination: {
page: validatedQuery.page,
limit: validatedQuery.limit,
Expand Down
28 changes: 27 additions & 1 deletion src/lib/s3Client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import {
S3Client,
PutObjectCommand,
GetObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3Client = new S3Client({
Expand Down Expand Up @@ -29,3 +33,25 @@ export async function generatePresignedUrl(
export function getPublicS3Url(filename: string): string {
return `${process.env.NEXT_PUBLIC_S3_URL}/${filename}`;
}

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,
});
Comment on lines +46 to +49

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,
    });


const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
return signedUrl;
} catch (error) {
console.error("Error generating read presigned URL:", error);
return imageUrl;
}
}