mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-06-30 17:50:31 -05:00
Replace cookie auth with URL token auth in admin portal
Cookies failed consistently in real browsers despite working in curl. Replaced with DB-stored token passed as ?_t=TOKEN in URL: - Login generates 64-char hex token, stores in admin_tokens table - Redirect to /admin/?_t=TOKEN after successful login - Every request validated via DB lookup (no cookies needed) - All 7 AJAX calls include &_t=TOKEN in POST body - Logout deletes token from DB - Requires admin_tokens table (created in DB) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+38
-45
@@ -6,58 +6,46 @@ header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
|
||||
|
||||
require_once dirname(__DIR__) . '/db.php';
|
||||
|
||||
// ── Cookie-based auth (no PHP sessions — avoids server caching/permission issues) ──
|
||||
define('AUTH_COOKIE', 'parker_auth');
|
||||
define('AUTH_SECRET', hash('sha256', ADMIN_PASS . ADMIN_SESSION_KEY)); // not reversible
|
||||
|
||||
function _authToken(): string {
|
||||
$t = bin2hex(random_bytes(32));
|
||||
$e = time() + 86400; // 24h
|
||||
$s = hash_hmac('sha256', "$t|$e", AUTH_SECRET);
|
||||
return "$t.$e.$s";
|
||||
// ── URL-token auth (no cookies — works in all browsers) ───────────────────────
|
||||
function _createToken(): string {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
db()->prepare("INSERT INTO admin_tokens (token, expires_at) VALUES (?, ?)")
|
||||
->execute([$token, date('Y-m-d H:i:s', time() + 86400)]);
|
||||
db()->exec("DELETE FROM admin_tokens WHERE expires_at < NOW()");
|
||||
return $token;
|
||||
}
|
||||
function _verifyAuth(): bool {
|
||||
$c = $_COOKIE[AUTH_COOKIE] ?? '';
|
||||
$p = explode('.', $c, 3);
|
||||
if (count($p) !== 3) return false;
|
||||
[$t, $e, $s] = $p;
|
||||
if ((int)$e < time()) return false;
|
||||
return hash_equals(hash_hmac('sha256', "$t|$e", AUTH_SECRET), $s);
|
||||
}
|
||||
function _setAuth(): void {
|
||||
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
setcookie(AUTH_COOKIE, _authToken(), [
|
||||
'expires' => time() + 86400,
|
||||
'path' => '/admin/',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
function _clearAuth(): void {
|
||||
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
setcookie(AUTH_COOKIE, '', ['expires' => time()-3600, 'path' => '/admin/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax']);
|
||||
function _verifyToken(string $token): bool {
|
||||
if (!preg_match('/^[a-f0-9]{64}$/', $token)) return false;
|
||||
$stmt = db()->prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()");
|
||||
$stmt->execute([$token]);
|
||||
return (bool)$stmt->fetch();
|
||||
}
|
||||
|
||||
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json');
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'login') {
|
||||
if ($_POST['username'] === ADMIN_USER && password_verify($_POST['password'] ?? '', ADMIN_PASS)) {
|
||||
_setAuth();
|
||||
if (($_POST['username'] ?? '') === ADMIN_USER && password_verify($_POST['password'] ?? '', ADMIN_PASS)) {
|
||||
$t = _createToken();
|
||||
header('Location: /admin/?_t=' . $t);
|
||||
} else {
|
||||
header('Location: /admin/?err=1');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
$rawToken = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? $_POST['_t'] ?? '');
|
||||
if (($_GET['action'] ?? '') === 'logout') {
|
||||
if ($rawToken) db()->prepare("DELETE FROM admin_tokens WHERE token=?")->execute([$rawToken]);
|
||||
header('Location: /admin/'); exit;
|
||||
}
|
||||
if (($_GET['action'] ?? '') === 'logout') {
|
||||
_clearAuth(); header('Location: /admin/'); exit;
|
||||
}
|
||||
$authed = _verifyAuth();
|
||||
$authed = $rawToken !== '' && _verifyToken($rawToken);
|
||||
$token = $authed ? $rawToken : '';
|
||||
|
||||
// ── AJAX handlers ─────────────────────────────────────────────────────────────
|
||||
if ($isAjax && !$authed) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error'=>'Session expired. Please reload and log in again.']);
|
||||
echo json_encode(['error'=>'Session expired. Please log in again.']);
|
||||
exit;
|
||||
}
|
||||
if ($isAjax) {
|
||||
@@ -280,6 +268,9 @@ button:hover{background:#ea580c}
|
||||
<div class="box">
|
||||
<h1>Parker Admin</h1>
|
||||
<p>Slingshot Rentals Management</p>
|
||||
<?php if (!empty($_GET['err'])): ?>
|
||||
<p style="color:#f87171;margin-bottom:1rem">Invalid username or password.</p>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="login">
|
||||
<label>Username</label>
|
||||
@@ -427,7 +418,7 @@ textarea.notes-ta:focus{border-color:#f97316}
|
||||
<body>
|
||||
<header>
|
||||
<h1>Parker County Slingshot — Admin</h1>
|
||||
<a href="?action=logout">Sign Out</a>
|
||||
<a href="?action=logout&_t=<?= htmlspecialchars($token) ?>">Sign Out</a>
|
||||
</header>
|
||||
<div class="main">
|
||||
|
||||
@@ -796,6 +787,8 @@ textarea.notes-ta:focus{border-color:#f97316}
|
||||
</div><!-- /.main -->
|
||||
|
||||
<script>
|
||||
const ADMIN_TOKEN = '<?= htmlspecialchars($token) ?>';
|
||||
|
||||
// ── Status filter (client-side — no page navigation) ─────────────────────────
|
||||
function filterBookings(status, btn) {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
@@ -822,7 +815,7 @@ function updateStatus(sel) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=update_status&id='+sel.dataset.id+'&status='+sel.value
|
||||
body:'action=update_status&id='+sel.dataset.id+'&status='+sel.value+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{ if(!d.ok) alert('Error saving status'); });
|
||||
}
|
||||
|
||||
@@ -832,7 +825,7 @@ function toggleReq(id, field, btn) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=toggle_requirement&id='+id+'&field='+field
|
||||
body:'action=toggle_requirement&id='+id+'&field='+field+'&_t='+ADMIN_TOKEN
|
||||
})
|
||||
.then(r=>r.json())
|
||||
.then(d=>{
|
||||
@@ -886,7 +879,7 @@ function saveNotes(id) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=save_admin_notes&id='+id+'¬es='+encodeURIComponent(ta.value)
|
||||
body:'action=save_admin_notes&id='+id+'¬es='+encodeURIComponent(ta.value)+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
|
||||
setTimeout(()=>btn.textContent='Save Notes', 1800);
|
||||
@@ -906,7 +899,7 @@ function sendReminder(id) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=send_reminder&id='+id+'&items='+checks.join(',')
|
||||
body:'action=send_reminder&id='+id+'&items='+checks.join(',')+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
btn.disabled = false;
|
||||
if (d.ok) {
|
||||
@@ -946,7 +939,7 @@ function squareAction(id, action, btn) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action='+action+'&id='+id
|
||||
body:'action='+action+'&id='+id+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
if (d.ok) {
|
||||
btn.textContent = done;
|
||||
@@ -997,7 +990,7 @@ function blockDate(e) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=block_date&date='+date+'&reason='+encodeURIComponent(reason)
|
||||
body:'action=block_date&date='+date+'&reason='+encodeURIComponent(reason)+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); else alert(d.error); });
|
||||
}
|
||||
function unblockDate(id) {
|
||||
@@ -1005,7 +998,7 @@ function unblockDate(id) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=unblock_date&id='+id
|
||||
body:'action=unblock_date&id='+id+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{ if(d.ok) document.getElementById('blocked-'+id).remove(); });
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user