Skip to content

Prevent supabase role deletion and creation of roles by users without permission#41600

Open
marsou001 wants to merge 8 commits intosupabase:masterfrom
marsou001:role-deletion-and-creation
Open

Prevent supabase role deletion and creation of roles by users without permission#41600
marsou001 wants to merge 8 commits intosupabase:masterfrom
marsou001:role-deletion-and-creation

Conversation

@marsou001
Copy link
Contributor

@marsou001 marsou001 commented Dec 25, 2025

I have read the CONTRIBUTING.md file.

YES

What kind of change does this PR introduce?

Bug fix, resolves #41599

What is the new behavior?

  1. if the user manually adds delete=${roleId} query param to the url, and roleId is the role id of a Supabase role, the confirmation dialog won't appear. Instead, the user will get a toast message saying "Cannot delete role as it is a Supabase role".

  2. if the user manually adds new=true query param to the url, and he/she isn't allowed to create roles, the action buttons of the side panel ("cancel", "save") are disabled, whether the form values change or not.

Edit: removed useQueryStateWithSelect.

Summary by CodeRabbit

  • New Features

    • Creation panel now respects permissions and shows a disabled state when role creation is not allowed.
    • Delete action selection updated to use explicit role selection, improving deletion flow clarity.
  • Bug Fixes

    • Deletion modal only appears for a valid selected role; invalid delete requests show contextual toasts and reset the selection.
    • Delete actions more reliably target the intended role, reducing accidental deletes.

✏️ Tip: You can customize this high-level summary in your review settings.

@marsou001 marsou001 requested a review from a team as a code owner December 25, 2025 17:35
@github-actions
Copy link
Contributor

Thanks for contributing to Supabase! ❤️ Our team will review your PR.

A few tips for a smoother review process:

  • If you have a local version of the repo, run pnpm run format to make sure formatting checks pass.
  • Once we've reviewed your PR, please don't trivially merge master (don't click Update branch if there are no merge conflicts to be fixed). This invalidates any pre-merge checks we've run.

@vercel
Copy link

vercel bot commented Dec 25, 2025

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

Project Deployment Review Updated (UTC)
design-system Ready Ready Preview, Comment Dec 30, 2025 5:03pm

@vercel
Copy link

vercel bot commented Dec 25, 2025

@marsou001 is attempting to deploy a commit to the Supabase Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 25, 2025

Walkthrough

Added a disabled: boolean prop to CreateRolePanel; switched deletion flow to a numeric roleToDeleteId derived from URL query state; RoleRow deletion callback now expects a number; DeleteRoleModal's role prop type changed to PgRole; added URL validation and contextual toasts for invalid delete IDs.

Changes

Cohort / File(s) Summary
CreateRolePanel
apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
Added disabled: boolean to CreateRolePanelProps and component signature; passed disabled to FormActions and external callers.
RolesList (creation & deletion flow)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx
Replaced string/selected deletion handling with numeric roleToDeleteId via useQueryState('delete', parseAsInteger); derive roleToDelete from roles; show DeleteRoleModal only for a valid role; pass disabled={!canUpdateRoles} to CreateRolePanel; added toast + reset for invalid IDs.
RoleRow callback signature
apps/studio/components/interfaces/Database/Roles/RoleRow.tsx
Changed onSelectDelete prop type from (role: string) => void to (role: number) => void; call sites now pass role.id (number).
DeleteRoleModal type update
apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx
Switched role prop type from PostgresRole to PgRole (import updated); runtime deletion flow unchanged.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BrowserURL as "Browser (query state)"
  participant RolesList
  participant RoleRow
  participant DeleteModal as "DeleteRoleModal"
  participant API
  participant Toast

  Note over BrowserURL,RolesList: Deletion flow uses numeric `delete` query param

  User->>BrowserURL: add ?delete=<id> or click delete
  BrowserURL->>RolesList: query state `delete` updates (number)
  RolesList->>RolesList: derive roleToDelete from roles lists
  alt role found
    RolesList->>DeleteModal: render with roleToDelete
    User->>DeleteModal: confirm deletion
    DeleteModal->>API: request delete(role.id)
    API-->>DeleteModal: success / error
    DeleteModal->>RolesList: close & reset delete query-state
    DeleteModal->>Toast: show success / error
  else role not found
    RolesList->>Toast: show contextual error
    RolesList->>BrowserURL: reset delete query-state
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • joshenlim
  • awaseem

Poem

🐰 I hopped through props and query strings,
Numbers fixed where old text stings,
Panels now heed the disabled sign,
Modals open only for the proper line,
A tiny hop — the UI sings!

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: preventing Supabase role deletion and unauthorized role creation, which matches the PR's primary objectives.
Description check ✅ Passed The PR description addresses the template requirements, confirms reading CONTRIBUTING.md, specifies bug fix with issue link, and explains new behavior with clear examples.
Linked Issues check ✅ Passed The code changes fully address both requirements from issue #41599: preventing Supabase role deletion via URL manipulation with toast error, and disabling create role panel buttons when user lacks permissions.
Out of Scope Changes check ✅ Passed All changes are scoped to role deletion and creation permission enforcement. Type updates (PostgresRole to PgRole) and prop modifications (disabled flag, onSelectDelete callback) directly support the linked issue's requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
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: 0

🧹 Nitpick comments (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (1)

71-89: LGTM: Deletion logic correctly prevents Supabase role deletion.

The useQueryStateWithSelect implementation properly prevents deletion of Supabase-managed roles via URL manipulation:

  1. The select function only searches in otherRoles (line 74), so Supabase roles are never selectable for deletion
  2. The onError callback correctly identifies when a user attempts to delete a Supabase role (lines 77-79) and displays the appropriate error message
  3. The enabled condition ensures the hook only operates when data is loaded

This successfully addresses the PR objective of preventing the ?delete=${roleId} bypass.

Minor: Simplify redundant condition

The id ? check on line 74 is redundant since the select function is only invoked when a non-empty selectedId exists. Consider simplifying:

-    select: (id: string) =>
-      id ? otherRoles?.find((role) => role.id.toString() === id) : undefined,
+    select: (id: string) => otherRoles?.find((role) => role.id.toString() === id),

This is defensive programming and doesn't cause issues, so it's purely a style preference.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89e0fe4 and 186f754.

📒 Files selected for processing (2)
  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧰 Additional context used
📓 Path-based instructions (2)
apps/studio/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.{ts,tsx}: Assign complex conditions to descriptive variables instead of using multiple conditions in a single expression
Use consistent naming conventions for boolean variables: is prefix for state/identity, has prefix for possession, can prefix for capability/permission, should prefix for conditional behavior
Derive boolean state from existing state instead of storing it separately
Use early returns for guard clauses instead of deeply nested conditionals
Extract complex logic into custom hooks when logic becomes reusable or complex
Return objects from custom hooks instead of arrays for better extensibility and clearer API
Use discriminated unions for complex state management instead of multiple independent state fields
Avoid type casting (e.g., as any, as Type); instead validate values at runtime using zod schemas

Files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
apps/studio/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.tsx: Components should ideally be under 200-300 lines; break down large components with multiple distinct UI sections, complex conditional rendering, or multiple unrelated useState hooks
Extract repeated JSX patterns into reusable components instead of copying similar JSX blocks
Use consistent loading/error/success pattern: handle loading state first with early returns, then error state, then empty state, then render success state
Keep state as local as possible and only lift up when needed
Group related state using objects or reducers instead of multiple useState calls, preferring react-hook-form for form state management
Name event handlers consistently: use on prefix for prop callbacks and handle prefix for internal handlers
Avoid inline arrow functions for expensive operations; use useCallback to maintain stable function references
Use appropriate conditional rendering patterns: && for simple show/hide, ternary for binary choice, early returns or extracted components for multiple conditions
Avoid nested ternaries in JSX; use separate conditions or early returns instead
Use useMemo for expensive computations when the computation is genuinely expensive and the value is passed to memoized children
Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧠 Learnings (8)
📓 Common learnings
Learnt from: ignaciodob
Repo: supabase/supabase PR: 41291
File: packages/api-types/types/platform.d.ts:7128-7128
Timestamp: 2025-12-12T11:28:04.537Z
Learning: In supabase/supabase, use the existing 'do-not-merge' label to block merges for entitlement-gated UI changes until the backend entitlement is live and API types are regenerated.
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : Handle dirty state by showing cancel buttons and disabling save buttons based on form.formState.isDirty

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.tsx : Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/pages/**/*.{ts,tsx} : Add table actions to primary and secondary actions of PageLayout if table is main content of a page without search or filters

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : When submit button is outside the form, add a formId variable outside the component, set it as id on form element and form prop on the button

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : Use FormItemLayout without form control for action fields with buttons for navigation or performable actions, wrapping buttons in a div with justify-end

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : Place submit/cancel buttons in SheetFooter for forms in sheets

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
📚 Learning: 2025-12-12T05:20:17.409Z
Learnt from: joshenlim
Repo: supabase/supabase PR: 41258
File: apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx:9-9
Timestamp: 2025-12-12T05:20:17.409Z
Learning: In apps/studio/**/*.{ts,tsx}, use named imports for DefaultLayout: import { DefaultLayout } from 'components/layouts/DefaultLayout' instead of default import. This is the new practice being adopted across the studio app.

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧬 Code graph analysis (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (3)
apps/studio/hooks/misc/useQueryStateWithSelect.ts (1)
  • useQueryStateWithSelect (17-46)
apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts (1)
  • role (741-757)
apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx (1)
  • CreateRolePanel (48-191)
🔇 Additional comments (3)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (2)

66-69: LGTM: URL state management prevents creation bypass.

The isCreatingRole state is now synced with the URL query parameter new, which properly handles the scenario where users manually add ?new=true to the URL. Combined with the disabled prop passed to CreateRolePanel (line 242), this ensures that even if the panel opens via URL manipulation, users without permission cannot submit the form.


240-244: LGTM: Permission-based disabling prevents unauthorized creation.

The disabled={!canUpdateRoles} prop correctly prevents users without permission from submitting the role creation form, even if they bypass the disabled "Add role" button by manually adding ?new=true to the URL. This directly addresses the second bypass scenario described in the PR objectives.

The combination of:

  • URL state management (lines 66-69) that opens the panel
  • Permission check that disables form actions (line 242)

...ensures that unauthorized users cannot create roles through any path.

apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx (1)

24-24: The disabled prop correctly handles permission-based disabling.

The prop is properly typed, destructured at line 48, and passed to FormActions at line 90. The component correctly prevents form submission when the user lacks permission—the disabled prop overrides button state in FormActions via the condition isSubmitting || disabled || (!hasChanges && hasChanges !== undefined), keeping buttons disabled regardless of form dirty state.

@coveralls
Copy link

Coverage Status

coverage: 67.308%. remained the same
when pulling 186f754 on marsou001:role-deletion-and-creation
into 89e0fe4 on supabase:master.

Copy link
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: 1

🧹 Nitpick comments (2)
apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx (1)

27-27: Consider removing redundant null guards.

The role prop is required per the interface, and RolesList.tsx only renders this modal when roleToDelete !== undefined. The guard on line 27 and optional chaining on line 43 are unnecessary.

🔎 Proposed fix
   const deleteRole = async () => {
     if (!project) return console.error('Project is required')
-    if (!role) return console.error('Failed to delete role: role is missing')
     onDelete?.()
     deleteDatabaseRole({
-      header={<h3>Confirm to delete role "{role?.name}"</h3>}
+      header={<h3>Confirm to delete role "{role.name}"</h3>}

Also applies to: 43-43

apps/studio/components/interfaces/Database/Roles/RolesList.tsx (1)

245-249: Redundant visible prop on DeleteRoleModal.

Since the modal is conditionally rendered only when roleToDelete !== undefined, the visible prop always evaluates to true. Consider removing it or relying solely on conditional rendering.

🔎 Proposed fix
-      {roleToDelete !== undefined && <DeleteRoleModal
-        role={roleToDelete}
-        visible={roleToDelete !== undefined}
-        onClose={() => setRoleToDeleteId(null)}
-      />}
+      {roleToDelete !== undefined && (
+        <DeleteRoleModal
+          role={roleToDelete}
+          visible
+          onClose={() => setRoleToDeleteId(null)}
+        />
+      )}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 186f754 and ecbe9d7.

📒 Files selected for processing (3)
  • apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx
  • apps/studio/components/interfaces/Database/Roles/RoleRow.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧰 Additional context used
📓 Path-based instructions (2)
apps/studio/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.{ts,tsx}: Assign complex conditions to descriptive variables instead of using multiple conditions in a single expression
Use consistent naming conventions for boolean variables: is prefix for state/identity, has prefix for possession, can prefix for capability/permission, should prefix for conditional behavior
Derive boolean state from existing state instead of storing it separately
Use early returns for guard clauses instead of deeply nested conditionals
Extract complex logic into custom hooks when logic becomes reusable or complex
Return objects from custom hooks instead of arrays for better extensibility and clearer API
Use discriminated unions for complex state management instead of multiple independent state fields
Avoid type casting (e.g., as any, as Type); instead validate values at runtime using zod schemas

Files:

  • apps/studio/components/interfaces/Database/Roles/RoleRow.tsx
  • apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
apps/studio/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.tsx: Components should ideally be under 200-300 lines; break down large components with multiple distinct UI sections, complex conditional rendering, or multiple unrelated useState hooks
Extract repeated JSX patterns into reusable components instead of copying similar JSX blocks
Use consistent loading/error/success pattern: handle loading state first with early returns, then error state, then empty state, then render success state
Keep state as local as possible and only lift up when needed
Group related state using objects or reducers instead of multiple useState calls, preferring react-hook-form for form state management
Name event handlers consistently: use on prefix for prop callbacks and handle prefix for internal handlers
Avoid inline arrow functions for expensive operations; use useCallback to maintain stable function references
Use appropriate conditional rendering patterns: && for simple show/hide, ternary for binary choice, early returns or extracted components for multiple conditions
Avoid nested ternaries in JSX; use separate conditions or early returns instead
Use useMemo for expensive computations when the computation is genuinely expensive and the value is passed to memoized children
Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Files:

  • apps/studio/components/interfaces/Database/Roles/RoleRow.tsx
  • apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: ignaciodob
Repo: supabase/supabase PR: 41291
File: packages/api-types/types/platform.d.ts:7128-7128
Timestamp: 2025-12-12T11:28:04.537Z
Learning: In supabase/supabase, use the existing 'do-not-merge' label to block merges for entitlement-gated UI changes until the backend entitlement is live and API types are regenerated.
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.tsx : Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RoleRow.tsx
📚 Learning: 2025-12-12T05:20:17.409Z
Learnt from: joshenlim
Repo: supabase/supabase PR: 41258
File: apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx:9-9
Timestamp: 2025-12-12T05:20:17.409Z
Learning: In apps/studio/**/*.{ts,tsx}, use named imports for DefaultLayout: import { DefaultLayout } from 'components/layouts/DefaultLayout' instead of default import. This is the new practice being adopted across the studio app.

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RoleRow.tsx
  • apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/pages/**/*.{ts,tsx} : Add table actions to primary and secondary actions of PageLayout if table is main content of a page without search or filters

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.{ts,tsx} : Use discriminated unions for complex state management instead of multiple independent state fields

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧬 Code graph analysis (3)
apps/studio/components/interfaces/Database/Roles/RoleRow.tsx (1)
apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts (1)
  • role (741-757)
apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx (1)
apps/studio/data/database-roles/database-roles-query.ts (1)
  • PgRole (14-14)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (2)
apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx (1)
  • CreateRolePanel (48-191)
apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx (1)
  • DeleteRoleModal (15-54)
🔇 Additional comments (5)
apps/studio/components/interfaces/Database/Roles/RoleRow.tsx (2)

24-28: LGTM! Type alignment for numeric role ID.

The prop type update from string to number correctly aligns with PgRole.id's type and the updated deletion state management in RolesList.tsx.


140-145: LGTM! Simplified callback invocation.

Passing role.id directly removes unnecessary type conversion and is consistent with the updated prop signature.

apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx (1)

1-1: LGTM! Type migration to PgRole.

The change from PostgresRole to PgRole aligns with the Zod-inferred type from database-roles-query.ts and improves consistency across the roles feature.

Also applies to: 8-9

apps/studio/components/interfaces/Database/Roles/RolesList.tsx (2)

64-72: LGTM! URL state management for role creation and deletion.

Using nuqs with parseAsBoolean and parseAsInteger for URL state provides type-safe query parameter handling with appropriate defaults and history management.


239-243: LGTM! Properly gates role creation panel with permissions.

Passing disabled={!canUpdateRoles} to CreateRolePanel ensures that users who bypass the button via URL manipulation (?new=true) cannot interact with the form actions, addressing the linked issue #41599.

Comment on lines 76 to 88
useEffect(() => {
if (!isLoading && roleToDeleteId !== null && roleToDelete === undefined) {
const isSupabaseRole =
supabaseRoles.find((role) => role.id === roleToDeleteId) !== undefined
if (isSupabaseRole) {
toast.error('Cannot delete role as it is a Supabase role')
} else {
toast.error('Database Role not found')
}

setRoleToDeleteId(null)
}
}, [isLoading, roleToDeleteId, roleToDelete])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add missing dependencies to useEffect.

The dependency array is missing supabaseRoles and setRoleToDeleteId. While setRoleToDeleteId from useQueryState is likely stable, supabaseRoles is derived fresh on each render and could cause stale closure issues if the data changes while the effect is pending.

🔎 Proposed fix
   useEffect(() => {
     if (!isLoading && roleToDeleteId !== null && roleToDelete === undefined) {
       const isSupabaseRole =
         supabaseRoles.find((role) => role.id === roleToDeleteId) !== undefined
       if (isSupabaseRole) {
         toast.error('Cannot delete role as it is a Supabase role')
       } else {
         toast.error('Database Role not found')
       }

       setRoleToDeleteId(null)
     }
-  }, [isLoading, roleToDeleteId, roleToDelete])
+  }, [isLoading, roleToDeleteId, roleToDelete, supabaseRoles, setRoleToDeleteId])
📝 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
useEffect(() => {
if (!isLoading && roleToDeleteId !== null && roleToDelete === undefined) {
const isSupabaseRole =
supabaseRoles.find((role) => role.id === roleToDeleteId) !== undefined
if (isSupabaseRole) {
toast.error('Cannot delete role as it is a Supabase role')
} else {
toast.error('Database Role not found')
}
setRoleToDeleteId(null)
}
}, [isLoading, roleToDeleteId, roleToDelete])
useEffect(() => {
if (!isLoading && roleToDeleteId !== null && roleToDelete === undefined) {
const isSupabaseRole =
supabaseRoles.find((role) => role.id === roleToDeleteId) !== undefined
if (isSupabaseRole) {
toast.error('Cannot delete role as it is a Supabase role')
} else {
toast.error('Database Role not found')
}
setRoleToDeleteId(null)
}
}, [isLoading, roleToDeleteId, roleToDelete, supabaseRoles, setRoleToDeleteId])
🤖 Prompt for AI Agents
In apps/studio/components/interfaces/Database/Roles/RolesList.tsx around lines
76 to 88, the useEffect callback references supabaseRoles and setRoleToDeleteId
but they are not listed in the dependency array causing potential stale
closures; update the dependency array to include supabaseRoles and
setRoleToDeleteId (even if stable) so the effect re-runs when role lists or the
setter change, ensuring the toast logic and reset of roleToDeleteId use current
values.

Copy link
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: 0

♻️ Duplicate comments (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (1)

30-30: Fix ref logic bug and dependency array issues.

The deletingRoleIdRef pattern has two critical flaws:

  1. Ref is never reset: After a deletion completes (Lines 251–255 set deletingRoleIdRef.current = roleToDeleteId), the ref retains that ID indefinitely. If a user later manually navigates to ?delete=<same-id> after the role is deleted, the condition on Line 84 (deletingRoleIdRef.current === null) will be false, preventing the error toast from displaying.

  2. Anti-pattern in dependency array (Line 95): Including deletingRoleIdRef.current has no effect—refs don't cause re-renders. Remove it from the array.

  3. Missing dependencies (flagged in past review): supabaseRoles and setRoleToDeleteId should be included.

🔎 Recommended fix

Change the ref check to compare with the current roleToDeleteId, and fix the dependency array:

  useEffect(() => {
-   if (
-     !isLoading &&
-     roleToDeleteId !== null &&
-     roleToDelete === undefined &&
-     deletingRoleIdRef.current === null
-   ) {
+   if (!isLoading && roleToDeleteId !== null && roleToDelete === undefined) {
+     // Only show error if this deletion attempt is not the one currently in progress
+     if (deletingRoleIdRef.current !== roleToDeleteId) {
        const isSupabaseRole = supabaseRoles.find((role) => role.id === roleToDeleteId) !== undefined
        if (isSupabaseRole) {
          toast.error('Cannot delete role as it is a Supabase role')
        } else {
          toast.error('Database Role not found')
        }
  
        setRoleToDeleteId(null)
+     }
    }
-  }, [isLoading, roleToDeleteId, roleToDelete, deletingRoleIdRef.current])
+  }, [isLoading, roleToDeleteId, roleToDelete, supabaseRoles, setRoleToDeleteId])

This ensures the error displays correctly for all invalid deletion attempts, including repeated attempts on the same deleted role.

Also applies to: 79-95, 251-255

🧹 Nitpick comments (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (1)

247-250: Avoid type assertion; the visibility check already ensures safety.

Line 248 uses as PGRole, which violates the coding guideline to avoid type casting. Since visible={roleToDelete !== undefined} guarantees that roleToDelete is defined when the modal renders, the cast is redundant and could mask type issues.

🔎 Refactor options

Option 1: Make DeleteRoleModal.role optional and handle undefined internally:

 <DeleteRoleModal
-  role={roleToDelete as PGRole}
+  role={roleToDelete}
   visible={roleToDelete !== undefined}

Option 2: Use non-null assertion with a comment explaining the visibility guard:

 <DeleteRoleModal
-  role={roleToDelete as PGRole}
+  role={roleToDelete!} // Safe: visibility ensures roleToDelete is defined
   visible={roleToDelete !== undefined}

Option 1 is preferred as it avoids all assertions.

As per coding guidelines: avoid type casting and validate values at runtime.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecbe9d7 and 50b5b82.

📒 Files selected for processing (1)
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧰 Additional context used
📓 Path-based instructions (2)
apps/studio/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.{ts,tsx}: Assign complex conditions to descriptive variables instead of using multiple conditions in a single expression
Use consistent naming conventions for boolean variables: is prefix for state/identity, has prefix for possession, can prefix for capability/permission, should prefix for conditional behavior
Derive boolean state from existing state instead of storing it separately
Use early returns for guard clauses instead of deeply nested conditionals
Extract complex logic into custom hooks when logic becomes reusable or complex
Return objects from custom hooks instead of arrays for better extensibility and clearer API
Use discriminated unions for complex state management instead of multiple independent state fields
Avoid type casting (e.g., as any, as Type); instead validate values at runtime using zod schemas

Files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
apps/studio/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.tsx: Components should ideally be under 200-300 lines; break down large components with multiple distinct UI sections, complex conditional rendering, or multiple unrelated useState hooks
Extract repeated JSX patterns into reusable components instead of copying similar JSX blocks
Use consistent loading/error/success pattern: handle loading state first with early returns, then error state, then empty state, then render success state
Keep state as local as possible and only lift up when needed
Group related state using objects or reducers instead of multiple useState calls, preferring react-hook-form for form state management
Name event handlers consistently: use on prefix for prop callbacks and handle prefix for internal handlers
Avoid inline arrow functions for expensive operations; use useCallback to maintain stable function references
Use appropriate conditional rendering patterns: && for simple show/hide, ternary for binary choice, early returns or extracted components for multiple conditions
Avoid nested ternaries in JSX; use separate conditions or early returns instead
Use useMemo for expensive computations when the computation is genuinely expensive and the value is passed to memoized children
Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧠 Learnings (9)
📓 Common learnings
Learnt from: ignaciodob
Repo: supabase/supabase PR: 41291
File: packages/api-types/types/platform.d.ts:7128-7128
Timestamp: 2025-12-12T11:28:04.537Z
Learning: In supabase/supabase, use the existing 'do-not-merge' label to block merges for entitlement-gated UI changes until the backend entitlement is live and API types are regenerated.
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.tsx : Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/pages/**/*.{ts,tsx} : Add table actions to primary and secondary actions of PageLayout if table is main content of a page without search or filters

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : When doing a mutation, always use the mutate function with onSuccess and onError callbacks that include toast.success and toast.error

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : Contain tables within a card

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.{ts,tsx} : Use discriminated unions for complex state management instead of multiple independent state fields

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.tsx : Avoid inline arrow functions for expensive operations; use useCallback to maintain stable function references

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : Use mutateAsync only if the mutation is part of multiple async actions, wrapping the call with try/catch block and adding toast.success and toast.error

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-12T05:20:17.409Z
Learnt from: joshenlim
Repo: supabase/supabase PR: 41258
File: apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx:9-9
Timestamp: 2025-12-12T05:20:17.409Z
Learning: In apps/studio/**/*.{ts,tsx}, use named imports for DefaultLayout: import { DefaultLayout } from 'components/layouts/DefaultLayout' instead of default import. This is the new practice being adopted across the studio app.

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧬 Code graph analysis (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (4)
apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts (1)
  • role (741-757)
apps/studio/components/interfaces/Database/Roles/RoleRow.tsx (1)
  • RoleRow (30-221)
apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx (1)
  • CreateRolePanel (48-191)
apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx (1)
  • DeleteRoleModal (15-54)
🔇 Additional comments (5)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (5)

1-1: LGTM: Imports support the new URL-based state management.

The new imports—PGRole for type safety, useQueryState with parsers for URL state, useEffect/useRef for side effects, and toast for user feedback—are appropriate for the deletion/creation flow changes.

Also applies to: 5-7


66-74: LGTM: URL query state configuration is appropriate.

Using parseAsBoolean for isCreatingRole and parseAsInteger for roleToDeleteId with history: 'push' and clearOnDefault: true correctly establishes URL-backed state for role creation and deletion flows.


76-76: LGTM: Derived state correctly restricts deletion to non-Supabase roles.

Deriving roleToDelete from otherRoles.find(...) ensures only non-Supabase-managed roles can be deleted, aligning with the PR objective.


215-215: LGTM: RoleRow deletion handlers correctly wired.

Passing setRoleToDeleteId to onSelectDelete aligns with the new ID-based deletion flow. According to the relevant code snippets, RoleRow invokes onSelectDelete(role.id) (a number), which matches the expected signature.

Also applies to: 231-231


241-245: LGTM: Correctly gates role creation based on permissions.

Passing disabled={!canUpdateRoles} to CreateRolePanel ensures users lacking creation permissions cannot interact with the role-creation form, even if they bypass the disabled "Add role" button by manually adding ?new=true to the URL. This directly addresses the second PR objective.

@marsou001
Copy link
Contributor Author

  1. deletingRoleIdRef.current is only supposed to be null at the start. Its value afterwards doesn't matter that much. Only reason it exists is after query invalidation following a successful role deletion, roleToDelete becomes undefined before roleToDeleteId is set to null, triggering the toast message 'Database Role not found', shortly before the success message. I just checked it.
  2. You have a point about the ref inside the dependency array, I'll remove it.
  3. I'm not concerned about supabaseRoles and setRoleToDeleteId because their values don't impact the logic inside of useEffect.

Copy link
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: 1

♻️ Duplicate comments (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (1)

78-95: Add missing dependencies to useEffect.

The dependency array is missing supabaseRoles and setRoleToDeleteId. While setRoleToDeleteId from useQueryState is likely stable, supabaseRoles is derived fresh on each render and could cause stale closure issues.

🧹 Nitpick comments (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (1)

247-256: Avoid type assertion; use conditional rendering instead.

Per coding guidelines, type casting should be avoided. While the cast is safe at runtime (modal is only visible when roleToDelete is defined), you can eliminate the assertion by conditionally rendering the modal.

🔎 Proposed fix
-      <DeleteRoleModal
-        role={roleToDelete as PGRole}
-        visible={roleToDelete !== undefined}
-        onClose={() => setRoleToDeleteId(null)}
-        onDelete={() => {
-          if (roleToDelete) {
-            deletingRoleIdRef.current = roleToDeleteId
-          }
-        }}
-      />
+      {roleToDelete !== undefined && (
+        <DeleteRoleModal
+          role={roleToDelete}
+          visible
+          onClose={() => setRoleToDeleteId(null)}
+          onDelete={() => {
+            deletingRoleIdRef.current = roleToDeleteId
+          }}
+        />
+      )}

This removes the type assertion and the redundant if (roleToDelete) check inside onDelete.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50b5b82 and 68775f5.

📒 Files selected for processing (1)
  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧰 Additional context used
📓 Path-based instructions (2)
apps/studio/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.{ts,tsx}: Assign complex conditions to descriptive variables instead of using multiple conditions in a single expression
Use consistent naming conventions for boolean variables: is prefix for state/identity, has prefix for possession, can prefix for capability/permission, should prefix for conditional behavior
Derive boolean state from existing state instead of storing it separately
Use early returns for guard clauses instead of deeply nested conditionals
Extract complex logic into custom hooks when logic becomes reusable or complex
Return objects from custom hooks instead of arrays for better extensibility and clearer API
Use discriminated unions for complex state management instead of multiple independent state fields
Avoid type casting (e.g., as any, as Type); instead validate values at runtime using zod schemas

Files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
apps/studio/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/studio-best-practices.mdc)

apps/studio/**/*.tsx: Components should ideally be under 200-300 lines; break down large components with multiple distinct UI sections, complex conditional rendering, or multiple unrelated useState hooks
Extract repeated JSX patterns into reusable components instead of copying similar JSX blocks
Use consistent loading/error/success pattern: handle loading state first with early returns, then error state, then empty state, then render success state
Keep state as local as possible and only lift up when needed
Group related state using objects or reducers instead of multiple useState calls, preferring react-hook-form for form state management
Name event handlers consistently: use on prefix for prop callbacks and handle prefix for internal handlers
Avoid inline arrow functions for expensive operations; use useCallback to maintain stable function references
Use appropriate conditional rendering patterns: && for simple show/hide, ternary for binary choice, early returns or extracted components for multiple conditions
Avoid nested ternaries in JSX; use separate conditions or early returns instead
Use useMemo for expensive computations when the computation is genuinely expensive and the value is passed to memoized children
Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧠 Learnings (7)
📓 Common learnings
Learnt from: ignaciodob
Repo: supabase/supabase PR: 41291
File: packages/api-types/types/platform.d.ts:7128-7128
Timestamp: 2025-12-12T11:28:04.537Z
Learning: In supabase/supabase, use the existing 'do-not-merge' label to block merges for entitlement-gated UI changes until the backend entitlement is live and API types are regenerated.
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : When doing a mutation, always use the mutate function with onSuccess and onError callbacks that include toast.success and toast.error

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/pages/**/*.{ts,tsx} : Add table actions to primary and secondary actions of PageLayout if table is main content of a page without search or filters

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.tsx : Define prop interfaces explicitly for React components with proper typing of props and callback handlers

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T17:04:40.037Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-ui.mdc:0-0
Timestamp: 2025-12-11T17:04:40.037Z
Learning: Applies to apps/studio/components/**/*.{ts,tsx} : Use mutateAsync only if the mutation is part of multiple async actions, wrapping the call with try/catch block and adding toast.success and toast.error

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-11T16:46:18.701Z
Learnt from: CR
Repo: supabase/supabase PR: 0
File: .cursor/rules/studio-best-practices.mdc:0-0
Timestamp: 2025-12-11T16:46:18.701Z
Learning: Applies to apps/studio/**/*.tsx : Avoid inline arrow functions for expensive operations; use useCallback to maintain stable function references

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
📚 Learning: 2025-12-12T05:20:17.409Z
Learnt from: joshenlim
Repo: supabase/supabase PR: 41258
File: apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx:9-9
Timestamp: 2025-12-12T05:20:17.409Z
Learning: In apps/studio/**/*.{ts,tsx}, use named imports for DefaultLayout: import { DefaultLayout } from 'components/layouts/DefaultLayout' instead of default import. This is the new practice being adopted across the studio app.

Applied to files:

  • apps/studio/components/interfaces/Database/Roles/RolesList.tsx
🧬 Code graph analysis (1)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (3)
apps/studio/components/interfaces/Database/Roles/RoleRow.tsx (1)
  • RoleRow (30-221)
apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx (1)
  • CreateRolePanel (48-191)
apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx (1)
  • DeleteRoleModal (15-54)
🔇 Additional comments (4)
apps/studio/components/interfaces/Database/Roles/RolesList.tsx (4)

1-7: LGTM!

The imports are appropriately added to support the new URL state management with nuqs, validation logic with useEffect, and user feedback with toast.


214-216: LGTM!

Supabase-managed roles are correctly rendered with disabled to prevent modification, and the delete option won't be shown in the dropdown.


226-233: LGTM!

Permission-based disabling is correctly applied, and the delete handler properly sets the URL state.


241-245: LGTM!

The disabled prop correctly prevents users without permission from submitting the form even if they bypass the button by adding ?new=true to the URL. This addresses the linked issue.

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.

Users can delete Supabase roles, and users without permission can still create roles

2 participants