From ef273c0c5dccf418cc401f303680e908b86e4a63 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Thu, 23 Oct 2025 20:23:29 +0000 Subject: [PATCH] User Management --- backend/app/models/ppr.py | 6 +- backend/app/schemas/ppr.py | 6 +- web/admin.html | 349 ++++++++++++++++++++++++++++++++++++- web/index.html | 7 +- 4 files changed, 358 insertions(+), 10 deletions(-) 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 @@ + + + + + + +
@@ -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