// NextSnap - Admin Panel Logic 'use strict'; const Admin = { techUsers: [], _activeUser: null, async init() { this.setupEventListeners(); await this.loadTechUsers(); }, setupEventListeners() { document.getElementById('add-tech-user-form').addEventListener('submit', (e) => { e.preventDefault(); this.createTechUser(); }); // User detail modal buttons document.getElementById('user-modal-close').addEventListener('click', () => this.closeModal('user-modal')); document.getElementById('user-modal-toggle-btn').addEventListener('click', () => this._handleToggle()); document.getElementById('user-modal-pin-btn').addEventListener('click', () => this._handlePinReset()); document.getElementById('user-modal-resetpw-btn').addEventListener('click', () => this._handleResetNCPassword()); document.getElementById('user-modal-delete-btn').addEventListener('click', () => this._handleDelete()); document.getElementById('user-modal-pw-toggle').addEventListener('click', () => this._toggleModalPassword()); document.getElementById('user-modal-pw-copy').addEventListener('click', () => this._copyModalPassword()); // PIN modal document.getElementById('pin-modal-cancel').addEventListener('click', () => this.closeModal('pin-modal')); document.getElementById('confirm-pin-reset').addEventListener('click', () => this._submitPinReset()); // Confirm modal document.getElementById('confirm-modal-cancel').addEventListener('click', () => this.closeModal('confirm-modal')); }, // ── Load & Render ──────────────────────────────────────────────────── async loadTechUsers() { const list = document.getElementById('tech-user-list'); const loading = document.getElementById('tech-loading-msg'); const error = document.getElementById('tech-error-msg'); loading.style.display = 'block'; error.style.display = 'none'; list.innerHTML = ''; try { const response = await fetch('/api/admin/tech-users'); if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Failed to load tech users'); } const data = await response.json(); this.techUsers = data.tech_users || []; loading.style.display = 'none'; if (this.techUsers.length === 0) { list.innerHTML = '
No tech users yet
'; return; } this.techUsers.forEach(user => { list.appendChild(this._createUserItem(user)); }); } catch (err) { console.error('Error loading tech users:', err); loading.style.display = 'none'; error.textContent = err.message; error.style.display = 'block'; } }, _createUserItem(user) { const item = document.createElement('div'); item.className = 'user-list-item'; const u = this.escapeHtml(user.username); const dn = user.display_name && user.display_name !== user.username ? this.escapeHtml(user.display_name) : ''; item.innerHTML = `
${u} ${dn ? `${dn}` : ''}
${user.enabled ? 'Enabled' : 'Disabled'}
`; item.addEventListener('click', () => this.showUserModal(user.username)); return item; }, // ── User Detail Modal ──────────────────────────────────────────────── showUserModal(username) { const user = this.techUsers.find(u => u.username === username); if (!user) return; this._activeUser = user; document.getElementById('user-modal-title').textContent = user.username; const dn = user.display_name && user.display_name !== user.username ? user.display_name : ''; document.getElementById('user-modal-displayname').textContent = dn; // Password const pwEl = document.getElementById('user-modal-pw'); pwEl.dataset.pw = user.nc_password; pwEl.dataset.masked = 'true'; pwEl.textContent = '\u2022'.repeat(12); document.getElementById('user-modal-pw-toggle').textContent = 'Show'; // Status const statusEl = document.getElementById('user-modal-status'); statusEl.textContent = user.enabled ? 'Enabled' : 'Disabled'; statusEl.className = 'badge ' + (user.enabled ? 'badge-success' : 'badge-danger'); // Toggle button const toggleBtn = document.getElementById('user-modal-toggle-btn'); if (user.enabled) { toggleBtn.textContent = 'Disable User'; toggleBtn.className = 'btn btn-block btn-warning-outline'; } else { toggleBtn.textContent = 'Enable User'; toggleBtn.className = 'btn btn-block btn-success-outline'; } document.getElementById('user-modal').style.display = 'flex'; }, _toggleModalPassword() { const pwEl = document.getElementById('user-modal-pw'); const btn = document.getElementById('user-modal-pw-toggle'); if (pwEl.dataset.masked === 'true') { pwEl.textContent = pwEl.dataset.pw; pwEl.dataset.masked = 'false'; btn.textContent = 'Hide'; } else { pwEl.textContent = '\u2022'.repeat(12); pwEl.dataset.masked = 'true'; btn.textContent = 'Show'; } }, _copyModalPassword() { const pw = document.getElementById('user-modal-pw').dataset.pw; navigator.clipboard.writeText(pw).then(() => { this.showToast('Password copied', 'success'); }).catch(() => { this.showToast('Copy failed', 'error'); }); }, // ── Actions from User Modal ────────────────────────────────────────── async _handleToggle() { const user = this._activeUser; if (!user) return; const enable = !user.enabled; try { const response = await fetch(`/api/admin/tech-users/${user.username}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: enable }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Failed'); } this.closeModal('user-modal'); this.showToast(`User "${user.username}" ${enable ? 'enabled' : 'disabled'}`, 'success'); this.loadTechUsers(); } catch (err) { this.showToast(err.message, 'error'); } }, _handlePinReset() { const user = this._activeUser; if (!user) return; this.closeModal('user-modal'); document.getElementById('pin-modal-username').textContent = user.username; document.getElementById('reset-pin').value = ''; document.getElementById('reset-pin-confirm').value = ''; document.getElementById('pin-modal-error').style.display = 'none'; document.getElementById('pin-modal').style.display = 'flex'; }, async _submitPinReset() { const user = this._activeUser; if (!user) return; const pin = document.getElementById('reset-pin').value; const pinConfirm = document.getElementById('reset-pin-confirm').value; const errorEl = document.getElementById('pin-modal-error'); errorEl.style.display = 'none'; if (!pin || pin.length < 4) { errorEl.textContent = 'PIN must be at least 4 digits'; errorEl.style.display = 'block'; return; } if (pin !== pinConfirm) { errorEl.textContent = 'PINs do not match'; errorEl.style.display = 'block'; return; } try { const response = await fetch(`/api/admin/tech-users/${user.username}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pin }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Failed to reset PIN'); } this.closeModal('pin-modal'); this.showToast(`PIN reset for "${user.username}"`, 'success'); } catch (err) { errorEl.textContent = err.message; errorEl.style.display = 'block'; } }, _handleResetNCPassword() { const user = this._activeUser; if (!user) return; this.closeModal('user-modal'); this._showConfirm( 'Reset NC Password?', `Generate a new Nextcloud password for "${user.username}"? They will need the new password to sync.`, async () => { try { const response = await fetch(`/api/admin/tech-users/${user.username}/reset-password`, { method: 'POST' }); const result = await response.json(); if (!response.ok) throw new Error(result.error || 'Failed'); this.closeModal('confirm-modal'); this.showToast(`NC password reset for "${user.username}"`, 'success'); this.loadTechUsers(); } catch (err) { document.getElementById('confirm-modal-error').textContent = err.message; document.getElementById('confirm-modal-error').style.display = 'block'; } } ); }, _handleDelete() { const user = this._activeUser; if (!user) return; this.closeModal('user-modal'); this._showConfirm( 'Delete Tech User?', `Delete "${user.username}"? Their Nextcloud account will be disabled.`, async () => { try { const response = await fetch(`/api/admin/tech-users/${user.username}`, { method: 'DELETE' }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Failed'); } this.closeModal('confirm-modal'); this.showToast(`Tech user "${user.username}" deleted`, 'success'); this.loadTechUsers(); } catch (err) { document.getElementById('confirm-modal-error').textContent = err.message; document.getElementById('confirm-modal-error').style.display = 'block'; } } ); }, // ── Generic Confirm Modal ──────────────────────────────────────────── _showConfirm(title, message, onConfirm) { document.getElementById('confirm-modal-title').textContent = title; document.getElementById('confirm-modal-msg').textContent = message; document.getElementById('confirm-modal-error').style.display = 'none'; document.getElementById('confirm-modal-ok').onclick = onConfirm; document.getElementById('confirm-modal').style.display = 'flex'; }, // ── Create Tech User ───────────────────────────────────────────────── async createTechUser() { const submitBtn = document.getElementById('tech-submit-btn'); const formError = document.getElementById('tech-form-error'); const formSuccess = document.getElementById('tech-form-success'); const username = document.getElementById('new-tech-username').value.trim(); const displayName = document.getElementById('new-tech-displayname').value.trim(); const pin = document.getElementById('new-tech-pin').value; const pinConfirm = document.getElementById('new-tech-pin-confirm').value; formError.style.display = 'none'; formSuccess.style.display = 'none'; if (!username || !pin) { formError.textContent = 'Username and PIN are required'; formError.style.display = 'block'; return; } if (pin.length < 4) { formError.textContent = 'PIN must be at least 4 digits'; formError.style.display = 'block'; return; } if (pin !== pinConfirm) { formError.textContent = 'PINs do not match'; formError.style.display = 'block'; return; } submitBtn.disabled = true; submitBtn.textContent = 'Creating...'; try { const response = await fetch('/api/admin/tech-users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, display_name: displayName, pin }) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || 'Failed to create tech user'); formSuccess.textContent = `Tech user "${username}" created!`; formSuccess.style.display = 'block'; document.getElementById('add-tech-user-form').reset(); this.loadTechUsers(); } catch (err) { console.error('Error creating tech user:', err); formError.textContent = err.message; formError.style.display = 'block'; } finally { submitBtn.disabled = false; submitBtn.textContent = 'Create Tech User'; } }, // ── Helpers ────────────────────────────────────────────────────────── closeModal(id) { document.getElementById(id).style.display = 'none'; }, showToast(message, type) { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = `toast ${type}`; toast.style.display = 'block'; setTimeout(() => { toast.style.display = 'none'; }, 3000); }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; window.Admin = Admin;