LO-015: Multi-User Role & Permission System#309
Conversation
📝 WalkthroughWalkthroughIntroduces a role-based access control system: new ChangesRole-Based Access Control System
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
backend/users/serializers.py (1)
34-35: ⚡ Quick winMake registration role assignment explicit and fix the stale comment.
RegisterSerializerhas norolefield, so Line 34 effectively always setsMEMBER. 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
📒 Files selected for processing (8)
backend/campaigns/views.pybackend/leads/views.pybackend/users/migrations/0002_alter_user_role.pybackend/users/models.pybackend/users/permissions.pybackend/users/serializers.pybackend/users/views.pyfrontend/settings.html
| @action(detail=False, methods=['delete'], url_path='delete-all') | ||
| @permission_classes([IsOrgAdmin]) |
There was a problem hiding this comment.
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.
| @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
| choices=[("ADMIN", "Admin"), ("MANAGER", "Manager"), ("USER", "User")], | ||
| default="MEMBER", |
There was a problem hiding this comment.
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.
| 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.
| @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, | ||
| ) |
There was a problem hiding this comment.
Critical: Code is syntactically broken and will not run.
The delete_organization and invite_user methods have severe structural issues:
- Line 72:
@permission_classesis incorrectly indented inside the method body rather than placed as a decorator beforedef. - Line 76: The
Responsedict is never closed (missing}and)). - Lines 77-90:
invite_useris defined inside the brokendelete_organizationbody. - 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
| <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> |
There was a problem hiding this comment.
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.
| <option value="MEMBER">Member</option> | ||
| <option value="MANAGER">Manager</option> | ||
| <option value="ADMIN">Admin</option> |
There was a problem hiding this comment.
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.
| <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.
| 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); |
There was a problem hiding this comment.
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.
| 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(''); |
There was a problem hiding this comment.
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, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .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
Closes #183
Summary
Implemented role-based access control (RBAC) for the multi-tenant system.
Changes
IsOrgAdminandIsOrgManagerOrAdmininusers/permissions.py.POST /api/users/invite/endpoint (admin-only) to invite users with a specified role.Files changed
backend/users/models.pybackend/users/serializers.pybackend/users/permissions.py(new)backend/campaigns/views.pybackend/leads/views.pybackend/users/views.pyfrontend/settings.htmlSummary by CodeRabbit