spotnotes.app/static/assets/js/admin.js

560 lines
21 KiB
JavaScript
Raw Permalink Normal View History

2026-05-12 23:58:18 +00:00
document.addEventListener('DOMContentLoaded', () => {
loadStats();
2026-10-06 19:32:24 +00:00
loadReportedNotes();
2026-05-12 23:58:18 +00:00
loadUsers();
2026-10-06 19:32:24 +00:00
loadBannedIps();
2026-05-12 23:58:18 +00:00
initCollapsibles();
initSearch();
});
let selectedUserId = null;
let selectedNotepadId = null;
2026-10-06 19:32:24 +00:00
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');
}
}
2026-05-12 23:58:18 +00:00
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) {
2026-10-06 19:32:24 +00:00
userListDiv.innerHTML = '<p class="empty-text">No users found.</p>';
2026-05-12 23:58:18 +00:00
return;
}
users.forEach(user => {
const userDiv = document.createElement('div');
userDiv.className = 'user-item';
2026-10-06 19:32:24 +00:00
if (selectedUserId === user.id) userDiv.classList.add('selected');
2026-05-12 23:58:18 +00:00
const infoDiv = document.createElement('div');
2026-10-06 19:32:24 +00:00
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}`;
2026-05-12 23:58:18 +00:00
const actionsDiv = document.createElement('div');
2026-10-06 19:32:24 +00:00
actionsDiv.className = 'item-actions';
2026-05-12 23:58:18 +00:00
2026-10-06 19:32:24 +00:00
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) => {
2026-05-12 23:58:18 +00:00
e.stopPropagation();
2026-10-06 19:32:24 +00:00
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);
}
2026-05-12 23:58:18 +00:00
};
2026-10-06 19:32:24 +00:00
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);
2026-05-12 23:58:18 +00:00
}
userDiv.appendChild(infoDiv);
userDiv.appendChild(actionsDiv);
userDiv.addEventListener('click', () => {
selectedUserId = user.id;
2026-10-06 19:32:24 +00:00
document.querySelectorAll('#user-list .user-item').forEach(el => el.classList.remove('selected'));
userDiv.classList.add('selected');
loadNotepads(user.id, user.email);
2026-05-12 23:58:18 +00:00
expandSection('notepad-list');
});
2026-10-06 19:32:24 +00:00
2026-05-12 23:58:18 +00:00
userListDiv.appendChild(userDiv);
});
}
2026-10-06 19:32:24 +00:00
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');
}
2026-05-12 23:58:18 +00:00
}
2026-10-06 19:32:24 +00:00
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) {
2026-05-12 23:58:18 +00:00
const notepadListDiv = document.getElementById('notepad-list');
2026-10-06 19:32:24 +00:00
const headerTitle = document.getElementById('notepads-header-title');
if (headerTitle) {
headerTitle.textContent = `📁 Notepads for: ${userEmail}`;
}
2026-05-12 23:58:18 +00:00
notepadListDiv.innerHTML = '<p>Loading notepads...</p>';
try {
const res = await fetch(`/admin/users/${userId}/notepads`);
const notepads = await res.json();
notepadListDiv.innerHTML = '';
2026-10-06 19:32:24 +00:00
if (notepads.length === 0) {
notepadListDiv.innerHTML = '<p class="empty-text">No notepads found for this user.</p>';
return;
}
2026-05-12 23:58:18 +00:00
notepads.forEach(notepad => {
const notepadDiv = document.createElement('div');
notepadDiv.className = 'user-item';
2026-10-06 19:32:24 +00:00
if (selectedNotepadId === notepad.id) notepadDiv.classList.add('selected');
2026-05-12 23:58:18 +00:00
2026-10-06 19:32:24 +00:00
const badge = notepad.is_public ?
'<span class="badge badge-public">🌐 Public</span>' :
'<span class="badge badge-encrypted">🔒 Zero-Knowledge Encrypted</span>';
2026-05-12 23:58:18 +00:00
const infoDiv = document.createElement('div');
2026-10-06 19:32:24 +00:00
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>`;
2026-05-12 23:58:18 +00:00
const deleteBtn = document.createElement('button');
2026-10-06 19:32:24 +00:00
deleteBtn.className = 'admin-button btn-danger';
2026-05-12 23:58:18 +00:00
deleteBtn.textContent = 'Delete Pad';
deleteBtn.onclick = async (e) => {
e.stopPropagation();
2026-10-06 19:32:24 +00:00
if(confirm(`Delete entire notepad "${notepad.name}" and all its notes?`)) {
2026-05-12 23:58:18 +00:00
await deleteNotepad(notepad.id);
2026-10-06 19:32:24 +00:00
loadNotepads(userId, userEmail);
loadStats();
2026-05-12 23:58:18 +00:00
}
};
notepadDiv.appendChild(infoDiv);
notepadDiv.appendChild(deleteBtn);
notepadDiv.addEventListener('click', () => {
selectedNotepadId = notepad.id;
2026-10-06 19:32:24 +00:00
document.querySelectorAll('#notepad-list .user-item').forEach(el => el.classList.remove('selected'));
notepadDiv.classList.add('selected');
loadNotes(notepad.id, notepad.name, notepad.is_public);
2026-05-12 23:58:18 +00:00
expandSection('note-list');
});
notepadListDiv.appendChild(notepadDiv);
});
} catch (err) {
console.error(err);
notepadListDiv.innerHTML = '<p style="color:red;">Error loading notepads.</p>';
}
}
2026-10-06 19:32:24 +00:00
async function loadNotes(notepadId, notepadName, isPublic) {
2026-05-12 23:58:18 +00:00
const noteListDiv = document.getElementById('note-list');
2026-10-06 19:32:24 +00:00
const headerTitle = document.getElementById('notes-header-title');
if (headerTitle) {
headerTitle.textContent = `📝 Notes in: ${notepadName}`;
}
2026-05-12 23:58:18 +00:00
noteListDiv.innerHTML = '<p>Loading notes...</p>';
try {
const res = await fetch(`/admin/notepads/${notepadId}/notes`);
const notes = await res.json();
noteListDiv.innerHTML = '';
2026-10-06 19:32:24 +00:00
if (notes.length === 0) {
noteListDiv.innerHTML = '<p class="empty-text">This notepad is empty.</p>';
return;
}
2026-05-12 23:58:18 +00:00
notes.forEach(note => {
const noteDiv = document.createElement('div');
noteDiv.className = 'user-item';
const infoDiv = document.createElement('div');
2026-10-06 19:32:24 +00:00
infoDiv.className = 'item-info';
if (note.is_encrypted) {
2026-10-06 19:32:24 +00:00
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 {
2026-10-06 19:32:24 +00:00
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>
`;
}
2026-05-12 23:58:18 +00:00
noteDiv.appendChild(infoDiv);
const deleteButton = document.createElement('button');
2026-10-06 19:32:24 +00:00
deleteButton.className = 'admin-button btn-danger';
2026-05-12 23:58:18 +00:00
deleteButton.textContent = 'Delete Note';
deleteButton.addEventListener('click', async (e) => {
2026-10-06 19:32:24 +00:00
e.stopPropagation();
if (confirm('Permanently delete this note?')) {
2026-05-12 23:58:18 +00:00
await deleteNote(note.id);
2026-10-06 19:32:24 +00:00
loadNotes(notepadId, notepadName, isPublic);
loadStats();
2026-05-12 23:58:18 +00:00
}
});
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 {
2026-10-06 19:32:24 +00:00
const res = await fetch(`/admin/notes/${noteId}`, { method: 'DELETE' });
2026-05-12 23:58:18 +00:00
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 {
2026-10-06 19:32:24 +00:00
const res = await fetch(`/admin/notepads/${notepadId}`, { method: 'DELETE' });
2026-05-12 23:58:18 +00:00
if (res.ok) {
alert('Notepad deleted.');
2026-10-06 19:32:24 +00:00
document.getElementById('note-list').innerHTML = '<p class="empty-text">Select a notepad to see its notes.</p>';
2026-05-12 23:58:18 +00:00
} else {
alert('Failed to delete notepad.');
}
} catch (err) {
console.error(err);
}
}
2026-10-06 19:32:24 +00:00
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);
});
2026-05-12 23:58:18 +00:00
}
function initCollapsibles() {
const headers = document.querySelectorAll('.section-title');
headers.forEach(header => {
header.addEventListener('click', () => {
2026-10-06 19:32:24 +00:00
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') ? '▶' : '▼';
}
}
2026-05-12 23:58:18 +00:00
});
});
}
function expandSection(id) {
const el = document.getElementById(id);
2026-10-06 19:32:24 +00:00
if (el) {
el.classList.remove('collapsed');
const header = el.parentElement.querySelector('.section-title .toggle-icon');
if (header) header.textContent = '▼';
}
2026-05-12 23:58:18 +00:00
}
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;");
}