560 lines
No EOL
21 KiB
JavaScript
560 lines
No EOL
21 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
loadStats();
|
|
loadReportedNotes();
|
|
loadUsers();
|
|
loadBannedIps();
|
|
initCollapsibles();
|
|
initSearch();
|
|
});
|
|
|
|
let selectedUserId = null;
|
|
let selectedNotepadId = null;
|
|
let allUsers = [];
|
|
|
|
async function loadStats() {
|
|
const container = document.getElementById('admin-stats');
|
|
const reportedBadge = document.getElementById('reported-badge');
|
|
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>
|
|
<small>${stats.banned_user_count} banned</small>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>${stats.notepad_count}</h3>
|
|
<p>Notepads</p>
|
|
<small>${stats.public_notepad_count} Public · ${stats.private_notepad_count} Encrypted</small>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>${stats.note_count}</h3>
|
|
<p>Total Notes</p>
|
|
<small>${stats.public_note_count} Public · ${stats.encrypted_note_count} Encrypted</small>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3 style="color:${stats.reported_note_count > 0 ? '#ef4444' : '#10b981'};">${stats.reported_note_count}</h3>
|
|
<p>Reported Notes</p>
|
|
<small>${stats.reported_note_count > 0 ? 'Requires attention' : 'All clear'}</small>
|
|
</div>
|
|
`;
|
|
|
|
if (reportedBadge) {
|
|
reportedBadge.textContent = stats.reported_note_count;
|
|
reportedBadge.style.display = stats.reported_note_count > 0 ? 'inline-block' : 'none';
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading stats:', err);
|
|
}
|
|
}
|
|
|
|
async function loadReportedNotes() {
|
|
const listDiv = document.getElementById('reported-list');
|
|
listDiv.innerHTML = '<p>Loading reported notes...</p>';
|
|
|
|
try {
|
|
const res = await fetch('/admin/reported_notes');
|
|
if (!res.ok) throw new Error('Failed to load reported notes');
|
|
const notes = await res.json();
|
|
|
|
listDiv.innerHTML = '';
|
|
if (notes.length === 0) {
|
|
listDiv.innerHTML = '<p class="empty-text">No notes reported. Public content is clean.</p>';
|
|
return;
|
|
}
|
|
|
|
notes.forEach(note => {
|
|
const item = document.createElement('div');
|
|
item.className = 'user-item';
|
|
|
|
const info = document.createElement('div');
|
|
info.className = 'item-info';
|
|
info.innerHTML = `
|
|
<div>
|
|
<strong>🚩 ${escapeHtml(note.title)}</strong>
|
|
<span class="badge badge-public">Pad: ${escapeHtml(note.notepad_name)}</span>
|
|
</div>
|
|
<div style="font-size:0.88rem; color:#475569; margin:4px 0;">
|
|
${escapeHtml(note.content)}
|
|
</div>
|
|
<div style="font-size:0.75rem; color:#94a3b8;">
|
|
Reported Note #${note.id} · Author: ${escapeHtml(note.author_email || 'Unknown')} · ${new Date(note.created_at).toLocaleString()}
|
|
</div>
|
|
`;
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'item-actions';
|
|
|
|
const restoreBtn = document.createElement('button');
|
|
restoreBtn.className = 'admin-button btn-success';
|
|
restoreBtn.textContent = '✓ Restore Note';
|
|
restoreBtn.onclick = async () => {
|
|
if (confirm('Restore this note? It will become visible on the public map again.')) {
|
|
await restoreNote(note.id);
|
|
}
|
|
};
|
|
|
|
const deleteBtn = document.createElement('button');
|
|
deleteBtn.className = 'admin-button btn-danger';
|
|
deleteBtn.textContent = '🗑️ Delete Note';
|
|
deleteBtn.onclick = async () => {
|
|
if (confirm('Permanently delete this reported note?')) {
|
|
await deleteNote(note.id);
|
|
loadReportedNotes();
|
|
loadStats();
|
|
}
|
|
};
|
|
|
|
actions.appendChild(restoreBtn);
|
|
actions.appendChild(deleteBtn);
|
|
|
|
item.appendChild(info);
|
|
item.appendChild(actions);
|
|
listDiv.appendChild(item);
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
listDiv.innerHTML = '<p style="color:red;">Error loading reported notes.</p>';
|
|
}
|
|
}
|
|
|
|
async function restoreNote(noteId) {
|
|
try {
|
|
const res = await fetch(`/admin/notes/${noteId}/restore`, { method: 'POST' });
|
|
const data = await res.json();
|
|
alert(data.message || 'Note restored');
|
|
loadReportedNotes();
|
|
loadStats();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Error restoring note');
|
|
}
|
|
}
|
|
|
|
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 class="empty-text">No users found.</p>';
|
|
return;
|
|
}
|
|
|
|
users.forEach(user => {
|
|
const userDiv = document.createElement('div');
|
|
userDiv.className = 'user-item';
|
|
if (selectedUserId === user.id) userDiv.classList.add('selected');
|
|
|
|
const infoDiv = document.createElement('div');
|
|
infoDiv.className = 'item-info';
|
|
|
|
const adminBadge = user.is_admin ? '<span class="badge badge-admin">Admin</span>' : '';
|
|
const bannedBadge = user.is_banned ? '<span class="badge badge-banned">Banned</span>' : '';
|
|
const ipDisplay = user.id === 0 ? '' : `<br><small style="color:#64748b;">IP: ${escapeHtml(user.last_ip || 'No IP recorded')}</small>`;
|
|
|
|
infoDiv.innerHTML = `<strong>${escapeHtml(user.email)}</strong> ${escapeHtml(user.first_name ? `(${user.first_name} ${user.last_name})` : '')} ${adminBadge}${bannedBadge}${ipDisplay}`;
|
|
|
|
const actionsDiv = document.createElement('div');
|
|
actionsDiv.className = 'item-actions';
|
|
|
|
if (user.id !== 0) {
|
|
// Ban / Unban User
|
|
const banUserBtn = document.createElement('button');
|
|
banUserBtn.className = `admin-button ${user.is_banned ? 'btn-success' : 'btn-warning'}`;
|
|
banUserBtn.textContent = user.is_banned ? 'Unban Account' : 'Ban Account';
|
|
banUserBtn.onclick = async (e) => {
|
|
e.stopPropagation();
|
|
const action = user.is_banned ? 'unban' : 'ban';
|
|
if (confirm(`Are you sure you want to ${action} user ${user.email}?`)) {
|
|
await toggleUserBan(user.id, user.is_banned);
|
|
}
|
|
};
|
|
actionsDiv.appendChild(banUserBtn);
|
|
|
|
// Ban IP Button
|
|
if (user.last_ip) {
|
|
const banIpBtn = document.createElement('button');
|
|
banIpBtn.className = 'admin-button btn-secondary';
|
|
banIpBtn.textContent = 'Ban IP';
|
|
banIpBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
banUserIp(user.id);
|
|
};
|
|
actionsDiv.appendChild(banIpBtn);
|
|
}
|
|
|
|
// Toggle Admin Button
|
|
const toggleAdminBtn = document.createElement('button');
|
|
toggleAdminBtn.className = 'admin-button btn-primary';
|
|
toggleAdminBtn.textContent = user.is_admin ? 'Demote Admin' : 'Make Admin';
|
|
toggleAdminBtn.onclick = async (e) => {
|
|
e.stopPropagation();
|
|
if (confirm(`${user.is_admin ? 'Demote' : 'Promote'} ${user.email} to Admin?`)) {
|
|
await toggleAdmin(user.id);
|
|
}
|
|
};
|
|
actionsDiv.appendChild(toggleAdminBtn);
|
|
|
|
// Delete User Button
|
|
const deleteUserBtn = document.createElement('button');
|
|
deleteUserBtn.className = 'admin-button btn-danger';
|
|
deleteUserBtn.textContent = 'Delete User';
|
|
deleteUserBtn.onclick = async (e) => {
|
|
e.stopPropagation();
|
|
if (confirm(`Permanently delete user ${user.email} and all their notepads/notes? This cannot be undone.`)) {
|
|
await deleteUser(user.id);
|
|
}
|
|
};
|
|
actionsDiv.appendChild(deleteUserBtn);
|
|
}
|
|
|
|
userDiv.appendChild(infoDiv);
|
|
userDiv.appendChild(actionsDiv);
|
|
|
|
userDiv.addEventListener('click', () => {
|
|
selectedUserId = user.id;
|
|
document.querySelectorAll('#user-list .user-item').forEach(el => el.classList.remove('selected'));
|
|
userDiv.classList.add('selected');
|
|
loadNotepads(user.id, user.email);
|
|
expandSection('notepad-list');
|
|
});
|
|
|
|
userListDiv.appendChild(userDiv);
|
|
});
|
|
}
|
|
|
|
async function toggleUserBan(userId, isCurrentlyBanned) {
|
|
try {
|
|
const endpoint = isCurrentlyBanned ? `/admin/users/${userId}/unban` : `/admin/users/${userId}/ban`;
|
|
const res = await fetch(endpoint, { method: 'POST' });
|
|
const data = await res.json();
|
|
alert(data.message || 'User status updated');
|
|
loadUsers();
|
|
loadStats();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Error updating user ban status');
|
|
}
|
|
}
|
|
|
|
async function toggleAdmin(userId) {
|
|
try {
|
|
const res = await fetch(`/admin/users/${userId}/toggle_admin`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
alert(data.message);
|
|
loadUsers();
|
|
} else {
|
|
alert(data.error || 'Failed to update admin status');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Error updating admin status');
|
|
}
|
|
}
|
|
|
|
async function deleteUser(userId) {
|
|
try {
|
|
const res = await fetch(`/admin/users/${userId}`, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
alert(data.message || 'User deleted');
|
|
selectedUserId = null;
|
|
loadUsers();
|
|
loadStats();
|
|
document.getElementById('notepad-list').innerHTML = '<p class="empty-text">Select a user above to see their notepads.</p>';
|
|
document.getElementById('note-list').innerHTML = '<p class="empty-text">Select a notepad to see its notes.</p>';
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Error deleting user');
|
|
}
|
|
}
|
|
|
|
async function loadBannedIps() {
|
|
const listDiv = document.getElementById('banned-ip-list');
|
|
try {
|
|
const res = await fetch('/admin/banned_ips');
|
|
const ips = await res.json();
|
|
|
|
listDiv.innerHTML = '';
|
|
if (ips.length === 0) {
|
|
listDiv.innerHTML = '<p class="empty-text">No IP addresses currently banned.</p>';
|
|
return;
|
|
}
|
|
|
|
ips.forEach(item => {
|
|
const row = document.createElement('div');
|
|
row.className = 'user-item';
|
|
|
|
const info = document.createElement('div');
|
|
info.className = 'item-info';
|
|
info.innerHTML = `<strong>🚫 ${escapeHtml(item.ip_address)}</strong> <span style="font-size:0.8rem; color:#94a3b8;">· Banned ${new Date(item.created_at).toLocaleString()}</span>`;
|
|
|
|
const unbanBtn = document.createElement('button');
|
|
unbanBtn.className = 'admin-button btn-success';
|
|
unbanBtn.textContent = 'Unban IP';
|
|
unbanBtn.onclick = async () => {
|
|
if (confirm(`Unban IP address ${item.ip_address}?`)) {
|
|
await unbanIp(item.id);
|
|
}
|
|
};
|
|
|
|
row.appendChild(info);
|
|
row.appendChild(unbanBtn);
|
|
listDiv.appendChild(row);
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
listDiv.innerHTML = '<p style="color:red;">Error loading banned IPs.</p>';
|
|
}
|
|
}
|
|
|
|
async function unbanIp(ipId) {
|
|
try {
|
|
const res = await fetch(`/admin/banned_ips/${ipId}`, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
alert(data.message || 'IP unbanned');
|
|
loadBannedIps();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Error unbanning IP');
|
|
}
|
|
}
|
|
|
|
async function banUserIp(userId) {
|
|
if(!confirm("Are you sure you want to ban this IP address? This will block registration and login 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);
|
|
loadBannedIps();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Error banning IP");
|
|
}
|
|
}
|
|
|
|
async function loadNotepads(userId, userEmail) {
|
|
const notepadListDiv = document.getElementById('notepad-list');
|
|
const headerTitle = document.getElementById('notepads-header-title');
|
|
if (headerTitle) {
|
|
headerTitle.textContent = `📁 Notepads for: ${userEmail}`;
|
|
}
|
|
notepadListDiv.innerHTML = '<p>Loading notepads...</p>';
|
|
|
|
try {
|
|
const res = await fetch(`/admin/users/${userId}/notepads`);
|
|
const notepads = await res.json();
|
|
|
|
notepadListDiv.innerHTML = '';
|
|
if (notepads.length === 0) {
|
|
notepadListDiv.innerHTML = '<p class="empty-text">No notepads found for this user.</p>';
|
|
return;
|
|
}
|
|
|
|
notepads.forEach(notepad => {
|
|
const notepadDiv = document.createElement('div');
|
|
notepadDiv.className = 'user-item';
|
|
if (selectedNotepadId === notepad.id) notepadDiv.classList.add('selected');
|
|
|
|
const badge = notepad.is_public ?
|
|
'<span class="badge badge-public">🌐 Public</span>' :
|
|
'<span class="badge badge-encrypted">🔒 Zero-Knowledge Encrypted</span>';
|
|
|
|
const infoDiv = document.createElement('div');
|
|
infoDiv.className = 'item-info';
|
|
infoDiv.innerHTML = `<strong>${escapeHtml(notepad.name)}</strong> ${badge} <br><small style="color:#64748b;">Pad ID: #${notepad.id} · Notes: ${notepad.note_count || 0}</small>`;
|
|
|
|
const deleteBtn = document.createElement('button');
|
|
deleteBtn.className = 'admin-button btn-danger';
|
|
deleteBtn.textContent = 'Delete Pad';
|
|
deleteBtn.onclick = async (e) => {
|
|
e.stopPropagation();
|
|
if(confirm(`Delete entire notepad "${notepad.name}" and all its notes?`)) {
|
|
await deleteNotepad(notepad.id);
|
|
loadNotepads(userId, userEmail);
|
|
loadStats();
|
|
}
|
|
};
|
|
|
|
notepadDiv.appendChild(infoDiv);
|
|
notepadDiv.appendChild(deleteBtn);
|
|
|
|
notepadDiv.addEventListener('click', () => {
|
|
selectedNotepadId = notepad.id;
|
|
document.querySelectorAll('#notepad-list .user-item').forEach(el => el.classList.remove('selected'));
|
|
notepadDiv.classList.add('selected');
|
|
loadNotes(notepad.id, notepad.name, notepad.is_public);
|
|
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, notepadName, isPublic) {
|
|
const noteListDiv = document.getElementById('note-list');
|
|
const headerTitle = document.getElementById('notes-header-title');
|
|
if (headerTitle) {
|
|
headerTitle.textContent = `📝 Notes in: ${notepadName}`;
|
|
}
|
|
noteListDiv.innerHTML = '<p>Loading notes...</p>';
|
|
|
|
try {
|
|
const res = await fetch(`/admin/notepads/${notepadId}/notes`);
|
|
const notes = await res.json();
|
|
|
|
noteListDiv.innerHTML = '';
|
|
if (notes.length === 0) {
|
|
noteListDiv.innerHTML = '<p class="empty-text">This notepad is empty.</p>';
|
|
return;
|
|
}
|
|
|
|
notes.forEach(note => {
|
|
const noteDiv = document.createElement('div');
|
|
noteDiv.className = 'user-item';
|
|
|
|
const infoDiv = document.createElement('div');
|
|
infoDiv.className = 'item-info';
|
|
|
|
if (note.is_encrypted) {
|
|
const preview = note.content ? (note.content.length > 50 ? note.content.substring(0, 50) + '...' : note.content) : '';
|
|
infoDiv.innerHTML = `
|
|
<div><strong>🔒 [Encrypted Private Note #${note.id}]</strong> <span class="badge badge-encrypted">E2EE Protected</span></div>
|
|
<div style="font-family:monospace; color:#64748b; font-size:0.8rem; margin:4px 0;">Zero-Knowledge Ciphertext: ${escapeHtml(preview)}</div>
|
|
<small style="color:#94a3b8;">Created: ${new Date(note.created_at).toLocaleString()} · Lat/Lng at Rest: Redacted [0, 0]</small>
|
|
`;
|
|
} else {
|
|
infoDiv.innerHTML = `
|
|
<div><strong>🌐 ${escapeHtml(note.title)}</strong> <span class="badge badge-public">Public Note #${note.id}</span></div>
|
|
<div style="font-size:0.88rem; color:#334155; margin:4px 0;">${escapeHtml(note.content)}</div>
|
|
<small style="color:#94a3b8;">Location: (${note.lat}, ${note.lng}) · Created: ${new Date(note.created_at).toLocaleString()}</small>
|
|
`;
|
|
}
|
|
noteDiv.appendChild(infoDiv);
|
|
|
|
const deleteButton = document.createElement('button');
|
|
deleteButton.className = 'admin-button btn-danger';
|
|
deleteButton.textContent = 'Delete Note';
|
|
deleteButton.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
if (confirm('Permanently delete this note?')) {
|
|
await deleteNote(note.id);
|
|
loadNotes(notepadId, notepadName, isPublic);
|
|
loadStats();
|
|
}
|
|
});
|
|
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.');
|
|
document.getElementById('note-list').innerHTML = '<p class="empty-text">Select a notepad to see its notes.</p>';
|
|
} else {
|
|
alert('Failed to delete notepad.');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
function initSearch() {
|
|
const searchInput = document.getElementById('user-search');
|
|
if (!searchInput) return;
|
|
searchInput.addEventListener('input', (e) => {
|
|
const term = e.target.value.toLowerCase().trim();
|
|
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)) ||
|
|
(user.last_ip && user.last_ip.toLowerCase().includes(term))
|
|
);
|
|
renderUsers(filtered);
|
|
});
|
|
}
|
|
|
|
function initCollapsibles() {
|
|
const headers = document.querySelectorAll('.section-title');
|
|
headers.forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
const section = header.parentElement;
|
|
const content = section.querySelector('.section-content');
|
|
if (content) {
|
|
content.classList.toggle('collapsed');
|
|
const icon = header.querySelector('.toggle-icon');
|
|
if (icon) {
|
|
icon.textContent = content.classList.contains('collapsed') ? '▶' : '▼';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function expandSection(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.classList.remove('collapsed');
|
|
const header = el.parentElement.querySelector('.section-title .toggle-icon');
|
|
if (header) header.textContent = '▼';
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (text === null || text === undefined) return '';
|
|
return String(text)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
} |