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:
co-authored by
Claude Opus 5
parent
852c6ece5c
commit
db4ca7dd7a
@@ -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 & 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, '\\$&');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user