Fix client portal review findings: PIN wipe, log indexing, statement currency (db 2.7.8)

Setting a PIN containing < or > silently cleared it: the length check ran before escapeSql(), whose strip_tags() then emptied the value, and the UPDATE stored the blank while flashing success. Length is now checked after sanitising.

Password and PIN changes require the current password. SSO contacts are exempt - no local password to check, and the IdP already did it.

New index on logs(log_user_id, log_client_id) for the portal profile and activity pages, which were scanning the whole table twice per profile view. admin/audit_logs.php's date filter rewritten as a half-open range so KEY log_created_at is usable - DATE(log_created_at) BETWEEN made it non-sargable.

Portal statement page and PDF now render in the client's currency, matching the guest view and the emailed statement.

Quick Send asks for confirmation; confirm-link extended to submit buttons.

Portal audit entries logged an empty name - client/post.php used , which only exists agent-side.
This commit is contained in:
johnnyq
2026-08-28 16:38:42 -04:00
parent 820103460f
commit 156c51224d
12 changed files with 213 additions and 36 deletions

View File

@@ -46,6 +46,14 @@ if (isset($_GET['action']) & !empty($_GET['action'])) {
$action_filter = '';
}
/*
* The date filter is a half-open range rather than DATE(log_created_at) BETWEEN
* ... - wrapping the column in a function makes the comparison non-sargable, so
* KEY log_created_at could never be used and every page view scanned the whole
* table. '< dtt + 1 day' keeps the end date inclusive, which BETWEEN on a bare
* DATETIME would not: '2026-08-28' alone means midnight, and would drop that
* day's rows.
*/
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS client_id, client_name, log_action, log_created_at, log_description, log_entity_id, log_id,
@@ -53,7 +61,7 @@ $sql = mysqli_query(
LEFT JOIN users ON log_user_id = user_id
LEFT JOIN clients ON log_client_id = client_id
WHERE (log_type LIKE '%$q%' OR log_action LIKE '%$q%' OR log_description LIKE '%$q%' OR log_ip LIKE '%$q%' OR log_user_agent LIKE '%$q%' OR user_name LIKE '%$q%' OR client_name LIKE '%$q%')
AND DATE(log_created_at) BETWEEN '$dtf' AND '$dtt'
AND log_created_at >= '$dtf 00:00:00' AND log_created_at < DATE_ADD('$dtt', INTERVAL 1 DAY)
$user_query
$client_query
$log_type_query

View File

@@ -0,0 +1,38 @@
<?php
/*
* ITFlow - Database update to version 2.7.8 (from 2.7.7)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// The logs table only ever had KEY log_created_at, which is the column the
// one page that reads it does NOT filter on usefully. Two shapes now do:
//
// 1. The client portal. profile.php shows a contact their recent sign-ins
// and recent actions, and activity.php lists the lot - all filtered on
// log_user_id + log_client_id. That is two full scans on every profile
// view of a table that grows forever and is never pruned.
//
// 2. admin/audit_logs.php, which filters a date range. It had an index
// available the whole time and could not use it, because wrapping the
// column in DATE() makes the comparison non-sargable. That query is
// rewritten as a half-open range in the same commit as this migration,
// so KEY log_created_at finally does its job.
//
// log_user_id leads the composite: a single user is a small slice of the
// table, whereas one client can account for most of it on a single-client
// install. Same selectivity rule as the 2.7.5 and 2.7.6 passes.
$itflow_index_exists = mysqli_query($mysqli, "SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'logs'
AND INDEX_NAME = 'log_user_id'
LIMIT 1");
if (!$itflow_index_exists || mysqli_num_rows($itflow_index_exists) === 0) {
mysqli_query($mysqli, "ALTER TABLE `logs` ADD KEY `log_user_id` (`log_user_id`, `log_client_id`)");
}
unset($itflow_index_exists);