Skip to content
Open
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
103 changes: 103 additions & 0 deletions app/api/projects/[id]/decisions/[decisionId]/export/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import {
formatDecision,
ExportFormat,
DecisionExportData,
} from "@/components/DecisionExportDialog";
Comment on lines +3 to +7

Choose a reason for hiding this comment

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

P1 Badge Move export formatter out of the client component

This route imports formatDecision from components/DecisionExportDialog.tsx, but that module is marked with "use client" and pulls in client-only React/UI dependencies. Calling a client export from a server route handler is not safe in Next’s server runtime, so /api/projects/[id]/decisions/[decisionId]/export can fail when invoked. The formatter should live in a server-safe shared module (and be imported by both the dialog and the route) instead of importing from a client component file.

Useful? React with πŸ‘Β / πŸ‘Ž.


export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string; decisionId: string }> }
) {
try {
const { id: projectId, decisionId } = await params;
const { searchParams } = new URL(request.url);
const format = (searchParams.get("format") as ExportFormat) || "markdown";

// Validate format
const validFormats = [
"cursor",
"linear",
"jira",
"slack",
"notion",
"markdown",
"json",
];
if (!validFormats.includes(format)) {
return NextResponse.json(
{ error: `Invalid format. Valid formats: ${validFormats.join(", ")}` },
{ status: 400 }
);
}

// Fetch the decision
const decision = await prisma.decision.findUnique({
where: { id: decisionId },
include: {
project: {
select: { name: true },
},
},
});

if (!decision) {
return NextResponse.json({ error: "Decision not found" }, { status: 404 });
}

// Verify the decision belongs to the project
if (decision.projectId !== projectId) {
return NextResponse.json(
{ error: "Decision does not belong to this project" },
{ status: 403 }
);
}

// Build export data
const exportData: DecisionExportData = {
id: decision.id,
title: decision.title,
summary: decision.summary,
scope: decision.scope,
nonGoals: decision.nonGoals,
acceptanceCriteria: decision.acceptanceCriteria,
risks: decision.risks,
confidenceScore: decision.confidenceScore,
linkedFeedbackIds: decision.linkedFeedbackIds,
projectName: decision.project.name,
};

// Format the decision
const content = formatDecision(exportData, format);

// Return appropriate response based on format
if (format === "json") {
return NextResponse.json(JSON.parse(content), {
headers: {
"Content-Type": "application/json",
},
});
}

// For text formats, return plain text with appropriate headers
const contentType =
format === "markdown" ? "text/markdown" : "text/plain";
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
Comment on lines +86 to +88
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Insufficient filename sanitization β€” special characters in title can break Content-Disposition header.

decision.title may contain characters like ", \, /, or non-ASCII that the whitespace-only regex won't strip. This can produce a malformed or exploitable Content-Disposition header.

πŸ›‘οΈ Proposed fix: sanitize the title more thoroughly
-    const filename = `decision-${decision.title
-      .toLowerCase()
-      .replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
+    const safeTitle = decision.title
+      .toLowerCase()
+      .replace(/[^a-z0-9]+/g, "-")
+      .replace(/^-|-$/g, "")
+      .slice(0, 100);
+    const extension = format === "markdown" ? "md" : "txt";
+    const filename = `decision-${safeTitle}.${extension}`;

Note: the current extension logic also produces non-standard extensions like .cursor, .linear, etc. Consider using .txt for all non-markdown text formats.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
const safeTitle = decision.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 100);
const extension = format === "markdown" ? "md" : "txt";
const filename = `decision-${safeTitle}.${extension}`;
πŸ€– Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts around lines 86
- 88, The generated filename built in the filename constant uses raw
decision.title and only strips whitespace, which can introduce quotes, slashes,
backslashes, and other characters that break the Content-Disposition header;
update the logic that builds filename (the filename const) to: sanitize
decision.title by removing or replacing all characters except ASCII letters,
numbers, hyphen and underscore (collapse runs and trim, and cap length to e.g.
100 chars), percent-encode or provide a RFC5987-safe fallback when setting
headers, and normalize extensions so format === "markdown" => ".md" and all
other formats use ".txt" (avoid producing non-standard extensions like
.cursor/.linear). Ensure you reference and change the filename construction and
the code that sets the Content-Disposition header to use the sanitized/encoded
filename.


return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
});
} catch (error) {
console.error("Error exporting decision:", error);
return NextResponse.json(
{ error: "Failed to export decision" },
{ status: 500 }
);
}
}
Comment on lines +9 to +103
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's auth middleware protecting API routes
fd "middleware" --type f --extension ts --extension js
echo "---AUTH_PATTERNS---"
# Check for auth patterns in existing API routes
rg -n "getServerSession|auth|currentUser|requireAuth" --type ts -g 'app/api/**'
echo "---NEXTAUTH_CONFIG---"
# Check for NextAuth or auth configuration
fd -e "route.ts" -e "route.js" -e "auth.ts" -e "auth.js" | head -20

Repository: Fresh-Industries/pm-analyzer

Length of output: 566


🏁 Script executed:

#!/bin/bash
# Search for middleware at root level
echo "---MIDDLEWARE_FILES---"
find . -maxdepth 2 -name "middleware.*" -type f
echo "---EXPORT_ROUTE_CONTENT---"
# Check the export route for any auth imports
head -15 app/api/projects/\[id\]/decisions/\[decisionId\]/export/route.ts
echo "---AUTH_LIB_FILES---"
# Check auth library files
ls -la lib/auth* 2>/dev/null || echo "No auth files in lib/"
echo "---OTHER_API_ROUTES---"
# Check a few other API routes to see auth patterns
find app/api -name "route.ts" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -20 {}'

Repository: Fresh-Industries/pm-analyzer

Length of output: 3688


🏁 Script executed:

#!/bin/bash
# Check if any routes import auth utilities
echo "---ROUTES_USING_AUTH---"
rg -l "from.*auth|import.*auth" app/api --type ts | grep -v "\[...all\]" | head -10
echo "---CHECK_AUTH_SERVER_USAGE---"
rg "getSession|auth\(\)" app/api --type ts | head -20
echo "---LIB_AUTH_FILES_CONTENT---"
echo "=== lib/auth.ts ==="
cat lib/auth.ts
echo "=== lib/auth-server.ts ==="
cat lib/auth-server.ts

Repository: Fresh-Industries/pm-analyzer

Length of output: 866


🏁 Script executed:

#!/bin/bash
# Check projects route in detail since it imports auth-client
echo "---PROJECTS_ROUTE_CONTENT---"
cat app/api/projects/route.ts
echo "---CHECK_IF_OTHER_ROUTES_USE_AUTH_CHECKS---"
rg "getServerSession|getSession" app/api --type ts -A 3 | head -30

Repository: Fresh-Industries/pm-analyzer

Length of output: 1470


Add authentication check to the export endpoint.

This route lacks user authentication and is publicly accessible to anyone who can guess a valid projectId and decisionId. The current project-ownership check (decision.projectId === projectId) verifies data consistency but does not authenticate the requesting user. Although the app has a getServerSession() function available in lib/auth-server.ts via better-auth, it is not used here. Add a session check to verify the user is authenticated and authorized to access this decision, similar to what should be done across other API routes that currently lack authentication.

πŸ€– Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts around lines 9
- 103, Add an authentication and authorization check at the start of the
exported GET handler: call getServerSession() from lib/auth-server.ts and if it
returns no session return a 401 JSON response; then verify the signed-in user is
allowed to access the decision by ensuring they belong to the project (e.g.
query the project using prisma (prisma.project.findFirst or findUnique with
membership/owner check) and confirm project.id === projectId and that
session.user.id is an owner/member), and only proceed to load the Decision (or
return 403) if that authorization check passes; keep the existing
decision.projectId === projectId validation as a secondary data check.

2 changes: 1 addition & 1 deletion app/api/projects/[id]/decisions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
scope: scope || "",
risks: risks || "",
nonGoals: nonGoals || "",
confidence: confidence || "medium",
confidenceScore: confidence || 0.5,

Choose a reason for hiding this comment

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

P2 Badge Preserve explicit zero confidence values

Using confidence || 0.5 overwrites a valid 0 input with 0.5 because 0 is falsy. The new decision forms send confidence on a 0–1 scale from the slider, so choosing 0% confidence is currently stored as 50%, which corrupts saved decision data and downstream exports/displays. Use a nullish check (??) or explicit numeric validation so 0 is persisted correctly.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

confidence || 0.5 treats 0 as falsy β€” use nullish coalescing.

If a caller sends confidence: 0 (a valid value representing 0%), the || operator will discard it and default to 0.5. Use ?? to only default on null/undefined.

πŸ› Proposed fix
-        confidenceScore: confidence || 0.5,
+        confidenceScore: confidence ?? 0.5,
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
confidenceScore: confidence || 0.5,
confidenceScore: confidence ?? 0.5,
πŸ€– Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/route.ts at line 53, The code sets
confidenceScore using the falsy-check operator (confidence || 0.5), which treats
0 as falsy; update the assignment to use nullish coalescing so only
null/undefined fall back to 0.5 (replace the use of || with ?? for the
confidence -> confidenceScore mapping), referencing the confidence variable and
confidenceScore field in the route handler that builds the decision payload.

status: "draft",
linkedFeedbackIds: linkedFeedbackIds || [],
},
Expand Down
Loading