Skip to content

LO-015: Multi-User Role & Permission System#309

Open
Anushreer22 wants to merge 7 commits into
Kuldeeep18:mainfrom
Anushreer22:feature/role-based-access
Open

LO-015: Multi-User Role & Permission System#309
Anushreer22 wants to merge 7 commits into
Kuldeeep18:mainfrom
Anushreer22:feature/role-based-access

Conversation

@Anushreer22

@Anushreer22 Anushreer22 commented Jun 18, 2026

Copy link
Copy Markdown

Closes #183

Summary

Implemented role-based access control (RBAC) for the multi-tenant system.

Changes

  • Roles defined: ADMIN, MANAGER, MEMBER with proper choices on User model.
  • Registration fix: RegisterSerializer no longer hardcodes ADMIN; new users get MEMBER by default.
  • Permission classes: Added IsOrgAdmin and IsOrgManagerOrAdmin in users/permissions.py.
  • Protected endpoints: Destructive actions (delete leads, campaigns, organization) are restricted to admins; campaign creation restricted to managers+.
  • Invite API: New POST /api/users/invite/ endpoint (admin-only) to invite users with a specified role.
  • Frontend: Team Members section in Settings page with role badges and invite form.

Files changed

  • backend/users/models.py
  • backend/users/serializers.py
  • backend/users/permissions.py (new)
  • backend/campaigns/views.py
  • backend/leads/views.py
  • backend/users/views.py
  • frontend/settings.html

Summary by CodeRabbit

  • New Features
    • Added team member invitation feature allowing admins to invite users via email.
    • Added Team Members section in settings to manage organization users.
    • Implemented role-based access control with Admin and Manager roles for improved permission management.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a role-based access control system: new IsOrgAdmin and IsOrgManagerOrAdmin DRF permission classes, a migration altering the role field with explicit choices, a fix to RegisterSerializer that replaces the hardcoded ADMIN role with a configurable default of MEMBER, a new invite_user endpoint on AuthViewSet, permission enforcement on destructive endpoints, and a Team Members UI on the settings page.

Changes

Role-Based Access Control System

Layer / File(s) Summary
Role field migration, permission classes & serializer fix
backend/users/migrations/0002_alter_user_role.py, backend/users/models.py, backend/users/permissions.py, backend/users/serializers.py
Migration alters user.role to a CharField with ADMIN/MANAGER/USER choices and MEMBER default. IsOrgAdmin and IsOrgManagerOrAdmin permission classes are introduced. RegisterSerializer.create now derives role from validated_data.get('role', 'MEMBER') instead of a hardcoded 'ADMIN'.
invite_user action & delete_organization permission tightening
backend/users/views.py
delete_organization is now gated by IsOrgAdmin. New invite_user POST action at url_path='invite' validates email and role, rejects duplicates in the org, creates an inactive user, and returns a JSON message.
IsOrgAdmin applied to leads delete-all & campaigns import
backend/leads/views.py, backend/campaigns/views.py
leads/views.py imports the new permission classes and applies @permission_classes([IsOrgAdmin]) to the delete-all bulk action. campaigns/views.py imports the same classes for future enforcement.
Settings page Team Members section
frontend/settings.html
Adds a Team Members card with a table populated from GET /api/users/, role badges, and an Invite Member form that POSTs to /api/users/invite/, alerts the result, and refreshes the table on success.

Sequence Diagram(s)

sequenceDiagram
  participant Admin as Admin Browser
  participant SettingsPage as settings.html
  participant AuthViewSet
  participant UserModel

  Admin->>SettingsPage: Opens Settings
  SettingsPage->>AuthViewSet: GET /api/users/
  AuthViewSet-->>SettingsPage: [{email, role}, ...]
  SettingsPage->>Admin: Renders members table with role badges

  Admin->>SettingsPage: Submits Invite Member form {email, role}
  SettingsPage->>AuthViewSet: POST /api/users/invite/ {email, role}
  AuthViewSet->>AuthViewSet: IsOrgAdmin.has_permission()
  AuthViewSet->>UserModel: filter(email, organization)
  alt duplicate
    AuthViewSet-->>SettingsPage: 400 error
    SettingsPage->>Admin: alert(error)
  else new user
    AuthViewSet->>UserModel: create_user(is_active=False)
    AuthViewSet-->>SettingsPage: 201 {message}
    SettingsPage->>Admin: alert(message) + reload table
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #183 (LO-015: Implement Multi-User Role & Permission System) — This PR directly implements all steps described in the issue: creates IsOrgAdmin/IsOrgManagerOrAdmin permission classes, fixes the hardcoded ADMIN role in RegisterSerializer, adds the invite_user endpoint, applies permissions to destructive endpoints, and builds the Team Members UI in settings.

Poem

🐇 A rabbit once guarded the warren with care,
Now only the ADMIN may delete what is there!
With MANAGER and MEMBER each given their place,
Invitations flow in — what an orderly space.
No more hardcoded roles in a serializer's den,
The roles hop through choices again and again! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing multi-user role and permission system (RBAC), which aligns with the PR's primary objectives and all substantial code changes.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #183: three-tier role system, invitation API endpoint, permission classes (IsOrgAdmin, IsOrgManagerOrAdmin), frontend Team Members section, and updated serializer to default MEMBER role.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue #183 objectives. Migration file, permission classes, serializer updates, view modifications, and frontend enhancements all serve the core RBAC implementation goal.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
backend/users/serializers.py (1)

34-35: ⚡ Quick win

Make registration role assignment explicit and fix the stale comment.

RegisterSerializer has no role field, so Line 34 effectively always sets MEMBER. The current comment (“First user in org is admin”) is now misleading.

Suggested cleanup
-            role = validated_data.get('role', 'MEMBER'),  # First user in org is admin
+            role='MEMBER',  # New users default to member
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/users/serializers.py` around lines 34 - 35, The RegisterSerializer
does not have a role field defined, which means the validated_data.get('role',
'MEMBER') call on line 34 will always use the default 'MEMBER' value regardless
of user input. The comment stating "First user in org is admin" is misleading
and stale since this code never implements that logic. Remove the misleading
comment and make the role assignment explicit by either using 'MEMBER' directly
without the get() call since the role field doesn't exist, or implement proper
logic to check if this is the first user in the organization and assign the
admin role accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/leads/views.py`:
- Around line 71-72: The `@permission_classes` decorator on the line following
`@action` is incorrectly indented at the method-body level instead of at the
decorator level, causing a syntax error. Remove the
`@permission_classes`([IsOrgAdmin]) decorator line entirely and instead pass the
permission_classes parameter directly into the `@action`() decorator call. This is
the correct DRF pattern for specifying permissions on action decorators.

In `@backend/users/migrations/0002_alter_user_role.py`:
- Around line 17-18: The migration file has a mismatch where the choices list on
Line 17 includes USER, ADMIN, and MANAGER, but the default value on Line 18 is
set to MEMBER which is not in the choices list. This conflicts with the default
value of USER in backend/users/models.py and the MEMBER value written in
backend/users/serializers.py. Change the default value from MEMBER to USER to
align with the choices and the models.py default. If any existing database rows
already contain USER values, create an additional data migration to map those
USER values to MEMBER to maintain consistency with the serializer behavior, or
adjust the serializer and models defaults to align with the final choice you
select.

In `@backend/users/views.py`:
- Around line 71-93: Fix the structural issues in the delete_organization and
invite_user methods: move the incorrectly indented `@permission_classes` decorator
from inside the delete_organization method body to be a proper decorator before
the method definition, properly close the Response object in delete_organization
with closing braces and parenthesis, move the invite_user method out of the
delete_organization method body to be a separate class-level method, remove the
orphaned code fragments at the end, and change the default role value from
'MEMBER' to 'USER' in the invite_user method on line 80 to match the valid
User.ROLE_CHOICES values defined in the models.

In `@frontend/settings.html`:
- Around line 475-489: The invite form containing the input with
id="invite-email", select with id="invite-role", and button with
id="send-invite-btn" is currently always visible and functional regardless of
user role, causing non-admin users to encounter preventable 403 errors. Add
client-side role checking to conditionally display or hide the entire invite
section (the h3 "Invite Member" heading and its associated form controls) based
on whether the current user has admin permissions, ensuring only admins see and
can interact with the invite functionality.
- Around line 493-507: The code references an undefined variable
`currentUserOrgId` in the fetch call and uses raw fetch instead of the page's
authenticated API helper for both the user list load and invite functionality.
Replace the raw fetch calls to `/api/users/?organization=` and
`/api/users/invite/` with the authenticated API helper that your page provides,
which should handle authentication and organization context automatically.
Remove the dependency on the undefined `currentUserOrgId` variable by using the
authenticated helper's built-in request flow instead.
- Around line 484-486: The invite role dropdown option with the text "Member"
uses the value "MEMBER", but the backend validation only accepts "ADMIN",
"MANAGER", and "USER". Change the value attribute of the option element that
displays "Member" from "MEMBER" to "USER" to align with the backend enum
validation and prevent invite failures.
- Line 496: The tbody.innerHTML assignment is directly injecting u.email and
u.role values into HTML without escaping, creating an XSS vulnerability. Instead
of building an HTML string with template literals, create DOM elements
programmatically using textContent to safely render the email and role values.
Additionally, implement a role-class whitelist to validate that only approved
role values are used when applying the badge CSS class name, preventing
injection through the role field.

---

Nitpick comments:
In `@backend/users/serializers.py`:
- Around line 34-35: The RegisterSerializer does not have a role field defined,
which means the validated_data.get('role', 'MEMBER') call on line 34 will always
use the default 'MEMBER' value regardless of user input. The comment stating
"First user in org is admin" is misleading and stale since this code never
implements that logic. Remove the misleading comment and make the role
assignment explicit by either using 'MEMBER' directly without the get() call
since the role field doesn't exist, or implement proper logic to check if this
is the first user in the organization and assign the admin role accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e06b380e-e115-45d6-ad70-30ec33ce10ea

📥 Commits

Reviewing files that changed from the base of the PR and between 90052f9 and 761a3d9.

📒 Files selected for processing (8)
  • backend/campaigns/views.py
  • backend/leads/views.py
  • backend/users/migrations/0002_alter_user_role.py
  • backend/users/models.py
  • backend/users/permissions.py
  • backend/users/serializers.py
  • backend/users/views.py
  • frontend/settings.html

Comment thread backend/leads/views.py
Comment on lines 71 to +72
@action(detail=False, methods=['delete'], url_path='delete-all')
@permission_classes([IsOrgAdmin])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Syntax error: Decorator incorrectly indented inside method body.

The @permission_classes([IsOrgAdmin]) on Line 72 is indented at method-body level rather than decorator level, causing a syntax error. In DRF, pass permission_classes directly to @action().

Proposed fix
-    `@action`(detail=False, methods=['delete'], url_path='delete-all')
-        `@permission_classes`([IsOrgAdmin])
+    `@action`(detail=False, methods=['delete'], url_path='delete-all', permission_classes=[IsOrgAdmin])
     def delete_all(self, request):
📝 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
@action(detail=False, methods=['delete'], url_path='delete-all')
@permission_classes([IsOrgAdmin])
`@action`(detail=False, methods=['delete'], url_path='delete-all', permission_classes=[IsOrgAdmin])
def delete_all(self, request):
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 72-72: Expected class, function definition or async function definition after decorator

(invalid-syntax)

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

In `@backend/leads/views.py` around lines 71 - 72, The `@permission_classes`
decorator on the line following `@action` is incorrectly indented at the
method-body level instead of at the decorator level, causing a syntax error.
Remove the `@permission_classes`([IsOrgAdmin]) decorator line entirely and instead
pass the permission_classes parameter directly into the `@action`() decorator
call. This is the correct DRF pattern for specifying permissions on action
decorators.

Source: Linters/SAST tools

Comment on lines +17 to +18
choices=[("ADMIN", "Admin"), ("MANAGER", "Manager"), ("USER", "User")],
default="MEMBER",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align role values across schema and application paths.

Line 17 allows USER while Line 18 defaults to MEMBER. That conflicts with backend/users/models.py (Line 27 default USER) and backend/users/serializers.py (Line 34 writing MEMBER), so role handling diverges across registration/invite and persisted data.

Suggested fix
field=models.CharField(
-    choices=[("ADMIN", "Admin"), ("MANAGER", "Manager"), ("USER", "User")],
+    choices=[("ADMIN", "Admin"), ("MANAGER", "Manager"), ("MEMBER", "Member")],
     default="MEMBER",
     max_length=20,
)
# backend/users/models.py
ROLE_CHOICES = (
    ('ADMIN', 'Admin'),
    ('MANAGER', 'Manager'),
-   ('USER', 'User')
+   ('MEMBER', 'Member')
)
-role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='USER')
+role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='MEMBER')

If any existing rows already store USER, add a data migration to map USER -> MEMBER.

📝 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
choices=[("ADMIN", "Admin"), ("MANAGER", "Manager"), ("USER", "User")],
default="MEMBER",
choices=[("ADMIN", "Admin"), ("MANAGER", "Manager"), ("MEMBER", "Member")],
default="MEMBER",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/users/migrations/0002_alter_user_role.py` around lines 17 - 18, The
migration file has a mismatch where the choices list on Line 17 includes USER,
ADMIN, and MANAGER, but the default value on Line 18 is set to MEMBER which is
not in the choices list. This conflicts with the default value of USER in
backend/users/models.py and the MEMBER value written in
backend/users/serializers.py. Change the default value from MEMBER to USER to
align with the choices and the models.py default. If any existing database rows
already contain USER values, create an additional data migration to map those
USER values to MEMBER to maintain consistency with the serializer behavior, or
adjust the serializer and models defaults to align with the final choice you
select.

Comment thread backend/users/views.py
Comment on lines 71 to 93
@action(detail=False, methods=['delete'], permission_classes=[IsAuthenticated], url_path='delete-organization')
@permission_classes([IsOrgAdmin])
def delete_organization(self, request):
request.user.organization.delete()
return Response(
{'message': 'Organization successfully deleted.'},
{'message': 'Organization successfully deleted.'
@action(detail=False, methods=['post'], permission_classes=[IsOrgAdmin], url_path='invite')
def invite_user(self, request):
email = request.data.get('email')
role = request.data.get('role', 'MEMBER')
if not email:
return Response({'error': 'Email is required'}, status=400)
if role not in dict(User.ROLE_CHOICES):
return Response({'error': 'Invalid role'}, status=400)
org = request.user.organization
if User.objects.filter(email=email, organization=org).exists():
return Response({'error': 'User already in organization'}, status=400)
user = User.objects.create_user(email=email, organization=org, role=role, is_active=False)
# TODO: send invitation email
return Response({'message': f'Invitation sent to {email} with role {role}'})
},
status=status.HTTP_200_OK,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Code is syntactically broken and will not run.

The delete_organization and invite_user methods have severe structural issues:

  1. Line 72: @permission_classes is incorrectly indented inside the method body rather than placed as a decorator before def.
  2. Line 76: The Response dict is never closed (missing } and )).
  3. Lines 77-90: invite_user is defined inside the broken delete_organization body.
  4. Lines 91-93: Orphaned },, status=HTTP_200_OK,, and ) fragments from the unclosed Response.

Additionally, Line 80 defaults to 'MEMBER' but User.ROLE_CHOICES (per backend/users/models.py:21-26) defines 'USER', not 'MEMBER'. This will pass validation on Line 83 but create users with an invalid role value.

Proposed fix
-    `@action`(detail=False, methods=['delete'], permission_classes=[IsAuthenticated], url_path='delete-organization')
-        `@permission_classes`([IsOrgAdmin])
-    def delete_organization(self, request):
-        request.user.organization.delete()
-        return Response(
-            {'message': 'Organization successfully deleted.'
-    `@action`(detail=False, methods=['post'], permission_classes=[IsOrgAdmin], url_path='invite')
-    def invite_user(self, request):
-        email = request.data.get('email')
-        role = request.data.get('role', 'MEMBER')
-        if not email:
-            return Response({'error': 'Email is required'}, status=400)
-        if role not in dict(User.ROLE_CHOICES):
-            return Response({'error': 'Invalid role'}, status=400)
-        org = request.user.organization
-        if User.objects.filter(email=email, organization=org).exists():
-            return Response({'error': 'User already in organization'}, status=400)
-        user = User.objects.create_user(email=email, organization=org, role=role, is_active=False)
-        # TODO: send invitation email
-        return Response({'message': f'Invitation sent to {email} with role {role}'})
-},
-            status=status.HTTP_200_OK,
-        )
+    `@action`(detail=False, methods=['delete'], permission_classes=[IsOrgAdmin], url_path='delete-organization')
+    def delete_organization(self, request):
+        request.user.organization.delete()
+        return Response(
+            {'message': 'Organization successfully deleted.'},
+            status=status.HTTP_200_OK,
+        )
+
+    `@action`(detail=False, methods=['post'], permission_classes=[IsOrgAdmin], url_path='invite')
+    def invite_user(self, request):
+        email = request.data.get('email')
+        role = request.data.get('role', 'USER')  # Match ROLE_CHOICES default
+        if not email:
+            return Response({'error': 'Email is required'}, status=status.HTTP_400_BAD_REQUEST)
+        if role not in dict(User.ROLE_CHOICES):
+            return Response({'error': 'Invalid role'}, status=status.HTTP_400_BAD_REQUEST)
+        org = request.user.organization
+        if User.objects.filter(email=email, organization=org).exists():
+            return Response({'error': 'User already in organization'}, status=status.HTTP_400_BAD_REQUEST)
+        user = User.objects.create_user(email=email, organization=org, role=role, is_active=False)
+        # TODO: send invitation email
+        return Response({'message': f'Invitation sent to {email} with role {role}'})
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 72-72: Expected class, function definition or async function definition after decorator

(invalid-syntax)


[warning] 73-73: Expected class, function definition or async function definition after decorator

(invalid-syntax)


[warning] 77-78: Expected ), found newline

(invalid-syntax)


[warning] 78-78: Expected ,, found def

(invalid-syntax)


[warning] 91-91: Expected a statement

(invalid-syntax)


[warning] 91-91: Expected a statement

(invalid-syntax)


[warning] 91-92: Expected a statement

(invalid-syntax)


[warning] 92-92: Unexpected indentation

(invalid-syntax)


[warning] 93-93: unindent does not match any outer indentation level

(invalid-syntax)


[warning] 93-93: Expected a statement

(invalid-syntax)

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

In `@backend/users/views.py` around lines 71 - 93, Fix the structural issues in
the delete_organization and invite_user methods: move the incorrectly indented
`@permission_classes` decorator from inside the delete_organization method body to
be a proper decorator before the method definition, properly close the Response
object in delete_organization with closing braces and parenthesis, move the
invite_user method out of the delete_organization method body to be a separate
class-level method, remove the orphaned code fragments at the end, and change
the default role value from 'MEMBER' to 'USER' in the invite_user method on line
80 to match the valid User.ROLE_CHOICES values defined in the models.

Source: Linters/SAST tools

Comment thread frontend/settings.html
Comment on lines +475 to +489
<div id="team-members" class="card">
<h2>Team Members</h2>
<table id="members-table">
<thead><tr><th>Email</th><th>Role</th><th>Actions</th></tr></thead>
<tbody></tbody>
</table>
<h3>Invite Member</h3>
<input type="email" id="invite-email" placeholder="user@company.com">
<select id="invite-role">
<option value="MEMBER">Member</option>
<option value="MANAGER">Manager</option>
<option value="ADMIN">Admin</option>
</select>
<button id="send-invite-btn">Send Invitation</button>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate invite UI/actions by admin role on the client as well.

The invite form/button is always visible and clickable, even though the endpoint is admin-only. Non-admin users will hit avoidable 403 flows and get misleading UX.

Also applies to: 498-509

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

In `@frontend/settings.html` around lines 475 - 489, The invite form containing
the input with id="invite-email", select with id="invite-role", and button with
id="send-invite-btn" is currently always visible and functional regardless of
user role, causing non-admin users to encounter preventable 403 errors. Add
client-side role checking to conditionally display or hide the entire invite
section (the h3 "Invite Member" heading and its associated form controls) based
on whether the current user has admin permissions, ensuring only admins see and
can interact with the invite functionality.

Comment thread frontend/settings.html
Comment on lines +484 to +486
<option value="MEMBER">Member</option>
<option value="MANAGER">Manager</option>
<option value="ADMIN">Admin</option>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align invite role values with backend enum (USER vs MEMBER).

Line 484 sends MEMBER, but the backend invite validation only accepts ADMIN, MANAGER, and USER. Inviting a basic member will fail with “Invalid role.”

Suggested fix
-        <option value="MEMBER">Member</option>
+        <option value="USER">Member</option>
📝 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
<option value="MEMBER">Member</option>
<option value="MANAGER">Manager</option>
<option value="ADMIN">Admin</option>
<option value="USER">Member</option>
<option value="MANAGER">Manager</option>
<option value="ADMIN">Admin</option>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/settings.html` around lines 484 - 486, The invite role dropdown
option with the text "Member" uses the value "MEMBER", but the backend
validation only accepts "ADMIN", "MANAGER", and "USER". Change the value
attribute of the option element that displays "Member" from "MEMBER" to "USER"
to align with the backend enum validation and prevent invite failures.

Comment thread frontend/settings.html
Comment on lines +493 to +507
const res = await fetch('/api/users/?organization=' + currentUserOrgId);
const users = await res.json();
const tbody = document.querySelector('#members-table tbody');
tbody.innerHTML = users.map(u => `<tr><td>${u.email}</td><td><span class="badge badge-${u.role.toLowerCase()}">${u.role}</span></td><td></td></tr>`).join('');
}
document.getElementById('send-invite-btn').addEventListener('click', async () => {
const email = document.getElementById('invite-email').value;
const role = document.getElementById('invite-role').value;
const res = await fetch('/api/users/invite/', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email, role})
});
const data = await res.json();
alert(data.message || data.error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix team API calls to use authenticated request flow and remove undefined org variable dependency.

Line 493 references currentUserOrgId, which is not defined in this file, and both Line 493 and Line 501 use raw fetch (not the page’s authenticated API helper). This can fail at runtime and break team load/invite flows in authenticated environments.

🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 495-495: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: tbody.innerHTML = users.map(u => <tr><td>${u.email}</td><td><span class="badge badge-${u.role.toLowerCase()}">${u.role}</span></td><td></td></tr>).join('')
Note: [CWE-79].

(inner-outer-html)

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

In `@frontend/settings.html` around lines 493 - 507, The code references an
undefined variable `currentUserOrgId` in the fetch call and uses raw fetch
instead of the page's authenticated API helper for both the user list load and
invite functionality. Replace the raw fetch calls to `/api/users/?organization=`
and `/api/users/invite/` with the authenticated API helper that your page
provides, which should handle authentication and organization context
automatically. Remove the dependency on the undefined `currentUserOrgId`
variable by using the authenticated helper's built-in request flow instead.

Comment thread frontend/settings.html
const res = await fetch('/api/users/?organization=' + currentUserOrgId);
const users = await res.json();
const tbody = document.querySelector('#members-table tbody');
tbody.innerHTML = users.map(u => `<tr><td>${u.email}</td><td><span class="badge badge-${u.role.toLowerCase()}">${u.role}</span></td><td></td></tr>`).join('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid rendering API fields with innerHTML directly.

Line 496 injects u.email and u.role directly into HTML. This is an XSS sink; render with textContent (or escape values) and use a role-class whitelist.

Suggested fix
+const escapeHtml = (value = '') =>
+  String(value)
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&`#39`;');
+
 async function loadTeam() {
     const res = await fetch('/api/users/?organization=' + currentUserOrgId);
     const users = await res.json();
     const tbody = document.querySelector('`#members-table` tbody');
-    tbody.innerHTML = users.map(u => `<tr><td>${u.email}</td><td><span class="badge badge-${u.role.toLowerCase()}">${u.role}</span></td><td></td></tr>`).join('');
+    const roleClass = { ADMIN: 'badge-admin', MANAGER: 'badge-manager', USER: 'badge-member' };
+    tbody.innerHTML = users.map((u) => {
+        const role = roleClass[u.role] ? u.role : 'USER';
+        const roleLabel = role === 'USER' ? 'Member' : role;
+        return `<tr><td>${escapeHtml(u.email)}</td><td><span class="badge ${roleClass[role]}">${roleLabel}</span></td><td></td></tr>`;
+    }).join('');
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/settings.html` at line 496, The tbody.innerHTML assignment is
directly injecting u.email and u.role values into HTML without escaping,
creating an XSS vulnerability. Instead of building an HTML string with template
literals, create DOM elements programmatically using textContent to safely
render the email and role values. Additionally, implement a role-class whitelist
to validate that only approved role values are used when applying the badge CSS
class name, preventing injection through the role field.

Source: Linters/SAST tools

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.

LO-015 [Hard]: Implement Multi-User Role & Permission System

1 participant