-
Notifications
You must be signed in to change notification settings - Fork 2
Research and implement community feedback features #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
R44VC0RP
wants to merge
1
commit into
main
Choose a base branch
from
cursor/research-and-implement-community-feedback-features-9bba
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| import { NextRequest, NextResponse } from "next/server" | ||
| import { auth } from "@/lib/auth" | ||
| import { db } from "@/lib/db" | ||
| import { comment, user } from "@/lib/schema" | ||
| import { nanoid } from "nanoid" | ||
| import { eq, and, desc, isNull } from "drizzle-orm" | ||
| import { track } from "@vercel/analytics/server" | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const { searchParams } = new URL(request.url) | ||
| const ruleId = searchParams.get("ruleId") | ||
|
|
||
| if (!ruleId) { | ||
| return NextResponse.json({ error: "Rule ID is required" }, { status: 400 }) | ||
| } | ||
|
|
||
| // Get all comments for the rule with user information | ||
| const comments = await db | ||
| .select({ | ||
| id: comment.id, | ||
| content: comment.content, | ||
| parentId: comment.parentId, | ||
| createdAt: comment.createdAt, | ||
| updatedAt: comment.updatedAt, | ||
| user: { | ||
| id: user.id, | ||
| name: user.name, | ||
| image: user.image, | ||
| }, | ||
| }) | ||
| .from(comment) | ||
| .leftJoin(user, eq(comment.userId, user.id)) | ||
| .where(eq(comment.ruleId, ruleId)) | ||
| .orderBy(desc(comment.createdAt)) | ||
|
|
||
| // Organize comments into threads (parent comments with their replies) | ||
| const parentComments = comments.filter(c => !c.parentId) | ||
| const replies = comments.filter(c => c.parentId) | ||
|
|
||
| const threaded = parentComments.map(parent => ({ | ||
| ...parent, | ||
| replies: replies.filter(reply => reply.parentId === parent.id) | ||
| .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) | ||
| })) | ||
|
|
||
| return NextResponse.json(threaded) | ||
| } catch (error) { | ||
| console.error("Error fetching comments:", error) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const session = await auth.api.getSession({ | ||
| headers: request.headers | ||
| }) | ||
|
|
||
| if (!session) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) | ||
| } | ||
|
|
||
| const body = await request.json() | ||
| const { ruleId, content, parentId } = body | ||
|
|
||
| if (!ruleId || !content?.trim()) { | ||
| return NextResponse.json({ | ||
| error: "Rule ID and content are required" | ||
| }, { status: 400 }) | ||
| } | ||
|
|
||
| // Validate parentId if provided (ensure it exists and belongs to the same rule) | ||
| if (parentId) { | ||
| const parentComment = await db | ||
| .select() | ||
| .from(comment) | ||
| .where(and( | ||
| eq(comment.id, parentId), | ||
| eq(comment.ruleId, ruleId) | ||
| )) | ||
| .limit(1) | ||
|
|
||
| if (parentComment.length === 0) { | ||
| return NextResponse.json({ | ||
| error: "Parent comment not found" | ||
| }, { status: 400 }) | ||
| } | ||
| } | ||
|
|
||
| const id = nanoid() | ||
| const now = new Date() | ||
|
|
||
| const [newComment] = await db.insert(comment).values({ | ||
| id, | ||
| userId: session.user.id, | ||
| ruleId, | ||
| parentId: parentId || null, | ||
| content: content.trim(), | ||
| createdAt: now, | ||
| updatedAt: now, | ||
| }).returning() | ||
|
|
||
| // Get the comment with user info for response | ||
| const commentWithUser = await db | ||
| .select({ | ||
| id: comment.id, | ||
| content: comment.content, | ||
| parentId: comment.parentId, | ||
| createdAt: comment.createdAt, | ||
| updatedAt: comment.updatedAt, | ||
| user: { | ||
| id: user.id, | ||
| name: user.name, | ||
| image: user.image, | ||
| }, | ||
| }) | ||
| .from(comment) | ||
| .leftJoin(user, eq(comment.userId, user.id)) | ||
| .where(eq(comment.id, id)) | ||
| .limit(1) | ||
|
|
||
| await track('Comment Posted', { | ||
| ruleId, | ||
| commentId: id, | ||
| isReply: Boolean(parentId) | ||
| }) | ||
|
|
||
| return NextResponse.json(commentWithUser[0]) | ||
| } catch (error) { | ||
| console.error("Error creating comment:", error) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } | ||
|
|
||
| export async function DELETE(request: NextRequest) { | ||
| try { | ||
| const session = await auth.api.getSession({ | ||
| headers: request.headers | ||
| }) | ||
|
|
||
| if (!session) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) | ||
| } | ||
|
|
||
| const { commentId } = await request.json() | ||
|
|
||
| if (!commentId) { | ||
| return NextResponse.json({ error: "Comment ID is required" }, { status: 400 }) | ||
| } | ||
|
|
||
| // Delete the comment (only if it belongs to the authenticated user) | ||
| const deletedComment = await db | ||
| .delete(comment) | ||
| .where(and( | ||
| eq(comment.id, commentId), | ||
| eq(comment.userId, session.user.id) | ||
| )) | ||
| .returning() | ||
|
|
||
| if (deletedComment.length === 0) { | ||
| return NextResponse.json({ | ||
| error: "Comment not found or unauthorized" | ||
| }, { status: 404 }) | ||
| } | ||
|
|
||
| await track('Comment Deleted', { commentId }) | ||
| return NextResponse.json({ message: "Comment deleted successfully" }) | ||
| } catch (error) { | ||
| console.error("Error deleting comment:", error) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { NextRequest, NextResponse } from "next/server" | ||
| import { db } from "@/lib/db" | ||
| import { rating, comment } from "@/lib/schema" | ||
| import { eq, avg, count, sql } from "drizzle-orm" | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const { searchParams } = new URL(request.url) | ||
| const ruleIds = searchParams.get("ruleIds")?.split(",") || [] | ||
|
|
||
| if (ruleIds.length === 0) { | ||
| return NextResponse.json({}) | ||
| } | ||
|
|
||
| // Get rating stats for all rules | ||
| const ratingStats = await db | ||
| .select({ | ||
| ruleId: rating.ruleId, | ||
| averageRating: avg(rating.rating), | ||
| totalRatings: count(rating.id), | ||
| }) | ||
| .from(rating) | ||
| .where(sql`${rating.ruleId} = ANY(${ruleIds})`) | ||
| .groupBy(rating.ruleId) | ||
|
|
||
| // Get comment counts for all rules | ||
| const commentStats = await db | ||
| .select({ | ||
| ruleId: comment.ruleId, | ||
| totalComments: count(comment.id), | ||
| }) | ||
| .from(comment) | ||
| .where(sql`${comment.ruleId} = ANY(${ruleIds})`) | ||
| .groupBy(comment.ruleId) | ||
|
|
||
| // Combine the stats | ||
| const stats: Record<string, { | ||
| averageRating: number | ||
| totalRatings: number | ||
| totalComments: number | ||
| }> = {} | ||
|
|
||
| // Initialize all rule IDs with zero stats | ||
| ruleIds.forEach(ruleId => { | ||
| stats[ruleId] = { | ||
| averageRating: 0, | ||
| totalRatings: 0, | ||
| totalComments: 0 | ||
| } | ||
| }) | ||
|
|
||
| // Add rating stats | ||
| ratingStats.forEach(stat => { | ||
| if (stat.ruleId) { | ||
| stats[stat.ruleId] = { | ||
| ...stats[stat.ruleId], | ||
| averageRating: Number(stat.averageRating) || 0, | ||
| totalRatings: stat.totalRatings || 0, | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| // Add comment stats | ||
| commentStats.forEach(stat => { | ||
| if (stat.ruleId) { | ||
| stats[stat.ruleId] = { | ||
| ...stats[stat.ruleId], | ||
| totalComments: stat.totalComments || 0, | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| return NextResponse.json(stats) | ||
| } catch (error) { | ||
| console.error("Error fetching feed stats:", error) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { NextRequest, NextResponse } from "next/server" | ||
| import { auth } from "@/lib/auth" | ||
| import { db } from "@/lib/db" | ||
| import { rating, user } from "@/lib/schema" | ||
| import { nanoid } from "nanoid" | ||
| import { eq, and, avg, count } from "drizzle-orm" | ||
| import { track } from "@vercel/analytics/server" | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const { searchParams } = new URL(request.url) | ||
| const ruleId = searchParams.get("ruleId") | ||
|
|
||
| if (!ruleId) { | ||
| return NextResponse.json({ error: "Rule ID is required" }, { status: 400 }) | ||
| } | ||
|
|
||
| // Get average rating and count | ||
| const ratingStats = await db | ||
| .select({ | ||
| averageRating: avg(rating.rating), | ||
| totalRatings: count(rating.id), | ||
| }) | ||
| .from(rating) | ||
| .where(eq(rating.ruleId, ruleId)) | ||
|
|
||
| // Get user's rating if authenticated | ||
| let userRating = null | ||
| try { | ||
| const session = await auth.api.getSession({ | ||
| headers: request.headers | ||
| }) | ||
|
|
||
| if (session) { | ||
| const userRatingResult = await db | ||
| .select({ | ||
| rating: rating.rating, | ||
| }) | ||
| .from(rating) | ||
| .where(and( | ||
| eq(rating.ruleId, ruleId), | ||
| eq(rating.userId, session.user.id) | ||
| )) | ||
| .limit(1) | ||
|
|
||
| userRating = userRatingResult[0]?.rating || null | ||
| } | ||
| } catch (error) { | ||
| // Continue without user rating if auth fails | ||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| averageRating: ratingStats[0]?.averageRating ? Number(ratingStats[0].averageRating) : 0, | ||
| totalRatings: ratingStats[0]?.totalRatings || 0, | ||
| userRating, | ||
| }) | ||
| } catch (error) { | ||
| console.error("Error fetching ratings:", error) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const session = await auth.api.getSession({ | ||
| headers: request.headers | ||
| }) | ||
|
|
||
| if (!session) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) | ||
| } | ||
|
|
||
| const body = await request.json() | ||
| const { ruleId, rating: ratingValue } = body | ||
|
|
||
| if (!ruleId || !ratingValue || ratingValue < 1 || ratingValue > 5) { | ||
| return NextResponse.json({ | ||
| error: "Rule ID and rating (1-5) are required" | ||
| }, { status: 400 }) | ||
| } | ||
|
|
||
| const id = nanoid() | ||
| const now = new Date() | ||
|
|
||
| // Check if user already rated this rule | ||
| const existingRating = await db | ||
| .select() | ||
| .from(rating) | ||
| .where(and( | ||
| eq(rating.ruleId, ruleId), | ||
| eq(rating.userId, session.user.id) | ||
| )) | ||
| .limit(1) | ||
|
|
||
| let result | ||
| if (existingRating.length > 0) { | ||
| // Update existing rating | ||
| [result] = await db | ||
| .update(rating) | ||
| .set({ | ||
| rating: ratingValue, | ||
| updatedAt: now, | ||
| }) | ||
| .where(and( | ||
| eq(rating.ruleId, ruleId), | ||
| eq(rating.userId, session.user.id) | ||
| )) | ||
| .returning() | ||
| } else { | ||
| // Create new rating | ||
| [result] = await db.insert(rating).values({ | ||
| id, | ||
| userId: session.user.id, | ||
| ruleId, | ||
| rating: ratingValue, | ||
| createdAt: now, | ||
| updatedAt: now, | ||
| }).returning() | ||
| } | ||
|
|
||
| await track('Rating Submitted', { | ||
| ruleId, | ||
| rating: ratingValue, | ||
| isUpdate: existingRating.length > 0 | ||
| }) | ||
|
|
||
| return NextResponse.json(result) | ||
| } catch (error) { | ||
| console.error("Error creating/updating rating:", error) | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical SQL injection vulnerability: User input is directly interpolated into SQL queries without proper sanitization.
View Details
📝 Patch Details
Analysis
Lines 23 and 33 use
sql${rating.ruleId} = ANY(${ruleIds})and `sql`${comment.ruleId} = ANY(${ruleIds})whereruleIdscomes directly from URL query parameters that are split by comma. This creates a SQL injection vulnerability because the user-controlledruleIdsarray is interpolated directly into the SQL query without proper sanitization or parameterization.An attacker could manipulate the
ruleIdsparameter to inject malicious SQL code. For example, a request like/api/feed/stats?ruleIds='; DROP TABLE rating; --could potentially execute dangerous SQL commands.The fix is to use Drizzle ORM's
inArray()function instead of raw SQL interpolation:.where(inArray(rating.ruleId, ruleIds))and.where(inArray(comment.ruleId, ruleIds)). This will properly parameterize the query and prevent SQL injection attacks.