257 lines
8.4 KiB
JavaScript
257 lines
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, "&")
|
||
|
|
.replace(/</g, "<")
|
||
|
|
.replace(/>/g, ">")
|
||
|
|
.replace(/"/g, """)
|
||
|
|
.replace(/'/g, "'");
|
||
|
|
}
|