diff --git a/backend/app/models/ppr.py b/backend/app/models/ppr.py
index 1076e2e..9b3cd16 100644
--- a/backend/app/models/ppr.py
+++ b/backend/app/models/ppr.py
@@ -14,9 +14,9 @@ class PPRStatus(str, Enum):
class UserRole(str, Enum):
- ADMINISTRATOR = "administrator"
- OPERATOR = "operator"
- READ_ONLY = "read_only"
+ ADMINISTRATOR = "ADMINISTRATOR"
+ OPERATOR = "OPERATOR"
+ READ_ONLY = "READ_ONLY"
class PPRRecord(Base):
diff --git a/backend/app/schemas/ppr.py b/backend/app/schemas/ppr.py
index 446873d..61baf24 100644
--- a/backend/app/schemas/ppr.py
+++ b/backend/app/schemas/ppr.py
@@ -14,9 +14,9 @@ class PPRStatus(str, Enum):
class UserRole(str, Enum):
- ADMINISTRATOR = "administrator"
- OPERATOR = "operator"
- READ_ONLY = "read_only"
+ ADMINISTRATOR = "ADMINISTRATOR"
+ OPERATOR = "OPERATOR"
+ READ_ONLY = "READ_ONLY"
class PPRBase(BaseModel):
diff --git a/web/admin.html b/web/admin.html
index 49e9747..8026299 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -577,6 +577,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Username |
+ Role |
+ Created |
+ Actions |
+
+
+
+
+
+
+
+
+
No users found
+
No users are configured in the system.
+
+
+
+
+
+
+
+
@@ -917,7 +1006,7 @@
});
// Authentication management
- function initializeAuth() {
+ async function initializeAuth() {
// Try to get cached token
const cachedToken = localStorage.getItem('ppr_access_token');
const cachedUser = localStorage.getItem('ppr_username');
@@ -930,6 +1019,7 @@
accessToken = cachedToken;
currentUser = cachedUser;
document.getElementById('current-user').textContent = cachedUser;
+ await updateUserRole(); // Update role-based UI
connectWebSocket(); // Connect WebSocket for real-time updates
loadPPRs();
return;
@@ -1022,6 +1112,7 @@
document.getElementById('current-user').textContent = username;
hideLogin();
+ await updateUserRole(); // Update role-based UI
connectWebSocket(); // Connect WebSocket for real-time updates
loadPPRs();
} else {
@@ -1190,8 +1281,13 @@
${ppr.notes}
` : '';
+ // Display callsign as main item if present, registration below; otherwise show registration
+ const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
+ `${ppr.ac_call}
${ppr.ac_reg}` :
+ `${ppr.ac_reg}`;
+
row.innerHTML = `
- ${ppr.ac_reg}${notesIndicator} |
+ ${aircraftDisplay}${notesIndicator} |
${ppr.ac_type} |
${ppr.in_from} |
${formatTimeOnly(ppr.eta)} |
@@ -1236,8 +1332,13 @@
${ppr.notes}
` : '';
+ // Display callsign as main item if present, registration below; otherwise show registration
+ const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
+ `${ppr.ac_call}
${ppr.ac_reg}` :
+ `${ppr.ac_reg}`;
+
row.innerHTML = `
- ${ppr.ac_reg}${notesIndicator} |
+ ${aircraftDisplay}${notesIndicator} |
${ppr.ac_type} |
${ppr.out_to || '-'} |
${ppr.etd ? formatTimeOnly(ppr.etd) : '-'} |
@@ -1639,10 +1740,246 @@
}
}
+ // User Management Functions
+ let currentUserRole = null;
+ let isNewUser = false;
+ let currentUserId = null;
+
+ async function openUserManagementModal() {
+ if (!accessToken) return;
+
+ document.getElementById('userManagementModal').style.display = 'block';
+ await loadUsers();
+ }
+
+ function closeUserManagementModal() {
+ document.getElementById('userManagementModal').style.display = 'none';
+ }
+
+ async function loadUsers() {
+ if (!accessToken) return;
+
+ document.getElementById('users-loading').style.display = 'block';
+ document.getElementById('users-table-content').style.display = 'none';
+ document.getElementById('users-no-data').style.display = 'none';
+
+ try {
+ const response = await authenticatedFetch('/api/v1/auth/users');
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch users');
+ }
+
+ const users = await response.json();
+ displayUsers(users);
+ } catch (error) {
+ console.error('Error loading users:', error);
+ if (error.message !== 'Session expired. Please log in again.') {
+ showNotification('Error loading users', true);
+ }
+ }
+
+ document.getElementById('users-loading').style.display = 'none';
+ }
+
+ function displayUsers(users) {
+ const tbody = document.getElementById('users-table-body');
+
+ if (users.length === 0) {
+ document.getElementById('users-no-data').style.display = 'block';
+ return;
+ }
+
+ tbody.innerHTML = '';
+ document.getElementById('users-table-content').style.display = 'block';
+
+ users.forEach(user => {
+ const row = document.createElement('tr');
+
+ // Format role for display
+ const roleDisplay = {
+ 'ADMINISTRATOR': 'Administrator',
+ 'OPERATOR': 'Operator',
+ 'READ_ONLY': 'Read Only'
+ }[user.role] || user.role;
+
+ // Format created date
+ const createdDate = user.created_at ? formatDateTime(user.created_at) : '-';
+
+ row.innerHTML = `
+ ${user.username} |
+ ${roleDisplay} |
+ ${createdDate} |
+
+
+ |
+ `;
+
+ tbody.appendChild(row);
+ });
+ }
+
+ function openUserCreateModal() {
+ isNewUser = true;
+ currentUserId = null;
+ document.getElementById('user-modal-title').textContent = 'Create New User';
+
+ // Clear form
+ document.getElementById('user-form').reset();
+ document.getElementById('user-id').value = '';
+ document.getElementById('user-password').required = true;
+
+ // Show password help text
+ const passwordHelp = document.querySelector('#user-password + small');
+ if (passwordHelp) passwordHelp.style.display = 'none';
+
+ document.getElementById('userModal').style.display = 'block';
+
+ // Auto-focus on username field
+ setTimeout(() => {
+ document.getElementById('user-username').focus();
+ }, 100);
+ }
+
+ async function openUserEditModal(userId) {
+ if (!accessToken) return;
+
+ isNewUser = false;
+ currentUserId = userId;
+ document.getElementById('user-modal-title').textContent = 'Edit User';
+
+ try {
+ const response = await authenticatedFetch(`/api/v1/auth/users/${userId}`);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch user details');
+ }
+
+ const user = await response.json();
+ populateUserForm(user);
+
+ document.getElementById('userModal').style.display = 'block';
+ } catch (error) {
+ console.error('Error loading user details:', error);
+ showNotification('Error loading user details', true);
+ }
+ }
+
+ function populateUserForm(user) {
+ document.getElementById('user-id').value = user.id;
+ document.getElementById('user-username').value = user.username;
+ document.getElementById('user-password').value = ''; // Don't populate password
+ document.getElementById('user-role').value = user.role;
+
+ // Make password optional for editing
+ document.getElementById('user-password').required = false;
+
+ // Show password help text
+ const passwordHelp = document.querySelector('#user-password + small');
+ if (passwordHelp) passwordHelp.style.display = 'block';
+ }
+
+ function closeUserModal() {
+ document.getElementById('userModal').style.display = 'none';
+ currentUserId = null;
+ isNewUser = false;
+ }
+
+ // User form submission
+ document.getElementById('user-form').addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ if (!accessToken) return;
+
+ const formData = new FormData(this);
+ const userData = {};
+
+ formData.forEach((value, key) => {
+ if (key !== 'id' && value.trim() !== '') {
+ userData[key] = value;
+ }
+ });
+
+ try {
+ let response;
+ if (isNewUser) {
+ response = await fetch('/api/v1/auth/users', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`
+ },
+ body: JSON.stringify(userData)
+ });
+ } else {
+ response = await fetch(`/api/v1/auth/users/${currentUserId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`
+ },
+ body: JSON.stringify(userData)
+ });
+ }
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Failed to save user');
+ }
+
+ const wasNewUser = isNewUser;
+ closeUserModal();
+ await loadUsers(); // Refresh user list
+ showNotification(wasNewUser ? 'User created successfully!' : 'User updated successfully!');
+ } catch (error) {
+ console.error('Error saving user:', error);
+ showNotification(`Error saving user: ${error.message}`, true);
+ }
+ });
+
+ // Update user role detection and UI visibility
+ async function updateUserRole() {
+ console.log('updateUserRole called'); // Debug log
+ if (!accessToken) {
+ console.log('No access token, skipping role update'); // Debug log
+ return;
+ }
+
+ try {
+ const response = await authenticatedFetch('/api/v1/auth/test-token', {
+ method: 'POST'
+ });
+
+ if (response.ok) {
+ const userData = await response.json();
+ currentUserRole = userData.role;
+ console.log('User role from API:', currentUserRole); // Debug log
+
+ // Show user management button only for administrators
+ const userManagementBtn = document.getElementById('user-management-btn');
+ if (currentUserRole && currentUserRole.toUpperCase() === 'ADMINISTRATOR') {
+ userManagementBtn.style.display = 'inline-block';
+ console.log('Showing user management button'); // Debug log
+ } else {
+ userManagementBtn.style.display = 'none';
+ console.log('Hiding user management button, current role:', currentUserRole); // Debug log
+ }
+ }
+ } catch (error) {
+ console.error('Error updating user role:', error);
+ // Hide user management by default on error
+ document.getElementById('user-management-btn').style.display = 'none';
+ }
+ }
+
// Close modal when clicking outside
window.onclick = function(event) {
const pprModal = document.getElementById('pprModal');
const timestampModal = document.getElementById('timestampModal');
+ const userManagementModal = document.getElementById('userManagementModal');
+ const userModal = document.getElementById('userModal');
if (event.target === pprModal) {
closePPRModal();
@@ -1650,6 +1987,12 @@
if (event.target === timestampModal) {
closeTimestampModal();
}
+ if (event.target === userManagementModal) {
+ closeUserManagementModal();
+ }
+ if (event.target === userModal) {
+ closeUserModal();
+ }
}
// Aircraft Lookup Functions
diff --git a/web/index.html b/web/index.html
index b1d0df8..f23a683 100644
--- a/web/index.html
+++ b/web/index.html
@@ -254,11 +254,16 @@
// Create PPR item HTML
function createPPRItem(ppr) {
+ // Display callsign as main item if present, registration below; otherwise show registration
+ const aircraftDisplay = ppr.ac_call && ppr.ac_call.trim() ?
+ `${ppr.ac_call}
${ppr.ac_reg}` :
+ ppr.ac_reg;
+
return `
Aircraft
- ${ppr.ac_reg}
+ ${aircraftDisplay}
Type