// 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;