mirror of
https://github.com/myronblair/tomtomgames
synced 2026-06-30 17:51:08 -05:00
Restrict Agent Info and Credit Accounting to master admin only; protect master admin account
- Agent Info: master admin sees full edit form; other admins see view-only panel with Copy and Open URL buttons - Credit Accounting: master admin can manage entries; other admins see total only (Manage Credits button hidden) - API: credits_create/update/delete require master admin; platform update strips agent fields for non-master - Players: suspend/delete buttons disabled when viewing master admin account (UI + JS guards) - URL fields (Agent Link, Games Link): open-in-new-tab arrow button added in both edit and view modes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+141
-38
@@ -793,8 +793,8 @@ tr:hover td{background:rgba(255,255,255,.015)}
|
||||
<label class="gm-edit-label">Player URL * <span style="font-weight:400;color:var(--text2)">shown to players — opens the game</span></label>
|
||||
<input class="fi-sm" id="gf-player-url" type="url" placeholder="https://game.example.com/play" style="width:100%;padding:10px 12px">
|
||||
</div>
|
||||
<!-- Agent Fields — admin only -->
|
||||
<div style="background:rgba(155,93,229,0.06);border:1px solid rgba(155,93,229,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px">
|
||||
<!-- Agent Fields — EDIT mode (master admin only) -->
|
||||
<div id="gf-agent-edit" style="background:rgba(155,93,229,0.06);border:1px solid rgba(155,93,229,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px;display:none">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:10px">🔐 Agent Info — Admin Only</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">
|
||||
<div>
|
||||
@@ -808,11 +808,19 @@ tr:hover td{background:rgba(255,255,255,.015)}
|
||||
</div>
|
||||
<div style="margin-bottom:10px">
|
||||
<label class="gm-edit-label">Agent Link <span style="font-weight:400;color:var(--text2)">admin/console URL</span></label>
|
||||
<input class="fi-sm" id="gf-agent-link" type="url" placeholder="https://admin.game.example.com" style="width:100%;padding:10px 12px">
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<input class="fi-sm" id="gf-agent-link" type="url" placeholder="https://admin.game.example.com" style="flex:1;padding:10px 12px">
|
||||
<a id="gf-agent-link-open" href="#" target="_blank" rel="noopener" onclick="return openFieldUrl('gf-agent-link')"
|
||||
style="background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);color:var(--cyan);border-radius:6px;padding:9px 12px;font-size:13px;font-weight:700;text-decoration:none;white-space:nowrap">↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px">
|
||||
<label class="gm-edit-label">Games Link <span style="font-weight:400;color:var(--text2)">game listing or lobby URL</span></label>
|
||||
<input class="fi-sm" id="gf-games-link" type="url" placeholder="https://game.example.com/lobby" style="width:100%;padding:10px 12px">
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<input class="fi-sm" id="gf-games-link" type="url" placeholder="https://game.example.com/lobby" style="flex:1;padding:10px 12px">
|
||||
<a id="gf-games-link-open" href="#" target="_blank" rel="noopener" onclick="return openFieldUrl('gf-games-link')"
|
||||
style="background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);color:var(--cyan);border-radius:6px;padding:9px 12px;font-size:13px;font-weight:700;text-decoration:none;white-space:nowrap">↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="gm-edit-label">Agent Guide <span style="font-weight:400;color:var(--text2)">notes, instructions, tips</span></label>
|
||||
@@ -845,6 +853,13 @@ tr:hover td{background:rgba(255,255,255,.015)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Fields — VIEW mode (non-master admins) -->
|
||||
<div id="gf-agent-view" style="background:rgba(155,93,229,0.06);border:1px solid rgba(155,93,229,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px;display:none">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:10px">🔐 Agent Info — View Only</div>
|
||||
<div id="gf-agent-view-content" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
</div>
|
||||
|
||||
<!-- Credit Accounting — admin only -->
|
||||
<div style="background:rgba(0,229,255,0.04);border:1px solid rgba(0,229,255,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||||
@@ -1463,13 +1478,26 @@ function renderGamerProfile(u) {
|
||||
// Update token display in tokens tab
|
||||
document.getElementById('gm-token-display').textContent = parseFloat(u.tokens||0) + ' 🪙';
|
||||
// Update suspend button
|
||||
const sb = document.getElementById('gm-suspend-btn');
|
||||
if (u.status === 'active') {
|
||||
sb.textContent = '🔒 SUSPEND ACCOUNT (Lock Login)';
|
||||
sb.className = 'btn btn-red';
|
||||
const sb = document.getElementById('gm-suspend-btn');
|
||||
const db2 = document.querySelector('[onclick="gmDeleteAccount()"]');
|
||||
const isMasterUser = parseInt(u.id) === MASTER_ADMIN_ID;
|
||||
if (isMasterUser) {
|
||||
sb.textContent = '🔒 Cannot Suspend Master Admin';
|
||||
sb.className = 'btn';
|
||||
sb.disabled = true;
|
||||
sb.style.cssText = 'opacity:.4;cursor:not-allowed;background:rgba(255,68,68,.06);color:var(--red);border:1px solid rgba(255,68,68,.2)';
|
||||
if (db2) { db2.disabled = true; db2.style.opacity = '.4'; db2.style.cursor = 'not-allowed'; db2.title = 'Cannot delete master admin account'; }
|
||||
} else {
|
||||
sb.textContent = '🔓 ACTIVATE ACCOUNT (Restore Login)';
|
||||
sb.className = 'btn btn-green';
|
||||
sb.disabled = false;
|
||||
sb.style.cssText = '';
|
||||
if (db2) { db2.disabled = false; db2.style.opacity = ''; db2.style.cursor = ''; db2.title = ''; }
|
||||
if (u.status === 'active') {
|
||||
sb.textContent = '🔒 SUSPEND ACCOUNT (Lock Login)';
|
||||
sb.className = 'btn btn-red';
|
||||
} else {
|
||||
sb.textContent = '🔓 ACTIVATE ACCOUNT (Restore Login)';
|
||||
sb.className = 'btn btn-green';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1714,6 +1742,7 @@ async function gmSaveEdit() {
|
||||
}
|
||||
|
||||
async function gmToggleSuspend() {
|
||||
if (GM.current && parseInt(GM.current.id) === MASTER_ADMIN_ID) { toast('Cannot suspend master admin','err'); return; }
|
||||
const isSuspended = GM.current.status === 'suspended';
|
||||
const action = isSuspended ? 'activate' : 'suspend';
|
||||
if (!isSuspended && !confirm(`Suspend ${GM.current.username}? They will be locked out immediately.`)) return;
|
||||
@@ -1790,6 +1819,7 @@ function updateAdminToggleBtn() {
|
||||
}
|
||||
|
||||
async function gmDeleteAccount() {
|
||||
if (GM.current && parseInt(GM.current.id) === MASTER_ADMIN_ID) { toast('Cannot delete master admin account','err'); return; }
|
||||
if (!confirm(`⚠️ PERMANENTLY delete ${GM.current.username}?\n\nThis removes all their data and cannot be undone.`)) return;
|
||||
if (!confirm(`Are you absolutely sure? Type OK to confirm deletion of ${GM.current.username}.`)) return;
|
||||
const d = await apiFetch('delete_user','POST',{user_id:GM.current.id});
|
||||
@@ -2887,27 +2917,72 @@ async function loadGames() {
|
||||
function editGame(id) {
|
||||
const g = (window._gamesData || []).find(x => x.id == id);
|
||||
if (!g) return;
|
||||
document.getElementById('gf-id').value = g.id;
|
||||
document.getElementById('gf-name').value = g.name;
|
||||
document.getElementById('gf-slug').value = g.slug;
|
||||
document.getElementById('gf-slug').disabled = true; // slug can't change (used as FK in purchases)
|
||||
document.getElementById('gf-player-url').value = g.player_url;
|
||||
document.getElementById('gf-agent-link').value = g.agent_link || '';
|
||||
document.getElementById('gf-agent-login').value = g.agent_login || '';
|
||||
document.getElementById('gf-agent-password').value= g.agent_password || '';
|
||||
document.getElementById('gf-games-link').value = g.games_link || '';
|
||||
document.getElementById('gf-agent-guide').value = g.agent_guide || '';
|
||||
document.getElementById('gf-sub-agent-login').value = g.sub_agent_login || '';
|
||||
document.getElementById('gf-sub-agent-password').value = g.sub_agent_password || '';
|
||||
document.getElementById('gf-cashier-login').value = g.cashier_login || '';
|
||||
document.getElementById('gf-cashier-password').value = g.cashier_password || '';
|
||||
document.getElementById('gf-color').value = g.color || '#f0c040';
|
||||
document.getElementById('gf-id').value = g.id;
|
||||
document.getElementById('gf-name').value = g.name;
|
||||
document.getElementById('gf-slug').value = g.slug;
|
||||
document.getElementById('gf-slug').disabled = true;
|
||||
document.getElementById('gf-player-url').value = g.player_url;
|
||||
document.getElementById('gf-color').value = g.color || '#f0c040';
|
||||
document.getElementById('gf-color-hex').value = g.color || '#f0c040';
|
||||
document.getElementById('gf-sort').value = g.sort_order;
|
||||
document.getElementById('gf-active').value = g.is_active;
|
||||
document.getElementById('game-form-title').textContent = '✏️ Editing: ' + g.name;
|
||||
document.getElementById('gf-credit-btn').disabled = false;
|
||||
// Load credit total for this platform
|
||||
|
||||
if (IS_MASTER_ADMIN) {
|
||||
// Show edit mode, hide view mode
|
||||
document.getElementById('gf-agent-edit').style.display = 'block';
|
||||
document.getElementById('gf-agent-view').style.display = 'none';
|
||||
document.getElementById('gf-agent-link').value = g.agent_link || '';
|
||||
document.getElementById('gf-agent-login').value = g.agent_login || '';
|
||||
document.getElementById('gf-agent-password').value = g.agent_password || '';
|
||||
document.getElementById('gf-games-link').value = g.games_link || '';
|
||||
document.getElementById('gf-agent-guide').value = g.agent_guide || '';
|
||||
document.getElementById('gf-sub-agent-login').value = g.sub_agent_login || '';
|
||||
document.getElementById('gf-sub-agent-password').value = g.sub_agent_password || '';
|
||||
document.getElementById('gf-cashier-login').value = g.cashier_login || '';
|
||||
document.getElementById('gf-cashier-password').value = g.cashier_password || '';
|
||||
document.getElementById('gf-credit-btn').disabled = false;
|
||||
} else {
|
||||
// Show view mode, hide edit mode
|
||||
document.getElementById('gf-agent-edit').style.display = 'none';
|
||||
document.getElementById('gf-agent-view').style.display = 'block';
|
||||
document.getElementById('gf-credit-btn').disabled = true;
|
||||
|
||||
const agentFields = [
|
||||
{label:'Agent Login', key:'agent_login', isUrl:false},
|
||||
{label:'Agent Password', key:'agent_password', isUrl:false},
|
||||
{label:'Agent Link', key:'agent_link', isUrl:true},
|
||||
{label:'Games Link', key:'games_link', isUrl:true},
|
||||
{label:'Agent Guide', key:'agent_guide', isUrl:false},
|
||||
{label:'Sub-Account Agent Login', key:'sub_agent_login', isUrl:false},
|
||||
{label:'Sub-Account Agent Password',key:'sub_agent_password', isUrl:false},
|
||||
{label:'Cashier Login', key:'cashier_login', isUrl:false},
|
||||
{label:'Cashier Password', key:'cashier_password', isUrl:false},
|
||||
];
|
||||
const content = document.getElementById('gf-agent-view-content');
|
||||
content.innerHTML = agentFields.map(f => {
|
||||
const val = g[f.key] || '';
|
||||
if (!val) return '';
|
||||
const openBtn = f.isUrl ? `<a href="${escHtmlA(val)}" target="_blank" rel="noopener"
|
||||
style="background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);color:var(--cyan);border-radius:5px;padding:3px 9px;font-size:12px;font-weight:700;cursor:pointer;flex-shrink:0;text-decoration:none">
|
||||
↗ Open
|
||||
</a>` : '';
|
||||
return `<div style="display:flex;align-items:center;gap:8px;padding:7px 10px;background:rgba(155,93,229,0.05);border-radius:6px">
|
||||
<span style="font-size:12px;font-weight:700;color:var(--purple);min-width:160px;flex-shrink:0">${escHtmlA(f.label)}</span>
|
||||
<span style="flex:1;color:var(--text);font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtmlA(val)}">${escHtmlA(val)}</span>
|
||||
${openBtn}
|
||||
<button onclick="copyToClipboard(${JSON.stringify(val)},this)"
|
||||
style="background:rgba(155,93,229,0.15);border:1px solid rgba(155,93,229,0.3);color:var(--purple);border-radius:5px;padding:3px 10px;font-size:12px;font-weight:700;cursor:pointer;flex-shrink:0">
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (!content.innerHTML.trim()) {
|
||||
content.innerHTML = '<div style="color:var(--text2);font-size:14px;padding:8px 0">No agent info has been set for this game.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load credit total
|
||||
fetch('/api/platforms.php?action=credits_list&platform_id=' + g.id).then(r=>r.json()).then(d=>{
|
||||
if (d.success) {
|
||||
const t = d.total||0;
|
||||
@@ -2922,22 +2997,27 @@ function resetGameForm() {
|
||||
document.getElementById('gf-name').value = '';
|
||||
document.getElementById('gf-slug').value = '';
|
||||
document.getElementById('gf-slug').disabled = false;
|
||||
document.getElementById('gf-player-url').value = '';
|
||||
document.getElementById('gf-agent-link').value = '';
|
||||
document.getElementById('gf-agent-login').value = '';
|
||||
document.getElementById('gf-agent-password').value= '';
|
||||
document.getElementById('gf-games-link').value = '';
|
||||
document.getElementById('gf-agent-guide').value = '';
|
||||
document.getElementById('gf-sub-agent-login').value = '';
|
||||
document.getElementById('gf-sub-agent-password').value = '';
|
||||
document.getElementById('gf-cashier-login').value = '';
|
||||
document.getElementById('gf-cashier-password').value = '';
|
||||
document.getElementById('gf-color').value = '#f0c040';
|
||||
document.getElementById('gf-player-url').value = '';
|
||||
document.getElementById('gf-color').value = '#f0c040';
|
||||
document.getElementById('gf-color-hex').value = '#f0c040';
|
||||
document.getElementById('gf-sort').value = '99';
|
||||
document.getElementById('gf-active').value = '1';
|
||||
document.getElementById('gf-credit-total').textContent = '—';
|
||||
document.getElementById('gf-credit-btn').disabled = true;
|
||||
// Always hide both agent panels when clearing
|
||||
document.getElementById('gf-agent-edit').style.display = 'none';
|
||||
document.getElementById('gf-agent-view').style.display = 'none';
|
||||
if (IS_MASTER_ADMIN) {
|
||||
document.getElementById('gf-agent-link').value = '';
|
||||
document.getElementById('gf-agent-login').value = '';
|
||||
document.getElementById('gf-agent-password').value = '';
|
||||
document.getElementById('gf-games-link').value = '';
|
||||
document.getElementById('gf-agent-guide').value = '';
|
||||
document.getElementById('gf-sub-agent-login').value = '';
|
||||
document.getElementById('gf-sub-agent-password').value = '';
|
||||
document.getElementById('gf-cashier-login').value = '';
|
||||
document.getElementById('gf-cashier-password').value = '';
|
||||
}
|
||||
document.getElementById('game-form-title').textContent = '➕ Add New Game';
|
||||
document.getElementById('game-form-alert').className = 'alert';
|
||||
}
|
||||
@@ -3483,6 +3563,29 @@ function formatAdminDateLabel(dateStr) {
|
||||
}
|
||||
|
||||
function escHtmlA(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>'); }
|
||||
|
||||
function openFieldUrl(inputId) {
|
||||
const url = document.getElementById(inputId)?.value?.trim();
|
||||
if (!url) return false;
|
||||
window.open(url, '_blank', 'noopener');
|
||||
return false;
|
||||
}
|
||||
|
||||
function copyToClipboard(text, btn) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const orig = btn.innerHTML;
|
||||
btn.innerHTML = '✅ Copied';
|
||||
btn.style.background = 'rgba(0,230,118,0.15)';
|
||||
btn.style.borderColor = 'rgba(0,230,118,0.4)';
|
||||
btn.style.color = 'var(--green)';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = orig;
|
||||
btn.style.background = '';
|
||||
btn.style.borderColor = '';
|
||||
btn.style.color = '';
|
||||
}, 1800);
|
||||
}).catch(() => toast('Copy failed — try manually','err'));
|
||||
}
|
||||
function escAttr(s) { return String(s||'').replace(/'/g,"\\'").replace(/"/g,'"'); }
|
||||
|
||||
// Poll chat badge every 10s
|
||||
|
||||
+15
-7
@@ -4,8 +4,9 @@ try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) {
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']);
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']);
|
||||
$isMasterAdmin = $isAdmin && (int)($_SESSION['user_id'] ?? 0) === MASTER_ADMIN_ID;
|
||||
|
||||
switch ($action) {
|
||||
|
||||
@@ -79,8 +80,15 @@ switch ($action) {
|
||||
$sort_order = (int)($d['sort_order'] ?? 99);
|
||||
$is_active = (int)(bool)($d['is_active'] ?? 1);
|
||||
if (!$id || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'ID, name, and player URL required']); exit; }
|
||||
db()->prepare("UPDATE platforms SET name=?,player_url=?,agent_link=?,agent_login=?,agent_password=?,games_link=?,agent_guide=?,sub_agent_login=?,sub_agent_password=?,cashier_login=?,cashier_password=?,color=?,sort_order=?,is_active=? WHERE id=?")
|
||||
->execute([$name,$player_url,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort_order,$is_active,$id]);
|
||||
if ($isMasterAdmin) {
|
||||
// Master admin: update all fields including agent info
|
||||
db()->prepare("UPDATE platforms SET name=?,player_url=?,agent_link=?,agent_login=?,agent_password=?,games_link=?,agent_guide=?,sub_agent_login=?,sub_agent_password=?,cashier_login=?,cashier_password=?,color=?,sort_order=?,is_active=? WHERE id=?")
|
||||
->execute([$name,$player_url,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort_order,$is_active,$id]);
|
||||
} else {
|
||||
// Regular admin: update only non-sensitive fields
|
||||
db()->prepare("UPDATE platforms SET name=?,player_url=?,color=?,sort_order=?,is_active=? WHERE id=?")
|
||||
->execute([$name,$player_url,$color,$sort_order,$is_active,$id]);
|
||||
}
|
||||
echo json_encode(['success'=>true]);
|
||||
break;
|
||||
|
||||
@@ -119,7 +127,7 @@ switch ($action) {
|
||||
|
||||
// ── Admin: add credit entry ───────────────────────────
|
||||
case 'credits_create':
|
||||
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
if (!$isMasterAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
$d = json_decode(file_get_contents('php://input'), true);
|
||||
$pid = (int)($d['platform_id'] ?? 0);
|
||||
$credits = (float)($d['credits_purchased'] ?? 0);
|
||||
@@ -137,7 +145,7 @@ switch ($action) {
|
||||
|
||||
// ── Admin: update credit entry ────────────────────────
|
||||
case 'credits_update':
|
||||
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
if (!$isMasterAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
$d = json_decode(file_get_contents('php://input'), true);
|
||||
$id = (int)($d['id'] ?? 0);
|
||||
$credits = (float)($d['credits_purchased'] ?? 0);
|
||||
@@ -157,7 +165,7 @@ switch ($action) {
|
||||
|
||||
// ── Admin: delete credit entry ────────────────────────
|
||||
case 'credits_delete':
|
||||
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
if (!$isMasterAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
$d = json_decode(file_get_contents('php://input'), true);
|
||||
$id = (int)($d['id'] ?? 0);
|
||||
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
|
||||
|
||||
Reference in New Issue
Block a user