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),