Compare commits
No commits in common. "2431231cc6dd757344eb8ab205c7a92241d99cad" and "02e05d664fb1622972e5e81de71e50d02be8f937" have entirely different histories.
2431231cc6
...
02e05d664f
14 changed files with 0 additions and 1681 deletions
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -1,13 +0,0 @@
|
|||
db.php
|
||||
*.log
|
||||
*.cache
|
||||
/vendor/
|
||||
*.env
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
251
admin.php
251
admin.php
|
|
@ -1,251 +0,0 @@
|
|||
<?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>
|
||||
453
api.php
453
api.php
|
|
@ -1,453 +0,0 @@
|
|||
<?php
|
||||
// ─────────────────────────────────────────────
|
||||
// api.php · JSON API — all AJAX endpoints
|
||||
// ─────────────────────────────────────────────
|
||||
require_once 'db.php';
|
||||
require_once 'auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
function respond(bool $ok, $data = null, string $error = ''): void {
|
||||
echo json_encode($ok
|
||||
? ['ok' => true, 'data' => $data]
|
||||
: ['ok' => false, 'error' => $error]
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
// All API calls require login
|
||||
if (!currentUserId()) respond(false, null, 'Not authenticated');
|
||||
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||
$uid = currentUserId();
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// ── ACCOUNTS ─────────────────────────────────────────────────────────────
|
||||
if ($action === 'list_accounts') {
|
||||
$st = $db->prepare("SELECT * FROM accounts WHERE user_id=? ORDER BY name");
|
||||
$st->execute([$uid]);
|
||||
respond(true, $st->fetchAll());
|
||||
}
|
||||
|
||||
if ($action === 'add_account') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$currency = strtoupper(trim($_POST['currency'] ?? 'GBP'));
|
||||
if (!$name) respond(false, null, 'Name required');
|
||||
$st = $db->prepare("INSERT INTO accounts (user_id, name, currency) VALUES (?,?,?)");
|
||||
$st->execute([$uid, $name, $currency]);
|
||||
respond(true, ['id' => $db->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'delete_account') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$db->prepare("DELETE FROM accounts WHERE id=? AND user_id=?")->execute([$id, $uid]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
// ── RECURRING TRANSACTIONS ────────────────────────────────────────────────
|
||||
if ($action === 'list_transactions') {
|
||||
$aid = (int)($_GET['account_id'] ?? 0);
|
||||
if ($aid) {
|
||||
$st = $db->prepare("SELECT t.*, a.currency FROM transactions t
|
||||
JOIN accounts a ON a.id=t.account_id
|
||||
WHERE t.account_id=? AND a.user_id=? ORDER BY t.name");
|
||||
$st->execute([$aid, $uid]);
|
||||
} else {
|
||||
$st = $db->prepare("SELECT t.*, a.currency FROM transactions t
|
||||
JOIN accounts a ON a.id=t.account_id WHERE a.user_id=? ORDER BY t.name");
|
||||
$st->execute([$uid]);
|
||||
}
|
||||
respond(true, $st->fetchAll());
|
||||
}
|
||||
|
||||
if ($action === 'add_transaction') {
|
||||
$st = $db->prepare("INSERT INTO transactions
|
||||
(account_id,name,type,amount,start_date,frequency,interval_value,ends_on,notes,tags)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)");
|
||||
$st->execute([
|
||||
(int)$_POST['account_id'],
|
||||
trim($_POST['name']),
|
||||
$_POST['type'],
|
||||
(float)$_POST['amount'],
|
||||
$_POST['start_date'],
|
||||
$_POST['frequency'],
|
||||
(int)($_POST['interval_value'] ?? 1),
|
||||
$_POST['ends_on'] ?: null,
|
||||
trim($_POST['notes'] ?? '') ?: null,
|
||||
trim($_POST['tags'] ?? '') ?: null,
|
||||
]);
|
||||
respond(true, ['id' => $db->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'get_transaction') {
|
||||
$st = $db->prepare("SELECT t.* FROM transactions t
|
||||
JOIN accounts a ON a.id=t.account_id WHERE t.id=? AND a.user_id=?");
|
||||
$st->execute([(int)($_GET['id']??0), $uid]);
|
||||
respond(true, $st->fetch());
|
||||
}
|
||||
|
||||
if ($action === 'update_transaction') {
|
||||
$st = $db->prepare("UPDATE transactions SET
|
||||
name=?,type=?,amount=?,start_date=?,frequency=?,interval_value=?,ends_on=?,notes=?,tags=?
|
||||
WHERE id=?");
|
||||
$st->execute([
|
||||
trim($_POST['name']),
|
||||
$_POST['type'],
|
||||
(float)$_POST['amount'],
|
||||
$_POST['start_date'],
|
||||
$_POST['frequency'],
|
||||
(int)($_POST['interval_value'] ?? 1),
|
||||
$_POST['ends_on'] ?: null,
|
||||
trim($_POST['notes'] ?? '') ?: null,
|
||||
trim($_POST['tags'] ?? '') ?: null,
|
||||
(int)$_POST['id'],
|
||||
]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
if ($action === 'delete_transaction') {
|
||||
$db->prepare("DELETE FROM transactions WHERE id=?")->execute([(int)($_POST['id']??0)]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
// ── BALANCES ─────────────────────────────────────────────────────────────
|
||||
if ($action === 'list_balances') {
|
||||
$aid = (int)($_GET['account_id'] ?? 0);
|
||||
if ($aid) {
|
||||
$st = $db->prepare("SELECT b.*,a.name AS account_name,a.currency FROM balances b
|
||||
JOIN accounts a ON a.id=b.account_id WHERE b.account_id=? AND a.user_id=?
|
||||
ORDER BY b.recorded_on DESC");
|
||||
$st->execute([$aid, $uid]);
|
||||
} else {
|
||||
$st = $db->prepare("SELECT b.*,a.name AS account_name,a.currency FROM balances b
|
||||
JOIN accounts a ON a.id=b.account_id WHERE a.user_id=?
|
||||
ORDER BY b.recorded_on DESC");
|
||||
$st->execute([$uid]);
|
||||
}
|
||||
respond(true, $st->fetchAll());
|
||||
}
|
||||
|
||||
if ($action === 'add_balance') {
|
||||
$st = $db->prepare("INSERT INTO balances (account_id,amount,type,recorded_on,notes) VALUES (?,?,?,?,?)");
|
||||
$st->execute([
|
||||
(int)$_POST['account_id'],
|
||||
(float)$_POST['amount'],
|
||||
$_POST['type'],
|
||||
$_POST['recorded_on'],
|
||||
trim($_POST['notes'] ?? '') ?: null,
|
||||
]);
|
||||
respond(true, ['id' => $db->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'delete_balance') {
|
||||
$db->prepare("DELETE FROM balances WHERE id=?")->execute([(int)($_POST['id']??0)]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
// ── ONE-OFF TRANSACTIONS ──────────────────────────────────────────────────
|
||||
if ($action === 'list_oneoff') {
|
||||
$aid = (int)($_GET['account_id'] ?? 0);
|
||||
$limit = (int)($_GET['limit'] ?? 100);
|
||||
if ($aid) {
|
||||
$st = $db->prepare("SELECT o.*,a.currency FROM oneoff_transactions o
|
||||
JOIN accounts a ON a.id=o.account_id
|
||||
WHERE o.account_id=? AND a.user_id=? ORDER BY o.date DESC LIMIT $limit");
|
||||
$st->execute([$aid, $uid]);
|
||||
} else {
|
||||
$st = $db->prepare("SELECT o.*,a.name AS account_name,a.currency FROM oneoff_transactions o
|
||||
JOIN accounts a ON a.id=o.account_id WHERE a.user_id=? ORDER BY o.date DESC LIMIT $limit");
|
||||
$st->execute([$uid]);
|
||||
}
|
||||
respond(true, $st->fetchAll());
|
||||
}
|
||||
|
||||
if ($action === 'add_oneoff') {
|
||||
$st = $db->prepare("INSERT INTO oneoff_transactions
|
||||
(account_id,name,type,amount,date,notes,tags) VALUES (?,?,?,?,?,?,?)");
|
||||
$st->execute([
|
||||
(int)$_POST['account_id'],
|
||||
trim($_POST['name']),
|
||||
$_POST['type'],
|
||||
(float)$_POST['amount'],
|
||||
$_POST['date'],
|
||||
trim($_POST['notes'] ?? '') ?: null,
|
||||
trim($_POST['tags'] ?? '') ?: null,
|
||||
]);
|
||||
respond(true, ['id' => $db->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'get_oneoff') {
|
||||
$st = $db->prepare("SELECT o.* FROM oneoff_transactions o
|
||||
JOIN accounts a ON a.id=o.account_id WHERE o.id=? AND a.user_id=?");
|
||||
$st->execute([(int)($_GET['id']??0), $uid]);
|
||||
respond(true, $st->fetch());
|
||||
}
|
||||
|
||||
if ($action === 'update_oneoff') {
|
||||
$st = $db->prepare("UPDATE oneoff_transactions SET
|
||||
name=?,type=?,amount=?,date=?,notes=?,tags=? WHERE id=?");
|
||||
$st->execute([
|
||||
trim($_POST['name']),
|
||||
$_POST['type'],
|
||||
(float)$_POST['amount'],
|
||||
$_POST['date'],
|
||||
trim($_POST['notes'] ?? '') ?: null,
|
||||
trim($_POST['tags'] ?? '') ?: null,
|
||||
(int)$_POST['id'],
|
||||
]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
if ($action === 'delete_oneoff') {
|
||||
$db->prepare("DELETE FROM oneoff_transactions WHERE id=?")->execute([(int)($_POST['id']??0)]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
// ── DEBTS ─────────────────────────────────────────────────────────────────
|
||||
if ($action === 'list_debts') {
|
||||
$st = $db->prepare("SELECT * FROM debts WHERE user_id=? ORDER BY balance DESC");
|
||||
$st->execute([$uid]);
|
||||
respond(true, $st->fetchAll());
|
||||
}
|
||||
|
||||
if ($action === 'add_debt') {
|
||||
$st = $db->prepare("INSERT INTO debts
|
||||
(user_id,name,type,balance,interest_rate,min_payment,due_day,credit_limit,notes,tags,opened_on,currency)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$st->execute([
|
||||
$uid,
|
||||
trim($_POST['name']),
|
||||
$_POST['dtype'],
|
||||
(float)$_POST['balance'],
|
||||
(float)($_POST['interest_rate'] ?? 0),
|
||||
(float)($_POST['min_payment'] ?? 0),
|
||||
$_POST['due_day'] ? (int)$_POST['due_day'] : null,
|
||||
$_POST['credit_limit'] ? (float)$_POST['credit_limit'] : null,
|
||||
trim($_POST['notes'] ?? '') ?: null,
|
||||
trim($_POST['tags'] ?? '') ?: null,
|
||||
$_POST['opened_on'] ?: null,
|
||||
strtoupper(trim($_POST['currency'] ?? 'GBP')),
|
||||
]);
|
||||
respond(true, ['id' => $db->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'get_debt') {
|
||||
$st = $db->prepare("SELECT * FROM debts WHERE id=? AND user_id=?");
|
||||
$st->execute([(int)($_GET['id']??0), $uid]);
|
||||
respond(true, $st->fetch());
|
||||
}
|
||||
|
||||
if ($action === 'update_debt') {
|
||||
$st = $db->prepare("UPDATE debts SET
|
||||
name=?,type=?,balance=?,interest_rate=?,min_payment=?,due_day=?,
|
||||
credit_limit=?,notes=?,tags=?,opened_on=?,currency=?
|
||||
WHERE id=? AND user_id=?");
|
||||
$st->execute([
|
||||
trim($_POST['name']),
|
||||
$_POST['dtype'],
|
||||
(float)$_POST['balance'],
|
||||
(float)($_POST['interest_rate'] ?? 0),
|
||||
(float)($_POST['min_payment'] ?? 0),
|
||||
$_POST['due_day'] ? (int)$_POST['due_day'] : null,
|
||||
$_POST['credit_limit'] ? (float)$_POST['credit_limit'] : null,
|
||||
trim($_POST['notes'] ?? '') ?: null,
|
||||
trim($_POST['tags'] ?? '') ?: null,
|
||||
$_POST['opened_on'] ?: null,
|
||||
strtoupper(trim($_POST['currency'] ?? 'GBP')),
|
||||
(int)$_POST['id'],
|
||||
$uid,
|
||||
]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
if ($action === 'delete_debt') {
|
||||
$db->prepare("DELETE FROM debts WHERE id=? AND user_id=?")->execute([(int)($_POST['id']??0), $uid]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
// ── TRENDS ────────────────────────────────────────────────────────────────
|
||||
if ($action === 'trends') {
|
||||
$aid = (int)($_GET['account_id'] ?? 0);
|
||||
$start = new DateTime('first day of this month');
|
||||
$end = (clone $start)->modify('+12 months');
|
||||
|
||||
if ($aid) {
|
||||
$balSt = $db->prepare("SELECT amount, type FROM balances WHERE account_id=? ORDER BY recorded_on DESC LIMIT 1");
|
||||
$balSt->execute([$aid]);
|
||||
} else {
|
||||
$balSt = $db->prepare("SELECT b.amount,b.type FROM balances b
|
||||
JOIN accounts a ON a.id=b.account_id WHERE a.user_id=? ORDER BY b.recorded_on DESC LIMIT 1");
|
||||
$balSt->execute([$uid]);
|
||||
}
|
||||
$balRow = $balSt->fetch();
|
||||
$runningBalance = $balRow
|
||||
? ($balRow['type'] === 'negative' ? -(float)$balRow['amount'] : (float)$balRow['amount'])
|
||||
: 0.0;
|
||||
|
||||
if ($aid) {
|
||||
$txSt = $db->prepare("SELECT t.* FROM transactions t
|
||||
JOIN accounts a ON a.id=t.account_id WHERE t.account_id=? AND a.user_id=?");
|
||||
$txSt->execute([$aid, $uid]);
|
||||
} else {
|
||||
$txSt = $db->prepare("SELECT t.* FROM transactions t
|
||||
JOIN accounts a ON a.id=t.account_id WHERE a.user_id=?");
|
||||
$txSt->execute([$uid]);
|
||||
}
|
||||
$txs = $txSt->fetchAll();
|
||||
|
||||
$points = [];
|
||||
$cursor = clone $start;
|
||||
while ($cursor <= $end) {
|
||||
foreach ($txs as $tx) {
|
||||
if (occursOn($tx, $cursor)) {
|
||||
$signed = $tx['type'] === 'income' ? (float)$tx['amount'] : -(float)$tx['amount'];
|
||||
$runningBalance += $signed;
|
||||
}
|
||||
}
|
||||
$points[] = ['date' => $cursor->format('M Y'), 'balance' => round($runningBalance, 2)];
|
||||
$cursor->modify('+1 month');
|
||||
}
|
||||
respond(true, $points);
|
||||
}
|
||||
|
||||
// ── SUMMARY ───────────────────────────────────────────────────────────────
|
||||
if ($action === 'summary') {
|
||||
$aid = (int)($_GET['account_id'] ?? 0);
|
||||
if ($aid) {
|
||||
$iSt = $db->prepare("SELECT COALESCE(SUM(amount),0) FROM transactions WHERE account_id=? AND type='income'");
|
||||
$iSt->execute([$aid]);
|
||||
$eSt = $db->prepare("SELECT COALESCE(SUM(amount),0) FROM transactions WHERE account_id=? AND type='expense'");
|
||||
$eSt->execute([$aid]);
|
||||
} else {
|
||||
$iSt = $db->prepare("SELECT COALESCE(SUM(t.amount),0) FROM transactions t
|
||||
JOIN accounts a ON a.id=t.account_id WHERE a.user_id=? AND t.type='income'");
|
||||
$iSt->execute([$uid]);
|
||||
$eSt = $db->prepare("SELECT COALESCE(SUM(t.amount),0) FROM transactions t
|
||||
JOIN accounts a ON a.id=t.account_id WHERE a.user_id=? AND t.type='expense'");
|
||||
$eSt->execute([$uid]);
|
||||
}
|
||||
$dSt = $db->prepare("SELECT COALESCE(SUM(balance),0) FROM debts WHERE user_id=?");
|
||||
$dSt->execute([$uid]);
|
||||
|
||||
respond(true, [
|
||||
'monthly_income' => (float)$iSt->fetchColumn(),
|
||||
'monthly_expense' => (float)$eSt->fetchColumn(),
|
||||
'total_debt' => (float)$dSt->fetchColumn(),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── USER SETTINGS ─────────────────────────────────────────────────────────
|
||||
if ($action === 'change_password') {
|
||||
$ok = changePassword($uid, $_POST['current'] ?? '', $_POST['new'] ?? '');
|
||||
$ok ? respond(true) : respond(false, null, 'Current password is incorrect.');
|
||||
}
|
||||
|
||||
if ($action === 'update_profile') {
|
||||
$dn = trim($_POST['display_name'] ?? '');
|
||||
$db->prepare("UPDATE users SET display_name=? WHERE id=?")->execute([$dn, $uid]);
|
||||
$_SESSION['user']['display_name'] = $dn;
|
||||
respond(true);
|
||||
}
|
||||
|
||||
// ── ADMIN ─────────────────────────────────────────────────────────────────
|
||||
// All admin actions re-verify admin status from DB for security
|
||||
|
||||
if ($action === 'admin_list_users') {
|
||||
if (!isAdmin()) respond(false, null, 'Forbidden');
|
||||
$st = $db->query("
|
||||
SELECT u.id, u.username, u.display_name, u.is_admin, u.is_suspended, u.created_at,
|
||||
(SELECT COUNT(*) FROM accounts WHERE user_id = u.id) AS account_count
|
||||
FROM users u
|
||||
ORDER BY u.created_at ASC
|
||||
");
|
||||
respond(true, $st->fetchAll());
|
||||
}
|
||||
|
||||
if ($action === 'admin_toggle_admin') {
|
||||
if (!isAdmin()) respond(false, null, 'Forbidden');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id === $uid) respond(false, null, 'You cannot change your own admin status.');
|
||||
$db->prepare("UPDATE users SET is_admin = 1 - is_admin WHERE id = ?")->execute([$id]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
if ($action === 'admin_toggle_suspend') {
|
||||
if (!isAdmin()) respond(false, null, 'Forbidden');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id === $uid) respond(false, null, 'You cannot suspend yourself.');
|
||||
$db->prepare("UPDATE users SET is_suspended = 1 - is_suspended WHERE id = ?")->execute([$id]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
if ($action === 'admin_delete_user') {
|
||||
if (!isAdmin()) respond(false, null, 'Forbidden');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id === $uid) respond(false, null, 'You cannot delete your own account.');
|
||||
// Safety: never delete another admin
|
||||
$st = $db->prepare("SELECT is_admin FROM users WHERE id = ?");
|
||||
$st->execute([$id]);
|
||||
$target = $st->fetch();
|
||||
if (!$target) respond(false, null, 'User not found.');
|
||||
if ($target['is_admin']) respond(false, null, 'Cannot delete another admin. Revoke their admin status first.');
|
||||
$db->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
if ($action === 'admin_reset_password') {
|
||||
if (!isAdmin()) respond(false, null, 'Forbidden');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$pw = $_POST['password'] ?? '';
|
||||
if (strlen($pw) < 6) respond(false, null, 'Password must be at least 6 characters.');
|
||||
$db->prepare("UPDATE users SET password_hash = ? WHERE id = ?")
|
||||
->execute([password_hash($pw, PASSWORD_DEFAULT), $id]);
|
||||
respond(true);
|
||||
}
|
||||
|
||||
if ($action === 'admin_get_settings') {
|
||||
if (!isAdmin()) respond(false, null, 'Forbidden');
|
||||
$st = $db->query("SELECT setting_key, setting_value FROM settings");
|
||||
$out = [];
|
||||
foreach ($st->fetchAll() as $row) {
|
||||
$out[$row['setting_key']] = $row['setting_value'];
|
||||
}
|
||||
respond(true, $out);
|
||||
}
|
||||
|
||||
if ($action === 'admin_save_settings') {
|
||||
if (!isAdmin()) respond(false, null, 'Forbidden');
|
||||
$allowed = ['site_name','registration_enabled','donation_message','paypal_link','kofi_link','buymeacoffee_link'];
|
||||
$st = $db->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?,?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
|
||||
foreach ($allowed as $k) {
|
||||
$st->execute([$k, trim($_POST[$k] ?? '')]);
|
||||
}
|
||||
respond(true);
|
||||
}
|
||||
|
||||
respond(false, null, "Unknown action: $action");
|
||||
|
||||
} catch (Exception $e) {
|
||||
respond(false, null, $e->getMessage());
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
function occursOn(array $tx, DateTime $date): bool {
|
||||
$start = new DateTime($tx['start_date']);
|
||||
if ($date < $start) return false;
|
||||
if ($tx['ends_on'] && $date > new DateTime($tx['ends_on'])) return false;
|
||||
$freq = $tx['frequency'];
|
||||
$iv = (int)$tx['interval_value'];
|
||||
if ($freq === 'monthly') {
|
||||
return $date->format('d') === $start->format('d')
|
||||
&& (($date->format('Ym') - $start->format('Ym')) % $iv === 0);
|
||||
}
|
||||
if ($freq === 'weekly') {
|
||||
return ((int)$start->diff($date)->days % (7 * $iv)) === 0;
|
||||
}
|
||||
if ($freq === 'yearly') {
|
||||
return $date->format('m-d') === $start->format('m-d');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
113
auth.php
113
auth.php
|
|
@ -1,113 +0,0 @@
|
|||
<?php
|
||||
// ─────────────────────────────────────────────
|
||||
// auth.php · Session-based authentication
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function currentUserId(): ?int {
|
||||
return isset($_SESSION['user']['id']) ? (int)$_SESSION['user']['id'] : null;
|
||||
}
|
||||
|
||||
function currentUser(): ?array {
|
||||
return $_SESSION['user'] ?? null;
|
||||
}
|
||||
|
||||
function isAdmin(): bool {
|
||||
return (int)($_SESSION['user']['is_admin'] ?? 0) === 1;
|
||||
}
|
||||
|
||||
function requireLogin(): void {
|
||||
if (!currentUserId()) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin(): void {
|
||||
requireLogin();
|
||||
if (!isAdmin()) {
|
||||
http_response_code(403);
|
||||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
|
||||
. '<title>Access Denied · Finance Planner</title>'
|
||||
. '<link rel="stylesheet" href="style.css"></head>'
|
||||
. '<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px">'
|
||||
. '<div style="text-align:center">'
|
||||
. '<div style="font-size:3rem;margin-bottom:12px">🚫</div>'
|
||||
. '<h2 style="margin-bottom:8px">Access Denied</h2>'
|
||||
. '<p style="color:var(--text-dim);margin-bottom:20px">Admin privileges required.</p>'
|
||||
. '<a href="index.php" style="color:var(--accent)">← Back to Dashboard</a>'
|
||||
. '</div></body></html>';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true on success, 'suspended' if account suspended, false on bad credentials.
|
||||
*/
|
||||
function attemptLogin(string $username, string $password): bool|string {
|
||||
require_once __DIR__ . '/db.php';
|
||||
$db = getDB();
|
||||
$st = $db->prepare("SELECT * FROM users WHERE username = ? LIMIT 1");
|
||||
$st->execute([trim($username)]);
|
||||
$user = $st->fetch();
|
||||
|
||||
if (!$user) return false;
|
||||
if ((int)($user['is_suspended'] ?? 0) === 1) return 'suspended';
|
||||
if (!password_verify($password, $user['password_hash'])) return false;
|
||||
|
||||
$_SESSION['user'] = [
|
||||
'id' => (int)$user['id'],
|
||||
'username' => $user['username'],
|
||||
'display_name' => $user['display_name'] ?? $user['username'],
|
||||
'is_admin' => (int)($user['is_admin'] ?? 0),
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
function logout(): void {
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$p['path'], $p['domain'], $p['secure'], $p['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
function changePassword(int $userId, string $currentPassword, string $newPassword): bool {
|
||||
require_once __DIR__ . '/db.php';
|
||||
$db = getDB();
|
||||
$st = $db->prepare("SELECT password_hash FROM users WHERE id = ?");
|
||||
$st->execute([$userId]);
|
||||
$row = $st->fetch();
|
||||
if (!$row || !password_verify($currentPassword, $row['password_hash'])) return false;
|
||||
$db->prepare("UPDATE users SET password_hash = ? WHERE id = ?")
|
||||
->execute([password_hash($newPassword, PASSWORD_DEFAULT), $userId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── App settings (stored in `settings` table) ─────────────────────────────
|
||||
|
||||
function getSetting(string $key, string $default = ''): string {
|
||||
try {
|
||||
require_once __DIR__ . '/db.php';
|
||||
$db = getDB();
|
||||
$st = $db->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
|
||||
$st->execute([$key]);
|
||||
$row = $st->fetch();
|
||||
return $row ? $row['setting_value'] : $default;
|
||||
} catch (Exception $e) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
function setSetting(string $key, string $value): void {
|
||||
require_once __DIR__ . '/db.php';
|
||||
$db = getDB();
|
||||
$db->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?,?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")
|
||||
->execute([$key, $value]);
|
||||
}
|
||||
95
donate.php
95
donate.php
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
// ─────────────────────────────────────────────
|
||||
// donate.php · Support / Donations
|
||||
// ─────────────────────────────────────────────
|
||||
require_once 'db.php';
|
||||
require_once 'auth.php';
|
||||
requireLogin();
|
||||
require_once 'nav.php';
|
||||
|
||||
$siteName = getSetting('site_name', 'Finance Planner');
|
||||
$donMsg = getSetting('donation_message', 'If you find this app useful, please consider supporting its development!');
|
||||
$paypalLink = getSetting('paypal_link', '');
|
||||
$kofiLink = getSetting('kofi_link', '');
|
||||
$bmcLink = getSetting('buymeacoffee_link', '');
|
||||
$hasAny = $paypalLink || $kofiLink || $bmcLink;
|
||||
|
||||
renderHead('Donate');
|
||||
renderTopBar('Support Us');
|
||||
?>
|
||||
<div class="page">
|
||||
|
||||
<div style="text-align:center;padding:28px 16px 20px">
|
||||
<div style="font-size:3rem;margin-bottom:12px">❤️</div>
|
||||
<h2 style="font-size:1.1rem;margin-bottom:10px">Support <?= htmlspecialchars($siteName) ?></h2>
|
||||
<p style="color:var(--text-dim);font-size:.88rem;max-width:340px;margin:0 auto;line-height:1.6">
|
||||
<?= htmlspecialchars($donMsg) ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if (!$hasAny): ?>
|
||||
<div class="card" style="padding:32px;text-align:center">
|
||||
<div style="font-size:2rem;margin-bottom:10px">🔧</div>
|
||||
<p style="color:var(--text-muted);font-size:.85rem">
|
||||
No donation options have been set up yet.<br>
|
||||
<?php if (isAdmin()): ?>
|
||||
<a href="admin.php" style="color:var(--accent)">Configure them in the Admin Panel →</a>
|
||||
<?php else: ?>
|
||||
Check back soon!
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="section-label">Choose a Platform</div>
|
||||
<div class="card">
|
||||
|
||||
<?php if ($paypalLink): ?>
|
||||
<div class="card-row">
|
||||
<div style="font-size:1.8rem;margin-right:14px;width:36px;text-align:center">💳</div>
|
||||
<div style="flex:1">
|
||||
<div class="row-label">PayPal</div>
|
||||
<div class="row-sub">Fast, secure, widely accepted</div>
|
||||
</div>
|
||||
<a href="<?= htmlspecialchars($paypalLink) ?>" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary" style="text-decoration:none;flex-shrink:0">Donate</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($kofiLink): ?>
|
||||
<div class="card-row">
|
||||
<div style="font-size:1.8rem;margin-right:14px;width:36px;text-align:center">☕</div>
|
||||
<div style="flex:1">
|
||||
<div class="row-label">Ko-fi</div>
|
||||
<div class="row-sub">Buy me a coffee, no account needed</div>
|
||||
</div>
|
||||
<a href="<?= htmlspecialchars($kofiLink) ?>" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary" style="text-decoration:none;flex-shrink:0">Donate</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($bmcLink): ?>
|
||||
<div class="card-row">
|
||||
<div style="font-size:1.8rem;margin-right:14px;width:36px;text-align:center">☕</div>
|
||||
<div style="flex:1">
|
||||
<div class="row-label">Buy Me a Coffee</div>
|
||||
<div class="row-sub">One-time or monthly support</div>
|
||||
</div>
|
||||
<a href="<?= htmlspecialchars($bmcLink) ?>" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary" style="text-decoration:none;flex-shrink:0">Donate</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<p style="text-align:center;color:var(--text-muted);font-size:.78rem;padding:20px 0">
|
||||
Every donation, however small, keeps this project going. Thank you! ❤️
|
||||
</p>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php renderNav('settings'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ─────────────────────────────────────────────
|
||||
# git-cron-setup.sh · Optional: auto-sync
|
||||
# on a schedule using cron.
|
||||
#
|
||||
# Adds a cron job that runs git-sync.sh
|
||||
# every night at 2am.
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SYNC_SCRIPT="$SCRIPT_DIR/git-sync.sh"
|
||||
LOG_FILE="$SCRIPT_DIR/git-sync.log"
|
||||
CRON_SCHEDULE="0 2 * * *" # 2:00am daily — change if you like
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Auto-Sync Cron Setup ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check the sync script exists
|
||||
if [ ! -f "$SYNC_SCRIPT" ]; then
|
||||
echo "❌ $SYNC_SCRIPT not found. Please upload git-sync.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$SYNC_SCRIPT"
|
||||
|
||||
CRON_LINE="$CRON_SCHEDULE $SYNC_SCRIPT >> $LOG_FILE 2>&1"
|
||||
|
||||
# Don't add duplicate
|
||||
if crontab -l 2>/dev/null | grep -qF "$SYNC_SCRIPT"; then
|
||||
echo "ℹ️ Cron job already exists — no changes made."
|
||||
else
|
||||
(crontab -l 2>/dev/null; echo "$CRON_LINE") | crontab -
|
||||
echo "✅ Cron job added:"
|
||||
echo " $CRON_LINE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Current cron jobs:"
|
||||
crontab -l
|
||||
echo ""
|
||||
echo "Logs will be written to: $LOG_FILE"
|
||||
echo ""
|
||||
echo "To remove the cron job: crontab -e (then delete the line)"
|
||||
echo "To run a sync right now: $SYNC_SCRIPT"
|
||||
echo ""
|
||||
98
git-setup.sh
98
git-setup.sh
|
|
@ -1,98 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ─────────────────────────────────────────────
|
||||
# git-setup.sh · Run ONCE to initialise the
|
||||
# local repo and connect it to Gitea.
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
set -e # stop on any error
|
||||
|
||||
REMOTE_URL="https://git.cdb-online.co.uk/Computers_Dont_Byte/FinancePlanner"
|
||||
BRANCH="main"
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Finance Planner — Git Setup ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# ── Move to the directory this script lives in
|
||||
cd "$(dirname "$0")"
|
||||
echo "📁 Working directory: $(pwd)"
|
||||
echo ""
|
||||
|
||||
# ── Ask for identity (used in commit messages)
|
||||
read -p "Your name (for git commits): " GIT_NAME
|
||||
read -p "Your email (for git commits): " GIT_EMAIL
|
||||
echo ""
|
||||
|
||||
# ── Initialise git if not already done
|
||||
if [ ! -d ".git" ]; then
|
||||
git init
|
||||
git checkout -b "$BRANCH" 2>/dev/null || git branch -m "$BRANCH"
|
||||
echo "✅ Git repository initialised."
|
||||
else
|
||||
echo "ℹ️ Git already initialised — skipping."
|
||||
fi
|
||||
|
||||
# ── Set local identity
|
||||
git config user.name "$GIT_NAME"
|
||||
git config user.email "$GIT_EMAIL"
|
||||
|
||||
# ── Set remote
|
||||
if git remote get-url origin &>/dev/null; then
|
||||
git remote set-url origin "$REMOTE_URL"
|
||||
echo "✅ Remote 'origin' updated."
|
||||
else
|
||||
git remote add origin "$REMOTE_URL"
|
||||
echo "✅ Remote 'origin' added."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Remote → $REMOTE_URL"
|
||||
echo ""
|
||||
|
||||
# ── Copy in the .gitignore if not present
|
||||
if [ ! -f ".gitignore" ]; then
|
||||
cat > .gitignore << 'GITIGNORE'
|
||||
db.php
|
||||
*.log
|
||||
*.cache
|
||||
/vendor/
|
||||
*.env
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
GITIGNORE
|
||||
echo "✅ .gitignore created (db.php is excluded to protect credentials)."
|
||||
fi
|
||||
|
||||
# ── Initial commit
|
||||
git add -A
|
||||
git status --short
|
||||
echo ""
|
||||
git commit -m "Initial commit — Finance Planner setup" || echo "ℹ️ Nothing new to commit."
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Setup complete! ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo " Option A — HTTPS (username + password / token):"
|
||||
echo " git push -u origin $BRANCH"
|
||||
echo " (You will be prompted for your Gitea credentials)"
|
||||
echo ""
|
||||
echo " Option B — SSH (no password prompts):"
|
||||
echo " ssh-keygen -t ed25519 -C \"$GIT_EMAIL\""
|
||||
echo " cat ~/.ssh/id_ed25519.pub # paste this into Gitea → Settings → SSH Keys"
|
||||
echo " git remote set-url origin git@git.cdb-online.co.uk:Computers_Dont_Byte/FinancePlanner.git"
|
||||
echo " git push -u origin $BRANCH"
|
||||
echo ""
|
||||
echo " Then run ./git-sync.sh any time you want to push changes."
|
||||
echo ""
|
||||
50
git-sync.sh
50
git-sync.sh
|
|
@ -1,50 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ─────────────────────────────────────────────
|
||||
# git-sync.sh · Commit & push all changes
|
||||
# to Gitea. Run this whenever you update files.
|
||||
#
|
||||
# Usage:
|
||||
# ./git-sync.sh (auto message)
|
||||
# ./git-sync.sh "My message" (custom message)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
set -e
|
||||
|
||||
BRANCH="main"
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# ── Check we're in a git repo
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "❌ No git repo found. Run ./git-setup.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Check there's anything to sync
|
||||
if git diff --quiet && git diff --cached --quiet && [ -z "$(git status --porcelain)" ]; then
|
||||
echo "✅ Nothing to sync — everything is already up to date."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Commit message
|
||||
if [ -n "$1" ]; then
|
||||
MSG="$1"
|
||||
else
|
||||
MSG="Sync: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📦 Files changed:"
|
||||
git status --short
|
||||
echo ""
|
||||
|
||||
git add -A
|
||||
git commit -m "$MSG"
|
||||
|
||||
echo ""
|
||||
echo "⬆️ Pushing to origin/$BRANCH …"
|
||||
git push origin "$BRANCH"
|
||||
|
||||
echo ""
|
||||
echo "✅ Synced successfully at $(date '+%H:%M:%S')"
|
||||
echo ""
|
||||
20
gitignore
20
gitignore
|
|
@ -1,20 +0,0 @@
|
|||
# ── Sensitive config ──────────────────────────
|
||||
db.php
|
||||
|
||||
# ── PHP / server ──────────────────────────────
|
||||
*.log
|
||||
*.cache
|
||||
/vendor/
|
||||
*.env
|
||||
|
||||
# ── OS junk ───────────────────────────────────
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# ── Editor files ──────────────────────────────
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
95
login.php
95
login.php
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
// ─────────────────────────────────────────────
|
||||
// login.php · Login & logout
|
||||
// ─────────────────────────────────────────────
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
// Already logged in
|
||||
if (currentUserId()) { header('Location: index.php'); exit; }
|
||||
|
||||
// Logout action
|
||||
if (($_GET['action'] ?? '') === 'logout') {
|
||||
logout();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$siteName = getSetting('site_name', 'Finance Planner');
|
||||
$registrationEnabled = getSetting('registration_enabled', '1') === '1';
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (!$username || !$password) {
|
||||
$error = 'Please enter your username and password.';
|
||||
} else {
|
||||
$result = attemptLogin($username, $password);
|
||||
if ($result === true) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} elseif ($result === 'suspended') {
|
||||
$error = 'Your account has been suspended. Please contact the administrator.';
|
||||
} else {
|
||||
$error = 'Incorrect username or password.';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login · <?= htmlspecialchars($siteName) ?></title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
.auth-wrap { display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px; }
|
||||
.auth-card {
|
||||
background:var(--surface);border:1px solid var(--border);
|
||||
border-radius:var(--radius);padding:36px 30px;max-width:380px;width:100%;
|
||||
}
|
||||
.auth-logo { font-size:2.5rem;text-align:center;margin-bottom:10px; }
|
||||
.auth-card h1 { font-size:1.2rem;text-align:center;margin-bottom:6px; }
|
||||
.auth-sub { color:var(--text-dim);font-size:.8rem;text-align:center;margin-bottom:24px; }
|
||||
.auth-footer { color:var(--text-muted);font-size:.8rem;text-align:center;margin-top:16px; }
|
||||
.auth-footer a { color:var(--accent);text-decoration:none; }
|
||||
.auth-hint { color:var(--text-muted);font-size:.75rem;text-align:center;margin-top:10px; }
|
||||
.auth-hint code { background:var(--surface2);padding:1px 5px;border-radius:4px;color:var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">💷</div>
|
||||
<h1><?= htmlspecialchars($siteName) ?></h1>
|
||||
<p class="auth-sub">Sign in to your account</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-err"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="login.php">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username"
|
||||
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
|
||||
placeholder="admin" autocomplete="username" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password"
|
||||
placeholder="••••••••" autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-full">Sign In</button>
|
||||
</form>
|
||||
|
||||
<?php if ($registrationEnabled): ?>
|
||||
<div class="auth-footer">No account? <a href="register.php">Create one →</a></div>
|
||||
<?php endif; ?>
|
||||
<p class="auth-hint">Default: <code>admin</code> / <code>admin</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
85
nav.php
85
nav.php
|
|
@ -1,85 +0,0 @@
|
|||
<?php
|
||||
// ─────────────────────────────────────────────
|
||||
// nav.php · Shared navigation partial
|
||||
// ─────────────────────────────────────────────
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
function renderHead(string $title): void {
|
||||
$site = getSetting('site_name', 'Finance Planner');
|
||||
echo <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{$title} · {$site}</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function renderTopBar(string $title, bool $addBtn = false, string $addId = ''): void {
|
||||
$user = currentUser();
|
||||
$name = htmlspecialchars($user['display_name'] ?? '');
|
||||
$btn = $addBtn ? "<button class='topbar-action' id='{$addId}' title='Add'>+</button>" : '';
|
||||
$initial = $name ? strtoupper(substr(strip_tags($name), 0, 1)) : '';
|
||||
$avatar = $initial
|
||||
? "<a href='settings.php' class='topbar-avatar' title='Settings'>{$initial}</a>"
|
||||
: '';
|
||||
echo <<<HTML
|
||||
<header class="topbar">
|
||||
{$avatar}
|
||||
<h1>{$title}</h1>
|
||||
{$btn}
|
||||
</header>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function renderNav(string $active): void {
|
||||
$pages = [
|
||||
'trends' => ['index.php', 'Trends', trendIcon()],
|
||||
'transactions' => ['transactions.php', 'Recurring', txIcon()],
|
||||
'oneoff' => ['oneoff.php', 'One-off', oneoffIcon()],
|
||||
'debts' => ['debts.php', 'Debts', debtIcon()],
|
||||
'balances' => ['balances.php', 'Balances', balIcon()],
|
||||
'accounts' => ['accounts.php', 'Accounts', acctIcon()],
|
||||
'settings' => ['settings.php', 'Settings', settingsIcon()],
|
||||
];
|
||||
if (isAdmin()) {
|
||||
$pages['admin'] = ['admin.php', 'Admin', adminIcon()];
|
||||
}
|
||||
|
||||
echo '<nav class="nav">';
|
||||
foreach ($pages as $key => [$href, $label, $icon]) {
|
||||
$cls = $key === $active ? ' class="active"' : '';
|
||||
echo "<a href='{$href}'{$cls}>{$icon}<span>{$label}</span></a>";
|
||||
}
|
||||
echo '</nav>';
|
||||
}
|
||||
|
||||
// ── Icons ─────────────────────────────────────
|
||||
function trendIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
|
||||
}
|
||||
function txIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>';
|
||||
}
|
||||
function balIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>';
|
||||
}
|
||||
function acctIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>';
|
||||
}
|
||||
function debtIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
|
||||
}
|
||||
function oneoffIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
|
||||
}
|
||||
function settingsIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
|
||||
}
|
||||
function adminIcon(): string {
|
||||
return '<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
|
||||
}
|
||||
136
register.php
136
register.php
|
|
@ -1,136 +0,0 @@
|
|||
<?php
|
||||
// ─────────────────────────────────────────────
|
||||
// register.php · New user registration
|
||||
// ─────────────────────────────────────────────
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
if (currentUserId()) { header('Location: index.php'); exit; }
|
||||
|
||||
$siteName = getSetting('site_name', 'Finance Planner');
|
||||
$registrationEnabled = getSetting('registration_enabled', '1') === '1';
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$registrationEnabled) {
|
||||
$error = 'Registration is currently disabled.';
|
||||
} else {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$displayName = trim($_POST['display_name'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$confirm = $_POST['confirm_password'] ?? '';
|
||||
|
||||
if (!$username || !$password) {
|
||||
$error = 'Username and password are required.';
|
||||
} elseif (!preg_match('/^[a-zA-Z0-9_]{3,30}$/', $username)) {
|
||||
$error = 'Username must be 3–30 characters: letters, numbers, underscores only.';
|
||||
} elseif (strlen($password) < 6) {
|
||||
$error = 'Password must be at least 6 characters.';
|
||||
} elseif ($password !== $confirm) {
|
||||
$error = 'Passwords do not match.';
|
||||
} else {
|
||||
try {
|
||||
require_once __DIR__ . '/db.php';
|
||||
$db = getDB();
|
||||
$st = $db->prepare("SELECT id FROM users WHERE username = ?");
|
||||
$st->execute([$username]);
|
||||
if ($st->fetch()) {
|
||||
$error = 'That username is already taken. Please choose another.';
|
||||
} else {
|
||||
$dn = $displayName ?: $username;
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$db->prepare("INSERT INTO users (username, password_hash, display_name) VALUES (?,?,?)")
|
||||
->execute([$username, $hash, $dn]);
|
||||
$newId = (int)$db->lastInsertId();
|
||||
// Give them a default account to get started
|
||||
$db->prepare("INSERT INTO accounts (user_id, name, currency) VALUES (?,?,?)")
|
||||
->execute([$newId, 'Main Account', 'GBP']);
|
||||
// Auto-login
|
||||
$_SESSION['user'] = [
|
||||
'id' => $newId,
|
||||
'username' => $username,
|
||||
'display_name' => $dn,
|
||||
'is_admin' => 0,
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = 'Registration failed. Please try again.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Register · <?= htmlspecialchars($siteName) ?></title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
.auth-wrap { display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px; }
|
||||
.auth-card {
|
||||
background:var(--surface);border:1px solid var(--border);
|
||||
border-radius:var(--radius);padding:36px 30px;max-width:380px;width:100%;
|
||||
}
|
||||
.auth-logo { font-size:2.5rem;text-align:center;margin-bottom:10px; }
|
||||
.auth-card h1 { font-size:1.2rem;text-align:center;margin-bottom:6px; }
|
||||
.auth-sub { color:var(--text-dim);font-size:.8rem;text-align:center;margin-bottom:24px; }
|
||||
.auth-footer { color:var(--text-muted);font-size:.8rem;text-align:center;margin-top:16px; }
|
||||
.auth-footer a { color:var(--accent);text-decoration:none; }
|
||||
.field-hint { font-size:.7rem;color:var(--text-muted);margin-top:3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">💷</div>
|
||||
<h1><?= htmlspecialchars($siteName) ?></h1>
|
||||
<p class="auth-sub">Create your free account</p>
|
||||
|
||||
<?php if (!$registrationEnabled): ?>
|
||||
<div class="alert alert-err">
|
||||
Registration is currently disabled.<br>
|
||||
Please contact the site administrator.
|
||||
</div>
|
||||
<div class="auth-footer"><a href="login.php">← Back to Login</a></div>
|
||||
<?php else: ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-err"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="register.php">
|
||||
<div class="form-group">
|
||||
<label>Username <span style="color:var(--red)">*</span></label>
|
||||
<input type="text" name="username"
|
||||
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
|
||||
placeholder="e.g. jane_doe" autocomplete="username" autofocus required>
|
||||
<div class="field-hint">3–30 characters · letters, numbers, underscores</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Display Name <span style="color:var(--text-muted)">(optional)</span></label>
|
||||
<input type="text" name="display_name"
|
||||
value="<?= htmlspecialchars($_POST['display_name'] ?? '') ?>"
|
||||
placeholder="e.g. Jane">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password <span style="color:var(--red)">*</span></label>
|
||||
<input type="password" name="password"
|
||||
placeholder="Minimum 6 characters" autocomplete="new-password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirm Password <span style="color:var(--red)">*</span></label>
|
||||
<input type="password" name="confirm_password"
|
||||
placeholder="Repeat password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-full">Create Account</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">Already have an account? <a href="login.php">Sign in →</a></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
155
settings.php
155
settings.php
|
|
@ -1,155 +0,0 @@
|
|||
<?php
|
||||
// ─────────────────────────────────────────────
|
||||
// settings.php · User settings
|
||||
// ─────────────────────────────────────────────
|
||||
require_once 'db.php';
|
||||
require_once 'auth.php';
|
||||
requireLogin();
|
||||
require_once 'nav.php';
|
||||
|
||||
$user = currentUser();
|
||||
|
||||
renderHead('Settings');
|
||||
renderTopBar('Settings');
|
||||
?>
|
||||
<div class="page">
|
||||
|
||||
<!-- Profile ─────────────────────────────── -->
|
||||
<div class="section-label">Profile</div>
|
||||
<div class="card" style="padding:16px 16px 20px">
|
||||
<div style="display:flex;align-items:center;gap:14px;margin-bottom:18px">
|
||||
<div style="width:52px;height:52px;border-radius:50%;background:var(--accent);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:1.4rem;font-weight:700;color:#fff;flex-shrink:0">
|
||||
<?= strtoupper(substr($user['display_name'] ?? $user['username'], 0, 1)) ?>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight:600"><?= htmlspecialchars($user['display_name'] ?? $user['username']) ?></div>
|
||||
<div style="font-size:.8rem;color:var(--text-dim)">@<?= htmlspecialchars($user['username']) ?></div>
|
||||
<?php if (isAdmin()): ?>
|
||||
<span class="badge badge-admin" style="margin-left:0;margin-top:4px">Admin</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Display Name</label>
|
||||
<input type="text" id="displayName"
|
||||
value="<?= htmlspecialchars($user['display_name'] ?? '') ?>"
|
||||
placeholder="Your name">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="saveProfile()">Save Name</button>
|
||||
<span id="profileMsg" style="margin-left:10px;font-size:.82rem"></span>
|
||||
</div>
|
||||
|
||||
<!-- Change Password ──────────────────────── -->
|
||||
<div class="section-label">Change Password</div>
|
||||
<div class="card" style="padding:16px 16px 20px">
|
||||
<div class="form-group">
|
||||
<label>Current Password</label>
|
||||
<input type="password" id="pwCurrent" placeholder="••••••••">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>New Password</label>
|
||||
<input type="password" id="pwNew" placeholder="Minimum 6 characters">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirm New Password</label>
|
||||
<input type="password" id="pwConfirm" placeholder="Repeat new password">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="changePassword()">Update Password</button>
|
||||
<span id="pwMsg" style="margin-left:10px;font-size:.82rem"></span>
|
||||
</div>
|
||||
|
||||
<!-- Support / Donate ─────────────────────── -->
|
||||
<?php
|
||||
$donMsg = getSetting('donation_message', '');
|
||||
$hasPayPal = getSetting('paypal_link', '') !== '';
|
||||
$hasKofi = getSetting('kofi_link', '') !== '';
|
||||
$hasBmc = getSetting('buymeacoffee_link', '') !== '';
|
||||
if ($hasPayPal || $hasKofi || $hasBmc):
|
||||
?>
|
||||
<div class="section-label">Support the App</div>
|
||||
<div class="card" style="padding:16px;text-align:center">
|
||||
<?php if ($donMsg): ?>
|
||||
<p style="color:var(--text-dim);font-size:.85rem;margin-bottom:14px">
|
||||
<?= htmlspecialchars($donMsg) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<a href="donate.php" class="btn btn-primary" style="text-decoration:none">❤️ Donate</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Admin Panel link (admins only) ──────── -->
|
||||
<?php if (isAdmin()): ?>
|
||||
<div class="section-label">Administration</div>
|
||||
<div class="card">
|
||||
<a href="admin.php" style="text-decoration:none">
|
||||
<div class="card-row">
|
||||
<div style="font-size:1.3rem;margin-right:12px">🛡️</div>
|
||||
<div style="flex:1">
|
||||
<div class="row-label">Admin Panel</div>
|
||||
<div class="row-sub">Manage users, settings & donation links</div>
|
||||
</div>
|
||||
<div style="color:var(--text-muted)">→</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Logout ──────────────────────────────── -->
|
||||
<div class="section-label">Session</div>
|
||||
<div class="card">
|
||||
<div class="card-row">
|
||||
<div style="flex:1">
|
||||
<div class="row-label">Sign Out</div>
|
||||
<div class="row-sub">Logged in as @<?= htmlspecialchars($user['username']) ?></div>
|
||||
</div>
|
||||
<a href="login.php?action=logout" class="btn btn-danger" style="text-decoration:none">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php renderNav('settings'); ?>
|
||||
|
||||
<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();
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
const dn = document.getElementById('displayName').value.trim();
|
||||
const res = await api('update_profile', 'POST', new URLSearchParams({display_name: dn}).toString());
|
||||
const el = document.getElementById('profileMsg');
|
||||
el.style.color = res.ok ? 'var(--green)' : 'var(--red)';
|
||||
el.textContent = res.ok ? '✓ Saved' : '✗ Failed';
|
||||
if (res.ok) setTimeout(() => el.textContent = '', 3000);
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
const current = document.getElementById('pwCurrent').value;
|
||||
const nw = document.getElementById('pwNew').value;
|
||||
const confirm = document.getElementById('pwConfirm').value;
|
||||
const el = document.getElementById('pwMsg');
|
||||
|
||||
if (nw.length < 6) {
|
||||
el.style.color = 'var(--red)'; el.textContent = 'Min. 6 characters'; return;
|
||||
}
|
||||
if (nw !== confirm) {
|
||||
el.style.color = 'var(--red)'; el.textContent = 'Passwords do not match'; return;
|
||||
}
|
||||
const res = await api('change_password', 'POST', new URLSearchParams({current, new: nw}).toString());
|
||||
el.style.color = res.ok ? 'var(--green)' : 'var(--red)';
|
||||
el.textContent = res.ok ? '✓ Password updated' : ('✗ ' + (res.error || 'Failed'));
|
||||
if (res.ok) {
|
||||
['pwCurrent','pwNew','pwConfirm'].forEach(id => document.getElementById(id).value = '');
|
||||
setTimeout(() => el.textContent = '', 4000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/* ─────────────────────────────────────────────
|
||||
style_additions.css
|
||||
APPEND these rules to the bottom of style.css
|
||||
───────────────────────────────────────────── */
|
||||
|
||||
/* ── Topbar avatar (initials circle) ───────── */
|
||||
.topbar-avatar {
|
||||
position: absolute; left: 14px;
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,.22);
|
||||
color: #fff;
|
||||
font-size: .8rem; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
text-decoration: none;
|
||||
transition: background .15s;
|
||||
user-select: none;
|
||||
}
|
||||
.topbar-avatar:hover { background: rgba(255,255,255,.35); }
|
||||
|
||||
/* ── User / admin badges ───────────────────── */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: .65rem; font-weight: 700;
|
||||
vertical-align: middle;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/* ── Admin user action buttons ─────────────── */
|
||||
.user-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.user-actions .btn { padding: 5px 10px; font-size: .75rem; }
|
||||
|
||||
/* ── Checkbox style in settings/admin ──────── */
|
||||
input[type="checkbox"] {
|
||||
width: 18px; height: 18px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Auth pages (login / register) ─────────── */
|
||||
.auth-wrap {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 100vh; padding: 24px;
|
||||
}
|
||||
.auth-card {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 36px 30px;
|
||||
max-width: 380px; width: 100%;
|
||||
}
|
||||
.auth-logo { font-size: 2.5rem; text-align: center; margin-bottom: 10px; }
|
||||
.auth-card h1 { font-size: 1.2rem; text-align: center; margin-bottom: 6px; }
|
||||
.auth-sub { color: var(--text-dim); font-size: .8rem; text-align: center; margin-bottom: 24px; }
|
||||
.auth-footer { color: var(--text-muted); font-size: .8rem; text-align: center; margin-top: 16px; }
|
||||
.auth-footer a { color: var(--accent); text-decoration: none; }
|
||||
.auth-hint { color: var(--text-muted); font-size: .75rem; text-align: center; margin-top: 10px; }
|
||||
.auth-hint code { background: var(--surface2); padding: 1px 5px; border-radius: 4px; color: var(--accent); }
|
||||
.field-hint { font-size: .7rem; color: var(--text-muted); margin-top: 3px; }
|
||||
Loading…
Add table
Reference in a new issue