spotnotes.app/static/assets/js/admin.js
2026-05-12 18:58:18 -05:00

257 lines
No EOL
8.4 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
loadStats();
loadUsers();
initCollapsibles();
initSearch();
});
let selectedUserId = null;
let selectedNotepadId = null;
let allUsers = []; // Store users for client-side searching
async function loadUsers() {
const userListDiv = document.getElementById('user-list');
userListDiv.innerHTML = '<p>Loading users...</p>';
try {
const res = await fetch('/admin/users');
if (res.status === 403) {
window.location.href = '/login.html';
return;
}
allUsers = await res.json();
renderUsers(allUsers);
} catch (err) {
console.error(err);
userListDiv.innerHTML = '<p style="color:red;">Error loading users.</p>';
}
}
function renderUsers(users) {
const userListDiv = document.getElementById('user-list');
userListDiv.innerHTML = '';
if (users.length === 0) {
userListDiv.innerHTML = '<p style="padding:10px;">No users found.</p>';
return;
}
users.forEach(user => {
const userDiv = document.createElement('div');
userDiv.className = 'user-item';
// Handle "Unowned" user display specifically or generic
const ipDisplay = user.id === 0 ? '' : `<br> <small>IP: ${escapeHtml(user.last_ip || 'Unknown')}</small>`;
const infoDiv = document.createElement('div');
infoDiv.innerHTML = `<strong>${escapeHtml(user.email)}</strong> (${escapeHtml(user.first_name)} ${escapeHtml(user.last_name)}) ${ipDisplay}`;
const actionsDiv = document.createElement('div');
// Ban IP Button (only for real users with IPs)
if (user.last_ip && user.id !== 0) {
const banIpBtn = document.createElement('button');
banIpBtn.className = 'admin-button';
banIpBtn.textContent = 'Ban IP';
banIpBtn.onclick = (e) => {
e.stopPropagation();
banUserIp(user.id);
};
actionsDiv.appendChild(banIpBtn);
}
userDiv.appendChild(infoDiv);
userDiv.appendChild(actionsDiv);
userDiv.addEventListener('click', () => {
selectedUserId = user.id;
loadNotepads(user.id);
expandSection('notepad-list');
});
userListDiv.appendChild(userDiv);
});
}
function initSearch() {
const searchInput = document.getElementById('user-search');
searchInput.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
const filtered = allUsers.filter(user =>
(user.email && user.email.toLowerCase().includes(term)) ||
(user.first_name && user.first_name.toLowerCase().includes(term)) ||
(user.last_name && user.last_name.toLowerCase().includes(term))
);
renderUsers(filtered);
});
}
async function loadNotepads(userId) {
const notepadListDiv = document.getElementById('notepad-list');
notepadListDiv.innerHTML = '<p>Loading notepads...</p>';
try {
const res = await fetch(`/admin/users/${userId}/notepads`);
const notepads = await res.json();
notepadListDiv.innerHTML = '';
notepads.forEach(notepad => {
const notepadDiv = document.createElement('div');
notepadDiv.className = 'user-item';
const infoDiv = document.createElement('div');
infoDiv.innerHTML = `<strong>${escapeHtml(notepad.name)}</strong> (Public: ${notepad.is_public})`;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'admin-button';
deleteBtn.textContent = 'Delete Pad';
deleteBtn.onclick = async (e) => {
e.stopPropagation();
if(confirm('Delete this entire notepad and all its notes?')) {
await deleteNotepad(notepad.id);
loadNotepads(userId);
}
};
notepadDiv.appendChild(infoDiv);
notepadDiv.appendChild(deleteBtn);
notepadDiv.addEventListener('click', () => {
selectedNotepadId = notepad.id;
loadNotes(notepad.id);
expandSection('note-list');
});
notepadListDiv.appendChild(notepadDiv);
});
} catch (err) {
console.error(err);
notepadListDiv.innerHTML = '<p style="color:red;">Error loading notepads.</p>';
}
}
async function loadNotes(notepadId) {
const noteListDiv = document.getElementById('note-list');
noteListDiv.innerHTML = '<p>Loading notes...</p>';
try {
const res = await fetch(`/admin/notepads/${notepadId}/notes`);
const notes = await res.json();
noteListDiv.innerHTML = '';
notes.forEach(note => {
const noteDiv = document.createElement('div');
noteDiv.className = 'user-item';
const infoDiv = document.createElement('div');
infoDiv.innerHTML = `<strong>${escapeHtml(note.title)}</strong>: ${escapeHtml(note.content)}`;
noteDiv.appendChild(infoDiv);
// Add delete button for each note
const deleteButton = document.createElement('button');
deleteButton.className = 'admin-button';
deleteButton.textContent = 'Delete Note';
deleteButton.addEventListener('click', async (e) => {
e.stopPropagation(); // Prevent selecting the note
if (confirm('Are you sure you want to delete this note?')) {
await deleteNote(note.id);
loadNotes(notepadId); // Refresh notes after deletion
}
});
noteDiv.appendChild(deleteButton);
noteListDiv.appendChild(noteDiv);
});
} catch (err) {
console.error(err);
noteListDiv.innerHTML = '<p style="color:red;">Error loading notes.</p>';
}
}
async function deleteNote(noteId) {
try {
const res = await fetch(`/admin/notes/${noteId}`, {
method: 'DELETE'
});
if (res.ok) {
alert('Note deleted successfully.');
} else {
alert('Failed to delete note.');
}
} catch (err) {
console.error(err);
alert('Error deleting note.');
}
}
async function deleteNotepad(notepadId) {
try {
const res = await fetch(`/admin/notepads/${notepadId}`, {
method: 'DELETE'
});
if (res.ok) {
alert('Notepad deleted.');
} else {
alert('Failed to delete notepad.');
}
} catch (err) {
console.error(err);
}
}
async function banUserIp(userId) {
if(!confirm("Are you sure you want to ban this IP address? This will block all future access from this IP.")) return;
try {
const res = await fetch(`/admin/users/${userId}/ban_ip`, { method: 'POST' });
const data = await res.json();
alert(data.message || data.error);
} catch (err) {
console.error(err);
alert("Error banning IP");
}
}
async function loadStats() {
const container = document.getElementById('admin-stats');
try {
const res = await fetch('/admin/stats');
if (res.status === 403) {
window.location.href = '/login.html';
return;
}
const stats = await res.json();
container.innerHTML = `
<div class="stat-card"><h3>${stats.user_count}</h3><p>Users</p></div>
<div class="stat-card"><h3>${stats.notepad_count}</h3><p>Notepads</p></div>
<div class="stat-card"><h3>${stats.note_count}</h3><p>Notes</p></div>
`;
} catch (err) {
console.error(err);
container.innerHTML = '';
}
}
function initCollapsibles() {
const headers = document.querySelectorAll('.section-title');
headers.forEach(header => {
header.addEventListener('click', () => {
const content = header.nextElementSibling;
content.classList.toggle('collapsed');
});
});
}
function expandSection(id) {
const el = document.getElementById(id);
if(el) el.classList.remove('collapsed');
}
function escapeHtml(text) {
if (text === null || text === undefined) return '';
return String(text)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}