Offline-first photo capture app for Nextcloud with: - Camera capture with continuous mode (auto-reopens after each photo) - File browser with fullscreen image gallery, swipe navigation, and rename - Upload queue with background sync engine - Admin panel for Nextcloud user management - Service worker for offline-first caching (v13) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
8.1 KiB
JavaScript
247 lines
8.1 KiB
JavaScript
// NextSnap - Admin Panel Logic
|
|
'use strict';
|
|
|
|
const Admin = {
|
|
users: [],
|
|
|
|
async init() {
|
|
await this.loadUsers();
|
|
this.setupEventListeners();
|
|
},
|
|
|
|
setupEventListeners() {
|
|
document.getElementById('add-user-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.createUser();
|
|
});
|
|
|
|
document.getElementById('refresh-btn').addEventListener('click', () => {
|
|
this.loadUsers();
|
|
});
|
|
},
|
|
|
|
async loadUsers() {
|
|
const userList = document.getElementById('user-list');
|
|
const loadingMsg = document.getElementById('loading-msg');
|
|
const errorMsg = document.getElementById('error-msg');
|
|
|
|
loadingMsg.style.display = 'block';
|
|
errorMsg.style.display = 'none';
|
|
userList.innerHTML = '';
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/users');
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to load users');
|
|
}
|
|
|
|
const data = await response.json();
|
|
this.users = data.users || [];
|
|
|
|
loadingMsg.style.display = 'none';
|
|
|
|
if (this.users.length === 0) {
|
|
userList.innerHTML = '<tr><td colspan="5" class="empty-state">No users found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
this.users.forEach(user => {
|
|
const row = this.createUserRow(user);
|
|
userList.appendChild(row);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading users:', error);
|
|
loadingMsg.style.display = 'none';
|
|
errorMsg.textContent = error.message;
|
|
errorMsg.style.display = 'block';
|
|
}
|
|
},
|
|
|
|
createUserRow(user) {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${this.escapeHtml(user.id)}</td>
|
|
<td>${this.escapeHtml(user.displayname || '-')}</td>
|
|
<td>${this.escapeHtml(user.email || '-')}</td>
|
|
<td>
|
|
<span class="badge ${user.enabled ? 'badge-success' : 'badge-danger'}">
|
|
${user.enabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
${user.enabled ?
|
|
`<button class="btn-action btn-warning" onclick="Admin.disableUser('${user.id}')">Disable</button>` :
|
|
`<button class="btn-action btn-success" onclick="Admin.enableUser('${user.id}')">Enable</button>`
|
|
}
|
|
<button class="btn-action btn-danger" onclick="Admin.confirmDeleteUser('${user.id}')">Delete</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
return row;
|
|
},
|
|
|
|
async createUser() {
|
|
const form = document.getElementById('add-user-form');
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
const formError = document.getElementById('form-error');
|
|
const formSuccess = document.getElementById('form-success');
|
|
|
|
const username = document.getElementById('new-username').value.trim();
|
|
const password = document.getElementById('new-password').value;
|
|
const email = document.getElementById('new-email').value.trim();
|
|
const displayName = document.getElementById('new-displayname').value.trim();
|
|
|
|
formError.style.display = 'none';
|
|
formSuccess.style.display = 'none';
|
|
|
|
if (!username || !password) {
|
|
formError.textContent = 'Username and password are required';
|
|
formError.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Creating...';
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/users', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username: username,
|
|
password: password,
|
|
email: email || null,
|
|
displayName: displayName || null
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.error || 'Failed to create user');
|
|
}
|
|
|
|
formSuccess.textContent = `User "${username}" created successfully!`;
|
|
formSuccess.style.display = 'block';
|
|
|
|
form.reset();
|
|
|
|
setTimeout(() => {
|
|
this.loadUsers();
|
|
}, 1000);
|
|
|
|
} catch (error) {
|
|
console.error('Error creating user:', error);
|
|
formError.textContent = error.message;
|
|
formError.style.display = 'block';
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Create User';
|
|
}
|
|
},
|
|
|
|
async enableUser(username) {
|
|
if (!confirm(`Enable user "${username}"?`)) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/users/${username}/enable`, {
|
|
method: 'PUT'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to enable user');
|
|
}
|
|
|
|
this.showToast(`User "${username}" enabled`, 'success');
|
|
this.loadUsers();
|
|
|
|
} catch (error) {
|
|
console.error('Error enabling user:', error);
|
|
this.showToast(error.message, 'error');
|
|
}
|
|
},
|
|
|
|
async disableUser(username) {
|
|
if (!confirm(`Disable user "${username}"?`)) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/users/${username}/disable`, {
|
|
method: 'PUT'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to disable user');
|
|
}
|
|
|
|
this.showToast(`User "${username}" disabled`, 'success');
|
|
this.loadUsers();
|
|
|
|
} catch (error) {
|
|
console.error('Error disabling user:', error);
|
|
this.showToast(error.message, 'error');
|
|
}
|
|
},
|
|
|
|
confirmDeleteUser(username) {
|
|
const modal = document.getElementById('delete-modal');
|
|
const confirmBtn = document.getElementById('confirm-delete');
|
|
|
|
document.getElementById('delete-username').textContent = username;
|
|
modal.style.display = 'flex';
|
|
|
|
confirmBtn.onclick = () => {
|
|
this.deleteUser(username);
|
|
this.hideDeleteModal();
|
|
};
|
|
},
|
|
|
|
hideDeleteModal() {
|
|
document.getElementById('delete-modal').style.display = 'none';
|
|
},
|
|
|
|
async deleteUser(username) {
|
|
try {
|
|
const response = await fetch(`/api/admin/users/${username}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to delete user');
|
|
}
|
|
|
|
this.showToast(`User "${username}" deleted`, 'success');
|
|
this.loadUsers();
|
|
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
this.showToast(error.message, 'error');
|
|
}
|
|
},
|
|
|
|
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;
|