Fix dashbord.php parse error; require super_admin on pricing and crypto tools

Hotfix: a comment added to the dashboard SQL contained double quotes inside
the double-quoted PHP string, terminating it and making dashbord.php fail to
parse. Production was returning a parse error for every dashboard request.

Authorisation gaps closed — connect.php only proves a token is valid, it does
not check what the caller is allowed to do:

- Admin/ggg.php decrypts any database field and was authorised solely by an
  admin phone number sent in the request body. Anyone who knew a listed
  number could decrypt platform data without signing in. It now runs behind
  connect.php, requires super_admin, keeps the phone list as a second factor,
  and records every use.
- ride/kazan/update.php, kazan/add.php and ride/promo/{add,update,delete}.php
  changed live pricing and discount codes with no role check at all, so any
  valid token — including a driver's or passenger's — could rewrite the fare
  table. All now require super_admin.

Staff/pending.php: adminUser has no `status` column in this deployment, so
the query failed with an opaque "unavailable". It now checks for the column
and reports the actual reason.

Console: Kazan tariff editor for super admins — sends only changed fields,
shows an old → new confirmation before saving, and stays read-only with an
explanatory notice for plain admins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-25 02:00:14 +03:00
co-authored by Claude Opus 5
parent 852c6ece5c
commit db4ca7dd7a
10 changed files with 299 additions and 12 deletions
+147 -6
View File
@@ -848,12 +848,14 @@
],
},
{
id: 'tariff', group: 'Growth & Pricing', icon: 'ph-currency-circle-dollar', title: 'Tariff & Promos',
subtitle: 'The live Kazan tariff table and active promo codes (read-only)',
panels: [
{ title: 'Kazan tariff', path: '/ride/kazan/get.php' },
{ title: 'Promo codes', path: '/ride/promo/get.php' },
],
id: 'tariff', group: 'Growth & Pricing', icon: 'ph-currency-circle-dollar', title: 'Tariff Editor',
subtitle: 'The live Kazan tariff — every change here alters what passengers pay',
custom: renderTariffEditor,
},
{
id: 'promos', group: 'Growth & Pricing', icon: 'ph-ticket', title: 'Promo Codes',
subtitle: 'Active discount codes',
panels: [{ title: 'Promo codes', path: '/ride/promo/get.php' }],
},
{
id: 'geofence', group: 'Growth & Pricing', icon: 'ph-map-trifold', title: 'Demand Heatmap',
@@ -949,6 +951,11 @@
loadedModules.add(mod.id);
const host = $(`panels_${mod.id}`);
if (mod.custom) {
await mod.custom(host);
return;
}
host.innerHTML = mod.panels.map((p) => `
<div class="card" data-panel="${esc(p.path)}">
<div class="card-header"><h3 class="card-title">${esc(p.title)}</h3></div>
@@ -967,6 +974,140 @@
}));
}
// ── Kazan tariff editor ──────────────────────────────────────────────────
// Only these columns are accepted by ride/kazan/update.php; anything else
// sent would be silently dropped, so the form mirrors that list exactly.
const TARIFF_FIELDS = [
{ key: 'kazanPercent', label: 'Platform commission', hint: '% taken by Siro' },
{ key: 'fuelPrice', label: 'Fuel price' },
{ key: 'currency', label: 'Currency', type: 'text' },
{ key: 'normalMinPrice', label: 'Minimum fare — normal' },
{ key: 'peakMinPrice', label: 'Minimum fare — peak' },
{ key: 'lateMinPrice', label: 'Minimum fare — late night' },
{ key: 'fixedPrice', label: 'Fixed price' },
{ key: 'speedPrice', label: 'Speed' },
{ key: 'comfortPrice', label: 'Comfort' },
{ key: 'ladyPrice', label: 'Lady' },
{ key: 'electricPrice', label: 'Electric' },
{ key: 'vanPrice', label: 'Van' },
{ key: 'deliveryPrice', label: 'Delivery' },
{ key: 'mishwarVipPrice', label: 'Mishwar VIP' },
{ key: 'awfarPrice', label: 'Awfar' },
];
let tariffRows = [];
async function renderTariffEditor(host) {
host.innerHTML = '<div class="card"><div class="table-msg">Loading tariff…</div></div>';
try {
const payload = await api('/ride/kazan/get.php');
tariffRows = Array.isArray(payload) ? payload : normaliseRows(payload);
} catch (err) {
if (handleApiError(err, 'tariff')) return;
host.innerHTML = `<div class="card"><div class="table-msg is-error">${esc(err.message)}</div></div>`;
return;
}
if (!tariffRows.length) {
host.innerHTML = '<div class="card"><div class="table-msg">No tariff rows configured.</div></div>';
return;
}
const readOnly = !isSuperAdmin();
host.innerHTML = `
${readOnly ? `
<div class="card notice-card">
<i class="ph-fill ph-info"></i>
<span>You are signed in as an admin, so the tariff is shown read-only. Only a super admin can change prices.</span>
</div>` : `
<div class="card notice-card notice-danger">
<i class="ph-fill ph-warning"></i>
<span><strong>These values are live.</strong> Saving changes what every passenger is charged from the next ride onwards. Changes are recorded in the audit log against your account.</span>
</div>`}
${tariffRows.map((row, index) => tariffCard(row, index, readOnly)).join('')}`;
if (readOnly) return;
host.querySelectorAll('[data-tariff-save]').forEach((btn) =>
btn.addEventListener('click', () => saveTariff(Number(btn.dataset.tariffSave), host)));
host.querySelectorAll('[data-tariff-reset]').forEach((btn) =>
btn.addEventListener('click', () => renderTariffEditor(host)));
}
function tariffCard(row, index, readOnly) {
const fields = TARIFF_FIELDS.filter((f) => row[f.key] !== undefined);
return `
<div class="card" data-tariff-card="${index}">
<div class="card-header">
<h3 class="card-title">
${esc(row.country || 'Tariff')} <span class="card-sub">row #${esc(row.id)}</span>
</h3>
${readOnly ? '' : `
<div style="display:flex; gap:0.5rem;">
<button class="btn btn-secondary btn-sm" data-tariff-reset="${index}"><i class="ph ph-arrow-counter-clockwise"></i> Reset</button>
<button class="btn btn-primary btn-sm" data-tariff-save="${index}"><i class="ph ph-floppy-disk"></i> <span>Review &amp; save</span></button>
</div>`}
</div>
<div class="tariff-grid">
${fields.map((f) => `
<label class="tariff-field">
<span class="tariff-label">${esc(f.label)}${f.hint ? ` <em>${esc(f.hint)}</em>` : ''}</span>
<input class="form-input" type="${f.type === 'text' ? 'text' : 'number'}" step="any"
data-tariff-input="${index}" data-field="${esc(f.key)}"
value="${esc(row[f.key] ?? '')}" ${readOnly ? 'disabled' : ''}>
</label>`).join('')}
</div>
</div>`;
}
async function saveTariff(index, host) {
if (!isSuperAdmin()) {
toast('Only a super admin can change pricing.', 'warning');
return;
}
const row = tariffRows[index];
const inputs = host.querySelectorAll(`[data-tariff-input="${index}"]`);
const changes = {};
inputs.forEach((input) => {
const field = input.dataset.field;
const current = String(row[field] ?? '');
const next = input.value.trim();
if (next !== current) changes[field] = next;
});
if (!Object.keys(changes).length) {
toast('Nothing changed on this tariff row.', 'info');
return;
}
const summary = Object.entries(changes)
.map(([field, value]) => {
const label = TARIFF_FIELDS.find((f) => f.key === field)?.label || field;
return `• ${label}: ${row[field] ?? '—'} → ${value}`;
})
.join('\n');
const confirmed = confirm(
`Apply these pricing changes to "${row.country || 'tariff'}" (row #${row.id})?\n\n` +
`${summary}\n\n` +
'This takes effect immediately for passengers.'
);
if (!confirmed) return;
try {
await api('/ride/kazan/update.php', {
params: { id: row.id, adminId: session?.id ?? '', ...changes },
});
toast('Tariff updated and recorded in the audit log.', 'success');
renderTariffEditor(host);
} catch (err) {
if (!handleApiError(err, 'tariff-save')) toast(`Update failed: ${err.message}`, 'danger');
}
}
function cssEscape(value) {
return String(value).replace(/["\\]/g, '\\$&');
}