payfrit-api/cron/expireStaleChats.php
Schwifty 601245d969 fix: harden auth middleware — exact route matching, remove admin bypass, add cron secret
1. Switch str_contains() to exact match ($path === $route) in PUBLIC_ROUTES check
   to prevent substring-based route bypass attacks.

2. Remove blanket /api/admin/ bypass that was letting all admin endpoints through
   without authentication.

3. Add requireCronSecret() — cron/scheduled task endpoints now require a valid
   X-Cron-Secret header matching the PAYFRIT_CRON_SECRET env var. Uses
   hash_equals() for timing-safe comparison. Applied to:
   - cron/expireStaleChats.php
   - cron/expireTabs.php
   - api/admin/scheduledTasks/runDue.php
2026-03-23 01:43:43 +00:00

53 lines
1.6 KiB
PHP

<?php
require_once __DIR__ . '/../api/helpers.php';
requireCronSecret();
/**
* Expire stale chats (older than 20 minutes with no recent activity).
* Called every minute by cron.
*/
try {
$staleChats = queryTimed("
SELECT t.ID, t.CreatedOn,
(SELECT MAX(cm.CreatedOn) FROM ChatMessages cm WHERE cm.TaskID = t.ID) AS LastMessageOn
FROM Tasks t
WHERE t.TaskTypeID = 2
AND t.CompletedOn IS NULL
AND t.CreatedOn < DATE_SUB(NOW(), INTERVAL 20 MINUTE)
");
$expiredCount = 0;
$expiredIds = [];
foreach ($staleChats as $chat) {
$shouldExpire = false;
if (empty($chat['LastMessageOn'])) {
$shouldExpire = true;
} else {
$lastMsg = new DateTime($chat['LastMessageOn'], new DateTimeZone('UTC'));
$now = new DateTime('now', new DateTimeZone('UTC'));
$diffMinutes = ($now->getTimestamp() - $lastMsg->getTimestamp()) / 60;
if ($diffMinutes > 20) {
$shouldExpire = true;
}
}
if ($shouldExpire) {
queryTimed("UPDATE Tasks SET CompletedOn = NOW() WHERE ID = ?", [$chat['ID']]);
$expiredCount++;
$expiredIds[] = (int) $chat['ID'];
}
}
jsonResponse([
'OK' => true,
'MESSAGE' => "Expired {$expiredCount} stale chat(s)",
'EXPIRED_TASK_IDS' => $expiredIds,
'CHECKED_COUNT' => count($staleChats),
]);
} catch (Exception $e) {
jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage()]);
}