Skip to content

[FOSSOVERFLOW-25] Added review engagement features#54

Open
alSN0W wants to merge 9 commits intoOpenLake:mainfrom
alSN0W:main
Open

[FOSSOVERFLOW-25] Added review engagement features#54
alSN0W wants to merge 9 commits intoOpenLake:mainfrom
alSN0W:main

Conversation

@alSN0W
Copy link

@alSN0W alSN0W commented Feb 14, 2026

Description

Implemented a complete voting system for course and professor reviews with Reddit-style UI, database triggers for automatic vote counting, and proper RLS security policies.

Users can now vote reviews as "helpful" or "unhelpful" with visual feedback, automatic vote count calculations, and persistent vote states across sessions.

Related Issue

Works on issue#16

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist:

  • My code follows the code style of this project.
  • Tested locally

Additional Notes

Technical Details:

  • Vote counts support negative values when unhelpful votes exceed helpful votes
  • Used SECURITY DEFINER on update_review_votes() trigger to bypass RLS policies
  • Implemented optimistic UI updates with automatic rollback on API errors
  • Added useEffect hooks in VoteButton to handle asynchronous vote data loading
  • Created separate Supabase client (server-pages.ts) for Pages Router compatibility

Database Changes:

  • Added votes table with unique constraint on (review_id, anonymous_id)
  • Created database trigger that auto-calculates: helpful_count - unhelpful_count
  • Added index on votes.review_id for query performance
  • Updated migration file with all voting schema and functions

Security:

  • RLS policies ensure users can only vote as themselves
  • Anonymous voting preserved through get_anonymous_id() function
  • Vote manipulation prevented by unique constraint per user per review

Summary by CodeRabbit

  • New Features

    • Added review voting system with upvote/downvote buttons and vote counts
    • Enabled magic link sign-in option
  • Updates

    • Upgraded Next.js framework from v13.5.1 to v16.1.6
    • Added Turbopack support for faster build performance

@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

Walkthrough

This PR upgrades Next.js from 13.5.1 to 16.1.6, adds Turbopack configuration, and implements a review voting system. It includes new voting UI components, state management hooks, API endpoints, database migrations, and support for async route parameters.

Changes

Cohort / File(s) Summary
Infrastructure & Configuration
package.json, next.config.js, next-env.d.ts, tsconfig.json
Updated Next.js to v16.1.6, added turbopack config, updated JSX settings, added route types, and expanded include paths for TypeScript.
Voting Feature Components
src/components/common/VoteButton.tsx, src/hooks/useVote.ts
New VoteButton component with three size variants and optimistic UI updates; new useVotes hook managing per-review voting state with rollback on errors and concurrency guards.
Voting Integration & Display
src/components/courses/course_page/CoursePageReviews.tsx
Integrated VoteButton into review rendering, added per-review user vote state tracking, updated layout to two-column with vote button on right, and fetches user votes alongside reviews.
Voting API Endpoints
src/pages/api/ratings/vote/index.ts, src/pages/api/ratings/vote/route.ts
New Next.js API route supporting POST (cast/toggle vote), GET (fetch votes), and DELETE (remove vote) operations; removed legacy route handler.
Database & Utilities
src/migrations/migration.sql, src/utils/supabase/server-pages.ts, src/utils/supabase/server.ts
Added SQL index rename, enhanced security on vote functions, new server-side Supabase client utility for API routes, and awaited async cookie retrieval.
Route Parameter Updates
src/app/courses/[courseId]/page.tsx, src/app/auth/signin/page.tsx
Updated CoursePage to accept params as Promise via React "use" hook; uncommented magic link form in SignIn component.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant VB as VoteButton
    participant Hook as useVotes Hook
    participant API as Vote API
    participant DB as Database

    User->>VB: Click upvote/downvote
    VB->>Hook: castVote(reviewId, voteType)
    Hook->>Hook: Optimistic update<br/>(update votes & counts)
    Hook->>VB: Return immediately
    VB->>VB: Update UI display
    Hook->>API: POST /api/ratings/vote
    API->>DB: Check existing vote
    alt Vote already exists (same type)
        DB->>DB: Delete vote
        API->>API: Return action: 'removed'
    else Vote exists (different type)
        DB->>DB: Update vote_type
        API->>API: Return action: 'updated'
    else No vote exists
        DB->>DB: Insert new vote
        API->>API: Return action: 'created'
    end
    API->>Hook: Return response
    alt Server error
        Hook->>Hook: Rollback to<br/>previous state
        Hook->>VB: Revert UI
        Hook->>Hook: Show error toast
    else Success
        Hook->>Hook: Persist server state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • fix review #39 - Modifies src/components/courses/course_page/CoursePageReviews.tsx for review rendering, directly related to the voting integration changes in this PR.

Poem

🐰 A voting system hops into place,
With optimistic updates at breakneck pace!
From buttons that toggle to hooks that persist,
Reviews now embrace the democracy gist.
Next.js leaps forward, our config takes flight—
The course ratings are voting just right!

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: adding a voting/engagement system for reviews. It aligns with the comprehensive changes across frontend, backend, and database layers.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/migrations/migration.sql (1)

489-510: ⚠️ Potential issue | 🟠 Major

handle_new_user() is missing SET search_path = public — inconsistent with update_review_votes().

This function is declared SECURITY DEFINER (line 510) but does not pin search_path. A user who can manipulate search_path could shadow the public.users table or the extensions schema functions (gen_random_bytes, digest), causing the function to operate on attacker-controlled objects with elevated privileges. Apply the same safeguard as update_review_votes() (lines 259–260):

-$$ LANGUAGE plpgsql SECURITY DEFINER;
+$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;

The same concern applies to is_admin() (line 370) and get_anonymous_id() (line 381), which are also SECURITY DEFINER without SET search_path.

next.config.js (1)

7-17: ⚠️ Potential issue | 🟠 Major

Custom webpack config will cause next build to fail with Next.js 16's default Turbopack bundler.

The project uses Next.js 16.1.6, where Turbopack is the default for both next dev and next build. Having a custom webpack() config in next.config.js will cause the build to fail. Turbopack does not recognize or run webpack overrides.

Fix by choosing one option:

  • Add --webpack flag to build scripts in package.json to opt out of Turbopack and use webpack instead
  • Migrate the webpack config (suppressing @supabase/realtime-js warnings) to Turbopack equivalents
  • Remove the webpack config if no longer needed
package.json (1)

19-19: ⚠️ Potential issue | 🟠 Major

Update companion packages to match Next.js 16.1.6.

The project uses Next.js 16.1.6, but @next/swc-wasm-nodejs and eslint-config-next are pinned to 13.5.1, creating version conflicts. Both packages must be updated to 16.1.6 to maintain compatibility.

Proposed fix
-    "@next/swc-wasm-nodejs": "13.5.1",
+    "@next/swc-wasm-nodejs": "16.1.6",
-    "eslint-config-next": "13.5.1",
+    "eslint-config-next": "16.1.6",
🤖 Fix all issues with AI agents
In `@package.json`:
- Line 64: The project is using the React 19-only use() API but package.json
still pins React 18; update the dependency versions so React 19 is installed and
types match: bump "react" and "react-dom" to a 19.x range compatible with Next
16 (e.g. ^19.0.0 or newer), and also update the type packages "@types/react" and
"@types/react-dom" to matching 19.x versions; ensure these four entries are
changed together so the use(params) calls (e.g., in
src/app/courses/[courseId]/page.tsx) run against React 19.

In `@src/app/courses/`[courseId]/page.tsx:
- Around line 11-16: The component is using React's experimental use() hook (see
CoursePage and the use import) but the project is on React 18.2.0, so either
upgrade React to 19+ in package.json to keep use(), or remove use() and accept
params synchronously: change CoursePage signature to ({ params }: { params: {
courseId: string } }) and extract const { courseId } = params; remove the use
import, merge imports into a single React import if present, and delete the
empty comment artifact.

In `@src/components/common/VoteButton.tsx`:
- Around line 102-113: The server response handler updates only setCurrentVote
but leaves the displayed voteCount as the optimistic value; modify the success
path in the async response code handling (where setCurrentVote(...) is called)
to also reconcile the displayed count by reading the server-provided value
(e.g., data.new_vote_count or data.vote_count) and calling the state setter that
controls voteCount (the same state variable updated optimistically), falling
back to the existing optimistic calculation if the field is absent; also ensure
the onVote(reviewId, data.vote_type) callback continues to be invoked after
reconciliation.
- Around line 116-120: The rollback in the catch block uses render-time closure
values (currentVote, voteCount) which is fragile; move the saved snapshot
variables oldVote and oldCount out of the try block (declare them immediately
before the try) and change the catch to call setCurrentVote(oldVote) and
setVoteCount(oldCount) so the rollback uses the explicit saved state; update
references in the catch accordingly and keep the optimistic
setCurrentVote/setVoteCount logic inside the try.

In `@src/components/courses/course_page/CoursePageReviews.tsx`:
- Around line 96-109: The current vote fetch in the CoursePageReviews component
builds a long query string from reviewsData (reviewIds) and calls
fetch(`/api/ratings/vote?review_ids=...`), which can exceed URL length limits;
change this to send the IDs in the request body (POST) or batch the IDs into
smaller requests: update the fetch call to POST with JSON body { review_ids:
reviewsData.map(r => r.id) }, adjust the endpoint to accept POST if needed, or
implement chunking of reviewIds and aggregate responses before calling
setUserVotes; ensure error handling and that setUserVotes(votesData.votes || {})
is called after combining batched responses.
- Around line 50-56: The VoteButton is being passed initialVoteType={userVote as
any} and the component key includes userVote which forces unnecessary remounts;
fix by typing userVote in the CourseReviewItem props/signature to the proper
vote union (e.g., VoteType | null or 'up'|'down'|null) so the as any cast can be
removed when passing initialVoteType to VoteButton, and simplify the key to only
use review.id (or remove the custom key if the map already keys by id) so
VoteButton re-renders via props/state instead of being unmounted and remounted.

In `@src/hooks/useVote.ts`:
- Around line 1-332: The useVotes hook is dead code in this PR—neither useVotes
nor its functions (useVotes, castVote, removeVote, toggleVote, refreshVotes) are
consumed by VoteButton or CoursePageReviews; either remove the hook from this PR
or wire it up to replace the duplicated optimistic-vote logic. To fix: if you
want to drop it, delete the useVotes file and any exports/imports, and ensure
VoteButton and CoursePageReviews keep their current behavior; if you want to
adopt it, replace the inline vote logic in VoteButton and the direct-fetch in
CoursePageReviews to call useVotes (import useVotes, derive votes and voteCounts
from the hook, and call castVote/removeVote/toggleVote/refreshVotes), remove
duplicated optimistic/update code from those components, and ensure
props/callbacks use onVoteSuccess/onVoteError as needed.

In `@src/pages/api/ratings/vote/index.ts`:
- Around line 44-112: The current check-then-act flow around existingVote (the
.single() check) plus the subsequent delete/update/insert on
supabase.from('votes') is vulnerable to TOCTOU races; replace it with an atomic
server-side operation by implementing a Postgres function (e.g.,
toggle_vote(review_id UUID, anonymous_id TEXT, vote_type TEXT)) that
encapsulates the logic (check existing row, delete if same type, update if
different, insert if missing) and returns the resulting action and vote_type,
then call it via supabase.rpc('toggle_vote', { review_id, anonymous_id,
vote_type }); remove the separate .single()/delete/update/insert branches and
handle rpc errors/response instead to eliminate the race window.
- Around line 138-145: The review_ids splitting creates an unbounded
reviewIdArray used in the supabase .in('review_id', reviewIdArray) query;
enforce a max cap (e.g., 100) on the number of IDs before querying by validating
review_ids and either returning a 400 error if the client exceeds the limit or
truncating with a clear behavior choice; update the code around review_ids,
reviewIdArray and the supabase .in call to check Array.isArray(reviewIdArray)
length and respond accordingly (reject requests >100 IDs or slice to first 100)
and include a clear error message referencing the limit.

In `@src/utils/supabase/server-pages.ts`:
- Around line 16-19: The current setAll implementation calls
res.setHeader('Set-Cookie', ...) inside the forEach which overwrites previous
cookies so only the last cookie remains; change setAll (the function handling
cookiesToSet) to either accumulate serialized cookie strings into an array and
call res.setHeader('Set-Cookie', cookiesArray) once, or call
res.appendHeader('Set-Cookie', cookieString) for each cookie instead of
setHeader; also replace the manual serialization logic with a proper serializer
(e.g., use cookie.serialize) to ensure boolean flags like HttpOnly and Secure
are emitted correctly (reference the setAll function, cookiesToSet items with
{name, value, options}, and the res.setHeader/res.appendHeader calls).
🧹 Nitpick comments (7)
src/pages/api/ratings/vote/index.ts (1)

211-214: Missing Allow header on 405 response.

Per HTTP spec (RFC 9110), a 405 response should include an Allow header listing the permitted methods.

Proposed fix
   else {
+    res.setHeader('Allow', 'POST, GET, DELETE');
     return res.status(405).json({ error: 'Method not allowed' });
   }
src/utils/supabase/server-pages.ts (1)

4-7: Missing validation for required environment variables.

process.env.NEXT_PUBLIC_SUPABASE_URL! and process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! use non-null assertions. If either is missing at runtime, Supabase will receive undefined and produce confusing downstream errors. The same pattern exists in server.ts, but since this is a new file, consider adding a guard.

src/components/common/VoteButton.tsx (2)

164-173: Redundant branches in vote count styling.

The voteCount < 0 branch (line 170–171) and the final else branch (line 172) apply identical classes (text-gray-500 dark:text-gray-400). Simplify by collapsing them.

Suggested fix
           ${
             currentVote === 'helpful' 
               ? 'text-orange-500' 
               : currentVote === 'unhelpful'
               ? 'text-blue-500'
               : voteCount > 0 
               ? 'text-gray-700 dark:text-gray-300' 
-              : voteCount < 0
-              ? 'text-gray-500 dark:text-gray-400'
               : 'text-gray-500 dark:text-gray-400'
           }

36-55: Size config object is recreated on every render.

sizes is a static constant. Move it outside the component to avoid re-creating the object on each render.

src/components/courses/course_page/CoursePageReviews.tsx (1)

64-116: useVote hook is never used—VoteButton and CoursePageReviews each roll their own vote logic.

useVote.ts provides fetchUserVotes, optimistic updates, rollback, and concurrency guards, yet CoursePageReviews re-implements vote fetching inline and VoteButton manages its own optimistic state. This duplicates logic and increases the risk of behavioral divergence. Consider wiring useVotes() here to centralize vote state management, or remove the unused hook.

src/hooks/useVote.ts (2)

86-90: Suppressed exhaustive-deps warning hides a real dependency gap.

fetchUserVotes is omitted from the dependency array, and the eslint-disable masks this. If onVoteError (a dependency of fetchUserVotes) changes, the effect won't re-run with the updated callback. Either add fetchUserVotes to the dependency array (which is safe since it's memoized with useCallback), or acknowledge that onVoteError changes are intentionally ignored.

Suggested fix
   useEffect(() => {
     if (reviewIds.length > 0) {
       fetchUserVotes(reviewIds);
     }
-  }, [reviewIds.join(',')]); // eslint-disable-line react-hooks/exhaustive-deps
+  }, [reviewIds.join(','), fetchUserVotes]); // eslint-disable-line react-hooks/exhaustive-deps

Note: The eslint-disable is still needed for reviewIds.join(',') (a derived value), but at least fetchUserVotes is now tracked.


140-212: castVote and removeVote include votes and voteCounts as dependencies, causing callback identity to change on every state update.

Every time a vote is cast, votes/voteCounts state changes, which recreates castVote, removeVote, and toggleVote. This cascading recreation can trigger unnecessary re-renders in consuming components and cause stale references when these callbacks are passed as props.

Consider reading state via functional updater or a ref to decouple callback identity from state:

const votesRef = useRef(votes);
votesRef.current = votes;

Then read votesRef.current inside callbacks, removing votes from the dependency arrays. This keeps callback identities stable.

"input-otp": "^1.2.4",
"lucide-react": "^0.446.0",
"next": "13.5.1",
"next": "^16.1.6",
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:

# First, let's check the package.json file to see the current dependency versions
fd package.json | head -1 | xargs cat -n

Repository: OpenLake/RateMyCourse

Length of output: 3540


🏁 Script executed:

# Search for "use client" directives in the codebase
rg '"use client"' -t jsx -t ts -t tsx

Repository: OpenLake/RateMyCourse

Length of output: 92


🏁 Script executed:

# Search for the use() API pattern in the codebase (React 19)
rg '\buse\(' -t jsx -t ts -t tsx -A 2 | head -100

Repository: OpenLake/RateMyCourse

Length of output: 92


🌐 Web query:

Next.js 16 React version requirements minimum compatibility

💡 Result:

Next.js 16.x declares these React peer dependency requirements (so your app must install matching versions):

  • react: ^18.2.0 or ^19.0.0 (it also explicitly allows a specific React 19 RC build) [1]
  • react-dom: ^18.2.0 or ^19.0.0 (same RC allowance) [1]

So the minimum compatible React version is React 18.2.0 (with React 19 also supported). Next.js also recommends at least react@18.2.0 / react-dom@18.2.0. [2]

Sources: [1] [2]


🌐 Web query:

Next.js 16 upgrade guide React 19 requirement

💡 Result:

Next.js 16 expects React 19 (specifically, the App Router “uses the latest React Canary release,” highlighting React 19.2 features) and the official manual-upgrade path is to install react@latest / react-dom@latest alongside next@latest. [1][2]

Practical upgrade steps (v15 → v16):

  • Run the official upgrade codemod: npx @next/codemod@canary upgrade latest [1]
    or upgrade manually: npm install next@latest react@latest react-dom@latest (and update @types/react / @types/react-dom if you use TS). [1]
  • Ensure your runtime meets the new baseline: Node.js 20.9+ (Node 18 is not supported). [1][2]

What “React 19 requirement” means for you:

  • If your app or dependencies are still pinned to React 18, you’ll need to move them to React 19.x to upgrade cleanly to Next.js 16 (and verify third‑party library compatibility). [1][2]

Sources:
[1] Next.js “Upgrading: Version 16” guide (updated Feb 11, 2026) (nextjs.org)
[2] Next.js 16 release post (“React 19.2”, version requirements) (nextjs.org)

Citations:


🏁 Script executed:

# Search for "use client" directive without file type restriction
rg '"use client"' -A 1 | head -50

Repository: OpenLake/RateMyCourse

Length of output: 1878


🏁 Script executed:

# Search for use() function calls in the codebase (React 19 API)
rg '\buse\(' | head -50

Repository: OpenLake/RateMyCourse

Length of output: 136


🏁 Script executed:

# Also check if there are any imports of React hooks from React 19 specifically
rg 'from .react' | head -30

Repository: OpenLake/RateMyCourse

Length of output: 2069


🌐 Web query:

React use() API version introduced React 18 vs React 19

💡 Result:

  • React 19 (stable): The use(resource) API (imported as import { use } from 'react') is part of the React 19 API surface and is documented in the React 19.x reference. It lets you read from a Promise or Context during render and integrates with Suspense / Error Boundaries. [1]
  • React 18 (stable): React 18’s stable release introduced hooks like useId and useTransition (and concurrent rendering features), but it did not include the use() API as a stable export. [2]
  • React 18 canary/experimental (pre-19): The React team shipped “canary” builds so frameworks could adopt near-final features before the next major release. React 19 was explicitly positioned as the next major version to bring these newer capabilities to latest. [3][4]

Answer: use() is a React 19 API (stable in React 19.x), not a React 18 stable API; in the React 18 era it was only available via canary/experimental channels, not react@18.x stable. [1][3][4]

Sources:
[1] React API Reference: use (React v19.2) https://react.dev/reference/react/use
[2] React v18.0 blog post (lists new hooks; no use) https://react.dev/blog/2022/03/29/react-v18
[3] React Canaries announcement https://react.dev/blog/2023/05/03/react-canaries
[4] React Labs Feb 2024 (next major = React 19; canary → latest) https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024


The use(params) React 19 API requires upgrading to React 19.

The codebase uses the use() function to read async values (e.g., const { courseId } = use(params); in src/app/courses/[courseId]/page.tsx), which is a React 19 feature not available in React 18. This will fail at runtime with React 18.2.0.

Upgrade react, react-dom, and their type packages to React 19:

Proposed fix
-    "react": "18.2.0",
+    "react": "^19.1.0",
     "react-day-picker": "^8.10.1",
-    "react-dom": "18.2.0",
+    "react-dom": "^19.1.0",

Also update the type packages (lines 51–52):

-    "@types/react": "18.2.22",
-    "@types/react-dom": "18.2.7",
+    "@types/react": "^19.0.0",
+    "@types/react-dom": "^19.0.0",

Also applies to: 67-67, 69-69

🤖 Prompt for AI Agents
In `@package.json` at line 64, The project is using the React 19-only use() API
but package.json still pins React 18; update the dependency versions so React 19
is installed and types match: bump "react" and "react-dom" to a 19.x range
compatible with Next 16 (e.g. ^19.0.0 or newer), and also update the type
packages "@types/react" and "@types/react-dom" to matching 19.x versions; ensure
these four entries are changed together so the use(params) calls (e.g., in
src/app/courses/[courseId]/page.tsx) run against React 19.

Comment on lines +11 to +16
import { use } from 'react';
//

export default function CoursePage({ params }: { params: { courseId: string } }) {

export default function CoursePage({ params }: { params: Promise<{ courseId: string }> }) {
const { courseId } = use(params);
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

use() hook requires React 19 — will crash with current React 18.2.0.

The use hook was introduced in React 19. With "react": "18.2.0" in package.json, this component will throw a runtime error. This is a downstream effect of the React version mismatch flagged in package.json — upgrading React to 19+ will resolve this.

Also minor cleanup: merge the use import with the existing React import on Line 3, and remove the empty comment artifact on Line 12.

Import cleanup
-import { useEffect, useState } from "react";
+import { use, useEffect, useState } from "react";
 import { supabase } from "@/lib/supabase";
 import { useCourses } from "@/hooks/useCourses";
 import CoursePageHeader from "@/components/courses/course_page/CoursePageHeader";
 import CoursePageStats from "@/components/courses/course_page/CoursePageStats";
 import CoursePageReviews from "@/components/courses/course_page/CoursePageReviews";
 import RateThisCourse from "@/components/courses/course_page/RateThisCourse";
 import Example from "@/components/courses/course_page/CoursePageLoader";
-import { use } from 'react';
-// 
-
🤖 Prompt for AI Agents
In `@src/app/courses/`[courseId]/page.tsx around lines 11 - 16, The component is
using React's experimental use() hook (see CoursePage and the use import) but
the project is on React 18.2.0, so either upgrade React to 19+ in package.json
to keep use(), or remove use() and accept params synchronously: change
CoursePage signature to ({ params }: { params: { courseId: string } }) and
extract const { courseId } = params; remove the use import, merge imports into a
single React import if present, and delete the empty comment artifact.

Comment on lines +102 to +113
const data = await response.json();

if (!data.success) {
throw new Error(data.error || 'Failed to vote');
}

// Update with server response
setCurrentVote(data.vote_type);

// Callback
if (onVote) {
onVote(reviewId, data.vote_type);
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

Server vote count is never reconciled after a successful API call.

After a successful vote, only currentVote is updated from the server response (line 109), but voteCount still reflects the optimistic calculation. If the server's actual net count differs (e.g., concurrent voters), the displayed count will silently drift. Consider updating voteCount from data.new_vote_count (or equivalent field) if the API returns it, or at least document why reconciliation is intentionally skipped.

🤖 Prompt for AI Agents
In `@src/components/common/VoteButton.tsx` around lines 102 - 113, The server
response handler updates only setCurrentVote but leaves the displayed voteCount
as the optimistic value; modify the success path in the async response code
handling (where setCurrentVote(...) is called) to also reconcile the displayed
count by reading the server-provided value (e.g., data.new_vote_count or
data.vote_count) and calling the state setter that controls voteCount (the same
state variable updated optimistically), falling back to the existing optimistic
calculation if the field is absent; also ensure the onVote(reviewId,
data.vote_type) callback continues to be invoked after reconciliation.

Comment on lines +116 to +120
} catch (error) {
console.error('Error voting:', error);
// Rollback on error
setCurrentVote(currentVote);
setVoteCount(voteCount);
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

Rollback relies on stale closure values—correct but fragile and confusing.

currentVote and voteCount on lines 119–120 refer to the values captured at render time (i.e., before the optimistic setState calls on lines 88–89), so rollback accidentally works. However, a future refactor (e.g., using functional state updates elsewhere) could break this assumption. Use the explicitly saved oldVote/oldCount variables for clarity and resilience.

Suggested fix
     } catch (error) {
       console.error('Error voting:', error);
       // Rollback on error
-      setCurrentVote(currentVote);
-      setVoteCount(voteCount);
+      setCurrentVote(oldVote);
+      setVoteCount(oldCount);
     } finally {

Note: oldVote and oldCount are declared inside the try block (lines 63–64). Move them before the try so they're accessible in catch:

+    const oldVote = currentVote;
+    const oldCount = voteCount;
+
     try {
-      const oldVote = currentVote;
-      const oldCount = voteCount;
🤖 Prompt for AI Agents
In `@src/components/common/VoteButton.tsx` around lines 116 - 120, The rollback in
the catch block uses render-time closure values (currentVote, voteCount) which
is fragile; move the saved snapshot variables oldVote and oldCount out of the
try block (declare them immediately before the try) and change the catch to call
setCurrentVote(oldVote) and setVoteCount(oldCount) so the rollback uses the
explicit saved state; update references in the catch accordingly and keep the
optimistic setCurrentVote/setVoteCount logic inside the try.

Comment on lines +50 to +56
<VoteButton
key={`${review.id}-${userVote || 'no-vote'}`}
reviewId={review.id}
initialVoteType={userVote as any}
initialVoteCount={review.votes || 0}
size="md"
/>
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

Unsafe as any cast bypasses type safety; key-based remount is unnecessary.

Two issues in this block:

  1. Line 53: initialVoteType={userVote as any} silently accepts any string (e.g., "typo"), which could cause subtle bugs. Type userVote properly instead:

    In the CourseReviewItem signature (line 15), change:

    -const CourseReviewItem = ({ review, userVote }: { review: any; userVote?: string | null }) => {
    +const CourseReviewItem = ({ review, userVote }: { review: any; userVote?: VoteType }) => {

    Then as any becomes unnecessary:

    -initialVoteType={userVote as any}
    +initialVoteType={userVote}
  2. Line 51: The key that includes userVote forces a full unmount/remount of VoteButton whenever vote data loads. VoteButton already has useEffect hooks that sync on prop changes, so this key is redundant and causes unnecessary component recycling.

🤖 Prompt for AI Agents
In `@src/components/courses/course_page/CoursePageReviews.tsx` around lines 50 -
56, The VoteButton is being passed initialVoteType={userVote as any} and the
component key includes userVote which forces unnecessary remounts; fix by typing
userVote in the CourseReviewItem props/signature to the proper vote union (e.g.,
VoteType | null or 'up'|'down'|null) so the as any cast can be removed when
passing initialVoteType to VoteButton, and simplify the key to only use
review.id (or remove the custom key if the map already keys by id) so VoteButton
re-renders via props/state instead of being unmounted and remounted.

Comment on lines +96 to +109
// Fetch user's votes for these reviews
if (reviewsData && reviewsData.length > 0) {
const reviewIds = reviewsData.map(r => r.id).join(',');

try {
const response = await fetch(`/api/ratings/vote?review_ids=${reviewIds}`);
const votesData = await response.json();

if (votesData.success) {
setUserVotes(votesData.votes || {});
}
} catch (error) {
console.error("Error fetching user votes:", error);
}
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

URL length risk when passing many review IDs as a query parameter.

All review IDs are joined into a single review_ids query parameter (line 98). For courses with a large number of reviews, this could exceed browser or server URL length limits (~2,000–8,000 chars). Consider batching, using a POST request for fetching votes, or paginating reviews.

🤖 Prompt for AI Agents
In `@src/components/courses/course_page/CoursePageReviews.tsx` around lines 96 -
109, The current vote fetch in the CoursePageReviews component builds a long
query string from reviewsData (reviewIds) and calls
fetch(`/api/ratings/vote?review_ids=...`), which can exceed URL length limits;
change this to send the IDs in the request body (POST) or batch the IDs into
smaller requests: update the fetch call to POST with JSON body { review_ids:
reviewsData.map(r => r.id) }, adjust the endpoint to accept POST if needed, or
implement chunking of reviewIds and aggregate responses before calling
setUserVotes; ensure error handling and that setUserVotes(votesData.votes || {})
is called after combining batched responses.

Comment on lines +1 to +332
import { useState, useEffect, useCallback, useRef } from 'react';
import { toast } from 'sonner';

export type VoteType = 'helpful' | 'unhelpful' | null;

interface VoteState {
[reviewId: string]: VoteType;
}

interface VoteCounts {
[reviewId: string]: {
helpful: number;
unhelpful: number;
};
}

interface UseVotesOptions {
reviewIds?: string[];
initialCounts?: VoteCounts;
onVoteSuccess?: (reviewId: string, voteType: VoteType) => void;
onVoteError?: (error: Error) => void;
}

interface UseVotesReturn {
votes: VoteState;
voteCounts: VoteCounts;
isLoading: boolean;
castVote: (reviewId: string, voteType: 'helpful' | 'unhelpful') => Promise<void>;
removeVote: (reviewId: string) => Promise<void>;
toggleVote: (reviewId: string, voteType: 'helpful' | 'unhelpful') => Promise<void>;
getUserVote: (reviewId: string) => VoteType;
refreshVotes: (reviewIds?: string[]) => Promise<void>;
}

export function useVotes(options: UseVotesOptions = {}): UseVotesReturn {
const { reviewIds = [], initialCounts = {}, onVoteSuccess, onVoteError } = options;

const [votes, setVotes] = useState<VoteState>({});
const [voteCounts, setVoteCounts] = useState<VoteCounts>(initialCounts);
const [isLoading, setIsLoading] = useState(false);

// Track pending operations to prevent race conditions
const pendingOperations = useRef<Set<string>>(new Set());

// Cache to store previous states for rollback
const rollbackCache = useRef<Map<string, {
vote: VoteType;
counts: { helpful: number; unhelpful: number };
}>>(new Map());


//Fetch user votes for specified review IDs

const fetchUserVotes = useCallback(async (ids: string[]) => {
if (ids.length === 0) return;

try {
setIsLoading(true);
const response = await fetch(`/api/ratings/vote?review_ids=${ids.join(',')}`);

if (!response.ok) {
throw new Error('Failed to fetch votes');
}

const data = await response.json();

if (data.success) {
setVotes(prev => ({
...prev,
...data.votes,
}));
}
} catch (error) {
console.error('Error fetching votes:', error);
if (onVoteError) {
onVoteError(error instanceof Error ? error : new Error('Failed to fetch votes'));
}
} finally {
setIsLoading(false);
}
}, [onVoteError]);


//Initial fetch on mount or when reviewIds change

useEffect(() => {
if (reviewIds.length > 0) {
fetchUserVotes(reviewIds);
}
}, [reviewIds.join(',')]); // eslint-disable-line react-hooks/exhaustive-deps


//Update vote counts optimistically

const updateVoteCountsOptimistically = useCallback(
(reviewId: string, oldVote: VoteType, newVote: VoteType) => {
setVoteCounts(prev => {
const current = prev[reviewId] || { helpful: 0, unhelpful: 0 };
let helpful = current.helpful;
let unhelpful = current.unhelpful;

// Remove old vote
if (oldVote === 'helpful') helpful--;
if (oldVote === 'unhelpful') unhelpful--;

// Add new vote
if (newVote === 'helpful') helpful++;
if (newVote === 'unhelpful') unhelpful++;

return {
...prev,
[reviewId]: { helpful, unhelpful },
};
});
},
[]
);


//Rollback optimistic updates on error

const rollbackVote = useCallback((reviewId: string) => {
const cached = rollbackCache.current.get(reviewId);
if (cached) {
setVotes(prev => ({
...prev,
[reviewId]: cached.vote,
}));
setVoteCounts(prev => ({
...prev,
[reviewId]: cached.counts,
}));
rollbackCache.current.delete(reviewId);
}
}, []);


//Cast or update a vote

const castVote = useCallback(
async (reviewId: string, voteType: 'helpful' | 'unhelpful') => {
// Prevent concurrent operations on the same review
if (pendingOperations.current.has(reviewId)) {
return;
}

pendingOperations.current.add(reviewId);

try {
const oldVote = votes[reviewId] || null;
const oldCounts = voteCounts[reviewId] || { helpful: 0, unhelpful: 0 };

// Save state for potential rollback
rollbackCache.current.set(reviewId, {
vote: oldVote,
counts: oldCounts,
});

// Optimistic update
setVotes(prev => ({
...prev,
[reviewId]: voteType,
}));
updateVoteCountsOptimistically(reviewId, oldVote, voteType);

// API call
const response = await fetch('/api/ratings/vote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ review_id: reviewId, vote_type: voteType }),
});

if (!response.ok) {
throw new Error('Failed to cast vote');
}

const data = await response.json();

if (!data.success) {
throw new Error(data.error || 'Failed to cast vote');
}

// Update state based on server response
setVotes(prev => ({
...prev,
[reviewId]: data.vote_type,
}));

// Clear rollback cache on success
rollbackCache.current.delete(reviewId);

if (onVoteSuccess) {
onVoteSuccess(reviewId, data.vote_type);
}

} catch (error) {
console.error('Error casting vote:', error);

// Rollback on error
rollbackVote(reviewId);

toast.error('Failed to cast vote. Please try again.');

if (onVoteError) {
onVoteError(error instanceof Error ? error : new Error('Failed to cast vote'));
}
} finally {
pendingOperations.current.delete(reviewId);
}
},
[votes, voteCounts, updateVoteCountsOptimistically, rollbackVote, onVoteSuccess, onVoteError]
);

//Remove a vote
const removeVote = useCallback(
async (reviewId: string) => {
if (pendingOperations.current.has(reviewId)) {
return;
}

pendingOperations.current.add(reviewId);

try {
const oldVote = votes[reviewId] || null;
const oldCounts = voteCounts[reviewId] || { helpful: 0, unhelpful: 0 };

// Save state for rollback
rollbackCache.current.set(reviewId, {
vote: oldVote,
counts: oldCounts,
});

// Optimistic update
setVotes(prev => ({
...prev,
[reviewId]: null,
}));
updateVoteCountsOptimistically(reviewId, oldVote, null);

// API call
const response = await fetch('/api/ratings/vote', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ review_id: reviewId }),
});

if (!response.ok) {
throw new Error('Failed to remove vote');
}

const data = await response.json();

if (!data.success) {
throw new Error(data.error || 'Failed to remove vote');
}

// Clear rollback cache
rollbackCache.current.delete(reviewId);

if (onVoteSuccess) {
onVoteSuccess(reviewId, null);
}

} catch (error) {
console.error('Error removing vote:', error);

// Rollback
rollbackVote(reviewId);

toast.error('Failed to remove vote. Please try again.');

if (onVoteError) {
onVoteError(error instanceof Error ? error : new Error('Failed to remove vote'));
}
} finally {
pendingOperations.current.delete(reviewId);
}
},
[votes, voteCounts, updateVoteCountsOptimistically, rollbackVote, onVoteSuccess, onVoteError]
);

//Toggle vote - if same type, remove; otherwise update

const toggleVote = useCallback(
async (reviewId: string, voteType: 'helpful' | 'unhelpful') => {
const currentVote = votes[reviewId];

if (currentVote === voteType) {
// Same vote - remove it
await removeVote(reviewId);
} else {
// Different vote or no vote - cast it
await castVote(reviewId, voteType);
}
},
[votes, castVote, removeVote]
);

/**
* Get user's vote for a specific review
*/
const getUserVote = useCallback(
(reviewId: string): VoteType => {
return votes[reviewId] || null;
},
[votes]
);

/**
* Manually refresh votes
*/
const refreshVotes = useCallback(
async (ids?: string[]) => {
const idsToFetch = ids || reviewIds;
if (idsToFetch.length > 0) {
await fetchUserVotes(idsToFetch);
}
},
[reviewIds, fetchUserVotes]
);

return {
votes,
voteCounts,
isLoading,
castVote,
removeVote,
toggleVote,
getUserVote,
refreshVotes,
};
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

This hook is entirely unused in the current PR.

Neither VoteButton nor CoursePageReviews consume useVotes. VoteButton manages its own optimistic state inline, and CoursePageReviews fetches votes directly. This hook is dead code as shipped. If it's intended for future use, consider removing it from this PR to reduce review surface and merge risk, or wire it up now to avoid the duplicated logic in the other two files.

🤖 Prompt for AI Agents
In `@src/hooks/useVote.ts` around lines 1 - 332, The useVotes hook is dead code in
this PR—neither useVotes nor its functions (useVotes, castVote, removeVote,
toggleVote, refreshVotes) are consumed by VoteButton or CoursePageReviews;
either remove the hook from this PR or wire it up to replace the duplicated
optimistic-vote logic. To fix: if you want to drop it, delete the useVotes file
and any exports/imports, and ensure VoteButton and CoursePageReviews keep their
current behavior; if you want to adopt it, replace the inline vote logic in
VoteButton and the direct-fetch in CoursePageReviews to call useVotes (import
useVotes, derive votes and voteCounts from the hook, and call
castVote/removeVote/toggleVote/refreshVotes), remove duplicated
optimistic/update code from those components, and ensure props/callbacks use
onVoteSuccess/onVoteError as needed.

Comment on lines +44 to +112
const { data: existingVote, error: checkError } = await supabase
.from('votes')
.select('id, vote_type')
.eq('review_id', review_id)
.eq('anonymous_id', anonymous_id)
.single();

if (checkError && checkError.code !== 'PGRST116') {
console.error('Error checking existing vote:', checkError);
return res.status(500).json({ error: 'Failed to check existing vote' });
}

// Case 1: Same vote type - remove vote (toggle off)
if (existingVote && existingVote.vote_type === vote_type) {
const { error: deleteError } = await supabase
.from('votes')
.delete()
.eq('id', existingVote.id);

if (deleteError) {
console.error('Error deleting vote:', deleteError);
return res.status(500).json({ error: 'Failed to remove vote' });
}

return res.status(200).json({
success: true,
action: 'removed',
vote_type: null,
});
}

// Case 2: Different vote type - update vote
if (existingVote && existingVote.vote_type !== vote_type) {
const { error: updateError } = await supabase
.from('votes')
.update({ vote_type, created_at: new Date().toISOString() })
.eq('id', existingVote.id);

if (updateError) {
console.error('Error updating vote:', updateError);
return res.status(500).json({ error: 'Failed to update vote' });
}

return res.status(200).json({
success: true,
action: 'updated',
vote_type,
});
}

// Case 3: New vote - insert
const { error: insertError } = await supabase
.from('votes')
.insert({
review_id,
anonymous_id,
vote_type,
});

if (insertError) {
console.error('Error inserting vote:', insertError);
return res.status(500).json({ error: 'Failed to cast vote' });
}

return res.status(200).json({
success: true,
action: 'created',
vote_type,
});
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

TOCTOU race condition on the check-then-act vote logic.

Between lines 44–49 (checking the existing vote with .single()) and the subsequent delete/update/insert (lines 57–106), a concurrent request from the same user can interleave and produce unexpected results (e.g., duplicate insert attempts or operating on stale data).

The unique constraint on (review_id, anonymous_id) prevents duplicate inserts (good), but the toggle and update paths still operate on data that could be stale. Consider using an UPSERT with ON CONFLICT logic or a Supabase RPC/database function that atomically handles the vote toggle to eliminate the race window.

🤖 Prompt for AI Agents
In `@src/pages/api/ratings/vote/index.ts` around lines 44 - 112, The current
check-then-act flow around existingVote (the .single() check) plus the
subsequent delete/update/insert on supabase.from('votes') is vulnerable to
TOCTOU races; replace it with an atomic server-side operation by implementing a
Postgres function (e.g., toggle_vote(review_id UUID, anonymous_id TEXT,
vote_type TEXT)) that encapsulates the logic (check existing row, delete if same
type, update if different, insert if missing) and returns the resulting action
and vote_type, then call it via supabase.rpc('toggle_vote', { review_id,
anonymous_id, vote_type }); remove the separate .single()/delete/update/insert
branches and handle rpc errors/response instead to eliminate the race window.

Comment on lines +138 to +145
const reviewIdArray = review_ids.split(',').map(id => id.trim());

// Batch fetch votes
const { data: votes, error: fetchError } = await supabase
.from('votes')
.select('review_id, vote_type')
.eq('anonymous_id', anonymous_id)
.in('review_id', reviewIdArray);
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

Unbounded review_ids array — no limit on query size.

The review_ids parameter is split by comma with no upper bound, allowing a client to send thousands of IDs in one request. This generates a potentially very large SQL IN clause that can degrade database performance or exceed query limits.

Consider adding a reasonable cap (e.g., 100 IDs):

Proposed fix
       const anonymous_id = anonData;
       const reviewIdArray = review_ids.split(',').map(id => id.trim());
+
+      if (reviewIdArray.length > 100) {
+        return res.status(400).json({ error: 'Too many review_ids. Max 100 allowed.' });
+      }

       // Batch fetch votes
📝 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 reviewIdArray = review_ids.split(',').map(id => id.trim());
// Batch fetch votes
const { data: votes, error: fetchError } = await supabase
.from('votes')
.select('review_id, vote_type')
.eq('anonymous_id', anonymous_id)
.in('review_id', reviewIdArray);
const reviewIdArray = review_ids.split(',').map(id => id.trim());
if (reviewIdArray.length > 100) {
return res.status(400).json({ error: 'Too many review_ids. Max 100 allowed.' });
}
// Batch fetch votes
const { data: votes, error: fetchError } = await supabase
.from('votes')
.select('review_id, vote_type')
.eq('anonymous_id', anonymous_id)
.in('review_id', reviewIdArray);
🤖 Prompt for AI Agents
In `@src/pages/api/ratings/vote/index.ts` around lines 138 - 145, The review_ids
splitting creates an unbounded reviewIdArray used in the supabase
.in('review_id', reviewIdArray) query; enforce a max cap (e.g., 100) on the
number of IDs before querying by validating review_ids and either returning a
400 error if the client exceeds the limit or truncating with a clear behavior
choice; update the code around review_ids, reviewIdArray and the supabase .in
call to check Array.isArray(reviewIdArray) length and respond accordingly
(reject requests >100 IDs or slice to first 100) and include a clear error
message referencing the limit.

Comment on lines +16 to +19
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
res.setHeader('Set-Cookie', `${name}=${value}; Path=/; ${options ? Object.entries(options).map(([k, v]) => `${k}=${v}`).join('; ') : ''}`)
})
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

res.setHeader('Set-Cookie', ...) overwrites previous cookies — only the last cookie survives.

Inside the forEach, each call to res.setHeader('Set-Cookie', ...) replaces the previous value. Supabase auth typically sets multiple cookies (access token, refresh token), so all but the last will be silently dropped, breaking authentication.

Use res.appendHeader or accumulate all cookies and set them as an array:

Proposed fix (array approach)
         setAll(cookiesToSet) {
-          cookiesToSet.forEach(({ name, value, options }) => {
-            res.setHeader('Set-Cookie', `${name}=${value}; Path=/; ${options ? Object.entries(options).map(([k, v]) => `${k}=${v}`).join('; ') : ''}`)
-          })
+          const cookies = cookiesToSet.map(({ name, value, options }) => {
+            const optionStr = options
+              ? Object.entries(options)
+                  .map(([k, v]) => (v === true ? k : `${k}=${v}`))
+                  .join('; ')
+              : '';
+            return `${name}=${value}; Path=/;${optionStr ? ` ${optionStr}` : ''}`;
+          });
+          res.setHeader('Set-Cookie', cookies);
         },

Additionally, the manual cookie serialization is fragile — boolean flags like HttpOnly and Secure are incorrectly serialized as HttpOnly=true instead of just HttpOnly. Consider using a proper cookie serialization library (e.g., the cookie package) for correctness.

📝 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
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
res.setHeader('Set-Cookie', `${name}=${value}; Path=/; ${options ? Object.entries(options).map(([k, v]) => `${k}=${v}`).join('; ') : ''}`)
})
setAll(cookiesToSet) {
const cookies = cookiesToSet.map(({ name, value, options }) => {
const optionStr = options
? Object.entries(options)
.map(([k, v]) => (v === true ? k : `${k}=${v}`))
.join('; ')
: '';
return `${name}=${value}; Path=/;${optionStr ? ` ${optionStr}` : ''}`;
});
res.setHeader('Set-Cookie', cookies);
}
🤖 Prompt for AI Agents
In `@src/utils/supabase/server-pages.ts` around lines 16 - 19, The current setAll
implementation calls res.setHeader('Set-Cookie', ...) inside the forEach which
overwrites previous cookies so only the last cookie remains; change setAll (the
function handling cookiesToSet) to either accumulate serialized cookie strings
into an array and call res.setHeader('Set-Cookie', cookiesArray) once, or call
res.appendHeader('Set-Cookie', cookieString) for each cookie instead of
setHeader; also replace the manual serialization logic with a proper serializer
(e.g., use cookie.serialize) to ensure boolean flags like HttpOnly and Secure
are emitted correctly (reference the setAll function, cookiesToSet items with
{name, value, options}, and the res.setHeader/res.appendHeader calls).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant