Fix/reply edit delete auth check 177#228
Conversation
|
@dreynox is attempting to deploy a commit to the Karan Mani Tripathi 's projects Team on Vercel. A member of the Team first needs to authorize it. |
2752699 to
a9420ff
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses issue #177 by adding authentication + authorization (owner/teacher) checks to the replies edit/delete API, and tightens reply upvote identity to use the authenticated Clerk user id instead of a client-provided name.
Changes:
- Enforced auth + ownership/teacher authorization on
PATCH/DELETEfor/api/replies/action/[id]. - Updated reply voting to use Clerk
user.idas the stored like identity and adjusted the client vote request accordingly. - Added Jest coverage for the vote endpoint identity behavior and the replies action endpoint auth/authorization responses.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| components/DoubtRepliesModal.tsx | Builds safer replies fetch URL and updates voting UX to rely on server auth (handles 401 + error toasts). |
| app/api/replies/vote/route.ts | Switches vote identity from client-provided userName to authenticated Clerk user.id. |
| app/api/replies/route.ts | Uses authenticated identity for computing hasUpvoted when returning replies. |
| app/api/replies/action/[id]/route.ts | Adds signed-in + owner/teacher checks for editing/deleting replies. |
| tests/api/replies-vote.test.ts | Adds test ensuring vote storage uses Clerk identity even if a client userName is sent. |
| tests/api/replies-action.test.ts | Adds tests for 401 unauthenticated delete, 403 non-owner patch, and successful owner delete. |
Comments suppressed due to low confidence (1)
app/api/replies/action/[id]/route.ts:70
- Same as PATCH: authorization depends on
email, but if the authenticated user lacksprimaryEmailAddress, the handler can't correctly evaluate ownership/teacher permissions and will respond with a misleading 403 after performing DB reads. Add an earlyif (!email) ...response to match the pattern used elsewhere (e.g.,app/api/replies/route.ts).
const user = await currentUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const email = user.primaryEmailAddress?.emailAddress;
const [reply] = await db.select().from(repliesTable).where(eq(repliesTable.id, replyId)).limit(1);
if (!reply) return NextResponse.json({ error: "Reply not found" }, { status: 404 });
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Authorization: must be authenticated and either owner or classroom teacher | ||
| const user = await currentUser(); | ||
| if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| const email = user.primaryEmailAddress?.emailAddress; |
| const [reply] = await db.select().from(repliesTable).where(eq(repliesTable.id, replyId)).limit(1); | ||
| if (!reply) return NextResponse.json({ error: "Reply not found" }, { status: 404 }); | ||
|
|
||
| // Find parent doubt for teacher check | ||
| const [doubt] = await db.select().from(doubtsTable).where(eq(doubtsTable.id, reply.doubtId)).limit(1); | ||
| let isTeacher = false; | ||
| if (doubt?.classroomId) { | ||
| const [room] = await db.select().from(classroomsTable).where(eq(classroomsTable.id, doubt.classroomId)).limit(1); | ||
| isTeacher = !!(room && email && room.teacherEmail === email); | ||
| } | ||
|
|
||
| const isOwner = email && reply.userEmail === email; |
Hi @knoxiboy, Before the current PR is merged, I just wanted to kindly ask you to add the GSSoC tracking label (gssoc:approved, quality:clean or quality:exceptional). It’s required on the issue/PR side for points to be counted, and it seems the previous PRs were merged without the label. Thanks again for your time and support! |
|
@knoxiboy Please add the required labels that are gssoc:approved, quality:clean or quality:exceptional, before merging the commit because i need points for the GSSoC '26 program, if these are not added then my contribution will mean nothing to me. If you add them after merging the commit, then they are of no use. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
app/api/replies/action/[id]/route.ts:108
- The DELETE handler also has duplicated blocks that redeclare
replyandisTeacher, which will fail TypeScript compilation and cause extra queries. Keep a single fetch of the reply and a single teacher/owner permission check.
const [reply] = await db.select().from(repliesTable).where(eq(repliesTable.id, replyId)).limit(1);
if (!reply) return NextResponse.json({ error: "Reply not found" }, { status: 404 });
let isTeacher = false;
if (reply.doubtId) {
const [doubt] = await db.select().from(doubtsTable).where(eq(doubtsTable.id, reply.doubtId)).limit(1);
if (doubt?.classroomId) {
const [room] = await db.select().from(classroomsTable).where(eq(classroomsTable.id, doubt.classroomId)).limit(1);
isTeacher = !!(room && email && room.teacherEmail === email);
}
}
const [reply] = await db.select().from(repliesTable).where(eq(repliesTable.id, replyId)).limit(1);
if (!reply) return NextResponse.json({ error: "Reply not found" }, { status: 404 });
let isTeacher = false;
if (reply.doubtId) {
const [doubt] = await db.select().from(doubtsTable).where(eq(doubtsTable.id, reply.doubtId)).limit(1);
app/api/replies/action/[id]/route.ts:114
- Unresolved git merge-conflict marker (
>>>>>>> upstream/main) is present in the DELETE handler. This must be removed by resolving the merge conflict before this can ship.
if (doubt?.classroomId) {
const [room] = await db.select().from(classroomsTable).where(eq(classroomsTable.id, doubt.classroomId)).limit(1);
isTeacher = !!(room && email && room.teacherEmail === email);
}
>>>>>>> upstream/main
}
| const [reply] = await db.select().from(repliesTable).where(eq(repliesTable.id, replyId)).limit(1); | ||
| if (!reply) return NextResponse.json({ error: "Reply not found" }, { status: 404 }); | ||
|
|
||
| let isTeacher = false; | ||
| if (reply.doubtId) { | ||
| const [doubt] = await db.select().from(doubtsTable).where(eq(doubtsTable.id, reply.doubtId)).limit(1); | ||
| if (doubt?.classroomId) { | ||
| const [room] = await db.select().from(classroomsTable).where(eq(classroomsTable.id, doubt.classroomId)).limit(1); | ||
| isTeacher = !!(room && email && room.teacherEmail === email); | ||
| } | ||
| } | ||
|
|
||
| const isOwner = email && reply.userEmail === email; | ||
| if (!isOwner && !isTeacher) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); | ||
| >>>>>>> upstream/main | ||
| } | ||
|
|
| const isOwner = email && reply.userEmail === email; | ||
| if (!isOwner && !isTeacher) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); | ||
| >>>>>>> upstream/main | ||
| } |
| let isTeacher = false; | ||
| if (reply.doubtId) { | ||
| const [doubt] = await db.select().from(doubtsTable).where(eq(doubtsTable.id, reply.doubtId)).limit(1); | ||
| if (doubt?.classroomId) { | ||
| const [room] = await db.select().from(classroomsTable).where(eq(classroomsTable.id, doubt.classroomId)).limit(1); | ||
| isTeacher = !!(room && email && room.teacherEmail === email); | ||
| } | ||
| } |
Description
Adds authentication and ownership checks to
app/api/replies/action/[id]/route.tsso PATCH and DELETE requests now require a signed-in user and verify that the caller is either the reply author or the classroom teacher.Related Issue
Closes #177
Type of Change
Screenshots (if UI change)
N/A
How Has This Been Tested?
npm run dev401Checklist
npm run dev)anytypes)main