diff --git a/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php
index a6e288f6..edb7d03f 100644
--- a/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php
+++ b/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php
@@ -363,124 +363,68 @@ class DisplayController extends BaseController
$app->close();
}
- $input = Factory::getApplication()->getInput();
+ // ==================================================================
+ // Support PIN
+ // ==================================================================
- $this->jsonResponse($this->getModel('Tickets')->updateStatus(
- $input->getInt('ticket_id', 0),
- $input->getInt('status', 0)
- ));
- }
+ public function requestPin()
+ {
+ Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
- $id = Factory::getApplication()->getInput()->getInt('id', 0);
- $this->jsonResponse($this->getModel('Tickets')->deletePriority($id));
- }
+ if (!$this->checkAcl('mokosuiteclient.dashboard'))
+ {
+ $this->jsonForbidden();
+ return;
+ }
try
{
- $db = Factory::getDbo();
- $escaped = $db->quote('%' . $db->escape($query, true) . '%');
-
- $results = $db->setQuery(
+ $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
+ $db->setQuery(
$db->getQuery(true)
- ->select([$db->quoteName('l.title'), $db->quoteName('l.url'), $db->quoteName('l.description')])
- ->from($db->quoteName('#__finder_links', 'l'))
- ->where($db->quoteName('l.published') . ' = 1')
- ->where('(' . $db->quoteName('l.title') . ' LIKE ' . $escaped
- . ' OR ' . $db->quoteName('l.description') . ' LIKE ' . $escaped . ')')
- ->order($db->quoteName('l.title') . ' ASC')
- ->setLimit(8)
- )->loadObjectList() ?: [];
+ ->select([$db->quoteName('extension_id'), $db->quoteName('params')])
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
+ );
+ $ext = $db->loadObject();
- foreach ($results as $r)
+ if (!$ext)
{
- $r->description = mb_substr(strip_tags($r->description ?? ''), 0, 150);
+ $this->jsonResponse(['success' => false, 'message' => 'Core plugin not found.']);
+ return;
}
- $this->jsonResponse(['results' => $results]);
+ $params = json_decode($ext->params, true) ?: [];
+ $token = $params['health_api_token'] ?? '';
+
+ if (empty($token))
+ {
+ $this->jsonResponse(['success' => false, 'message' => 'Health token not configured.']);
+ return;
+ }
+
+ $now = time();
+ $params['support_pin_requested_at'] = $now;
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
+ ->where($db->quoteName('extension_id') . ' = ' . (int) $ext->extension_id)
+ )->execute();
+
+ $pinTtl = 72 * 3600;
+ $window = floor($now / $pinTtl);
+ $hash = hash_hmac('sha256', (string) $window, $token);
+ $pin = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
+
+ $this->jsonResponse(['success' => true, 'pin' => $pin, 'message' => 'PIN generated — valid for 72 hours.']);
}
catch (\Throwable $e)
{
- Log::add('KB search failed: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
- $this->jsonResponse(['results' => [], 'error' => 'Search unavailable']);
- }
- }
-
- // ==================================================================
- // Maintenance (#127, #128)
- // ==================================================================
-
- public function optimizeDb()
- {
- Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
- if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
- $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
- $this->jsonResponse($model->optimizeTables());
- }
-
- public function repairDb()
- {
- Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
- if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
- $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
- $this->jsonResponse($model->repairTables());
- }
-
- public function purgeSessions()
- {
- Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
- if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
- $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
- $this->jsonResponse($model->purgeSessions());
- }
-
- public function cleanDirectory()
- {
- Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
- if (!$this->checkAcl('mokosuiteclient.cache')) { $this->jsonForbidden(); return; }
- $dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
- $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
- $this->jsonResponse($model->cleanDirectory($dirKey));
- }
-
- $input = Factory::getApplication()->getInput();
-
- $this->jsonResponse($this->getModel('Tickets')->updateStatus(
- $input->getInt('ticket_id', 0),
- $input->getInt('status', 0)
- ));
- }
-
- $id = Factory::getApplication()->getInput()->getInt('id', 0);
- $this->jsonResponse($this->getModel('Tickets')->deletePriority($id));
- }
-
- try
- {
- $db = Factory::getDbo();
- $escaped = $db->quote('%' . $db->escape($query, true) . '%');
-
- $results = $db->setQuery(
- $db->getQuery(true)
- ->select([$db->quoteName('l.title'), $db->quoteName('l.url'), $db->quoteName('l.description')])
- ->from($db->quoteName('#__finder_links', 'l'))
- ->where($db->quoteName('l.published') . ' = 1')
- ->where('(' . $db->quoteName('l.title') . ' LIKE ' . $escaped
- . ' OR ' . $db->quoteName('l.description') . ' LIKE ' . $escaped . ')')
- ->order($db->quoteName('l.title') . ' ASC')
- ->setLimit(8)
- )->loadObjectList() ?: [];
-
- foreach ($results as $r)
- {
- $r->description = mb_substr(strip_tags($r->description ?? ''), 0, 150);
- }
-
- $this->jsonResponse(['results' => $results]);
- }
- catch (\Throwable $e)
- {
- Log::add('KB search failed: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
- $this->jsonResponse(['results' => [], 'error' => 'Search unavailable']);
+ $this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
}
}
diff --git a/source/packages/com_mokosuiteclient/admin/src/View/Dashboard/HtmlView.php b/source/packages/com_mokosuiteclient/admin/src/View/Dashboard/HtmlView.php
index 322ba7b1..8eef3af2 100644
--- a/source/packages/com_mokosuiteclient/admin/src/View/Dashboard/HtmlView.php
+++ b/source/packages/com_mokosuiteclient/admin/src/View/Dashboard/HtmlView.php
@@ -27,6 +27,7 @@ class HtmlView extends BaseHtmlView
protected $loginChartData = [];
protected $mokoExtensions = [];
public $supportPin = '';
+ public $supportPinAvailable = false;
public function display($tpl = null)
{
@@ -47,12 +48,21 @@ class HtmlView extends BaseHtmlView
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
);
- $token = (json_decode((string) $db->loadResult()))->health_api_token ?? '';
+ $coreParams = json_decode((string) $db->loadResult());
+ $healthToken = $coreParams->health_api_token ?? '';
+ $this->supportPinAvailable = !empty($healthToken);
- if (!empty($token))
+ if (!empty($healthToken))
{
- $hash = hash_hmac('sha256', gmdate('Y-m-d'), $token);
- $this->supportPin = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
+ $pinRequestedAt = $coreParams->support_pin_requested_at ?? '';
+ $pinTtl = 72 * 3600;
+
+ if (!empty($pinRequestedAt) && (time() - (int) $pinRequestedAt) < $pinTtl)
+ {
+ $window = floor((int) $pinRequestedAt / $pinTtl);
+ $hash = hash_hmac('sha256', (string) $window, $healthToken);
+ $this->supportPin = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
+ }
}
}
catch (\Throwable $e) {}
diff --git a/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php b/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php
index b878b2c5..739f72dd 100644
--- a/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php
+++ b/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php
@@ -57,13 +57,20 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
escape($siteInfo->sitename); ?>
MokoSuite escape($siteInfo->mokosuiteclient_version); ?>
supportPin)): ?>
- escape($this->supportPin); ?>
+ escape($this->supportPin); ?>
+ supportPinAvailable)): ?>
+
Joomla escape($siteInfo->joomla_version); ?>
PHP escape($siteInfo->php_version); ?>
diff --git a/source/packages/com_mokosuiteclient/media/js/dashboard.js b/source/packages/com_mokosuiteclient/media/js/dashboard.js
index 065fde22..625c218d 100644
--- a/source/packages/com_mokosuiteclient/media/js/dashboard.js
+++ b/source/packages/com_mokosuiteclient/media/js/dashboard.js
@@ -144,6 +144,43 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
+ // Request PIN button
+ var pinBtn = document.getElementById('mokosuiteclient-request-pin');
+ if (pinBtn) {
+ pinBtn.addEventListener('click', function () {
+ var btn = this;
+ btn.disabled = true;
+ btn.textContent = '...';
+ var fd = new FormData();
+ fd.append(btn.dataset.token, '1');
+ fetch(btn.dataset.url, {method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}})
+ .then(function (r) { return r.json(); })
+ .then(function (d) {
+ if (d.success && d.pin) {
+ var badge = document.createElement('span');
+ badge.className = 'badge bg-dark';
+ badge.style.cssText = 'font-family:monospace;letter-spacing:0.08em;cursor:help;';
+ badge.title = 'Support PIN — valid for 72 hours';
+ badge.textContent = d.pin;
+ var icon = document.createElement('span');
+ icon.className = 'icon-key small me-1';
+ icon.setAttribute('aria-hidden', 'true');
+ badge.prepend(icon);
+ btn.replaceWith(badge);
+ } else {
+ Joomla.renderMessages({error: [d.message || 'Failed to generate PIN']});
+ btn.disabled = false;
+ btn.textContent = 'Request PIN';
+ }
+ })
+ .catch(function () {
+ Joomla.renderMessages({error: ['Network error']});
+ btn.disabled = false;
+ btn.textContent = 'Request PIN';
+ });
+ });
+ }
+
// Akeeba import buttons
['btn-import-admintools', 'btn-import-ats-dash'].forEach(function(id) {
var btn = document.getElementById(id);
diff --git a/source/packages/mod_mokosuiteclient_cpanel/src/Dispatcher/Dispatcher.php b/source/packages/mod_mokosuiteclient_cpanel/src/Dispatcher/Dispatcher.php
index a3e5ffcf..d652da6d 100644
--- a/source/packages/mod_mokosuiteclient_cpanel/src/Dispatcher/Dispatcher.php
+++ b/source/packages/mod_mokosuiteclient_cpanel/src/Dispatcher/Dispatcher.php
@@ -47,8 +47,9 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
$data['currentIp'] = $helper->getCurrentIp();
$data['ssl'] = $helper->getSslStatus();
- // Daily support PIN derived from health token + today's date (UTC)
+ // Support PIN — only shown if requested within last 72 hours
$data['supportPin'] = '';
+ $data['supportPinAvailable'] = false;
try
{
@@ -65,9 +66,17 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
if (!empty($token))
{
- $date = gmdate('Y-m-d');
- $hash = hash_hmac('sha256', $date, $token);
- $data['supportPin'] = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
+ $data['supportPinAvailable'] = true;
+ $pinRequestedAt = $coreParams->support_pin_requested_at ?? '';
+ $pinTtl = 72 * 3600; // 72 hours
+
+ if (!empty($pinRequestedAt) && (time() - (int) $pinRequestedAt) < $pinTtl)
+ {
+ // PIN is active — generate from the request timestamp (stable for 72h window)
+ $window = floor((int) $pinRequestedAt / $pinTtl);
+ $hash = hash_hmac('sha256', (string) $window, $token);
+ $data['supportPin'] = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
+ }
}
}
catch (\Throwable $e) {}
diff --git a/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php b/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php
index 29412f9e..4263c6ea 100644
--- a/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php
+++ b/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php
@@ -71,7 +71,14 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
sitename ?? ''); ?>
MokoSuite mokosuiteclient_version ?? ''); ?>
-
+
+
+
Joomla joomla_version ?? ''); ?>
PHP php_version ?? ''); ?>
@@ -88,3 +95,37 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
+