251 lines
12 KiB
PHP
251 lines
12 KiB
PHP
<?php
|
|
// ─────────────────────────────────────────────
|
|
// admin.php · Admin Panel
|
|
// ─────────────────────────────────────────────
|
|
require_once 'db.php';
|
|
require_once 'auth.php';
|
|
requireAdmin();
|
|
require_once 'nav.php';
|
|
|
|
renderHead('Admin Panel');
|
|
renderTopBar('Admin Panel');
|
|
?>
|
|
<div class="page">
|
|
|
|
<!-- User stats bar -->
|
|
<div class="summary-row" id="statsRow" style="margin-top:8px">
|
|
<div class="summary-chip"><div class="chip-label">Total Users</div><div class="chip-val" id="statTotal">—</div></div>
|
|
<div class="summary-chip"><div class="chip-label">Admins</div><div class="chip-val income" id="statAdmins">—</div></div>
|
|
<div class="summary-chip"><div class="chip-label">Suspended</div><div class="chip-val expense" id="statSuspended">—</div></div>
|
|
<div class="summary-chip"><div class="chip-label">Active</div><div class="chip-val" id="statActive">—</div></div>
|
|
</div>
|
|
|
|
<!-- User list -->
|
|
<div class="section-label">Users</div>
|
|
<div class="card" id="userList">
|
|
<div class="empty"><div class="empty-icon">⏳</div><p>Loading…</p></div>
|
|
</div>
|
|
|
|
<!-- System Settings -->
|
|
<div class="section-label">System Settings</div>
|
|
<div class="card" style="padding:16px 16px 20px">
|
|
<div class="form-group">
|
|
<label>Site Name</label>
|
|
<input type="text" id="siteName" placeholder="Finance Planner">
|
|
</div>
|
|
<div class="form-group" style="display:flex;align-items:center;gap:10px;margin-bottom:0">
|
|
<input type="checkbox" id="regEnabled"
|
|
style="width:18px;height:18px;accent-color:var(--accent);cursor:pointer;flex-shrink:0">
|
|
<label for="regEnabled" style="cursor:pointer;font-size:.88rem;color:var(--text)">
|
|
Allow new user registration
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Donation Settings -->
|
|
<div class="section-label">Donation Settings</div>
|
|
<div class="card" style="padding:16px 16px 20px">
|
|
<p style="font-size:.8rem;color:var(--text-dim);margin-bottom:14px">
|
|
Configure donation links shown to all users on the Donate page and Settings. Leave blank to hide.
|
|
</p>
|
|
<div class="form-group">
|
|
<label>Message shown to users</label>
|
|
<input type="text" id="donationMsg"
|
|
placeholder="If you find this app useful, consider buying me a coffee!">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>💳 PayPal link</label>
|
|
<input type="url" id="paypalLink" placeholder="https://paypal.me/yourname">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>☕ Ko-fi link</label>
|
|
<input type="url" id="kofiLink" placeholder="https://ko-fi.com/yourname">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>☕ Buy Me a Coffee link</label>
|
|
<input type="url" id="bmcLink" placeholder="https://buymeacoffee.com/yourname">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="saveSettings()">Save All Settings</button>
|
|
<span id="settingsMsg" style="margin-left:10px;font-size:.82rem"></span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Reset Password Modal -->
|
|
<div class="modal-backdrop" id="resetModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h2>Reset Password</h2>
|
|
<button class="modal-close" onclick="closeResetModal()">✕</button>
|
|
</div>
|
|
<input type="hidden" id="resetUserId">
|
|
<p id="resetUserLabel" style="color:var(--text-dim);font-size:.85rem;margin-bottom:16px"></p>
|
|
<div class="form-group">
|
|
<label>New Password</label>
|
|
<input type="password" id="resetPw" placeholder="Minimum 6 characters">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Confirm Password</label>
|
|
<input type="password" id="resetPwConfirm" placeholder="Repeat password">
|
|
</div>
|
|
<button class="btn btn-primary btn-full" onclick="doReset()">Set New Password</button>
|
|
<div id="resetMsg" style="margin-top:10px;font-size:.85rem;text-align:center"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php renderNav('admin'); ?>
|
|
|
|
<style>
|
|
.badge { display:inline-block;padding:2px 8px;border-radius:20px;font-size:.65rem;font-weight:700;vertical-align:middle; }
|
|
.badge-admin { background:rgba(58,180,242,.18);color:var(--accent);border:1px solid rgba(58,180,242,.3); }
|
|
.badge-suspended { background:rgba(248,113,113,.18);color:var(--red);border:1px solid rgba(248,113,113,.3); }
|
|
.user-actions { display:flex;gap:6px;flex-wrap:wrap;margin-top:6px; }
|
|
.user-actions .btn { padding:5px 10px;font-size:.75rem; }
|
|
@media(min-width:600px){ .user-actions { margin-top:0; } }
|
|
</style>
|
|
|
|
<script>
|
|
async function api(action, method='GET', body=null) {
|
|
const url = `api.php?action=${action}`;
|
|
const opt = { method };
|
|
if (body) { opt.headers={'Content-Type':'application/x-www-form-urlencoded'}; opt.body=body; }
|
|
const r = await fetch(url, opt);
|
|
return r.json();
|
|
}
|
|
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
function fmtDate(d) {
|
|
return new Date(d).toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'});
|
|
}
|
|
|
|
// ── Load users ──────────────────────────────
|
|
async function loadUsers() {
|
|
const res = await api('admin_list_users');
|
|
const el = document.getElementById('userList');
|
|
if (!res.ok || !res.data.length) {
|
|
el.innerHTML = '<div class="empty"><div class="empty-icon">👥</div><p>No users found.</p></div>';
|
|
return;
|
|
}
|
|
|
|
// Stats
|
|
const total = res.data.length;
|
|
const admins = res.data.filter(u => u.is_admin == 1).length;
|
|
const suspended = res.data.filter(u => u.is_suspended == 1).length;
|
|
document.getElementById('statTotal').textContent = total;
|
|
document.getElementById('statAdmins').textContent = admins;
|
|
document.getElementById('statSuspended').textContent = suspended;
|
|
document.getElementById('statActive').textContent = total - suspended;
|
|
|
|
const myId = <?= (int)currentUserId() ?>;
|
|
|
|
el.innerHTML = res.data.map(u => {
|
|
const isMe = u.id == myId;
|
|
const badges = [];
|
|
if (u.is_admin == 1) badges.push('<span class="badge badge-admin">Admin</span>');
|
|
if (u.is_suspended == 1) badges.push('<span class="badge badge-suspended">Suspended</span>');
|
|
|
|
return `<div class="card-row" style="flex-wrap:wrap;align-items:flex-start;gap:6px">
|
|
<div style="flex:1;min-width:180px">
|
|
<div class="row-label">
|
|
${escHtml(u.display_name || u.username)}
|
|
${badges.join(' ')}
|
|
${isMe ? '<span style="color:var(--text-muted);font-size:.7rem"> (you)</span>' : ''}
|
|
</div>
|
|
<div class="row-sub">
|
|
@${escHtml(u.username)} · Joined ${fmtDate(u.created_at)}
|
|
· ${u.account_count} account(s)
|
|
</div>
|
|
</div>
|
|
<div class="user-actions">
|
|
<button class="btn btn-ghost" onclick="openReset(${u.id},'${escHtml(u.username)}')">🔑 Reset PW</button>
|
|
${!isMe ? `<button class="btn btn-ghost" onclick="toggleAdmin(${u.id})">
|
|
${u.is_admin == 1 ? '⬇️ Revoke Admin' : '⬆️ Make Admin'}
|
|
</button>` : ''}
|
|
${!isMe ? `<button class="btn btn-ghost" onclick="toggleSuspend(${u.id})">
|
|
${u.is_suspended == 1 ? '✅ Unsuspend' : '🚫 Suspend'}
|
|
</button>` : ''}
|
|
${!isMe && u.is_admin != 1 ? `<button class="btn btn-danger" onclick="deleteUser(${u.id},'${escHtml(u.username)}')">🗑 Delete</button>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function toggleAdmin(id) {
|
|
if (!confirm('Toggle admin status for this user?')) return;
|
|
const res = await api('admin_toggle_admin', 'POST', new URLSearchParams({id}).toString());
|
|
if (res.ok) loadUsers(); else alert(res.error || 'Failed');
|
|
}
|
|
|
|
async function toggleSuspend(id) {
|
|
if (!confirm('Toggle suspension for this user?')) return;
|
|
const res = await api('admin_toggle_suspend', 'POST', new URLSearchParams({id}).toString());
|
|
if (res.ok) loadUsers(); else alert(res.error || 'Failed');
|
|
}
|
|
|
|
async function deleteUser(id, username) {
|
|
if (!confirm(`Permanently delete @${username} and ALL their data? This cannot be undone.`)) return;
|
|
const res = await api('admin_delete_user', 'POST', new URLSearchParams({id}).toString());
|
|
if (res.ok) loadUsers(); else alert(res.error || 'Failed');
|
|
}
|
|
|
|
// ── Reset password modal ─────────────────────
|
|
function openReset(id, username) {
|
|
document.getElementById('resetUserId').value = id;
|
|
document.getElementById('resetUserLabel').textContent = `Setting a new password for @${username}`;
|
|
document.getElementById('resetPw').value = '';
|
|
document.getElementById('resetPwConfirm').value = '';
|
|
document.getElementById('resetMsg').textContent = '';
|
|
document.getElementById('resetModal').classList.add('open');
|
|
setTimeout(() => document.getElementById('resetPw').focus(), 200);
|
|
}
|
|
function closeResetModal() { document.getElementById('resetModal').classList.remove('open'); }
|
|
document.getElementById('resetModal').addEventListener('click', e => {
|
|
if (e.target === document.getElementById('resetModal')) closeResetModal();
|
|
});
|
|
|
|
async function doReset() {
|
|
const id = document.getElementById('resetUserId').value;
|
|
const pw = document.getElementById('resetPw').value;
|
|
const pw2 = document.getElementById('resetPwConfirm').value;
|
|
const el = document.getElementById('resetMsg');
|
|
if (pw.length < 6) { el.style.color='var(--red)'; el.textContent='Min. 6 characters'; return; }
|
|
if (pw !== pw2) { el.style.color='var(--red)'; el.textContent='Passwords do not match'; return; }
|
|
const res = await api('admin_reset_password', 'POST', new URLSearchParams({id, password: pw}).toString());
|
|
el.style.color = res.ok ? 'var(--green)' : 'var(--red)';
|
|
el.textContent = res.ok ? '✓ Password updated' : ('✗ ' + (res.error || 'Failed'));
|
|
if (res.ok) setTimeout(closeResetModal, 1200);
|
|
}
|
|
|
|
// ── Settings ─────────────────────────────────
|
|
async function loadSettings() {
|
|
const res = await api('admin_get_settings');
|
|
if (!res.ok) return;
|
|
const s = res.data;
|
|
document.getElementById('siteName').value = s.site_name || '';
|
|
document.getElementById('regEnabled').checked = s.registration_enabled === '1';
|
|
document.getElementById('donationMsg').value = s.donation_message || '';
|
|
document.getElementById('paypalLink').value = s.paypal_link || '';
|
|
document.getElementById('kofiLink').value = s.kofi_link || '';
|
|
document.getElementById('bmcLink').value = s.buymeacoffee_link || '';
|
|
}
|
|
|
|
async function saveSettings() {
|
|
const fd = new URLSearchParams({
|
|
site_name: document.getElementById('siteName').value.trim(),
|
|
registration_enabled: document.getElementById('regEnabled').checked ? '1' : '0',
|
|
donation_message: document.getElementById('donationMsg').value.trim(),
|
|
paypal_link: document.getElementById('paypalLink').value.trim(),
|
|
kofi_link: document.getElementById('kofiLink').value.trim(),
|
|
buymeacoffee_link: document.getElementById('bmcLink').value.trim(),
|
|
});
|
|
const res = await api('admin_save_settings', 'POST', fd.toString());
|
|
const el = document.getElementById('settingsMsg');
|
|
el.style.color = res.ok ? 'var(--green)' : 'var(--red)';
|
|
el.textContent = res.ok ? '✓ Settings saved' : '✗ Failed to save';
|
|
if (res.ok) setTimeout(() => el.textContent = '', 3000);
|
|
}
|
|
|
|
loadUsers();
|
|
loadSettings();
|
|
</script>
|
|
</body>
|
|
</html>
|