Add fetchAPIUsers endpoint and corresponding UI: Implement a new API endpoint to fetch users with API access, including search functionality. Update JavaScript to handle user data loading and searching, and enhance the API access HTML template with a new tab for managing API users. Improve styling and user interaction elements for better usability.

This commit is contained in:
Master3395
2025-09-21 22:06:51 +02:00
parent 5d4897f7e5
commit f5d4c46c37
4 changed files with 578 additions and 3 deletions

View File

@@ -1659,6 +1659,147 @@ app.controller('apiAccessCTRL', function ($scope, $http) {
});
/* Java script code for api access */
/* Java script code for api users list */
app.controller('apiUsersCTRL', function ($scope, $http) {
$scope.apiUsers = [];
$scope.filteredUsers = [];
$scope.searchQuery = '';
$scope.apiUsersLoading = true;
$scope.loadAPIUsers = function() {
$scope.apiUsersLoading = false;
var url = "/users/fetchAPIUsers";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.get(url, config).then(loadAPIUsersSuccess, loadAPIUsersError);
};
function loadAPIUsersSuccess(response) {
$scope.apiUsersLoading = true;
if (response.data.status === 1) {
$scope.apiUsers = response.data.users;
$scope.filteredUsers = response.data.users;
new PNotify({
title: 'Success!',
text: 'API users loaded successfully',
type: 'success'
});
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
function loadAPIUsersError(response) {
$scope.apiUsersLoading = true;
new PNotify({
title: 'Error!',
text: 'Could not load API users. Please refresh the page.',
type: 'error'
});
}
$scope.searchUsers = function() {
if (!$scope.searchQuery || $scope.searchQuery.trim() === '') {
$scope.filteredUsers = $scope.apiUsers;
return;
}
var query = $scope.searchQuery.toLowerCase();
$scope.filteredUsers = $scope.apiUsers.filter(function(user) {
return user.userName.toLowerCase().includes(query) ||
user.firstName.toLowerCase().includes(query) ||
user.lastName.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query) ||
user.aclName.toLowerCase().includes(query);
});
};
$scope.clearSearch = function() {
$scope.searchQuery = '';
$scope.filteredUsers = $scope.apiUsers;
};
$scope.viewUserDetails = function(user) {
new PNotify({
title: 'User Details',
text: 'Username: ' + user.userName + '<br>' +
'Full Name: ' + user.firstName + ' ' + user.lastName + '<br>' +
'Email: ' + user.email + '<br>' +
'ACL: ' + user.aclName + '<br>' +
'Token Status: ' + user.tokenStatus + '<br>' +
'State: ' + user.state,
type: 'info',
styling: 'bootstrap3',
delay: 10000
});
};
$scope.disableAPI = function(user) {
if (confirm('Are you sure you want to disable API access for ' + user.userName + '?')) {
$scope.apiUsersLoading = false;
var url = "/users/saveChangesAPIAccess";
var data = {
accountUsername: user.userName,
access: 'Disable'
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(disableAPISuccess, disableAPIError);
}
};
function disableAPISuccess(response) {
$scope.apiUsersLoading = true;
if (response.data.status === 1) {
// Remove user from the list
$scope.apiUsers = $scope.apiUsers.filter(function(u) {
return u.userName !== response.data.accountUsername;
});
$scope.filteredUsers = $scope.apiUsers;
new PNotify({
title: 'Success!',
text: 'API access disabled for ' + response.data.accountUsername,
type: 'success'
});
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
function disableAPIError(response) {
$scope.apiUsersLoading = true;
new PNotify({
title: 'Error!',
text: 'Could not disable API access. Please try again.',
type: 'error'
});
}
// Load API users when controller initializes
$scope.loadAPIUsers();
});
/* Java script code to list table users */

View File

@@ -166,6 +166,220 @@
.text-muted {
color: var(--text-secondary, #8893a7);
}
/* Tab Navigation Styles */
.tab-navigation {
display: flex;
margin-bottom: 20px;
border-bottom: 2px solid var(--border-color, #e8e9ff);
}
.tab-button {
background: none;
border: none;
padding: 15px 25px;
font-size: 16px;
font-weight: 600;
color: var(--text-secondary, #8893a7);
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.tab-button:hover {
color: var(--accent-color, #5b5fcf);
background: var(--bg-hover, #f8f9ff);
}
.tab-button.active {
color: var(--accent-color, #5b5fcf);
border-bottom-color: var(--accent-color, #5b5fcf);
background: var(--bg-hover, #f8f9ff);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Search Container Styles */
.search-container {
margin-bottom: 25px;
}
.search-box {
position: relative;
max-width: 400px;
}
.search-box i {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary, #8893a7);
}
.search-input {
width: 100%;
padding: 12px 45px 12px 45px;
border: 1px solid var(--border-color, #e8e9ff);
border-radius: 8px;
font-size: 16px;
background: var(--bg-secondary, white);
color: var(--text-primary, #2f3640);
transition: all 0.3s ease;
}
.search-input:focus {
border-color: var(--accent-color, #5b5fcf);
box-shadow: 0 0 0 3px rgba(91, 95, 207, 0.1);
outline: none;
}
.clear-search {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-secondary, #8893a7);
cursor: pointer;
padding: 5px;
border-radius: 50%;
transition: all 0.3s ease;
}
.clear-search:hover {
background: var(--bg-hover, #f8f9ff);
color: var(--accent-color, #5b5fcf);
}
.search-results-info {
margin-top: 10px;
color: var(--text-secondary, #8893a7);
font-size: 14px;
}
/* Users Table Styles */
.users-table-container {
overflow-x: auto;
}
.users-table {
width: 100%;
border-collapse: collapse;
background: var(--bg-secondary, white);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px var(--shadow-color, rgba(0,0,0,0.08));
}
.users-table th {
background: var(--bg-hover, #f8f9ff);
color: var(--text-primary, #2f3640);
font-weight: 600;
padding: 15px 12px;
text-align: left;
border-bottom: 2px solid var(--border-color, #e8e9ff);
}
.users-table td {
padding: 15px 12px;
border-bottom: 1px solid var(--border-color, #e8e9ff);
vertical-align: middle;
}
.users-table tbody tr:hover {
background: var(--bg-hover, #f8f9ff);
}
/* Badge Styles */
.acl-badge {
background: var(--accent-color, #5b5fcf);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
/* Token Status Styles */
.token-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
}
.token-valid {
color: var(--success-text, #10b981);
}
.token-warning {
color: var(--warning-text, #f59e0b);
}
.token-error {
color: var(--danger-text, #ef4444);
}
.token-status i {
font-size: 8px;
}
/* User State Styles */
.user-state {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.state-active {
background: var(--success-bg, #f0fdf4);
color: var(--success-text, #166534);
}
.state-inactive {
background: var(--danger-bg, #fef2f2);
color: var(--danger-text, #991b1b);
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
background: none;
border: 1px solid var(--border-color, #e8e9ff);
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--text-secondary, #8893a7);
}
.btn-action:hover {
background: var(--bg-hover, #f8f9ff);
border-color: var(--accent-color, #5b5fcf);
color: var(--accent-color, #5b5fcf);
}
.btn-view:hover {
color: var(--info-text, #3b82f6);
border-color: var(--info-text, #3b82f6);
}
.btn-disable:hover {
color: var(--danger-text, #ef4444);
border-color: var(--danger-text, #ef4444);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary, #8893a7);
}
.empty-state i {
font-size: 48px;
margin-bottom: 20px;
color: var(--text-secondary, #8893a7);
}
.empty-state h3 {
margin-bottom: 10px;
color: var(--text-primary, #2f3640);
}
.empty-state p {
margin: 0;
line-height: 1.5;
}
@media (max-width: 768px) {
.content-card {
padding: 20px;
@@ -176,6 +390,17 @@
.section-title {
font-size: 1.1rem;
}
.tab-button {
padding: 12px 15px;
font-size: 14px;
}
.users-table {
font-size: 14px;
}
.users-table th,
.users-table td {
padding: 10px 8px;
}
}
</style>
@@ -185,10 +410,22 @@
<div class="page-header">
<h1 class="page-title">{% trans "API Access" %}</h1>
</div>
<div class="content-card" ng-controller="apiAccessCTRL">
<!-- Tab Navigation -->
<div class="tab-navigation">
<button class="tab-button active" onclick="switchTab('configure')" id="configure-tab">
<i class="fa fa-cog"></i> {% trans "Configure API Access" %}
</button>
<button class="tab-button" onclick="switchTab('users')" id="users-tab">
<i class="fa fa-users"></i> {% trans "API Users" %}
</button>
</div>
<!-- Configure API Access Tab -->
<div class="content-card tab-content active" ng-controller="apiAccessCTRL" id="configure-content">
<h3 class="section-title">
{% trans "Configure API Access" %}
<img class="loading-icon" ng-hide="cyberpanelLoading" src="{% static 'images/loading.gif' %}">
<img class="loading-icon" ng-hide="cyberpanelLoading" src="{% static 'images/loading.gif' %}" alt="Loading configuration">
</h3>
<div class="info-box">
<h4><i class="fa fa-info-circle"></i> {% trans "Important Information" %}</h4>
@@ -197,7 +434,7 @@
<form action="/" class="form-horizontal">
<div class="form-group">
<label class="form-label">{% trans "Select User Account" %}</label>
<select ng-change="showApiAccessDropDown()" ng-model="accountUsername" class="form-control">
<select ng-change="showApiAccessDropDown()" ng-model="accountUsername" class="form-control" title="Select user account" aria-label="Select user account">
<option value="">-- {% trans "Choose a user" %} --</option>
{% for items in acctNames %}
<option>{{ items }}</option>
@@ -241,7 +478,130 @@
</div>
</form>
</div>
<!-- API Users Tab -->
<div class="content-card tab-content" ng-controller="apiUsersCTRL" id="users-content">
<h3 class="section-title">
{% trans "Users with API Access" %}
<img class="loading-icon" ng-hide="apiUsersLoading" src="{% static 'images/loading.gif' %}" alt="Loading API users">
</h3>
<!-- Search Box -->
<div class="search-container">
<div class="search-box">
<i class="fa fa-search"></i>
<input type="text" ng-model="searchQuery" ng-keyup="searchUsers()" placeholder="{% trans 'Search users by name, email, or username...' %}" class="search-input" title="Search API users" aria-label="Search API users">
<button ng-click="clearSearch()" ng-show="searchQuery" class="clear-search">
<i class="fa fa-times"></i>
</button>
</div>
<div class="search-results-info" ng-show="searchQuery">
<span class="results-count">{{ filteredUsers.length }} {% trans "users found" %}</span>
</div>
</div>
<!-- Users Table -->
<div class="users-table-container">
<table class="users-table" ng-show="apiUsers.length > 0">
<thead>
<tr>
<th>{% trans "Username" %}</th>
<th>{% trans "Full Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "ACL" %}</th>
<th>{% trans "Token Status" %}</th>
<th>{% trans "State" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in filteredUsers = (apiUsers | filter:searchQuery)">
<td>
<strong>{{ user.userName }}</strong>
</td>
<td>{{ user.firstName }} {{ user.lastName }}</td>
<td>{{ user.email }}</td>
<td>
<span class="acl-badge">{{ user.aclName }}</span>
</td>
<td>
<span ng-class="{
'token-valid': user.tokenStatus === 'Valid',
'token-warning': user.tokenStatus === 'Needs Generation',
'token-error': user.tokenStatus === 'Not Generated'
}" class="token-status">
<i class="fa fa-circle"></i> {{ user.tokenStatus }}
</span>
</td>
<td>
<span ng-class="{
'state-active': user.state === 'ACTIVE',
'state-inactive': user.state !== 'ACTIVE'
}" class="user-state">
{{ user.state }}
</span>
</td>
<td>
<div class="action-buttons">
<button ng-click="viewUserDetails(user)" class="btn-action btn-view" title="{% trans 'View Details' %}">
<i class="fa fa-eye"></i>
</button>
<button ng-click="disableAPI(user)" class="btn-action btn-disable" title="{% trans 'Disable API Access' %}">
<i class="fa fa-power-off"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Empty State -->
<div class="empty-state" ng-show="apiUsers.length === 0">
<i class="fa fa-users"></i>
<h3>{% trans "No users with API access found" %}</h3>
<p>{% trans "No users currently have API access enabled. Use the Configure tab to enable API access for users." %}</p>
</div>
<!-- No Search Results -->
<div class="empty-state" ng-show="apiUsers.length > 0 && filteredUsers.length === 0">
<i class="fa fa-search"></i>
<h3>{% trans "No users found" %}</h3>
<p>{% trans "No users match your search criteria. Try adjusting your search terms." %}</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Tab switching functionality
function switchTab(tabName) {
// Hide all tab contents
document.getElementById('configure-content').classList.remove('active');
document.getElementById('users-content').classList.remove('active');
// Remove active class from all tabs
document.getElementById('configure-tab').classList.remove('active');
document.getElementById('users-tab').classList.remove('active');
// Show selected tab content and add active class
if (tabName === 'configure') {
document.getElementById('configure-content').classList.add('active');
document.getElementById('configure-tab').classList.add('active');
} else if (tabName === 'users') {
document.getElementById('users-content').classList.add('active');
document.getElementById('users-tab').classList.add('active');
// Load API users when switching to users tab
if (typeof angular !== 'undefined' && angular.element(document.getElementById('users-content')).scope()) {
angular.element(document.getElementById('users-content')).scope().loadAPIUsers();
}
}
}
// Initialize with configure tab active
document.addEventListener('DOMContentLoaded', function() {
switchTab('configure');
});
</script>
{% endblock %}

View File

@@ -25,6 +25,7 @@ urlpatterns = [
path('saveResellerChanges', views.saveResellerChanges, name='saveResellerChanges'),
path('apiAccess', views.apiAccess, name='apiAccess'),
path('saveChangesAPIAccess', views.saveChangesAPIAccess, name='saveChangesAPIAccess'),
path('fetchAPIUsers', views.fetchAPIUsers, name='fetchAPIUsers'),
path('listUsers', views.listUsers, name='listUsers'),
path('fetchTableUsers', views.fetchTableUsers, name='fetchTableUsers'),
path('controlUserState', views.controlUserState, name='controlUserState'),

View File

@@ -3,6 +3,7 @@
from django.shortcuts import render, redirect
from django.http import HttpResponse
from django.db import models
from loginSystem.views import loadLoginPage
from loginSystem.models import Administrator, ACL
import json
@@ -122,6 +123,78 @@ def saveChangesAPIAccess(request):
return HttpResponse(json_data)
def fetchAPIUsers(request):
"""
Fetch all users with API access enabled, with optional search functionality
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
finalResponse = {'status': 0, "error_message": "Only administrators are allowed to perform this task."}
json_data = json.dumps(finalResponse)
return HttpResponse(json_data)
# Get search query if provided
search_query = request.GET.get('search', '').strip()
# Fetch all users with API access enabled
api_users = Administrator.objects.filter(api=1).select_related('acl')
# Apply search filter if provided
if search_query:
api_users = api_users.filter(
models.Q(userName__icontains=search_query) |
models.Q(firstName__icontains=search_query) |
models.Q(lastName__icontains=search_query) |
models.Q(email__icontains=search_query)
)
# Prepare user data
users_data = []
for user in api_users:
# Determine token status
token_status = "Valid"
if not user.token or user.token == 'None' or user.token == '':
token_status = "Not Generated"
elif user.token == 'TOKEN_NEEDS_GENERATION':
token_status = "Needs Generation"
# Get ACL name
acl_name = user.acl.name if user.acl else "Default"
users_data.append({
'id': user.pk,
'userName': user.userName,
'firstName': user.firstName,
'lastName': user.lastName,
'email': user.email,
'aclName': acl_name,
'tokenStatus': token_status,
'state': user.state,
'createdDate': user.pk, # Using pk as a proxy for creation order
'lastLogin': 'N/A' # This would need to be tracked separately
})
# Sort by username
users_data.sort(key=lambda x: x['userName'].lower())
finalResponse = {
'status': 1,
'users': users_data,
'totalCount': len(users_data)
}
json_data = json.dumps(finalResponse)
return HttpResponse(json_data)
except Exception as e:
secure_log_error(e, 'fetchAPIUsers', request.session.get('userID', 'Unknown'))
finalResponse = secure_error_response(e, 'Failed to fetch API users')
json_data = json.dumps(finalResponse)
return HttpResponse(json_data)
def submitUserCreation(request):
try: