-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Enrolled partner page #2821
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
Enrolled partner page #2821
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds partner-focused pages and routing, implements partner comments end-to-end (DB model, APIs, actions, hooks, UI), filters bounties by partner eligibility via program enrollment, introduces enrollment update action + audit, refactors the messaging composer into a shared MessageInput, and applies multiple UI/redirect/navigation tweaks. Changes
Sequence Diagram(s)sequenceDiagram
actor U as User
participant UI as Partner Comments UI
participant Action as createPartnerCommentAction
participant DB as Prisma
U->>UI: submit comment (text)
UI->>UI: show optimistic comment (tmp id)
UI->>Action: createPartnerCommentAction({ partnerId, text, createdAt })
Action->>DB: INSERT PartnerComment (resolve programId)
DB-->>Action: created comment with user
Action-->>UI: { comment }
UI->>UI: replace optimistic entry and revalidate /api/partners/{id}/comments
sequenceDiagram
actor C as Client
participant API as GET /api/bounties
participant EN as getProgramEnrollmentOrThrow
participant DB as Prisma
C->>API: GET /api/bounties?workspaceId&partnerId=...
API->>EN: if partnerId present -> lookup enrollment (includeProgram)
EN-->>API: enrollment or null
API->>DB: query bounties with filter: groups none OR groupId in [enrollment.groupId, program.defaultGroupId]
DB-->>API: bounties[]
API-->>C: JSON bounties
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-stats.tsx
Outdated
Show resolved
Hide resolved
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/layout.tsx
Outdated
Show resolved
Hide resolved
| await prisma.partnerComment.delete({ | ||
| where: { | ||
| id: commentId, | ||
| programId, | ||
| }, | ||
| }); |
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.
The delete comment action allows any authenticated user to delete any comment in their workspace, not just their own comments.
View Details
📝 Patch Details
diff --git a/apps/web/lib/actions/partners/delete-partner-comment.ts b/apps/web/lib/actions/partners/delete-partner-comment.ts
index 0453d02df..4c41e1885 100644
--- a/apps/web/lib/actions/partners/delete-partner-comment.ts
+++ b/apps/web/lib/actions/partners/delete-partner-comment.ts
@@ -9,7 +9,7 @@ import { authActionClient } from "../safe-action";
export const deletePartnerCommentAction = authActionClient
.schema(deleteProgramPartnerCommentSchema)
.action(async ({ parsedInput, ctx }) => {
- const { workspace } = ctx;
+ const { workspace, user } = ctx;
const { commentId } = parsedInput;
const programId = getDefaultProgramIdOrThrow(workspace);
@@ -18,6 +18,7 @@ export const deletePartnerCommentAction = authActionClient
where: {
id: commentId,
programId,
+ userId: user.id,
},
});
});
Analysis
Authorization bypass in deletePartnerCommentAction allows users to delete other users' comments
What fails: deletePartnerCommentAction() in apps/web/lib/actions/partners/delete-partner-comment.ts checks commentId and programId but missing userId check, allowing any workspace member to delete any comment in their program
How to reproduce:
# As authenticated user A in workspace:
# Call deletePartnerCommentAction with commentId created by user B
# Action succeeds despite user A not owning the commentResult: Any authenticated workspace member can delete comments created by other users within the same program, violating ownership-based authorization
Expected: Users should only be able to delete their own comments per Prisma security best practices requiring ownership verification before deletion
Root cause: Missing userId: user.id constraint in Prisma delete query where clause
| ) | ||
| } | ||
| /> | ||
| <Link href={`/program/partners/${partnerId}`} target="_blank"> |
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.
| <Link href={`/program/partners/${partnerId}`} target="_blank"> | |
| <Link href={`/${workspaceSlug}/program/partners/${partnerId}`} target="_blank"> |
The "View profile" link is missing the workspace slug, which will result in a broken navigation link.
View Details
Analysis
Missing workspace slug in partner profile navigation link
What fails: Link component in ProgramMessagesPartnerPageClient (line 219) navigates to /program/partners/${partnerId} instead of /${workspaceSlug}/program/partners/${partnerId}, causing broken navigation
How to reproduce:
- Navigate to program messages for any partner
- Click "View profile" button in the right panel
- Link attempts to navigate to
/program/partners/[partnerId]route which doesn't exist
Result: Navigation fails because Next.js App Router requires all dynamic segments including [slug] to be present in navigation hrefs
Expected: Should navigate to /${workspaceSlug}/program/partners/${partnerId} as confirmed by the route structure at apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/ and similar patterns in partner-nav.tsx
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (1)
134-145: Add rel="noopener noreferrer" to target=_blank Commissions link; confirm final route.Missing rel is a security gap (reverse‑tabnabbing). Also please re‑confirm that this is the intended canonical route for Commissions in this PR.
Apply:
- <Link - href={`/${workspaceSlug}/program/commissions?partnerId=${partnerId}`} - target="_blank" + <Link + href={`/${workspaceSlug}/program/commissions?partnerId=${partnerId}`} + target="_blank" + rel="noopener noreferrer"Optionally verify the route shape used elsewhere:
#!/bin/bash # Inspect commissions routes/usages rg -nP -C2 '/program/(partners/[^/]+/commissions|commissions\?partnerId=)' apps/web | sed -n '1,200p' fd -H 'commissions' apps/web/app | sed -n '1,200p'
🧹 Nitpick comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (4)
32-49: Wheel listener should be non‑passive to allow preventDefault.Chrome may ignore preventDefault on passive listeners; set passive: false and mirror options in removeEventListener.
useEffect(() => { if (!containerRef.current) return; - const handleWheel = (event: WheelEvent) => { + const handleWheel = (event: WheelEvent) => { if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) return; event.preventDefault(); containerRef.current?.scrollBy({ left: event.deltaY, behavior: "smooth", }); }; - containerRef.current.addEventListener("wheel", handleWheel); + const wheelOptions: AddEventListenerOptions = { passive: false }; + containerRef.current.addEventListener("wheel", handleWheel, wheelOptions); - return () => - containerRef.current?.removeEventListener("wheel", handleWheel); + return () => + containerRef.current?.removeEventListener("wheel", handleWheel, wheelOptions); }, []);
51-55: Initialize scroll progress on mount to avoid a stale gradient until first scroll/resize.Call update once after mount.
const { scrollProgress, updateScrollProgress } = useScrollProgress( containerRef, { direction: "horizontal" }, ); + + useEffect(() => { + updateScrollProgress(); + }, [updateScrollProgress]);
99-106: Make active‑tab detection robust for nested routes; add aria‑current for a11y.endsWith fails if viewing a nested child (e.g., /comments/123). Use last segment and expose aria-current.
- {tabs.map(({ id, label, icon: Icon, badge }) => { - const isSelected = pathname.endsWith(`/${id}`); + {tabs.map(({ id, label, icon: Icon, badge }) => { + const lastSeg = pathname.replace(/\/$/, "").split("/").pop(); + const isSelected = lastSeg === id || pathname.includes(`/${id}/`); return ( <Link key={id} href={`/${workspaceSlug}/program/partners/${partnerId}/${id}`} data-selected={isSelected} + aria-current={isSelected ? "page" : undefined} className={cn(Also applies to: 120-129
103-105: Optional: centralize partner URL generationThere's already a getPartnerUrl helper in apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (around line 91). Import and use it here instead of the inline template — or extract it to a shared util — to avoid string-template drift across files.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (3)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/swr/use-partner-comments-count.ts (1)
usePartnerCommentsCount(5-31)packages/ui/src/hooks/use-scroll-progress.ts (1)
useScrollProgress(6-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build
- GitHub Check: Vade Review
🔇 Additional comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (1)
74-85: Comments badge logic LGTM.Hides on zero, caps at 99+, and renders correctly as string/number. Nice.
Also applies to: 115-119
…rolled-partner-page
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/lib/actions/partners/update-partner-enrollment.ts (1)
13-17: Validate or dropworkspaceIdto match the schema.
workspaceIdis required by the schema but ignored at runtime. Either validate it againstctx.workspace.idor remove it from the schema.Option A — validate:
- .action(async ({ parsedInput, ctx }) => { + .action(async ({ parsedInput, ctx }) => { const { workspace, user } = ctx; - const { partnerId, tenantId } = parsedInput; + const { partnerId, tenantId, workspaceId } = parsedInput; + if (workspaceId !== workspace.id) { + throw new Error("workspaceId mismatch"); + }Option B — drop from schema:
-const updatePartnerEnrollmentSchema = z.object({ - workspaceId: z.string(), +const updatePartnerEnrollmentSchema = z.object({ partnerId: z.string(), tenantId: z.string().nullable(), });Also applies to: 23-25
🧹 Nitpick comments (4)
apps/web/lib/actions/partners/update-partner-enrollment.ts (4)
26-31: EnsureprogramIdis the canonical ID (not a slug) before write queries.
getProgramEnrollmentOrThrowaccepts slugs or IDs, but Prisma writes below require the ID. Guard by resolving the ID from the fetched enrollment.- const programId = getDefaultProgramIdOrThrow(workspace); - - const { partner } = await getProgramEnrollmentOrThrow({ - partnerId, - programId, - includePartner: true, - }); + const programIdentifier = getDefaultProgramIdOrThrow(workspace); + const enrollment = await getProgramEnrollmentOrThrow({ + partnerId, + programId: programIdentifier, + includePartner: true, + includeProgram: true, + }); + const { partner } = enrollment; + const programId = enrollment.program.id; // canonical IDAlso applies to: 34-38, 46-49
64-77: Limit PII/noise in audit metadata.Logging the full
partnerobject may capture PII and bloat logs. Prefer a minimal, stable subset.Example:
- targets: [ - { - type: "partner", - id: partnerId, - metadata: partner, - }, - ], + targets: [ + { + type: "partner", + id: partnerId, + metadata: { + id: partner.id, + name: partner.name, + handle: partner.handle, + companyName: partner.companyName, + }, + }, + ],
39-59: Short-circuit no-op writes whentenantIdis unchanged.Avoid unnecessary writes and downstream logging when the value doesn’t change.
- const programEnrollment = await prisma.$transaction(async (tx) => { + if (enrollment.tenantId === tenantId) { + return; // nothing to do + } + const programEnrollment = await prisma.$transaction(async (tx) => {
61-63: ScalerecordLinkfor large link sets.If
programEnrollment.linkscan be large, chunk or stream to avoid timeouts/limits in the background task. Also confirmrecordLinkaccepts an array.Example (conceptual):
- waitUntil( - Promise.allSettled([ - recordLink(programEnrollment.links), + const batches = []; + for (let i = 0; i < programEnrollment.links.length; i += 500) { + batches.push(programEnrollment.links.slice(i, i + 500)); + } + waitUntil( + Promise.allSettled([ + ...batches.map((batch) => recordLink(batch)), recordAuditLog({ /* ... */ }), ]), );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/lib/actions/partners/update-partner-enrollment.ts(1 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/zod/schemas/partners.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/actions/partners/update-partner-enrollment.ts (1)
apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
getProgramEnrollmentOrThrow(6-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (2)
apps/web/lib/actions/partners/update-partner-enrollment.ts (2)
61-79: Background tasks wiring LGTM.Passing a single promise via
waitUntil(Promise.allSettled([...]))correctly retains both async side-effects post-response.
16-17: Confirm DB allows settingtenantIdto NULL.You update both Links and ProgramEnrollment to a nullable
tenantId. Verify the columns are nullable and not part of non-null FKs/unique constraints.Also applies to: 41-45, 51-52
| @@ -0,0 +1,91 @@ | |||
| import { MAX_MESSAGE_LENGTH } from "@/lib/zod/schemas/messages"; | |||
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.
The MessageInput component imports MAX_MESSAGE_LENGTH from messages schema but is used for partner comments, which should use MAX_PROGRAM_PARTNER_COMMENT_LENGTH instead.
View Details
📝 Patch Details
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx
index 2e73c3ce9..87789b954 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx
@@ -7,6 +7,7 @@ import { usePartnerComments } from "@/lib/swr/use-partner-comments";
import useUser from "@/lib/swr/use-user";
import useWorkspace from "@/lib/swr/use-workspace";
import { PartnerCommentProps } from "@/lib/types";
+import { MAX_PROGRAM_PARTNER_COMMENT_LENGTH } from "@/lib/zod/schemas/programs";
import { ThreeDots } from "@/ui/shared/icons";
import { MessageInput } from "@/ui/shared/message-input";
import { Button, LoadingSpinner, Popover, Trash } from "@dub/ui";
@@ -33,6 +34,7 @@ export function ProgramPartnerCommentsPageClient() {
return (
<div className="mt-4">
<MessageInput
+ maxLength={MAX_PROGRAM_PARTNER_COMMENT_LENGTH}
onSendMessage={(text) => {
if (!user) return false;
diff --git a/apps/web/ui/shared/message-input.tsx b/apps/web/ui/shared/message-input.tsx
index 4a7218b9b..fde89f3ce 100644
--- a/apps/web/ui/shared/message-input.tsx
+++ b/apps/web/ui/shared/message-input.tsx
@@ -11,12 +11,14 @@ export function MessageInput({
placeholder = "Type a message...",
sendButtonText = "Send",
className,
+ maxLength = MAX_MESSAGE_LENGTH,
}: {
onSendMessage: (message: string) => void | false;
autoFocus?: boolean;
placeholder?: string;
sendButtonText?: string;
className?: string;
+ maxLength?: number;
}) {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const selectionStartRef = useRef<number | null>(null);
@@ -42,7 +44,7 @@ export function MessageInput({
className="placeholder:text-content-subtle block max-h-24 w-full resize-none border-none p-3 text-base focus:ring-0 sm:text-sm"
placeholder={placeholder}
value={typedMessage}
- maxLength={MAX_MESSAGE_LENGTH}
+ maxLength={maxLength}
onChange={(e) => setTypedMessage(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
Analysis
MessageInput uses wrong maxLength constant for partner comments validation
What fails: MessageInput component in apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx uses MAX_MESSAGE_LENGTH (2000) from messages schema, but partner comments are validated server-side against MAX_PROGRAM_PARTNER_COMMENT_LENGTH (2000) from programs schema.
How to reproduce:
- Navigate to partner comments page at
[slug]/program/partners/[partnerId]/comments - Compare client-side maxLength validation in MessageInput component vs server-side validation in createPartnerCommentSchema
Result: Semantic mismatch between client and server validation constants. Currently both are 2000 chars so no functional issue, but if either constant changes independently, validation will become inconsistent.
Expected: Partner comment input should use MAX_PROGRAM_PARTNER_COMMENT_LENGTH for semantic correctness and future-proofing against independent constant changes.
Evidence: MessageInput imports from /lib/zod/schemas/messages.ts but partner comments validate against /lib/zod/schemas/programs.ts schema in createPartnerCommentSchema.
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.
Actionable comments posted: 2
♻️ Duplicate comments (4)
apps/web/lib/actions/partners/update-partner-comment.ts (1)
13-17: Validate or drop workspaceId from schema
updatePartnerCommentSchemaincludesworkspaceIdbut the action ignores it. Either assert equality withctx.workspace.idhere or remove it from the schema to avoid cross-tenant confusion.apps/web/lib/actions/partners/delete-partner-comment.ts (2)
10-13: Validate or drop workspaceIdSchema includes
workspaceIdbut action doesn’t verify it againstctx.workspace.id. Validate here or remove from schema.
9-22: Enforce ownership and fix Prisma delete selector
deleteneeds a unique selector;{ id, programId }isn’t unique. Also enforceuserIdto prevent deleting others’ comments. UsedeleteMany+countcheck, and return a result.export const deletePartnerCommentAction = authActionClient .schema(deletePartnerCommentSchema) .action(async ({ parsedInput, ctx }) => { - const { workspace } = ctx; + const { workspace, user } = ctx; const { commentId } = parsedInput; const programId = getDefaultProgramIdOrThrow(workspace); - await prisma.partnerComment.delete({ - where: { - id: commentId, - programId, - }, - }); + const { count } = await prisma.partnerComment.deleteMany({ + where: { id: commentId, programId, userId: user.id }, + }); + if (count === 0) { + throw new Error("Comment not found or not owned by you."); + } + return { success: true }; });apps/web/lib/actions/partners/create-partner-comment.ts (1)
17-18: Don’t accept client-supplied createdAt; set on serverClient timestamps are spoofable and race-prone. Set
createdAtserver-side (or rely on DB default) and remove it from input.- const { partnerId, text, createdAt } = parsedInput; + const { partnerId, text, workspaceId } = parsedInput; + if (workspaceId !== workspace.id) { + throw new Error("workspaceId mismatch"); + } ... - const comment = await prisma.partnerComment.create({ + const comment = await prisma.partnerComment.create({ data: { programId, partnerId, userId: user.id, text, - createdAt, },
🧹 Nitpick comments (10)
apps/web/lib/actions/partners/create-partner-comment.ts (1)
13-15: Schema alignment requiredAfter dropping
createdAtfrom input, updatecreatePartnerCommentSchemaaccordingly and trim text to prevent whitespace-only comments.apps/web/lib/zod/schemas/programs.ts (3)
188-191: Align date handling with JSON transportThese are serialized to ISO strings over HTTP. Use
z.coerce.date()to accept strings and Dates seamlessly.- createdAt: z.date(), - updatedAt: z.date(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(),
195-209: Remove clientcreatedAt; trim comment textLet the server/DB set timestamps. Also trim text to disallow whitespace-only.
-export const createPartnerCommentSchema = z.object({ - workspaceId: z.string(), - partnerId: z.string(), - text: z.string().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), - createdAt: z.coerce - .date() - .refine( - (date) => - date.getTime() <= Date.now() && - date.getTime() >= Date.now() - 1000 * 60, - { - message: "Comment timestamp must be within the last 60 seconds", - }, - ), -}); +export const createPartnerCommentSchema = z.object({ + workspaceId: z.string(), + partnerId: z.string(), + text: z.string().trim().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), +});
211-215: Trim on update as wellAdd
.trim()to ensure updates can’t set whitespace-only text.-export const updatePartnerCommentSchema = z.object({ +export const updatePartnerCommentSchema = z.object({ workspaceId: z.string(), id: z.string(), - text: z.string().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), + text: z.string().trim().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), });apps/web/app/(ee)/api/partners/[partnerId]/comments/route.ts (2)
27-28: Date schema vs JSON: preferz.coerce.date()in schema
NextResponse.jsonreturns strings; downstream code re-parses withnew Date(...). UpdatePartnerCommentSchematoz.coerce.date()(see schema comment) so server- and client-side validation are consistent. No code change needed here if schema is updated.
14-25: Consider pagination to avoid unbounded result setsIf comment volumes grow, add
take/skip(or cursor) with query params.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx (4)
61-66: Stop sending clientcreatedAtAfter moving timestamping server-side, remove
createdAtfrom the action call.- const result = await createPartnerComment({ - workspaceId: workspaceId!, - partnerId, - text, - createdAt, - }); + const result = await createPartnerComment({ + workspaceId: workspaceId!, + partnerId, + text, + });
73-81: Replace optimistic entry to avoid duplicates; don’t revalidate immediatelyFilter out the optimistic item when inserting the real one; also set
revalidate: falseto avoid double-fetch churn.- return data - ? [result.data.comment, ...data] - : [result.data.comment]; + const real = result.data.comment; + return data + ? [real, ...data.filter((c) => c.id !== optimisticComment.id)] + : [real]; }, { optimisticData: (data) => data ? [optimisticComment, ...data] : [optimisticComment], rollbackOnError: true, + revalidate: false, },
191-239: Hide delete action for undelivered optimistic commentsAvoid sending delete mutations for temporary
tmp_*comments.- {comment ? ( + {comment && comment.delivered !== false ? ( <Popover content={ <div className="grid w-full grid-cols-1 gap-px p-2 sm:w-48"> <Button text="Delete comment" variant="danger-outline" onClick={async () => { setOpenPopover(false); if ( !confirm("Are you sure you want to delete this comment?") ) return; await deleteComment({ workspaceId: workspaceId!, commentId: comment.id, }); }} icon={<Trash className="size-4" />} className="h-9 justify-start px-2 font-medium" /> </div> } align="end" openPopover={openPopover} setOpenPopover={setOpenPopover} > <Button
155-157: Avatar/name fallback robustnessHandle null names and URL-encode to prevent broken avatars.
- src={comment.user.image || `${OG_AVATAR_URL}${comment.user.name}`} - alt={`${comment.user.name} avatar`} + src={ + comment.user.image || + `${OG_AVATAR_URL}${encodeURIComponent(comment.user.name || "User")}` + } + alt={`${comment.user.name || "User"} avatar`}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/web/app/(ee)/api/partners/[partnerId]/comments/route.ts(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx(1 hunks)apps/web/lib/actions/partners/create-partner-comment.ts(1 hunks)apps/web/lib/actions/partners/delete-partner-comment.ts(1 hunks)apps/web/lib/actions/partners/update-partner-comment.ts(1 hunks)apps/web/lib/swr/use-partner-comments.ts(1 hunks)apps/web/lib/types.ts(2 hunks)apps/web/lib/zod/schemas/programs.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/lib/swr/use-partner-comments.ts
- apps/web/lib/types.ts
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/lib/actions/partners/delete-partner-comment.ts (1)
apps/web/lib/zod/schemas/programs.ts (1)
deletePartnerCommentSchema(217-220)
apps/web/lib/actions/partners/update-partner-comment.ts (1)
apps/web/lib/zod/schemas/programs.ts (2)
updatePartnerCommentSchema(211-215)PartnerCommentSchema(178-191)
apps/web/lib/zod/schemas/programs.ts (1)
apps/web/lib/zod/schemas/users.ts (1)
UserSchema(3-7)
apps/web/app/(ee)/api/partners/[partnerId]/comments/route.ts (2)
apps/web/lib/auth/workspace.ts (1)
withWorkspace(41-435)apps/web/lib/zod/schemas/programs.ts (1)
PartnerCommentSchema(178-191)
apps/web/lib/actions/partners/create-partner-comment.ts (2)
apps/web/lib/zod/schemas/programs.ts (2)
createPartnerCommentSchema(195-209)PartnerCommentSchema(178-191)apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
getProgramEnrollmentOrThrow(6-97)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx (7)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/swr/use-partner-comments.ts (1)
usePartnerComments(6-34)apps/web/lib/actions/partners/create-partner-comment.ts (1)
createPartnerCommentAction(13-42)apps/web/ui/shared/message-input.tsx (1)
MessageInput(8-91)apps/web/lib/actions/partners/delete-partner-comment.ts (1)
deletePartnerCommentAction(9-23)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)packages/ui/src/popover.tsx (1)
Popover(25-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: api-tests
- GitHub Check: Vade Review
| const comment = await prisma.partnerComment.update({ | ||
| where: { | ||
| id, | ||
| programId, | ||
| userId: user.id, | ||
| }, | ||
| data: { | ||
| text, | ||
| }, | ||
| include: { | ||
| user: true, | ||
| }, |
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.
Fix Prisma update: non-unique WHERE; enforce ownership via updateMany + fetch
update requires a unique selector. { id, programId, userId } isn’t a unique input. Use updateMany guarded by { id, programId, userId }, check count, then fetch the record to return. This also correctly enforces ownership.
- const comment = await prisma.partnerComment.update({
- where: {
- id,
- programId,
- userId: user.id,
- },
- data: {
- text,
- },
- include: {
- user: true,
- },
- });
+ const { count } = await prisma.partnerComment.updateMany({
+ where: { id, programId, userId: user.id },
+ data: { text },
+ });
+ if (count === 0) {
+ throw new Error("Comment not found or not owned by you.");
+ }
+ const comment = await prisma.partnerComment.findUnique({
+ where: { id },
+ include: { user: true },
+ });📝 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.
| const comment = await prisma.partnerComment.update({ | |
| where: { | |
| id, | |
| programId, | |
| userId: user.id, | |
| }, | |
| data: { | |
| text, | |
| }, | |
| include: { | |
| user: true, | |
| }, | |
| const { count } = await prisma.partnerComment.updateMany({ | |
| where: { id, programId, userId: user.id }, | |
| data: { text }, | |
| }); | |
| if (count === 0) { | |
| throw new Error("Comment not found or not owned by you."); | |
| } | |
| const comment = await prisma.partnerComment.findUnique({ | |
| where: { id }, | |
| include: { user: true }, | |
| }); |
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/update-partner-comment.ts around lines 20 to
31, the prisma.partnerComment.update call uses a non-unique selector ({ id,
programId, userId }) which Prisma rejects; replace it with
prisma.partnerComment.updateMany({ where: { id, programId, userId }, data: {
text } }), check the returned count and throw a NotFound/Forbidden error if
count === 0, then retrieve the updated comment with
prisma.partnerComment.findUnique or findFirst (include: { user: true }) and
return that record so ownership is enforced and a proper comment object is
returned.
🧩 Analysis chain
Verify PartnerComment unique keys before merging
Confirm there’s no composite unique on (id, programId, userId) so the updateMany approach is required.
🏁 Script executed:
#!/bin/bash
# Verify PartnerComment uniques (no composite on programId/userId)
fd -HI --type f 'schema.prisma' packages/prisma | while read -r f; do
echo "---- $f ----"
sed -n '1,240p' "$f" | sed -n '/model \(PartnerComment\|Comment\)/,/^}/p'
done
rg -n --hidden -S '(@@unique|@@id).*(programId|userId).*' packages/prisma || trueLength of output: 730
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Listing .prisma files under packages/prisma:"
if command -v fd >/dev/null 2>&1; then
fd -HI -e prisma packages/prisma || true
else
find packages/prisma -type f -name '*.prisma' -print || true
fi
echo
echo "Search for 'model PartnerComment' (case-sensitive) in prisma schema files:"
rg -n --hidden -S 'model\s+PartnerComment\b' packages/prisma || true
echo
echo "If found, print the PartnerComment model blocks:"
rg -n --hidden -S 'model\s+PartnerComment\b' packages/prisma | cut -d: -f1 | uniq | while read -r f; do
echo "---- $f ----"
sed -n '/model\s\+PartnerComment\b/,/}/p' "$f" || true
done
echo
echo "Search for any occurrence of 'PartnerComment' anywhere in the repo (case-sensitive):"
rg -n --hidden -S 'PartnerComment' || true
echo
echo "Search for @@unique or @@id lines mentioning programId, userId, or id across prisma schema files:"
rg -n --hidden -S '(@@unique|@@id).*(programId|userId|id)' packages/prisma || true
echo
echo "If a PartnerComment model was found, show any field-level @unique or @id annotations inside it:"
rg -n --hidden -S 'model\s+PartnerComment\b' packages/prisma | cut -d: -f1 | uniq | while read -r f; do
echo "---- field-level @unique/@id in $f ----"
sed -n '/model\s\+PartnerComment\b/,/}/p' "$f" | rg -n --hidden -S '@unique|@id' || true
done
echo
echo "Search for prisma.client usages of partnerComment (update/updateMany) in the codebase:"
rg -n --hidden -S 'prisma\.partnerComment\.(update|updateMany|findUnique|findFirst|findMany|upsert|delete)' || trueLength of output: 8684
Fix prisma.partnerComment.update where — PartnerComment.id is the only unique key
packages/prisma/schema/comment.prisma shows PartnerComment has id @id and no composite @@unique on (programId, userId); using where: { id, programId, userId } is invalid. Change to one of:
- Use the unique id: prisma.partnerComment.update({ where: { id }, data: { text }, include: { user: true } })
- If you must ensure programId+userId match: use updateMany({ where: { id, programId, userId }, data: { text } }) then fetch the record with findUnique({ where: { id }, include: { user: true } }) (or perform both in a transaction).
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/update-partner-comment.ts around lines 20 to
31, the prisma.partnerComment.update call uses a non-unique composite where {
id, programId, userId } although PartnerComment.id is the only unique key;
replace the where with the unique key (where: { id }) and keep data: { text }
and include: { user: true } OR if you need to enforce programId+userId matching,
switch to updateMany({ where: { id, programId, userId }, data: { text } }) and
then fetch the updated record with findUnique({ where: { id }, include: { user:
true } }) (or perform both operations inside a transaction).
Summary by CodeRabbit
New Features
Improvements
Style