Skip to content

solved potential issue#5

Merged
Remi561 merged 1 commit into
mainfrom
features/connected-to-frontend
Jun 23, 2026
Merged

solved potential issue#5
Remi561 merged 1 commit into
mainfrom
features/connected-to-frontend

Conversation

@Remi561

@Remi561 Remi561 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added Admin dashboard for managing users and viewing platform statistics.
    • Added Settings page for account information and logout functionality.
    • Implemented role-based navigation with admin-only menu items.
    • Added 404 error page for unmatched routes.
  • Improvements

    • Enhanced History page with better filtering, pagination, and date range support.
    • Improved error handling and loading states across dashboard.
    • Made CORS configuration more flexible for deployment.

@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
subly Ready Ready Preview, Comment Jun 23, 2026 8:23am

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds an admin role enforcement layer (route loader, nav filtering, Admin dashboard page), fully implements the History and Settings pages, adds a NotFound page and wildcard routes, extracts an API base URL helper, hardens server CORS with a dynamic allowlist, rewrites the history API controller with stricter filtering, and wraps subscription edit/renew in Prisma transactions with typed history records.

Changes

Admin Role, New Pages, and Server Hardening

Layer / File(s) Summary
API base URL helper, env config, and CORS allowlist
client/src/lib/utils.js, client/src/lib/action.js, client/.env, server/src/config/env.js, server/server.js
Extracts getApiBaseUrl() and updates all callers to use it; removes the hardcoded VITE_API_URL env value; adds CLIENT_URL/CORS_ORIGINS to server env; rewrites CORS middleware to use a normalized Set-based allowlist.
dashboardLoader redirect fix and adminLoader guard
client/src/lib/loader.js
dashboardLoader catch now returns the redirect. New adminLoader delegates to dashboardLoader, propagates its Response, and redirects non-admin users to /dashboard.
Role-based nav filtering and Admin navLink
client/src/lib/var.js, client/src/components/Navbar.jsx, client/src/components/Sidebar.jsx
Adds adminOnly: true Admin entry to navLinks. Both Navbar and Sidebar read loader data via useRouteLoaderData("dashboard") and filter nav items by user role before rendering.
Admin page, NotFound page, and route wiring
client/src/pages/dashboard/Admin.jsx, client/src/pages/NotFound.jsx, client/src/main.jsx
Implements the Admin page with stats/users queries and a promote/demote mutation. Adds a 404 NotFound page. Wires admin, settings, dashboard *, and top-level * routes.
History page full implementation
client/src/pages/dashboard/History.jsx
Replaces the placeholder with a full History page: HistoryTypeBadge, URL-backed filter/pagination state, debounced search, useQuery fetching, formatted table, and PaginationList.
Settings page full implementation
client/src/pages/dashboard/Settings.jsx
Replaces the placeholder with a complete Settings page: displays user info from loader data, logout mutation with cache clearing, toasts, and redirect.
Subscriptions error state and Chart empty state
client/src/pages/dashboard/Subscriptions.jsx, client/src/components/dashboard/Chart.jsx
Subscriptions adds isError render path and keepPreviousData. SpendingByCategoryChart guards categoryTotal/grandTotal and adds an explicit empty-state message.
Server history controller rewrite
server/src/controllers/history.controller.js
Adds type/category validation sets, clamps page/limit, normalizes range to filter on paidAt, switches search param name, filters by subscription.userId, sorts by paidAt, and includes limit in response metadata.
Subscription edit/renew transactional history
server/src/controllers/subs.controller.js
Moves baseCurrency init to handler top, wraps subscription update and history.create in prisma.$transaction with type: "EDITED", and sets type: "RENEWED" for renewal history.
UI component import cleanup
client/src/components/ui/...
Removes unused import * as React from "react" from alert-dialog, breadcrumb, card, input, label, pagination, select, separator, table; replaces React import in badge/button with an ESLint disable comment; adds "use client" to separator.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant adminLoader
  participant dashboardLoader
  participant Admin as Admin Page
  participant API as Express API

  Browser->>adminLoader: navigate /dashboard/admin
  adminLoader->>dashboardLoader: await dashboardLoader()
  dashboardLoader->>API: GET /api/me
  alt fetch fails
    dashboardLoader-->>Browser: redirect /auth/login
  end
  dashboardLoader-->>adminLoader: { user }
  alt user.role !== ADMIN
    adminLoader-->>Browser: redirect /dashboard
  end
  adminLoader-->>Admin: render
  Admin->>API: GET /api/admin/stats
  Admin->>API: GET /api/admin/users
  API-->>Admin: stats + users list
  Admin->>API: PATCH /api/admin/promote|demote/:userId
  API-->>Admin: { message }
  Admin-->>Browser: invalidate queries, show toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop hop, the admin gate is here,
A shield on the nav makes the role clear.
History filters sorted by paid,
Transactions wrapped so none shall fade.
CORS now checks a tidy Set,
The cleanest dashboard you've seen yet! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'solved potential issue' is vague and does not convey meaningful information about the substantial changes across client and server components. Use a more specific title that captures the main change, such as 'Add role-based admin navigation and page routing' or 'Implement admin dashboard and role-based access control'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch features/connected-to-frontend

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
server/src/controllers/subs.controller.js (1)

500-526: 🎯 Functional Correctness | 🟠 Major

Return the updated entity from the transaction callback.

At line 500, the transaction callback never returns subscription, so updatedSubscription is undefined and line 525 sends subscription: undefined.

Suggested fix
     const updatedSubscription = await prisma.$transaction(async (tx) => {
       const subscription = await tx.subscription.update({
         where: {
           id: existingSubscription.id,
         },
         data: updateData,
       });
       await tx.history.create({
         data: {
           subscriptionId: subscription.id,
           subscriptionName: subscription.name,
           subscriptionLogoUrl: subscription.logoUrl,
           paidAmount: subscription.amount,
           paidCurrency: subscription.currency,
           baseCurrency,
           exchangeRate: subscription.exchangeRate,
           type: "EDITED",
           category: subscription.category,
           settledAmount: subscription.settledAmount,
         },
       });
+      return subscription;
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/controllers/subs.controller.js` around lines 500 - 526, The
transaction callback in the prisma.$transaction call does not return the updated
subscription object. After the tx.history.create() call completes, add a return
statement to return the subscription variable from the callback so that
updatedSubscription receives the actual subscription data instead of undefined,
which will allow the response to correctly include the updated subscription in
line 525.
🧹 Nitpick comments (1)
client/src/pages/dashboard/Subscriptions.jsx (1)

23-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove debug console.log statement.

This debug logging should be removed before merging to production.

🧹 Proposed cleanup
-  console.log("User data in Subscriptions:", userData);
-
   const { data, isLoading, isError, error } = useQuery({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/pages/dashboard/Subscriptions.jsx` at line 23, Remove the debug
console.log statement that logs "User data in Subscriptions:" from the
Subscriptions component. This statement is unnecessary for production code and
should be deleted entirely to clean up the codebase before merging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/lib/loader.js`:
- Around line 10-13: The catch block in the loader redirects to `/auth/login`
for all errors regardless of type, but this incorrectly treats transient backend
failures (like 5xx errors from `/api/me`) as auth failures. Modify the catch
block to check the error status before redirecting to login, only redirect if
the error indicates an actual authentication failure (such as 401 or 403 status
codes). Additionally, ensure the `fetchWithAuth` function attaches a status
property to thrown errors so the error handler can reliably determine the
failure type and handle non-auth errors appropriately without redirecting to the
login page.

In `@client/src/lib/utils.js`:
- Around line 9-12: The getApiBaseUrl function returns a hardcoded fallback of
http://localhost:3000, but the backend server defaults to port 3001 according to
server/src/config/env.js. When VITE_API_URL is unset during local development,
API calls will fail because they target the wrong port. Update the fallback URL
in the getApiBaseUrl function to use port 3001 instead of 3000 to align with the
server's default configuration.

In `@client/src/pages/dashboard/History.jsx`:
- Around line 67-83: The issue is that the useEffect hook unconditionally resets
the page parameter to 1 whenever debouncedValue changes, which destroys deep
links like `?page=3` on initial load. Additionally, inputValue can drift from
the URL search parameter when navigation occurs. To fix this, modify the
setSearchParams logic to only reset page to 1 when the search query actually
transitions from empty to non-empty or vice versa (comparing previous and
current debouncedValue states), rather than on every change. Also ensure
inputValue stays synchronized with the searchQuery from the URL by initializing
it correctly and adding logic to update it when searchQuery from the URL
parameters changes.

In `@client/src/pages/dashboard/Settings.jsx`:
- Around line 42-56: In the logoutMutation's mutationFn, the response status
must be checked before attempting to parse the JSON body. Move the response.ok
check before the response.json() call so that if the server returns a non-OK
status with non-JSON content (like an HTML error page), the parsing error is
avoided and your custom error message logic executes properly instead.

In `@client/src/pages/dashboard/Subscriptions.jsx`:
- Around line 61-63: The error message fallback text in the Subscriptions page
is incorrect. In the error display block where it shows error.message || "Unable
to load history.", change the fallback string from "Unable to load history." to
"Unable to load subscriptions." to match the actual context of the Subscriptions
page.
- Around line 70-76: The PaginationList component is rendering even when data is
undefined during error states, which causes both currentPage and totalPages to
be undefined. Add a conditional check before rendering the PaginationList div to
ensure it only renders when data is successfully loaded and there are no errors.
Check both the isError state and that data exists with valid pagination
properties before rendering the entire pagination section containing the
PaginationList component.
- Around line 16-22: The handlePageChange function is mutating the searchParams
object directly by calling set() on it before passing it to setSearchParams,
which violates React's immutability principle. Instead, create a new
URLSearchParams instance from the existing searchParams, set the "page" value on
the new instance, and then pass this new instance to setSearchParams. This
ensures the original state object is not mutated and allows React to properly
detect the state change.

In `@server/server.js`:
- Around line 35-38: The allowedOrigins Set is unconditionally whitelisting
localhost origins for all environments, including production. Modify the
allowedOrigins initialization to only include localhost origins when the
environment is not production, such as by checking process.env.NODE_ENV or a
similar environment variable. Localhost origins like http://localhost:5173 and
http://127.0.0.1:5173 should be added conditionally within the Set constructor
or immediately after, only when running in development or non-production
environments.

In `@server/src/controllers/history.controller.js`:
- Around line 17-79: The code at the search parameter validation block calls
`.trim()` on the search variable without verifying it is a string first. Query
parameters can be arrays when repeated in the URL (e.g., ?search=a&search=b
becomes ['a', 'b']), which causes a TypeError when calling .trim() on an array.
Add a type check using typeof search === 'string' before calling .trim() in the
condition that checks if search is truthy to ensure only string values are
processed and prevent runtime 500 errors from user-controlled input.

---

Outside diff comments:
In `@server/src/controllers/subs.controller.js`:
- Around line 500-526: The transaction callback in the prisma.$transaction call
does not return the updated subscription object. After the tx.history.create()
call completes, add a return statement to return the subscription variable from
the callback so that updatedSubscription receives the actual subscription data
instead of undefined, which will allow the response to correctly include the
updated subscription in line 525.

---

Nitpick comments:
In `@client/src/pages/dashboard/Subscriptions.jsx`:
- Line 23: Remove the debug console.log statement that logs "User data in
Subscriptions:" from the Subscriptions component. This statement is unnecessary
for production code and should be deleted entirely to clean up the codebase
before merging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae4241bf-a436-46cf-95df-95f33a14b212

📥 Commits

Reviewing files that changed from the base of the PR and between b88f638 and ce93962.

📒 Files selected for processing (29)
  • client/.env
  • client/src/components/Navbar.jsx
  • client/src/components/Sidebar.jsx
  • client/src/components/dashboard/Chart.jsx
  • client/src/components/ui/alert-dialog.jsx
  • client/src/components/ui/badge.jsx
  • client/src/components/ui/breadcrumb.jsx
  • client/src/components/ui/button.jsx
  • client/src/components/ui/card.jsx
  • client/src/components/ui/input.jsx
  • client/src/components/ui/label.jsx
  • client/src/components/ui/pagination.jsx
  • client/src/components/ui/select.jsx
  • client/src/components/ui/separator.jsx
  • client/src/components/ui/table.jsx
  • client/src/lib/action.js
  • client/src/lib/loader.js
  • client/src/lib/utils.js
  • client/src/lib/var.js
  • client/src/main.jsx
  • client/src/pages/NotFound.jsx
  • client/src/pages/dashboard/Admin.jsx
  • client/src/pages/dashboard/History.jsx
  • client/src/pages/dashboard/Settings.jsx
  • client/src/pages/dashboard/Subscriptions.jsx
  • server/server.js
  • server/src/config/env.js
  • server/src/controllers/history.controller.js
  • server/src/controllers/subs.controller.js
💤 Files with no reviewable changes (10)
  • client/src/components/ui/breadcrumb.jsx
  • client/src/components/ui/separator.jsx
  • client/src/components/ui/table.jsx
  • client/.env
  • client/src/components/ui/pagination.jsx
  • client/src/components/ui/input.jsx
  • client/src/components/ui/alert-dialog.jsx
  • client/src/components/ui/select.jsx
  • client/src/components/ui/label.jsx
  • client/src/components/ui/card.jsx

Comment thread client/src/lib/loader.js
Comment on lines 10 to 13
} catch (err) {
console.error("Error fetching user data:", err);
redirect("/auth/login"); // Redirect to login page if unauthorized or on error
return redirect("/auth/login");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop redirecting to /auth/login for non-auth failures.

At Line 12, every caught error redirects to login. Since fetchWithAuth throws on any non-OK status, transient backend failures (e.g. /api/me 5xx) are treated as auth failures and force incorrect redirects.

💡 Suggested fix
 export async function dashboardLoader() {
   try {
     const response = await fetchWithAuth("/api/me");
     return await response.json();
   } catch (err) {
     console.error("Error fetching user data:", err);
-    return redirect("/auth/login");
+    if (err?.status === 401 || err?.status === 403) {
+      return redirect("/auth/login");
+    }
+    throw err;
    }
 }

Also make sure fetchWithAuth attaches status to thrown errors so this branch is reliable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/lib/loader.js` around lines 10 - 13, The catch block in the loader
redirects to `/auth/login` for all errors regardless of type, but this
incorrectly treats transient backend failures (like 5xx errors from `/api/me`)
as auth failures. Modify the catch block to check the error status before
redirecting to login, only redirect if the error indicates an actual
authentication failure (such as 401 or 403 status codes). Additionally, ensure
the `fetchWithAuth` function attaches a status property to thrown errors so the
error handler can reliably determine the failure type and handle non-auth errors
appropriately without redirecting to the login page.

Comment thread client/src/lib/utils.js
Comment on lines +9 to +12
export function getApiBaseUrl() {
return import.meta.env.VITE_API_URL || "http://localhost:3000";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align fallback API port with server default to avoid local breakage.

Line 10 defaults to http://localhost:3000, but server/src/config/env.js defaults the backend to port 3001 (Line 6). If VITE_API_URL is unset, calls routed via Line 148 will target the wrong port and fail.

🔧 Proposed fix
 export function getApiBaseUrl() {
-  return import.meta.env.VITE_API_URL || "http://localhost:3000";
+  return import.meta.env.VITE_API_URL || "http://localhost:3001";
 }

Also applies to: 148-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/lib/utils.js` around lines 9 - 12, The getApiBaseUrl function
returns a hardcoded fallback of http://localhost:3000, but the backend server
defaults to port 3001 according to server/src/config/env.js. When VITE_API_URL
is unset during local development, API calls will fail because they target the
wrong port. Update the fallback URL in the getApiBaseUrl function to use port
3001 instead of 3000 to align with the server's default configuration.

Comment on lines +67 to +83
const [inputValue, setInputValue] = useState(searchQuery);
const [debouncedValue] = useDebounce(inputValue, 500);

useEffect(() => {
setSearchParams(
(prevParams) => {
if (debouncedValue) {
prevParams.set("search", debouncedValue);
} else {
prevParams.delete("search");
}
prevParams.set("page", 1);
return prevParams;
},
{ replace: true },
);
}, [debouncedValue, setSearchParams]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve URL state instead of forcing page=1 on initial render.

Line 70 always rewrites params and resets pagination, so deep links like ?page=3 get collapsed to page 1 on load. Also, inputValue (Line 67) can drift from URL search when navigation changes search params.

Suggested fix
   const [inputValue, setInputValue] = useState(searchQuery);
   const [debouncedValue] = useDebounce(inputValue, 500);
 
+  useEffect(() => {
+    setInputValue(searchQuery);
+  }, [searchQuery]);
+
   useEffect(() => {
+    if (debouncedValue === searchQuery) return;
     setSearchParams(
       (prevParams) => {
+        const nextParams = new URLSearchParams(prevParams);
         if (debouncedValue) {
-          prevParams.set("search", debouncedValue);
+          nextParams.set("search", debouncedValue);
         } else {
-          prevParams.delete("search");
+          nextParams.delete("search");
         }
-        prevParams.set("page", 1);
-        return prevParams;
+        nextParams.set("page", "1");
+        return nextParams;
       },
       { replace: true },
     );
-  }, [debouncedValue, setSearchParams]);
+  }, [debouncedValue, searchQuery, setSearchParams]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/pages/dashboard/History.jsx` around lines 67 - 83, The issue is
that the useEffect hook unconditionally resets the page parameter to 1 whenever
debouncedValue changes, which destroys deep links like `?page=3` on initial
load. Additionally, inputValue can drift from the URL search parameter when
navigation occurs. To fix this, modify the setSearchParams logic to only reset
page to 1 when the search query actually transitions from empty to non-empty or
vice versa (comparing previous and current debouncedValue states), rather than
on every change. Also ensure inputValue stays synchronized with the searchQuery
from the URL by initializing it correctly and adding logic to update it when
searchQuery from the URL parameters changes.

Comment on lines +42 to +56
const logoutMutation = useMutation({
mutationFn: async () => {
const response = await fetch(`${getApiBaseUrl()}/api/auth/logout`, {
method: "POST",
credentials: "include",
});

const result = await response.json();

if (!response.ok) {
throw new Error(result.message || "Logout failed");
}

return result;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Check response status before parsing JSON.

Parsing response.json() at line 49 before checking response.ok at line 51 can cause issues. If the server returns a non-OK status with non-JSON content (e.g., a 500 error with an HTML page), response.json() will throw a parsing error, bypassing your custom error message logic and producing cryptic messages like "Unexpected token '<'".

🛡️ Proposed fix to check status first
 const logoutMutation = useMutation({
   mutationFn: async () => {
     const response = await fetch(`${getApiBaseUrl()}/api/auth/logout`, {
       method: "POST",
       credentials: "include",
     });

-    const result = await response.json();
-
     if (!response.ok) {
+      let message = "Logout failed";
+      try {
+        const result = await response.json();
+        message = result.message || message;
+      } catch {
+        // Ignore JSON parse errors for non-JSON responses
+      }
-      throw new Error(result.message || "Logout failed");
+      throw new Error(message);
     }

-    return result;
+    return await response.json();
   },
📝 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 logoutMutation = useMutation({
mutationFn: async () => {
const response = await fetch(`${getApiBaseUrl()}/api/auth/logout`, {
method: "POST",
credentials: "include",
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || "Logout failed");
}
return result;
},
const logoutMutation = useMutation({
mutationFn: async () => {
const response = await fetch(`${getApiBaseUrl()}/api/auth/logout`, {
method: "POST",
credentials: "include",
});
if (!response.ok) {
let message = "Logout failed";
try {
const result = await response.json();
message = result.message || message;
} catch {
// Ignore JSON parse errors for non-JSON responses
}
throw new Error(message);
}
return await response.json();
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/pages/dashboard/Settings.jsx` around lines 42 - 56, In the
logoutMutation's mutationFn, the response status must be checked before
attempting to parse the JSON body. Move the response.ok check before the
response.json() call so that if the server returns a non-OK status with non-JSON
content (like an HTML error page), the parsing error is avoided and your custom
error message logic executes properly instead.

Comment on lines 16 to 22
const handlePageChange = (newPage) => {
// Update the "page" parameter inside our search params object

searchParams.set("page", newPage);

// Push the updated parameters back into the browser's address bar

setSearchParams(searchParams);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid mutating searchParams before passing it to setSearchParams.

The current implementation mutates the searchParams object via .set() and then passes the same reference to setSearchParams. This violates React's state immutability principle and can cause issues with change detection and batched updates.

✅ Recommended fix using functional update or new URLSearchParams

Option 1 (preferred): Functional update

 const handlePageChange = (newPage) => {
-  searchParams.set("page", newPage);
-  setSearchParams(searchParams);
+  setSearchParams((prev) => {
+    const next = new URLSearchParams(prev);
+    next.set("page", newPage);
+    return next;
+  });
 };

Option 2: Create new URLSearchParams

 const handlePageChange = (newPage) => {
-  searchParams.set("page", newPage);
-  setSearchParams(searchParams);
+  const nextParams = new URLSearchParams(searchParams);
+  nextParams.set("page", newPage);
+  setSearchParams(nextParams);
 };
📝 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 handlePageChange = (newPage) => {
// Update the "page" parameter inside our search params object
searchParams.set("page", newPage);
// Push the updated parameters back into the browser's address bar
setSearchParams(searchParams);
};
const handlePageChange = (newPage) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set("page", newPage);
return next;
});
};
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 20-20: Avoid using the initial state variable in setState
Context: setSearchParams(searchParams)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/pages/dashboard/Subscriptions.jsx` around lines 16 - 22, The
handlePageChange function is mutating the searchParams object directly by
calling set() on it before passing it to setSearchParams, which violates React's
immutability principle. Instead, create a new URLSearchParams instance from the
existing searchParams, set the "page" value on the new instance, and then pass
this new instance to setSearchParams. This ensures the original state object is
not mutated and allows React to properly detect the state change.

Source: Linters/SAST tools

Comment on lines +61 to +63
<p className="rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-subly-danger mt-7">
{error.message || "Unable to load history."}
</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Incorrect error message text.

The error message says "Unable to load history." but this is the Subscriptions page. It should say "Unable to load subscriptions."

📝 Proposed fix
       <p className="rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-subly-danger mt-7">
-        {error.message || "Unable to load history."}
+        {error.message || "Unable to load subscriptions."}
       </p>
📝 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
<p className="rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-subly-danger mt-7">
{error.message || "Unable to load history."}
</p>
<p className="rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-subly-danger mt-7">
{error.message || "Unable to load subscriptions."}
</p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/pages/dashboard/Subscriptions.jsx` around lines 61 - 63, The error
message fallback text in the Subscriptions page is incorrect. In the error
display block where it shows error.message || "Unable to load history.", change
the fallback string from "Unable to load history." to "Unable to load
subscriptions." to match the actual context of the Subscriptions page.

Comment on lines 70 to 76
<div className="mt-2">
<PaginationList currentPage={data?.pagination?.currentPage} totalPages={data?.pagination?.totalPages} onPageChange={handlePageChange}/>
<PaginationList
currentPage={data?.pagination?.currentPage}
totalPages={data?.pagination?.totalPages}
onPageChange={handlePageChange}
/>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

PaginationList renders with undefined data during error state.

When isError is true, data is undefined, causing PaginationList to receive undefined for both currentPage and totalPages. This can lead to unexpected UI behavior. The pagination should only render when data is successfully loaded.

🛡️ Proposed fix to conditionally render pagination
       )}
-      <div className="mt-2">
-        <PaginationList
-          currentPage={data?.pagination?.currentPage}
-          totalPages={data?.pagination?.totalPages}
-          onPageChange={handlePageChange}
-        />
-      </div>
+      {!isLoading && !isError && data && (
+        <div className="mt-2">
+          <PaginationList
+            currentPage={data.pagination.currentPage}
+            totalPages={data.pagination.totalPages}
+            onPageChange={handlePageChange}
+          />
+        </div>
+      )}
     </section>
📝 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
<div className="mt-2">
<PaginationList currentPage={data?.pagination?.currentPage} totalPages={data?.pagination?.totalPages} onPageChange={handlePageChange}/>
<PaginationList
currentPage={data?.pagination?.currentPage}
totalPages={data?.pagination?.totalPages}
onPageChange={handlePageChange}
/>
</div>
{!isLoading && !isError && data && (
<div className="mt-2">
<PaginationList
currentPage={data.pagination.currentPage}
totalPages={data.pagination.totalPages}
onPageChange={handlePageChange}
/>
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/pages/dashboard/Subscriptions.jsx` around lines 70 - 76, The
PaginationList component is rendering even when data is undefined during error
states, which causes both currentPage and totalPages to be undefined. Add a
conditional check before rendering the PaginationList div to ensure it only
renders when data is successfully loaded and there are no errors. Check both the
isError state and that data exists with valid pagination properties before
rendering the entire pagination section containing the PaginationList component.

Comment thread server/server.js
Comment on lines +35 to +38
const allowedOrigins = new Set([
"http://localhost:5173",
"http://127.0.0.1:5173",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not always trust localhost origins in production CORS policy.

Line 35-38 whitelists localhost origins for every environment. For credentialed endpoints, this weakens production origin restrictions and should be limited to non-production only.

🔐 Proposed fix
-const allowedOrigins = new Set([
-  "http://localhost:5173",
-  "http://127.0.0.1:5173",
-
-  ...configuredOrigins,
-]);
+const localDevOrigins =
+  env.NODE_ENV === "production"
+    ? []
+    : ["http://localhost:5173", "http://127.0.0.1:5173"];
+
+const allowedOrigins = new Set([...localDevOrigins, ...configuredOrigins]);
📝 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 allowedOrigins = new Set([
"http://localhost:5173",
"http://127.0.0.1:5173",
const localDevOrigins =
env.NODE_ENV === "production"
? []
: ["http://localhost:5173", "http://127.0.0.1:5173"];
const allowedOrigins = new Set([...localDevOrigins, ...configuredOrigins]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/server.js` around lines 35 - 38, The allowedOrigins Set is
unconditionally whitelisting localhost origins for all environments, including
production. Modify the allowedOrigins initialization to only include localhost
origins when the environment is not production, such as by checking
process.env.NODE_ENV or a similar environment variable. Localhost origins like
http://localhost:5173 and http://127.0.0.1:5173 should be added conditionally
within the Set constructor or immediately after, only when running in
development or non-production environments.

Comment on lines +17 to +79
const search = req.query.search || req.query.query;
const page = Math.max(parseInt(req.query.page) || 1, 1);
const limit = Math.min(Math.max(parseInt(req.query.limit) || 10, 1), 100);
const userId = req.user.id;

const whereClause = {
subscription: {
userId,
},
};

const normalizedType = type ? String(type).toUpperCase() : null;
const normalizedCategory = category ? String(category).toUpperCase() : null;

if (normalizedType && normalizedType !== "ALL") {

if (!HISTORY_TYPES.has(normalizedType)) {
return res.status(400).json({ message: "Invalid history type" });
}

whereClause.type = normalizedType;
}

if (normalizedCategory && normalizedCategory !== "ALL") {
if (!HISTORY_CATEGORIES.has(normalizedCategory)) {
return res.status(400).json({ message: "Invalid history category" });
}

whereClause.category = normalizedCategory;
}

if (range && range !== "ALL") {
const normalizedRange = String(range).toLowerCase().replace(/[\s_-]/g, "");
const dateThreshold = new Date();

if (["7d", "last7d", "7days", "last7days"].includes(normalizedRange)) {
dateThreshold.setDate(dateThreshold.getDate() - 7);
whereClause.paidAt = { gte: dateThreshold };
}

if (
["30d", "last30d", "30days", "last30days"].includes(normalizedRange)
) {
dateThreshold.setDate(dateThreshold.getDate() - 30);
whereClause.paidAt = { gte: dateThreshold };
}

if (
["12m", "last12m", "12months", "last12months"].includes(
normalizedRange,
)
) {
dateThreshold.setMonth(dateThreshold.getMonth() - 12);
whereClause.paidAt = { gte: dateThreshold };
}
}


try {
const { query, category, type, range } = req.query
const page = parseInt(req.query.page) || 1
const userId = req.user.id;

const ITEMS_PER_PAGE = 10

const whereClause = { userId }

if (type && type !== "ALL") {
whereClause.type = type
}

if (category && category !== "ALL") {
whereClause.category = category
}
if (range) {
const dateThreshold = new Date();

if (range === "7days") {
dateThreshold.setDate(dateThreshold.getDate() - 7);
} else if (range === "30days") {
dateThreshold.setDate(dateThreshold.getDate() - 30);
} else if (range === "12months") {
dateThreshold.setMonth(dateThreshold.getMonth() - 12);
}

// Add the date boundary condition to our where clause
whereClause.createdAt = { gte: dateThreshold };
}

if (query && query.trim() !== '') {
whereClause.OR = [
{
subscriptionName: {
contains: query,
mode: "insensitive",
},
}
]
}

const [historyLogs, totalRecords] = await prisma.$transaction([

prisma.history.findMany({
where: whereClause,
orderBy: { createdAt: "desc" },
skip: (page - 1) * ITEMS_PER_PAGE,
take: ITEMS_PER_PAGE,
}),

prisma.history.count({
where: whereClause,
}),
]);


const totalPages = Math.ceil(totalRecords / ITEMS_PER_PAGE);


return res.status(200).json({
history: historyLogs,
meta: {
currentPage: page,
totalPages: totalPages || 1,
totalRecords,
},
})

} catch (err) {
next(err)
if (search && search.trim() !== "") {
whereClause.OR = [
{
subscriptionName: {
contains: search.trim(),
mode: "insensitive",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "history.controller.js" --search-path server

Repository: Remi561/Subly

Length of output: 196


🏁 Script executed:

cat -n server/src/controllers/history.controller.js | head -100

Repository: Remi561/Subly

Length of output: 3629


🌐 Web query:

Express req.query array values multiple parameters same name

💡 Result:

In Express, when a query string contains multiple parameters with the same name (e.g.,?id=1&id=2&id=3), the default query parser automatically groups these values into an array assigned to that key on the req.query object (e.g., req.query.id becomes ['1', '2', '3']) [1][2][3]. Important considerations regarding this behavior include: Configuration and Parsing: The parsing of req.query is determined by the query parser application setting [4]. By default, Express uses the qs module, which performs this automatic grouping [4][2]. You can change this behavior by setting the parser to 'simple' (which parses values as strings and prevents object/array creation) or by providing a custom parsing function if specific requirements exist [4][2][5]. Version-Specific Behavior (Limits): In some versions of Express (specifically 4.22.0 through 4.22.1), an update to the underlying qs dependency introduced an arrayLimit of 20 [6][7]. In those versions, if a query parameter appeared more than 20 times, the values were silently collapsed into a plain object with numeric keys instead of an array [6][7][8]. This was identified as a bug and subsequently patched in version 4.22.2, which restored the expected array behavior for repeated keys [6]. Security: Because req.query is based on user-controlled input, it is untrusted [4]. Always validate and sanitize values before using them, as a malicious actor could pass an object or array where a single string is expected [4][2]. If you find that your application is receiving an object instead of an array for repeated keys, ensure you are using an updated version of Express (4.22.2 or later) or verify that no custom query parser settings are overriding the default behavior [9][6].

Citations:


Guard search type before calling .trim() to prevent runtime 500 errors.

Line 74 calls .trim() assuming search is a string. Query parameters can be arrays when repeated in the URL (e.g., ?search=a&search=b becomes ['a', 'b']), causing a TypeError that crashes the endpoint. This is user-controlled input and should be validated.

Suggested fix
-    const search = req.query.search || req.query.query;
+    const rawSearch = req.query.search ?? req.query.query;
+    const search =
+      typeof rawSearch === "string"
+        ? rawSearch
+        : Array.isArray(rawSearch) && typeof rawSearch[0] === "string"
+          ? rawSearch[0]
+          : "";
@@
-    if (search && search.trim() !== "") {
+    const normalizedSearch = search.trim();
+    if (normalizedSearch !== "") {
       whereClause.OR = [
         {
           subscriptionName: {
-            contains: search.trim(),
+            contains: normalizedSearch,
             mode: "insensitive",
           },
         },
       ];
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/controllers/history.controller.js` around lines 17 - 79, The code
at the search parameter validation block calls `.trim()` on the search variable
without verifying it is a string first. Query parameters can be arrays when
repeated in the URL (e.g., ?search=a&search=b becomes ['a', 'b']), which causes
a TypeError when calling .trim() on an array. Add a type check using typeof
search === 'string' before calling .trim() in the condition that checks if
search is truthy to ensure only string values are processed and prevent runtime
500 errors from user-controlled input.

@Remi561 Remi561 merged commit 9a5889e into main Jun 23, 2026
3 checks passed
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