Add NextSnap PWA with photo gallery viewer and continuous capture
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>
This commit is contained in:
246
app/static/js/admin.js
Normal file
246
app/static/js/admin.js
Normal file
@@ -0,0 +1,246 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user