diff --git a/public/css/main.css b/public/css/main.css index 4fc0265..feda1fa 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -4141,4 +4141,166 @@ kbd { overflow: hidden; text-overflow: ellipsis; max-width: 60%; +} + +/* ===================================================== + Edit User Modal + ===================================================== */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + z-index: 10000; + align-items: center; + justify-content: center; +} + +.modal-overlay.active { + display: flex; +} + +.modal-overlay .modal { + display: block; + position: relative; + inset: auto; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow: hidden; + box-shadow: var(--shadow-lg); + animation: modalFadeIn 0.2s ease; +} + +@keyframes modalFadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +.modal-overlay .modal-header { + padding: var(--space-md); + border-bottom: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-overlay .modal-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0; +} + +.modal-overlay .modal-close { + background: none; + border: none; + color: var(--color-text-muted); + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + padding: 0; + transition: color 0.2s; +} + +.modal-overlay .modal-close:hover { + color: var(--color-text-primary); +} + +.modal-overlay .modal-body { + padding: var(--space-lg); +} + +.modal-overlay .modal-footer { + padding: var(--space-md); + border-top: 1px solid var(--color-border); + display: flex; + justify-content: flex-end; + gap: var(--space-sm); + background: var(--color-bg-tertiary); +} + +/* Modal Form Styles */ +.modal-overlay .modal-form-group { + margin-bottom: var(--space-md); +} + +.modal-overlay .modal-form-group label { + display: block; + margin-bottom: var(--space-xs); + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.modal-overlay .modal-form-group .form-input { + width: 100%; + padding: var(--space-sm); + background: var(--color-bg-primary); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-primary); +} + +.modal-overlay .modal-form-group .form-input:focus { + outline: none; + border-color: var(--color-accent); +} + +.modal-overlay .modal-form-group .form-input:disabled { + opacity: 0.6; + cursor: not-allowed; + background: var(--color-bg-tertiary); +} + +/* ===================================================== + User Management Badges + ===================================================== */ +.user-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + border-radius: 12px; + white-space: nowrap; +} + +.user-badge-sso { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff; + box-shadow: 0 2px 4px rgba(99, 102, 241, 0.3); +} + +.user-badge-local { + background: rgba(255, 255, 255, 0.08); + color: var(--color-text-secondary); + border: 1px solid var(--color-border); +} + +.user-badge-admin { + background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%); + color: #000; + box-shadow: 0 2px 4px rgba(245, 158, 11, 0.3); +} + +.user-badge-viewer { + background: rgba(255, 255, 255, 0.05); + color: var(--color-text-muted); + border: 1px solid rgba(255, 255, 255, 0.1); } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 788d2a6..c17662e 100644 --- a/public/index.html +++ b/public/index.html @@ -339,6 +339,7 @@ +
@@ -710,6 +711,7 @@ Username + Email Role Created Actions @@ -976,6 +978,57 @@ Hide
+ + + diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js index 92b140e..ea6045b 100644 --- a/public/js/pages/Settings.js +++ b/public/js/pages/Settings.js @@ -303,53 +303,166 @@ class SettingsPage { try { const users = await API.users.getAll(); + // Store users in memory for easy access during edit + this.users = users; if (users.length === 0) { - userList.innerHTML = 'No users found'; + userList.innerHTML = 'No users found'; return; } - userList.innerHTML = users.map(user => ` + userList.innerHTML = users.map(user => { + const isSSO = !!user.oidcId; + const typeBadge = isSSO + ? 'SSO' + : 'Local'; + + const roleBadge = user.role === 'admin' + ? 'Admin' + : 'Viewer'; + + return ` - ${user.username} - ${user.role} + +
+ ${user.username} + ${typeBadge} +
+ + ${user.email || '-'} + ${roleBadge} ${user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'} - + - `).join(''); + `}).join(''); } catch (err) { console.error('Error loading users:', err); - userList.innerHTML = 'Error loading users'; + userList.innerHTML = 'Error loading users'; } } - async editUser(userId) { - const username = prompt('Enter new username (leave blank to keep current):'); - const password = prompt('Enter new password (leave blank to keep current):'); - const role = prompt('Enter role (admin or viewer, leave blank to keep current):'); + openEditUserModal(userId) { + console.log('openEditUserModal called with ID:', userId, 'Type:', typeof userId); + console.log('Current users list:', this.users); - const updates = {}; - if (username) updates.username = username; - if (password) updates.password = password; - if (role) updates.role = role; + const user = this.users.find(u => u.id === userId); + if (!user) { + console.error('User not found in this.users cache!'); + console.log('Available IDs:', this.users.map(u => u.id)); + return; + } + console.log('User found:', user); - if (Object.keys(updates).length === 0) { - alert('No changes made'); + const modal = document.getElementById('edit-user-modal'); + console.log('Modal element:', modal); + if (!modal) { + console.error('CRITICAL: Modal element #edit-user-modal not found in DOM!'); + alert('Error: Modal not found. Please refresh the page.'); return; } + const isSSO = !!user.oidcId; + console.log('Is SSO user:', isSSO); + + // Populate form with null checks try { - await API.users.update(userId, updates); - alert('User updated successfully!'); - this.loadUsers(); + const editId = document.getElementById('edit-user-id'); + const editUsername = document.getElementById('edit-username'); + const editEmail = document.getElementById('edit-email'); + const editRole = document.getElementById('edit-role'); + const editPassword = document.getElementById('edit-password'); + + console.log('Form elements found:', { editId, editUsername, editEmail, editRole, editPassword }); + + if (editId) editId.value = user.id; + if (editUsername) editUsername.value = user.username; + if (editEmail) editEmail.value = user.email || ''; + if (editRole) editRole.value = user.role; + if (editPassword) editPassword.value = ''; + + // Handle SSO specific UI + const passwordHint = document.getElementById('edit-password-hint'); + const oidcGroup = document.getElementById('oidc-info-group'); + const oidcIdDisplay = document.getElementById('edit-oidc-id'); + + if (isSSO) { + if (editPassword) { + editPassword.disabled = true; + editPassword.placeholder = "Managed by SSO Provider"; + } + if (passwordHint) passwordHint.textContent = "Password cannot be changed for SSO users."; + if (oidcGroup) oidcGroup.classList.remove('hidden'); + if (oidcIdDisplay) oidcIdDisplay.textContent = user.oidcId; + } else { + if (editPassword) { + editPassword.disabled = false; + editPassword.placeholder = "Leave blank to keep current"; + } + if (passwordHint) passwordHint.textContent = "Optional. Leave blank to keep unchanged."; + if (oidcGroup) oidcGroup.classList.add('hidden'); + } + + // Show modal + console.log('Adding active class to modal...'); + modal.classList.add('active'); + console.log('Modal classes after add:', modal.classList.toString()); + + // Setup Close/Cancel handlers (once) + this.setupModalHandlers(modal); + console.log('Modal should now be visible!'); } catch (err) { - alert('Error updating user: ' + err.message); + console.error('Error populating modal:', err); + alert('Error opening edit modal: ' + err.message); } } + setupModalHandlers(modal) { + if (this.modalHandlersSetup) return; + + const closeBtn = document.getElementById('edit-user-close'); + const cancelBtn = document.getElementById('edit-user-cancel'); + const saveBtn = document.getElementById('edit-user-save'); + + const closeModal = () => modal.classList.remove('active'); + + closeBtn.onclick = closeModal; + cancelBtn.onclick = closeModal; + + // Click outside to close + modal.onclick = (e) => { + if (e.target === modal) closeModal(); + }; + + // Save Handler + saveBtn.onclick = async () => { + const userId = document.getElementById('edit-user-id').value; + const updates = { + username: document.getElementById('edit-username').value, + role: document.getElementById('edit-role').value + }; + + const newPassword = document.getElementById('edit-password').value; + if (newPassword && !document.getElementById('edit-password').disabled) { + updates.password = newPassword; + } + + try { + await API.users.update(userId, updates); + // alert('User updated successfully!'); // Optional: Replace with toast? + closeModal(); + this.loadUsers(); + } catch (err) { + alert('Error updating user: ' + err.message); + } + }; + + this.modalHandlersSetup = true; + } + + async deleteUser(userId, username) { if (!confirm(`Are you sure you want to delete user "${username}"?`)) { return; @@ -357,7 +470,6 @@ class SettingsPage { try { await API.users.delete(userId); - alert('User deleted successfully!'); this.loadUsers(); } catch (err) { alert('Error deleting user: ' + err.message); diff --git a/server/auth.js b/server/auth.js index bce218d..ac76206 100644 --- a/server/auth.js +++ b/server/auth.js @@ -110,6 +110,25 @@ function configureJwtStrategy(getUserById) { })); } +/** + * Configure Passport session serialization + * Required for OIDC flow which uses sessions + */ +function configureSessionSerialization(getUserById) { + passport.serializeUser((user, done) => { + done(null, user.id); + }); + + passport.deserializeUser(async (id, done) => { + try { + const user = await getUserById(id); + done(null, user); + } catch (err) { + done(err, null); + } + }); +} + /** * Configure Passport OpenID Connect Strategy */ @@ -173,8 +192,10 @@ function configureOidcStrategy(findUserByOidcId, findUserByEmail, createUser) { let user = await findUserByOidcId(sub); // 2. If not found, try to match by email - if (!user && profile.emails && profile.emails.length > 0) { - const email = profile.emails[0].value; + // Extract email - handle both profile.emails[] (Google) and profile.email (others) + const email = profile.emails?.[0]?.value || profile.email || profile._json?.email; + + if (!user && email) { user = await findUserByEmail(email); // If found by email but no OIDC ID, link them @@ -187,13 +208,13 @@ function configureOidcStrategy(findUserByOidcId, findUserByEmail, createUser) { // 3. If still not found, create new user (JIT Provisioning) if (!user) { - const username = profile.username || profile.displayName || (profile.emails ? profile.emails[0].value.split('@')[0] : `user_${sub.substring(0, 8)}`); + const username = profile.username || profile.displayName || (email ? email.split('@')[0] : `user_${sub.substring(0, 8)}`); user = await createUser({ username: username, role: 'viewer', // Default role for SSO users oidcId: sub, - email: profile.emails ? profile.emails[0].value : null + email: email || null }); } @@ -239,6 +260,7 @@ module.exports = { verifyToken, configureLocalStrategy, configureJwtStrategy, + configureSessionSerialization, configureOidcStrategy, requireAuth, requireAdmin, diff --git a/server/routes/auth.js b/server/routes/auth.js index b6c5640..73ce607 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -13,6 +13,11 @@ auth.configureJwtStrategy( async (id) => await db.users.getById(id) ); +// Configure Passport session serialization (required for OIDC) +auth.configureSessionSerialization( + async (id) => await db.users.getById(id) +); + // Configure OIDC Strategy auth.configureOidcStrategy( async (oidcId) => await db.users.getByOidcId(oidcId),