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 = `

${stats.user_count}

Users

${stats.banned_user_count} banned

${stats.notepad_count}

Notepads

${stats.public_notepad_count} Public ยท ${stats.private_notepad_count} Encrypted

${stats.note_count}

Total Notes

${stats.public_note_count} Public ยท ${stats.encrypted_note_count} Encrypted

${stats.reported_note_count}

Reported Notes

${stats.reported_note_count > 0 ? 'Requires attention' : 'All clear'}
`; 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 = '

Loading reported notes...

'; 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 = '

No notes reported. Public content is clean.

'; return; } notes.forEach(note => { const item = document.createElement('div'); item.className = 'user-item'; const info = document.createElement('div'); info.className = 'item-info'; info.innerHTML = `
๐Ÿšฉ ${escapeHtml(note.title)} Pad: ${escapeHtml(note.notepad_name)}
${escapeHtml(note.content)}
Reported Note #${note.id} ยท Author: ${escapeHtml(note.author_email || 'Unknown')} ยท ${new Date(note.created_at).toLocaleString()}
`; 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 = '

Error loading reported notes.

'; } } 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 = '

Loading users...

'; 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 = '

Error loading users.

'; } } function renderUsers(users) { const userListDiv = document.getElementById('user-list'); userListDiv.innerHTML = ''; if (users.length === 0) { userListDiv.innerHTML = '

No users found.

'; 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 ? 'Admin' : ''; const bannedBadge = user.is_banned ? 'Banned' : ''; const ipDisplay = user.id === 0 ? '' : `
IP: ${escapeHtml(user.last_ip || 'No IP recorded')}`; infoDiv.innerHTML = `${escapeHtml(user.email)} ${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 = '

Select a user above to see their notepads.

'; document.getElementById('note-list').innerHTML = '

Select a notepad to see its notes.

'; } 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 = '

No IP addresses currently banned.

'; return; } ips.forEach(item => { const row = document.createElement('div'); row.className = 'user-item'; const info = document.createElement('div'); info.className = 'item-info'; info.innerHTML = `๐Ÿšซ ${escapeHtml(item.ip_address)} ยท Banned ${new Date(item.created_at).toLocaleString()}`; 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 = '

Error loading banned IPs.

'; } } 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 = '

Loading notepads...

'; try { const res = await fetch(`/admin/users/${userId}/notepads`); const notepads = await res.json(); notepadListDiv.innerHTML = ''; if (notepads.length === 0) { notepadListDiv.innerHTML = '

No notepads found for this user.

'; 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 ? '๐ŸŒ Public' : '๐Ÿ”’ Zero-Knowledge Encrypted'; const infoDiv = document.createElement('div'); infoDiv.className = 'item-info'; infoDiv.innerHTML = `${escapeHtml(notepad.name)} ${badge}
Pad ID: #${notepad.id} ยท Notes: ${notepad.note_count || 0}`; 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 = '

Error loading notepads.

'; } } 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 = '

Loading notes...

'; try { const res = await fetch(`/admin/notepads/${notepadId}/notes`); const notes = await res.json(); noteListDiv.innerHTML = ''; if (notes.length === 0) { noteListDiv.innerHTML = '

This notepad is empty.

'; 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 = `
๐Ÿ”’ [Encrypted Private Note #${note.id}] E2EE Protected
Zero-Knowledge Ciphertext: ${escapeHtml(preview)}
Created: ${new Date(note.created_at).toLocaleString()} ยท Lat/Lng at Rest: Redacted [0, 0] `; } else { infoDiv.innerHTML = `
๐ŸŒ ${escapeHtml(note.title)} Public Note #${note.id}
${escapeHtml(note.content)}
Location: (${note.lat}, ${note.lng}) ยท Created: ${new Date(note.created_at).toLocaleString()} `; } 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 = '

Error loading notes.

'; } } 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 = '

Select a notepad to see its notes.

'; } 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, "'"); }