Skip to content

Staging#1670

Merged
shahar-biron merged 166 commits into
mainfrom
staging
Apr 23, 2026
Merged

Staging#1670
shahar-biron merged 166 commits into
mainfrom
staging

Conversation

@Anchel123
Copy link
Copy Markdown
Contributor

@Anchel123 Anchel123 commented Apr 23, 2026

Summary by CodeRabbit

  • New Features

    • Key/graph-scoped user permissions, xAI model support, and URL-based connection validation.
  • UI/UX Improvements

    • Redesigned header and navigation, collapsible search, updated editor toolbar (maximize), improved user management with inline edit and tag input, chat overlay and read-only indicators.
  • Reliability & Misc

    • Better session invalidation handling, ACL "save" action, dependency, Docker, and CI workflow updates.

Balance8 and others added 30 commits March 31, 2026 15:25
- Add xAI to AIProvider type definition
- Implement API key detection for xai- prefix format
- Add xAI provider display name and console URL
- Add Grok model detection and categorization
- Add Rocket icon for xAI category in ModelSelector
- Support all xAI Grok models (grok-4.20 and grok-4-1-fast variants)

This enables users to select and use xAI's Grok models for text-to-cypher
functionality with proper API key validation and UI integration.
- Updated various API routes to include a "sentinel" query parameter for handling read-only access based on the user's role and sentinel status.
- Modified the runQuery utility function to accept an optional sentinel parameter.
- Enhanced components (CreateGraph, CypherEditor, ForceGraph, Header, DataPanel, DataTable, MetadataView, SelectGraph, and Providers) to utilize the sentinel role when making API requests.
- Improved memory usage display in the Header component.
- Ensured that the connection information is correctly displayed and copied for both Sentinel and Cluster connection types.
- Updated API routes in `app/api/graph/[graph]/count/route.ts`, `app/api/graph/[graph]/info/route.ts`, and `app/api/graph/[graph]/route.ts` to remove user role checks and utilize a new `readOnly` query parameter.
- Modified the `runQuery` utility function to accept a boolean `isReadOnly` parameter instead of user role and sentinel checks.
- Adjusted various components including `CreateGraph`, `CypherEditor`, `ForceGraph`, `DataPanel`, and others to pass the `readOnly` parameter in API requests.
- Updated context provider to include `isReadOnly` state derived from user role and connection type.
- Refactored UI components to conditionally render based on `isReadOnly` state, ensuring proper access control for editing features.
- Enhanced tests to reflect changes in the handling of read-only states.
- Mute label colors: replace saturated background pills with subtle
  left-border + color dot style in Graph Info panel
- Improve typography hierarchy: use smaller section headers (xs uppercase)
  with semibold data values for better scanning
- Compact property keys: change from individual chips to inline
  comma-separated clickable text
- Compact legend: reduce label/relationship overlay size with smaller
  dots and tighter spacing, add backdrop blur
- Clean up sidebar: remove version number from sidebar (kept in About),
  add separator between nav and action groups, soften borders
- Improve connection indicators: replace cryptic Si/Se/C circles with
  a clear labeled badge showing connection type
- Bottom toolbar: group zoom controls with separator, improve tooltips,
  add hover backgrounds
- Query bar: increase height and spacing for breathing room
- Fix chat panel sizing: reduce from 40%/45% to 35%/30% to prevent
  excessive canvas squeeze
- DataPanel: reduce header sizes, increase padding
- Light theme: soften borders (50% -> 50% lightness), lighten secondary
  (90% -> 95%), soften muted color, improve scrollbar contrast
- Increase whitespace: more padding on main page, Graph Info panel,
  and DataPanel

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The label pills now use border-left-color instead of background-color
for the label color. Update getLabelButtonColor to check the left
border color first, falling back to background color for compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix play/pause icon and tooltip inversion in controls.tsx
- Add borderLeftStyle: 'solid' to label/relationship pills in graphInfo.tsx
- Add max-width constraint on relationship chips to prevent overflow
- Restore list semantics (<ul>/<li>) for property keys section
- Make connection badge keyboard-focusable with aria-label
- Revert dark scrollbar thumb to #666666 for better contrast
- Use w-fit min-w-[120px] for query bar action group
- Centralize panel size presets in page.tsx
- Improve getLabelButtonColor to check inner dot span

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add aria-label to animation Switch control in controls.tsx
- Make tooltip-wrapped elements keyboard-focusable with tabIndex={0} and role
- Use tag-agnostic selector for dot element in customizeStylePage.ts
- Add rgba(0,0,0,0) to transparency rejection list in color extraction
- Improve light scrollbar thumb contrast (#999999 → #888888, ~3.5:1 ratio)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
shahar-biron and others added 6 commits April 21, 2026 14:45
feat(users): update user permissions handling and improve UI for key …
chore: bump version to 2.0.1 in package.json and package-lock.json
Bump the following dependencies:

Production:
- react-hook-form: 7.72.1 → 7.73.1
- tailwindcss: 4.2.2 → 4.2.3
- @tailwindcss/postcss: 4.2.2 → 4.2.3

Development:
- @typescript-eslint/eslint-plugin: 8.58.2 → 8.59.0
- @typescript-eslint/parser: 8.58.1 → 8.59.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eslint-visitor-keys@5.0.1 (transitive dependency of @typescript-eslint/*
and eslint) requires Node ^20.19.0 || ^22.13.0 || >=24. Update the
engines field to reflect the actual minimum requirement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
chore(deps): combine dependabot dependency updates
@overcut-ai
Copy link
Copy Markdown

overcut-ai Bot commented Apr 23, 2026

Completed Working on "Code Review"

✅ Review submission failed at finalization: Cannot submit review because no pending review exists on PR #1670. Chunk processing result: posted 0 comments from review-chunk1.

✅ Workflow completed successfully.


👉 View complete log

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

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

Project Deployment Actions Updated (UTC)
falkordb-browser Ready Ready Preview, Comment Apr 23, 2026 11:30am

Request Review

@Anchel123 Anchel123 requested a review from shahar-biron April 23, 2026 08:19
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@shahar-biron has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 16 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 16 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 794e8748-2edd-4ef2-8bec-ee197c485177

📥 Commits

Reviewing files that changed from the base of the PR and between e07656a and 7f9e90e.

📒 Files selected for processing (11)
  • app/components/FormComponent.tsx
  • app/settings/users/AddUser.tsx
  • app/settings/users/EditUser.tsx
  • app/settings/users/Users.tsx
  • e2e/infra/utils.ts
  • e2e/logic/POM/settingsTokensPage.ts
  • e2e/tests/canvas.spec.ts
  • e2e/tests/chat.spec.ts
  • e2e/tests/settingsTokens.spec.ts
  • lib/token-storage/FileTokenStorage.ts
  • lib/token-storage/StorageFactory.ts

Walkthrough

Refactors authentication to store encrypted credentials in a Token DB (replacing JWT-stored passwords), switches many endpoints to use a readOnly=true query flag instead of session role checks, adds ACL key/graph permissions, UI/context changes (Header/Navbar, read-only context), numerous component/typing updates, and upgrades dependencies and workflows.

Changes

Cohort / File(s) Summary
Auth & Token Storage
app/api/auth/[...nextauth]/options.ts, app/api/auth/tokenUtils.ts, app/api/auth/tokens/*.ts, lib/token-storage/*, types/next-auth.d.ts
Moves encrypted passwords into Token DB via storeEncryptedCredential; JWT/session carry opaque credentialRef; server resolves secrets server-side; adds token kind and deleteToken support; signOut hard-deletes session credentials.
Read-only Authorization (query flag)
app/api/graph/**, app/api/schema/**, app/api/info/route.ts, app/api/graph/*/count/*, app/api/*/utils.ts
Replaces session.role checks with ?readOnly=true parsing and boolean isReadOnly; updates runQuery signature and adds SSE error streaming helper for getClient failures.
User ACLs & Management
app/api/user/*, app/api/validate-body.ts, app/api/user/model.ts, app/api/user/model.test.ts, app/api/user/save/route.ts, app/api/swagger/swagger-spec.ts
Adds keys to User model, getRoleWithKeys and extractKeysFromACL, updates create/patch routes and OpenAPI spec, and adds POST /api/user/save for ACL persistence; tests added for ACL helpers.
Frontend Context & UI components
app/components/Header.tsx, app/components/Navbar.tsx, app/components/provider.ts, many app/* components (graph/schema/settings/*), app/layout.tsx
Introduces ConnectionContext.isReadOnly, GraphContext.expand, PanelContext.panelOpen; splits Header/Navbar and threads isReadOnly/expand through UI, appending ?readOnly=true to client requests where needed and adjusting many UI behaviors/controls.
Form, Table & Input API
app/components/FormComponent.tsx, app/components/TableComponent.tsx, app/components/PaginationList.tsx, app/components/ui/Input.tsx, components/ui/*
Adds TagField and TagInput UX, expands field metadata, changes table headers to HeaderDef with width support, relaxes several ref types to nullable, and exports PopoverClose; adds warning toast variant.
Graph model & utilities
app/api/graph/model.ts, app/api/utils.ts, lib/utils.ts
Graph.extendCell made async (concurrent node/edge extension); adds writeGetClientErrorAsSSE; runQuery now accepts isReadOnly; session-invalidation/signOut handling added to fetch/SSE flows.
Login & URL validation
app/login/*, app/api/validate-url/route.ts
Removes endpoint-mode; centralizes URL parsing/validation, safe decode, port parsing changes; adds POST /api/validate-url to validate connection URLs and optional NOAUTH detection.
E2E & Playwright
playwright.config.ts, e2e/*, e2e/logic/POM/*, e2e/tests/*
Per-project persistent storageState for sign-out projects, new sign-out projects, updated POMs to match UI changes (edit-user flow, selectors, escaping), updated tests and fixtures, added api.settingsUser URL config.
Build, CI, Config & deps
.github/workflows/*, Dockerfile, package.json, next.config.js, tailwind.config.js
Bumped pinned action versions and docker action, updated Node Alpine base, added allowedDevOrigins, upgraded many dependencies (Next/React/NextAuth/etc.), and adjusted Tailwind color key rename.
Type declarations & misc
types/falkordb-canvas.d.ts, types/next-auth.d.ts, lib/ai-provider-utils.ts, components/ui/table.tsx
Adds JSX type for falkordb-canvas; replaces User.password with User.credentialRef; adds xai provider support and KNOWN_PROVIDERS; nullable ref typings adjusted.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as Frontend
    participant AuthAPI as Auth API
    participant TokenDB as Token DB
    participant FalkorDB as FalkorDB

    User->>Client: Submit credentials
    Client->>AuthAPI: POST /api/auth/callback/credentials
    AuthAPI->>TokenDB: storeEncryptedCredential(password)
    TokenDB-->>AuthAPI: credentialRef
    AuthAPI->>AuthAPI: Create JWT containing credentialRef (no password)
    AuthAPI-->>Client: Return JWT

    Client->>AuthAPI: Request protected resource (with JWT)
    AuthAPI->>TokenDB: Lookup and decrypt via credentialRef
    TokenDB-->>AuthAPI: decrypted password
    AuthAPI->>FalkorDB: Connect using decrypted password
    FalkorDB-->>AuthAPI: Data
    AuthAPI-->>Client: Response
Loading
sequenceDiagram
    participant User
    participant Client as Frontend
    participant API as Graph API
    participant FalkorDB as FalkorDB

    User->>Client: Run graph query (read-only)
    Client->>API: GET /api/graph/{g}?readOnly=true
    API->>API: parse readOnly -> isReadOnly=true
    API->>FalkorDB: graph.roQuery(cypher)
    FalkorDB-->>API: results
    API-->>Client: return results

    User->>Client: Run graph query (read-write)
    Client->>API: GET /api/graph/{g}
    API->>API: parse readOnly -> isReadOnly=false
    API->>FalkorDB: graph.query(cypher)
    FalkorDB-->>API: results
    API-->>Client: return results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested PR title candidates:

  • feat(auth): store encrypted credentials in Token DB and remove passwords from JWT
  • feat(authz): add readOnly query flag and route handlers to use isReadOnly
  • feat(users): add keys-based ACLs + POST /api/user/save and UI edit flows

Poem

🔐 In vaults of tokens the secrets now hide,

credentialRef whispers — no password inside.
Roles traded for flags, readOnly in tow,
Keys lace the graphs where permissions now grow. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% 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 'Staging' is vague and does not convey meaningful information about the changeset, which includes significant credential security improvements, authentication refactoring, user/role/key management enhancements, and multiple workflow updates. Use a conventional commit format title that reflects the primary change, such as 'refactor: move credential storage server-side and implement read-only query parameter routing' or 'feat: enhance user key permissions and session credential encryption'.
✅ 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staging

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

🔒 Trivy Security Scan Results


Report Summary

┌───────────────────────────────────────────────────────────────────────────┬──────────┬─────────────────┐
│                                  Target                                   │   Type   │ Vulnerabilities │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ falkordb/falkordb-browser:test (alpine 3.23.4)                            │  alpine  │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@falkordb/text-to-cypher/package.json                    │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@img/colour/package.json                                 │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@img/sharp-libvips-linux-x64/package.json                │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json            │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@img/sharp-linux-x64/package.json                        │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@img/sharp-linuxmusl-x64/package.json                    │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@js-temporal/polyfill/package.json                       │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@next/env/package.json                                   │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@redis/bloom/package.json                                │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@redis/client/dist/package.json                          │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@redis/client/package.json                               │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@redis/json/package.json                                 │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@redis/search/package.json                               │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@redis/time-series/package.json                          │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/@swc/helpers/package.json                                │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/client-only/package.json                                 │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/cluster-key-slot/package.json                            │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/detect-libc/package.json                                 │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/falkordb/package.json                                    │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/jsbi/package.json                                        │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/lodash/package.json                                      │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/next/dist/compiled/@edge-runtime/cookies/package.json    │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/next/dist/compiled/@edge-runtime/ponyfill/package.json   │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/next/dist/compiled/@edge-runtime/primitives/package.json │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/next/dist/compiled/react-is/package.json                 │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/next/dist/compiled/regenerator-runtime/package.json      │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/next/package.json                                        │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/react-dom/package.json                                   │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/react/package.json                                       │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/redis/node_modules/@redis/client/dist/package.json       │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/redis/node_modules/@redis/client/package.json            │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/redis/package.json                                       │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/semver/package.json                                      │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/sharp/package.json                                       │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/node_modules/styled-jsx/package.json                                  │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ app/package.json                                                          │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ opt/yarn-v1.22.22/package.json                                            │ node-pkg │        0        │
├───────────────────────────────────────────────────────────────────────────┼──────────┼─────────────────┤
│ usr/local/lib/node_modules/corepack/package.json                          │ node-pkg │        0        │
└───────────────────────────────────────────────────────────────────────────┴──────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)


Copy link
Copy Markdown
Contributor

@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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (21)
app/providers.tsx (1)

385-406: ⚠️ Potential issue | 🟠 Major

Stale isReadOnly closure in fetchCount (missing dependency, no eslint-disable).

fetchCount reads isReadOnly at line 394 but its dependency array is [graphName, toast] without an eslint-disable-next-line react-hooks/exhaustive-deps directive. When the user's role changes or connectionInfo.sentinelRole flips (e.g. Sentinel failover promoting a replica), the memoized callback keeps the old value and will send the wrong readOnly query param to /api/graph/.../count — which, given the new authorization model relies on this flag, could silently route write-intent or read-intent requests to the wrong branch.

Same pattern exists in fetchInfo (line 428) and runQuery (line 537), both of which suppress the lint rule — worth adding isReadOnly to their deps too so the callbacks actually refresh on role/sentinel changes.

🐛 Proposed fix
-    }, [graphName, toast]);
+    }, [graphName, toast, isReadOnly]);

And for the other two:

-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [graphName]);
+  }, [graphName, isReadOnly, toast]);
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [graphName, limit, timeout, fetchInfo, fetchCount, handleCooldown, handelGetNewQueries, showMemoryUsage, captionsKeys, showPropertyKeyPrefix, tutorialOpen, prefixReady]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [graphName, limit, timeout, isReadOnly, fetchInfo, fetchCount, handleCooldown, handelGetNewQueries, showMemoryUsage, captionsKeys, showPropertyKeyPrefix, tutorialOpen, prefixReady]);

Suggested PR title (conventional commits): fix(providers): include isReadOnly in memoized fetch callback deps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/providers.tsx` around lines 385 - 406, fetchCount currently closes over
isReadOnly but the hook deps only include [graphName, toast], causing stale
readOnly query params; update the useCallback for fetchCount to include
isReadOnly in its dependency array (and remove any eslint-disable-next-line
react-hooks/exhaustive-deps if present) so the callback rememoizes when
role/read-only state changes; apply the same change to the other memoized
callbacks named fetchInfo and runQuery so each includes isReadOnly in their
dependency arrays to avoid sending incorrect readOnly flags.
app/graph/controls.tsx (1)

48-61: ⚠️ Potential issue | 🟡 Minor

Use one animation-running predicate.

cooldownTicks is typed as number | undefined, so positive values currently render as checked but show “Resume animation” and click to resume again. Use cooldownTicks !== 0 consistently.

🐛 Proposed fix
-                            {cooldownTicks === undefined ? <Pause size={18} /> : <Play size={18} />}
+                            {cooldownTicks !== 0 ? <Pause size={18} /> : <Play size={18} />}
                             <Switch
                                 data-testid="animationControl"
-                                aria-label={cooldownTicks === undefined ? "Pause animation" : "Resume animation"}
+                                aria-label={cooldownTicks !== 0 ? "Pause animation" : "Resume animation"}
                                 className="pointer-events-auto data-[state=unchecked]:bg-border"
                                 checked={cooldownTicks !== 0}
                                 onCheckedChange={() => {
-                                    handleCooldown(cooldownTicks === undefined ? 0 : undefined);
+                                    handleCooldown(cooldownTicks !== 0 ? 0 : undefined);
                                 }}
                             />
@@
-                        <p>{cooldownTicks === undefined ? "Pause animation" : "Resume animation"}</p>
+                        <p>{cooldownTicks !== 0 ? "Pause animation" : "Resume animation"}</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/controls.tsx` around lines 48 - 61, The UI uses inconsistent
predicates for animation state causing wrong labels/icons; make all checks use
the same predicate (cooldownTicks !== 0). Update the Switch checked prop, icon
conditional, aria-label, TooltipContent text, and the onCheckedChange toggle to
use cooldownTicks !== 0 and call handleCooldown(cooldownTicks !== 0 ? 0 :
undefined) so toggling consistently sets pause (0) or resume (undefined).
app/api/schema/[schema]/[element]/[key]/_route.ts (1)

45-53: ⚠️ Potential issue | 🔴 Critical

Enforce read-only permissions server-side before mutating.

Lines 45 and 108 trust ?readOnly=true; omitting that parameter sends POST/DELETE through graph.query, so a read-only user can attempt schema mutations. Use the authenticated ACL/session permissions for this decision and return 403 Forbidden for denied writes instead of relying on a client flag or roQuery failure.

Also applies to: 108-112

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/schema/`[schema]/[element]/[key]/_route.ts around lines 45 - 53, The
code currently trusts the client-sent flag isReadOnly
(request.nextUrl.searchParams.get("readOnly")) to decide whether to call
graph.roQuery or graph.query; instead, fetch the authenticated session/ACL (the
same auth context used elsewhere) and check the user's write permission for this
schema/element before any mutation call in the handlers that call
graph.query/graph.roQuery (references: isReadOnly, request.nextUrl.searchParams,
graph.query, graph.roQuery); if the session lacks write permission, return a 403
Forbidden response and do not call graph.query; only call graph.query when the
server-side permission check grants write rights, otherwise use read-only paths
or roQuery as appropriate.
app/api/schema/[schema]/[element]/_route.ts (1)

85-129: ⚠️ Potential issue | 🔴 Critical

Block write operations with trusted ACL checks, not readOnly URL flags.

Lines 85 and 171 make POST/DELETE authorization depend on a caller-controlled query parameter. A read-only user can omit it and hit graph.query for create/delete operations. Derive write permission from the authenticated ACL/session and return 403 Forbidden before executing the query when writes are not allowed.

Also applies to: 171-181

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/schema/`[schema]/[element]/_route.ts around lines 85 - 129, The code
currently trusts the request.nextUrl.searchParams "readOnly" flag (isReadOnly)
to choose between graph.roQuery and graph.query, letting clients bypass ACLs;
instead, check the authenticated session/ACL (e.g., use the request auth/session
or ACL helper used elsewhere in this file) to determine write permission before
running any create/delete queries and return a 403 when writes are not allowed;
replace use of isReadOnly for authorization with a server-side permission check
and only call graph.query when the session/ACL permits writes (use graph.roQuery
for permitted read-only operations), and apply the same change to the similar
block around lines 171-181 so all mutations (references: isReadOnly,
graph.query, graph.roQuery, formatAttributes, selectedNodes) are gated by
server-side ACL checks rather than the readOnly URL param.
app/api/schema/[schema]/_route.ts (1)

16-39: ⚠️ Potential issue | 🟠 Major

Do not use readOnly query params as the authorization source.

Line 20 lets the caller choose whether the server uses the read-only or write-capable query path. Even though this fixed GET query is read-only today, the backend should derive this from authenticated session/ACL state, not from a request-controlled flag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/schema/`[schema]/_route.ts around lines 16 - 39, The handler
currently trusts request.nextUrl.searchParams.get("readOnly") (and the derived
isReadOnly) to pick graph.roQuery vs graph.query; remove trust in that query
param and instead derive a read-only decision from the authenticated session/ACL
(e.g., check session.user roles/permissions or a permissions list on session)
before calling graph.roQuery or graph.query; specifically, stop using
request.nextUrl.searchParams.get("readOnly") and the isReadOnly variable as the
authorization source, validate the caller via session (or a permission-check
helper) and then call graph.roQuery when the session indicates read-only access
and graph.query only when the session authorizes writes. Ensure any
request-controlled flag is ignored or validated against session permissions to
prevent elevation.
app/api/info/route.ts (1)

10-22: ⚠️ Potential issue | 🟠 Major

Add section parameter validation and readOnly authorization check.

Two confirmed issues with this endpoint:

  1. Unvalidated section parameter passed to Redis INFO. Any authenticated user can request arbitrary sections. While node-redis prevents CRLF injection via discrete argv, Redis INFO still exposes sensitive internals (replication, clients, keyspace, commandstats, etc.). Implement a whitelist of allowed sections and return 400 for invalid requests.

  2. Missing readOnly authorization context. Unlike other endpoints in the codebase, this route doesn't check the readOnly query parameter. READ-ONLY users should not access certain INFO sections. Add the same readOnly check pattern used in /api/graph/[graph]/info and other routes.

Current callers use only section=memory and no-section defaults, but the endpoint should enforce restrictions server-side rather than relying on UI discipline.

🛡️ Proposed fix
+const ALLOWED_SECTIONS = new Set([
+  "", "server", "clients", "memory", "persistence", "stats",
+  "replication", "cpu", "commandstats", "latencystats", "cluster", "keyspace", "everything", "default", "all",
+]);
+
 export async function GET(request: NextRequest) {
   try {
     const session = await getClient(request);
     if (session instanceof NextResponse) return session;

     const { client } = session;
-    const section = request.nextUrl.searchParams.get("section") || "";
+    const section = (request.nextUrl.searchParams.get("section") || "").toLowerCase();
+    const isReadOnly = request.nextUrl.searchParams.get("readOnly") === "true";
+    
+    if (!ALLOWED_SECTIONS.has(section)) {
+      return NextResponse.json(
+        { message: `Invalid section: ${section}` },
+        { status: 400, headers: getCorsHeaders(request) }
+      );
+    }
+    
+    if (isReadOnly) {
+      return NextResponse.json(
+        { message: "INFO command not available in read-only mode" },
+        { status: 403, headers: getCorsHeaders(request) }
+      );
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/info/route.ts` around lines 10 - 22, The GET handler currently passes
an unvalidated section to client.connection.info and lacks the readOnly
authorization check; add a whitelist array of allowed sections (e.g.,
['default','memory', ...] as appropriate) and validate
request.nextUrl.searchParams.get("section") against it, returning a 400 response
for invalid values, and implement the same readOnly query check pattern used
elsewhere by reading request.nextUrl.searchParams.get("readOnly") and denying
access to restricted INFO sections for readOnly users before calling await
(await client.connection).info(section); update logic around the GET function,
session.client and client.connection.info to enforce these validations and early
returns.
app/api/schema/[schema]/count/_route.ts (1)

24-37: ⚠️ Potential issue | 🟡 Minor

Count endpoints are always reads — the readOnly toggle is pointless and inherits the auth issue.

This handler only runs MATCH (n) RETURN count(n) / MATCH ()-[e]->() RETURN count(e). Both are pure reads; using graph.roQuery unconditionally is strictly safer (no accidental write locks, no replica mismatches) and sidesteps the client-controlled readOnly concern flagged on the root-cause route (app/api/schema/[schema]/[element]/label/_route.ts).

♻️ Simplification
-    const { client } = session;
+    const { client } = session;
     const { schema } = await params;
     const schemaName = `${schema}_schema`;
-    const isReadOnly = request.nextUrl.searchParams.get("readOnly") === "true";
@@
-      const nodesResult = await runQuery(graph, nodesQuery, isReadOnly);
+      const nodesResult = await runQuery(graph, nodesQuery, true);
@@
-      const edgesResult = await runQuery(graph, edgesQuery, isReadOnly);
+      const edgesResult = await runQuery(graph, edgesQuery, true);

As per coding guidelines: "Review API routes for security, error handling, and proper HTTP status codes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/schema/`[schema]/count/_route.ts around lines 24 - 37, This handler
treats count endpoints as potentially writable via the request-controlled
readOnly flag; since both queries are pure reads (nodesQuery and edgesQuery
executed via runQuery against graph selected by client.selectGraph(schemaName)),
remove the request-controlled isReadOnly branch and always execute read-only
queries using the graph read-only API (use graph.roQuery or call runQuery with a
guaranteed read-only flag) so counts run against replicas safely and cannot be
toggled by clients; update/remove the isReadOnly variable and ensure runQuery
invocation(s) for nodesQuery and edgesQuery use the read-only path
unconditionally.
app/globals.css (1)

168-174: ⚠️ Potential issue | 🟡 Minor

Light-mode scrollbar now has less contrast than before.

Swapping #666666#888888 on .light moves the thumb closer to a typical light background, reducing contrast. Meanwhile the dark-mode thumb is still #666666, which is darker than the dark background and harder to see. The two rules look inverted — usually you want a darker thumb on light, lighter thumb on dark.

💡 Likely intended values
 .light ::-webkit-scrollbar-thumb {
-    background: `#888888`;
+    background: `#666666`;
 }

 .dark ::-webkit-scrollbar-thumb {
-    background: `#666666`;
+    background: `#888888`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/globals.css` around lines 168 - 174, The scrollbar thumb colors for the
light and dark themes are inverted: update the .light ::-webkit-scrollbar-thumb
and .dark ::-webkit-scrollbar-thumb rules so the light theme uses the darker
thumb and the dark theme uses the lighter thumb (swap the current hex values on
the .light and .dark rules, i.e., set .light ::-webkit-scrollbar-thumb to the
darker value and .dark ::-webkit-scrollbar-thumb to the lighter value) to
restore proper contrast.
app/components/ForceGraph.tsx (1)

127-184: ⚠️ Potential issue | 🟡 Minor

Stale isReadOnly in onFetchNode closure.

isReadOnly is used inside onFetchNode but omitted from the useCallback dependency array (line 184). Toggling the connection's read-only mode while the component is mounted won't update the captured value, so the URL may keep using the previous flag. Add isReadOnly to the dependency list.

-    }, [canvasRef, canvasLoaded, type, graph, toast, setIndicator, cooldownTicks, handleCooldown]);
+    }, [canvasRef, canvasLoaded, type, graph, toast, setIndicator, cooldownTicks, handleCooldown, isReadOnly]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ForceGraph.tsx` around lines 127 - 184, The onFetchNode
closure created by useCallback captures isReadOnly but does not include it in
the dependency array, causing stale read-only state; update the dependency list
of the useCallback that defines onFetchNode to include isReadOnly so the
callback is recreated when the read-only flag changes (look for the onFetchNode
function and its useCallback(...) call and add isReadOnly alongside canvasRef,
canvasLoaded, type, graph, toast, setIndicator, cooldownTicks, handleCooldown).
app/api/graph/[graph]/[element]/[key]/route.ts (1)

30-54: ⚠️ Potential issue | 🟠 Major

fix: Remove readOnly flag from mutation endpoints

Both POST and DELETE handlers here execute SET or DELETE Cypher operations. Passing isReadOnly=true to graph.roQuery() with a write clause will fail at FalkorDB, since read-only queries reject all mutations. The same issue affects sibling mutation routes: app/api/graph/[graph]/[element]/route.ts (DELETE), and app/api/graph/[graph]/[element]/label/route.ts (POST and DELETE).

On mutation endpoints, skip the isReadOnly check and always use graph.query(). The endpoint's HTTP method and path semantics already communicate intent; the server should enforce authorization policy server-side (via ACLs), not via client-supplied query strings.

Suggested fix
-    const isReadOnly = request.nextUrl.searchParams.get("readOnly") === "true";
     ...
-      if (isReadOnly)
-        await graph.roQuery(query, { params: { id: elementId, value } });
-      else await graph.query(query, { params: { id: elementId, value } });
+      await graph.query(query, { params: { id: elementId, value } });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/graph/`[graph]/[element]/[key]/route.ts around lines 30 - 54, The
handler currently reads isReadOnly and may call graph.roQuery for mutations (see
isReadOnly, graph.roQuery, graph.query in route.ts) which fails because roQuery
rejects write Cypher; remove the isReadOnly conditional for this mutation
endpoint and always invoke graph.query with the params (keep the existing
validation via validateBody and updateGraphElementAttribute, and continue using
the same query construction and params: { id: elementId, value }), ensuring
sibling mutation handlers (DELETE in app/api/graph/[graph]/[element]/route.ts
and POST/DELETE in app/api/graph/[graph]/[element]/label/route.ts) are changed
the same way so write operations never call graph.roQuery.
app/api/graph/[graph]/[element]/label/route.ts (1)

25-46: ⚠️ Potential issue | 🔴 Critical

Don’t let a query parameter choose write access.

readOnly is client-controlled; omitting it routes these POST/DELETE label mutations through graph.query(...). Derive write permission from trusted session/ACL state instead, and return 403 for read-only callers before executing the write query.

As per coding guidelines, **/api/**/*: Review API routes for security, error handling, and proper HTTP status codes.

Also applies to: 79-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/graph/`[graph]/[element]/label/route.ts around lines 25 - 46, The
code currently trusts the client-controlled query param isReadOnly
(request.nextUrl.searchParams.get("readOnly")) to decide whether to run a write
(graph.query) or read (graph.roQuery); instead, check the caller's permission
from the server-trusted session/ACL (use session or session.client ACL methods)
and reject unauthorized write attempts with NextResponse.json(..., { status: 403
}) before running any graph.query; ensure both the POST/DELETE label handler
paths that call graph.query and graph.roQuery validate session write permission
(and fall back to roQuery only when the session lacks write rights), and remove
reliance on the readOnly query param for enforcing write access.
app/api/graph/[graph]/[element]/route.ts (1)

71-125: ⚠️ Potential issue | 🔴 Critical

Keep write authorization server-side.

readOnly comes from the request URL, so a caller can omit it and force POST/DELETE through graph.query(...). Use trusted session/ACL permissions to decide whether the user can mutate this graph, and return 403 for read-only access before executing CREATE/DELETE.

As per coding guidelines, **/api/**/*: Review API routes for security, error handling, and proper HTTP status codes.

Also applies to: 155-181

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/graph/`[graph]/[element]/route.ts around lines 71 - 125, The handler
currently trusts the readOnly flag from the request URL (isReadOnly) and may
call graph.query(...) for mutations; instead enforce write permission from the
authenticated session/ACL before running any CREATE/DELETE. Use the session/ACL
associated with session (e.g., session, client.selectGraph(graphId)) to compute
an authoritative canWrite (or isReadOnlyFromSession) and if the incoming
validated payload implies a mutation (createGraphElement result where you build
a CREATE query: when type is truthy for node CREATE or when you build the
relationship CREATE branch) return NextResponse.json({ message: "Forbidden" }, {
status: 403, headers: getCorsHeaders(request) }) if the session lacks write
rights; only call graph.query(...) when session permits writes, otherwise call
graph.roQuery(...) or reject. Ensure this check happens after
validateBody(createGraphElement, body) and before building/executing the query
so graph.query is never called based on an untrusted URL param.
app/schema/DataPanel.tsx (1)

231-253: ⚠️ Potential issue | 🟠 Major

Prevent read-only users from entering edit mode.

The action buttons are hidden, but row clicks still call handleSetEditable, so read-only users can see editable controls and submit via keyboard. Gate the edit state and key handlers as well. As per coding guidelines, **/*.tsx: Review React components for proper hooks usage, accessibility, and performance.

Proposed guard
     const handleSetKeyDown = (evt: React.KeyboardEvent) => {
+        if (isReadOnly) return;
+
         if (evt.code === "Escape") {
             evt.preventDefault();
             handleSetEditable();
         }
     const handleAddKeyDown = (evt: React.KeyboardEvent) => {
+        if (isReadOnly) return;
+
         if (evt.code === "Escape") {
             evt.preventDefault();
             handleSetEditable();
         }
                                 onClick={() => {
+                                    if (isReadOnly) return;
                                     if (editable === key) return;
                                     handleSetEditable([key, [...val]]);
                                 }}
-                                    editable === key ?
+                                    !isReadOnly && editable === key ?

Also applies to: 447-464, 520-520

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/schema/DataPanel.tsx` around lines 231 - 253, The handlers currently
allow entering edit mode via keyboard and row clicks even for read-only users;
update handleSetKeyDown, handleAddKeyDown and the row click handler (which calls
handleSetEditable) to early-return when the user lacks edit permission (e.g.,
check a prop/flag like isReadOnly or !canEdit), and also prevent calling
handleSetEditable, handleSetAttribute, and handleAddAttribute when that flag is
set; apply the same guard at the other places mentioned (the handlers around
lines 447-464 and 520) so read-only users cannot toggle edit state or submit via
keyboard.
app/login/urlUtils.ts (1)

83-91: ⚠️ Potential issue | 🟠 Major

Keep non-numeric ports invalid after parsing.

Line 89 now moves any suffix after : into port, but Line 186 still treats every non-empty value as valid. This makes inputs like redis://localhost:abc pass validation despite the documented “must be digits” rule. As per coding guidelines, **/*.ts: Review TypeScript code for type safety, proper error handling, and Next.js best practices.

Proposed fix
-  // 4. Parse host[:port] — split on last colon, only if part after is all digits
+  // 4. Parse host[:port] — split on last colon so validation can color invalid ports
   let host = hostPortStr;
   let port = "";
   const lastColon = hostPortStr.lastIndexOf(":");
   if (lastColon >= 0) {
     const portCandidate = hostPortStr.slice(lastColon + 1);
     host = hostPortStr.slice(0, lastColon);
     port = portCandidate;
   }
   // Port
   let portStatus: "good" | "warn" | "neutral" = "neutral";
   if (hasPortColon) {
-    if (parsed.port) {
+    if (/^\d+$/.test(parsed.port)) {
       portStatus = "good";
     } else {
       portStatus = "warn";
       valid = false;
     }
   }

Also applies to: 183-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/login/urlUtils.ts` around lines 83 - 91, The parsing logic currently
moves any suffix after the last ":" into port (hostPortStr -> portCandidate ->
port) regardless of digits, which allows non-numeric ports like
"redis://localhost:abc" to pass later validation; change the host/port split so
that you only assign port = portCandidate and trim host when portCandidate
matches /^\d+$/ (all digits), otherwise leave host as hostPortStr and port as ""
(or mark invalid), and update the downstream validation that currently treats
any non-empty port as valid to explicitly check numeric-only (use the same
/^\d+$/ test) before accepting the port; refer to hostPortStr, lastColon,
portCandidate, host, port and the validation branch that accepts non-empty port
to locate and fix both parsing and validation spots.
app/api/auth/tokens/route.ts (1)

145-179: ⚠️ Potential issue | 🟠 Major

Reject malformed expiration inputs before creating tokens.

ttlSeconds: "abc" passes the current range check, skips expiration assignment, and stores expiresAtUnix = -1, creating a never-expiring token. Invalid expiresAt values also pass the date comparison. As per coding guidelines, **/api/**/*: Review API routes for security, error handling, and proper HTTP status codes.

🛡️ Proposed validation fix
     const {
       name = "API Token",
       expiresAt = null,
       ttlSeconds = undefined,
     } = body;
+    const parsedTtlSeconds = ttlSeconds === undefined ? undefined : Number(ttlSeconds);
 
     // 4. Validate expiration parameters
     let expiresAtDate: Date | null = null;
     if (expiresAt) {
       expiresAtDate = new Date(expiresAt);
+      if (Number.isNaN(expiresAtDate.getTime())) {
+        return NextResponse.json(
+          { message: "Invalid expiration date" },
+          { status: 400, headers: getCorsHeaders(request) }
+        );
+      }
       if (expiresAtDate <= new Date()) {
         return NextResponse.json(
           { message: "Expiration date must be in the future" },
           { status: 400, headers: getCorsHeaders(request) }
         );
       }
     }
-    if (ttlSeconds !== undefined && (ttlSeconds > 31622400 || ttlSeconds < 1)) {
+    if (
+      ttlSeconds !== undefined &&
+      (!Number.isInteger(parsedTtlSeconds) || parsedTtlSeconds < 1 || parsedTtlSeconds > 31622400)
+    ) {
       return NextResponse.json(
         { message: "Invalid TTL value" },
         { status: 400, headers: getCorsHeaders(request) }
       );
     }
@@
-    } else if (typeof ttlSeconds === "number") {
-      expirationTime = currentTime + ttlSeconds;
+    } else if (parsedTtlSeconds !== undefined) {
+      expirationTime = currentTime + parsedTtlSeconds;
     }

Also applies to: 211-223

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/auth/tokens/route.ts` around lines 145 - 179, The expiration inputs
are not being strictly validated: validate that body.ttlSeconds is a numeric
integer (reject strings like "abc" or non-integers) and that body.expiresAt
parses to a valid future Date (check isNaN(expiresAtDate.getTime()) and that
it's > now) before proceeding to generateTimeUUID or compute expirationTime; on
invalid input return NextResponse.json with a 400 and getCorsHeaders. Update the
validation logic around ttlSeconds, expiresAt, expiresAtDate, and expirationTime
so malformed values are rejected (do not allow skipping expiration assignment
that later results in a never-expiring token).
app/api/graph/[graph]/count/route.ts (1)

31-44: ⚠️ Potential issue | 🟠 Major

Remove client-controlled authorization mode from all API routes and derive from authenticated session instead.

The isReadOnly parameter should come from session.user.role === "Read-Only", not from request.nextUrl.searchParams.get("readOnly"). Currently, a "Read-Only" authenticated user can pass ?readOnly=false to bypass write protection and call graph.query() instead of graph.roQuery(), escalating privileges.

This pattern affects multiple API routes (count, info, graph queries, schema operations, etc.) and should be refactored consistently across the entire API layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/graph/`[graph]/count/route.ts around lines 31 - 44, Replace
client-controlled readOnly detection with session-derived authorization: remove
use of request.nextUrl.searchParams.get("readOnly") and instead derive
isReadOnly from the authenticated session (e.g., session.user.role ===
"Read-Only") before calling client.selectGraph/runQuery; ensure calls to
graph.query vs graph.roQuery (and runQuery) use that session-derived flag.
Update route.ts (the count handler) and apply the same change to other API
routes (info, graph queries, schema ops) so no route trusts a query parameter to
grant read-only access. Ensure session is validated and denies privilege
escalation if session is missing or role is not "Read-Only".
app/settings/users/DeleteUser.tsx (1)

21-41: ⚠️ Potential issue | 🟡 Minor

Handle onSave rejection.

await onSave() has no error handling. If the subsequent persistence step fails, the dialog closes, local users/rows are already mutated, and the user sees no feedback. Consider wrapping in try/catch and surfacing via toast, or rolling back the optimistic state.

             setRows(prev => prev.filter(row => !users.find(u => row.cells[0].value === u.username)));
-            await onSave();
+            try {
+                await onSave();
+            } catch (e) {
+                toast({
+                    title: "Warning",
+                    description: "Users deleted, but saving changes failed.",
+                    variant: "destructive",
+                });
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/users/DeleteUser.tsx` around lines 21 - 41, The deleteSelected
function performs optimistic local updates then calls await onSave() without
error handling; capture the previous state (e.g., prevUsers and prevRows) before
calling setUsers/setRows, wrap the await onSave() call in a try/catch, and on
failure restore the saved prevUsers/prevRows (call setUsers(prevUsers) and
setRows(prevRows)) and surface the error via toast.error (include a helpful
message). Keep references to deleteSelected, onSave, setUsers, setRows, and
toast so you restore state and notify the user if persistence fails.
e2e/logic/POM/graphPage.ts (1)

203-211: ⚠️ Potential issue | 🟡 Minor

Escape before editor click may have side effects.

A blanket keyboard.press("Escape") will also close any open Radix dialog/popover/select — not just tooltips. If a future test opens a dialog and then types into the editor, this will dismiss it. Prefer dismissing tooltips explicitly (e.g., hovering a neutral area or waiting for tooltips to auto-hide).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/logic/POM/graphPage.ts` around lines 203 - 211, The clickEditorInput
method uses this.page.keyboard.press("Escape") which can close dialogs/popovers;
remove that blanket Escape and instead dismiss tooltips explicitly before
clicking: either hover a neutral non-interactive area (e.g., use
this.page.mouse.move or this.page.hover on a known neutral selector) or wait for
tooltip locators to be hidden (use a tooltip selector and waitFor state
'hidden') prior to calling interactWhenVisible(this.editorContainer, ...);
reference the clickEditorInput method, this.page.keyboard.press usage, and
this.editorContainer/interactWhenVisible when making the change.
app/api/auth/tokens/credentials/route.ts (1)

185-190: ⚠️ Potential issue | 🟠 Major

Do not return internal storage errors to token callers.

Line 189 can expose encryption/storage implementation details, such as missing secret/config messages. Keep the detailed error in logs and return a generic 500 response.

Proposed fix
       return NextResponse.json(
-        { message: storageError instanceof Error ? storageError.message : "Failed to store token" },
+        { message: "Failed to store token" },
         { status: 500 }
       );

As per coding guidelines, **/api/**/*: Review API routes for security, error handling, and proper HTTP status codes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/auth/tokens/credentials/route.ts` around lines 185 - 190, The catch
block around token storage currently returns the raw storageError message to
callers (see storageError and NextResponse.json used in the try/catch where
authenticatedUser.username is logged); change this to log the full storageError
locally (console.error or processLogger) but return a generic 500 response body
and message to the client (e.g., { message: "Failed to store token" }) without
including storage/encryption details, ensuring NextResponse.json no longer
exposes storageError.message to external callers.
app/graph/selectGraph.tsx (1)

46-62: ⚠️ Potential issue | 🟠 Major

Gate inline renames with isReadOnly too.

Lines 295-323 hide bulk mutations for read-only connections, but Lines 133 and 161 still render editable name cells for Admin sessions. That leaves graph/schema rename available through the table cell editor in read-only mode.

Proposed fix
 const { data: session } = useSession();
 const sessionRole = session?.user.role;
+const canEditNames = sessionRole === "Admin" && !isReadOnly;
-const baseCell = sessionRole === "Admin"
+const baseCell = canEditNames
     ? { value: opt, onChange: (value: string) => handleSetOption(value, opt), type: "text" as const }
     : { value: opt, type: "readonly" as const };
-}, [type, toast, setIndicator, options, setOptions, setSelectedValue, selectedValue, sessionRole, showMemoryUsage, loadNodesCount, loadEdgesCount, loadMemory]);
+}, [type, toast, setIndicator, options, setOptions, setSelectedValue, selectedValue, canEditNames, showMemoryUsage, loadNodesCount, loadEdgesCount, loadMemory]);
-}, [sessionRole, handleSetOption, loadMemory, loadNodesCount, loadEdgesCount, showMemoryUsage]);
+}, [canEditNames, handleSetOption, loadMemory, loadNodesCount, loadEdgesCount, showMemoryUsage]);

As per coding guidelines, **/*.tsx: Review React components for proper hooks usage, accessibility, and performance.

Also applies to: 131-163, 295-323

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/selectGraph.tsx` around lines 46 - 62, The inline rename capability
is still enabled when sessionRole === 'admin' even for read-only connections;
update the checks that render the editable name cells/table cell editor in
selectGraph.tsx so they require both admin rights and writable connection by
combining sessionRole === 'admin' with !isReadOnly (from
useContext(ConnectionContext)). Locate the JSX/handler that currently gates
editing by sessionRole (the inline rename/table cell editor and any
onDoubleClick/onEdit handlers that reference sessionRole or inputRef) and change
the condition to sessionRole === 'admin' && !isReadOnly, and also ensure any
props passed to the cell editor (editable/readOnly/disabled) reflect isReadOnly
so focus/keyboard editing is prevented when read-only.
app/login/LoginForm.tsx (1)

226-228: ⚠️ Potential issue | 🟡 Minor

decodeURIComponent can throw on malformed query params.

If a user lands on /login?host=%E0 (or any half-encoded value), decodeURIComponent throws URIError and the effect crashes the login page. You already have safeDecode in this very file — reuse it for query params too.

🛡️ Proposed fix
-    setHost(decodeURIComponent(hostParam || ""));
-    setPort(decodeURIComponent(portParam || ""));
-    setUsername(decodeURIComponent(usernameParam ?? ""));
+    setHost(safeDecode(hostParam || ""));
+    setPort(safeDecode(portParam || ""));
+    setUsername(safeDecode(usernameParam ?? ""));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/login/LoginForm.tsx` around lines 226 - 228, The effect uses
decodeURIComponent directly for query params which can throw on malformed input;
replace calls to decodeURIComponent(hostParam || ""),
decodeURIComponent(portParam || ""), and decodeURIComponent(usernameParam ?? "")
with the existing safeDecode helper in this file (use safeDecode(hostParam),
safeDecode(portParam), safeDecode(usernameParam)) and pass an empty string
default to setHost/setPort/setUsername when safeDecode returns undefined/null so
the effect no longer throws on half-encoded params.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2b4bdd68-088f-4288-b99f-9d268c89e062

📥 Commits

Reviewing files that changed from the base of the PR and between fc84462 and e9a3831.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (102)
  • .github/workflows/helm-chart.yml
  • .github/workflows/nextjs.yml
  • .github/workflows/playwright.yml
  • .github/workflows/release-image.yml
  • Dockerfile
  • app/api/auth/[...nextauth]/options.ts
  • app/api/auth/tokenUtils.ts
  • app/api/auth/tokens/credentials/route.ts
  • app/api/auth/tokens/route.ts
  • app/api/graph/[graph]/[element]/[key]/route.ts
  • app/api/graph/[graph]/[element]/label/route.ts
  • app/api/graph/[graph]/[element]/route.ts
  • app/api/graph/[graph]/count/edges/route.ts
  • app/api/graph/[graph]/count/nodes/route.ts
  • app/api/graph/[graph]/count/route.ts
  • app/api/graph/[graph]/info/route.ts
  • app/api/graph/[graph]/route.ts
  • app/api/graph/model.ts
  • app/api/info/route.ts
  • app/api/schema/[schema]/[element]/[key]/_route.ts
  • app/api/schema/[schema]/[element]/_route.ts
  • app/api/schema/[schema]/[element]/label/_route.ts
  • app/api/schema/[schema]/_route.ts
  • app/api/schema/[schema]/count/_route.ts
  • app/api/swagger/swagger-spec.ts
  • app/api/user/[user]/route.ts
  • app/api/user/model.test.ts
  • app/api/user/model.ts
  • app/api/user/route.ts
  • app/api/user/save/route.ts
  • app/api/utils.ts
  • app/api/validate-body.ts
  • app/api/validate-url/route.ts
  • app/components/CreateGraph.tsx
  • app/components/CypherEditor.tsx
  • app/components/ForceGraph.tsx
  • app/components/FormComponent.tsx
  • app/components/Header.tsx
  • app/components/Navbar.tsx
  • app/components/PaginationList.tsx
  • app/components/TableComponent.tsx
  • app/components/provider.ts
  • app/components/ui/Input.tsx
  • app/globals.css
  • app/graph/Chat.tsx
  • app/graph/CreateElementPanel.tsx
  • app/graph/DataPanel.tsx
  • app/graph/DataTable.tsx
  • app/graph/GraphView.tsx
  • app/graph/MetadataView.tsx
  • app/graph/Selector.tsx
  • app/graph/controls.tsx
  • app/graph/graphInfo.tsx
  • app/graph/labels.tsx
  • app/graph/page.tsx
  • app/graph/selectGraph.tsx
  • app/graph/toolbar.tsx
  • app/layout.tsx
  • app/login/LoginForm.tsx
  • app/login/urlUtils.ts
  • app/loginVerification.tsx
  • app/providers.tsx
  • app/schema/DataPanel.tsx
  • app/schema/_page.tsx
  • app/settings/ModelSelector.tsx
  • app/settings/tokens/PersonalAccessTokens.tsx
  • app/settings/users/AddUser.tsx
  • app/settings/users/DeleteUser.tsx
  • app/settings/users/EditUser.tsx
  • app/settings/users/Users.tsx
  • components/ui/popover.tsx
  • components/ui/table.tsx
  • components/ui/toast.tsx
  • e2e/config/urls.json
  • e2e/infra/ui/browserWrapper.ts
  • e2e/logic/POM/chatComponent.ts
  • e2e/logic/POM/customizeStylePage.ts
  • e2e/logic/POM/graphInfoPage.ts
  • e2e/logic/POM/graphPage.ts
  • e2e/logic/POM/loginPage.ts
  • e2e/logic/POM/settingsBrowserPage.ts
  • e2e/logic/POM/settingsTokensPage.ts
  • e2e/logic/POM/settingsUsersPage.ts
  • e2e/logic/api/apiCalls.ts
  • e2e/logic/api/responses/userResponses.ts
  • e2e/tests/auth.setup.ts
  • e2e/tests/chat.spec.ts
  • e2e/tests/graphInfo.spec.ts
  • e2e/tests/login.spec.ts
  • e2e/tests/settingsBrowser.spec.ts
  • e2e/tests/settingsUsers.spec.ts
  • lib/ai-provider-utils.ts
  • lib/token-storage/FalkorDBTokenStorage.ts
  • lib/token-storage/FileTokenStorage.ts
  • lib/token-storage/ITokenStorage.ts
  • lib/utils.ts
  • next.config.js
  • package.json
  • playwright.config.ts
  • tailwind.config.js
  • types/falkordb-canvas.d.ts
  • types/next-auth.d.ts
💤 Files with no reviewable changes (2)
  • e2e/tests/login.spec.ts
  • e2e/logic/POM/loginPage.ts

Comment thread app/api/graph/[graph]/route.ts
Comment thread app/api/schema/[schema]/[element]/label/_route.ts
Comment thread app/api/user/[user]/route.ts
Comment thread app/api/validate-url/route.ts
Comment thread app/components/FormComponent.tsx Outdated
Comment thread app/graph/page.tsx
Comment thread app/schema/_page.tsx
Comment thread lib/utils.ts
Comment thread package.json
Comment thread tailwind.config.js
Anchel123 and others added 2 commits April 23, 2026 11:41
fix: update key pattern descriptions for clarity in AddUser and EditU…
Anchel123 and others added 2 commits April 23, 2026 12:10
fix(FormComponent): optimize error handling by re-validating only on …
Copy link
Copy Markdown
Contributor

@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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/FormComponent.tsx`:
- Around line 99-128: The tag UI still allows mutation when field.disabled is
true; update the wrapper div click handler (which currently calls
inputRef.current?.focus()) to be a no-op when field.disabled, make each tag
remove button non-interactive by adding a disabled attribute and preventing its
onClick from calling field.onRemoveTag when field.disabled, and ensure the
input's onKeyDown/onBlur handlers (handleKeyDown and addTags) do nothing when
field.disabled; also add aria-disabled on the wrapper and aria-hidden or
aria-disabled on the remove buttons to reflect non-editable state so the Badge/X
controls are inert when disabled.
- Around line 78-84: The addTags function can call field.onAddTag multiple times
for duplicate tags within the same input batch because field.tags reflects
previous render state; fix by deduplicating locally before calling onAddTag:
create a local Set (e.g., seen) and for each normalized part skip if it's
already in field.tags or already in seen, otherwise add to seen and call
field.onAddTag(part). Update references to field.tags and field.onAddTag in
addTags so duplicates in the comma-separated value are prevented within the same
invocation.

In `@app/settings/users/AddUser.tsx`:
- Around line 12-13: Change the AddUser component's onAddUser prop to match
EditUser's pattern by accepting and returning Promise<boolean> instead of
Promise<void>, and update the component logic that calls onAddUser (in AddUser)
to only close the drawer when the returned boolean is true (i.e., success). Also
normalize the permissions payload: when the permissions input is empty or equals
[""]/[], convert it to ["*"] before passing to onAddUser so the UI placeholder
"*" and backend default behavior align; update any local vars or submit handler
inside AddUser that build the CreateUser/keys payload accordingly.

In `@app/settings/users/EditUser.tsx`:
- Around line 102-120: Normalize empty key lists before calling the submit
handler: in handleEditUser ensure that if keys is empty or all-empty-string
entries, you replace it with ["*"] and then call onEditUser(username, role,
normalizedKeys, password || undefined). Update references to the keys array only
at submission (function handleEditUser) rather than relying on external
normalization in Users.tsx so the payload always expresses wildcard intent;
mention the functions handleEditUser and onEditUser to locate the change.
- Around line 24-31: The current useEffect resets the form whenever initialKeys
or initialRole change while the drawer is open; change it to only reset on the
closed→open transition by tracking the previous open state (e.g., using a
prevOpen ref) and, inside an effect that depends on open, if open is true and
prevOpen was false then call setRole(initialRole), setKeys(initialKeys ?? []),
setPassword(""), and setConfirmPassword(""); update prevOpen after the check so
subsequent renders while open won't re-reset the form.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 108e1c62-484a-43b5-9587-75523abe0f56

📥 Commits

Reviewing files that changed from the base of the PR and between e9a3831 and e07656a.

📒 Files selected for processing (3)
  • app/components/FormComponent.tsx
  • app/settings/users/AddUser.tsx
  • app/settings/users/EditUser.tsx

Comment thread app/components/FormComponent.tsx
Comment thread app/components/FormComponent.tsx
Comment thread app/settings/users/AddUser.tsx Outdated
Comment thread app/settings/users/EditUser.tsx Outdated
Comment thread app/settings/users/EditUser.tsx Outdated
Anchel123 and others added 4 commits April 23, 2026 13:11
…c FileTokenStorage writes

Root cause of the @readonly token persistence failures in CI:

The parallel Playwright test workers (4 workers, 2 with @readonly) were
concurrently reading and writing the same FileTokenStorage JSON file.
A non-atomic writeFile during a concurrent read caused JSON.parse to
fail; readTokens() silently returned []. This made getPasswordFromTokenDB
throw, triggering SESSION_INVALID + X-Session-Invalid:1, which caused the
client to auto-signOut. The signOut event then permanently deleted the
session credentialRef from the file, breaking every subsequent test that
relied on the readonlyuser session.

Fix 1 — StorageFactory: use FalkorDB when PAT_FALKORDB_HOST is set.
The CI already runs a dedicated PAT FalkorDB container on port 6380 and
sets PAT_FALKORDB_HOST=localhost, but StorageFactory only switched to
FalkorDB when API_TOKEN_FALKORDB_URL was present. Now it also switches
when PAT_FALKORDB_HOST is configured, matching the intent expressed by
the PAT_FALKORDB_* env vars. FalkorDB handles concurrent access natively.

Fix 2 — FileTokenStorage: atomic writes + retry reads.
For local development (no PAT FalkorDB configured), writeTokens now
writes to a .tmp file and atomically renames it so readers never see
a partial write. readTokens retries up to 3 times with 50 ms back-off
before giving up, to handle the brief rename window.

Co-Authored-By: Oz <oz-agent@warp.dev>
fix(tests): enhance timeout settings for node movement tests and impr…
@shahar-biron shahar-biron merged commit 9071592 into main Apr 23, 2026
34 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.

6 participants