feat: support PIN on-demand with 72-hour TTL + controller cleanup
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 41s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m1s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m2s
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 41s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m1s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m2s
- PIN hidden by default — "Request PIN" button generates on click - PIN valid for 72 hours (stored as support_pin_requested_at in params) - HMAC uses 72h window instead of daily date for stability - requestPin() AJAX endpoint in controller stores timestamp + returns PIN - Applied to both dashboard info bar and cpanel module - Dashboard JS handles PIN request with badge replacement - Cpanel JS handles same with inline script - Fixed orphaned ticket code fragments in controller (syntax error) - Removed duplicate maintenance section
This commit is contained in:
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -57,13 +57,20 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
|
||||
<span class="fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
|
||||
<span class="badge bg-primary">MokoSuite <?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span>
|
||||
<?php if (!empty($this->supportPin)): ?>
|
||||
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span>
|
||||
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Support PIN — valid for 72 hours"><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-1" id="mokosuiteclient-btn-heartbeat-pin"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.sendHeartbeat&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>"
|
||||
title="Send heartbeat with PIN to MokoSuiteHQ">
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
</button>
|
||||
<?php elseif (!empty($this->supportPinAvailable)): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2" id="mokosuiteclient-request-pin"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>"
|
||||
style="font-size:0.75rem;" title="Request a support PIN (valid 72 hours)">
|
||||
<span class="icon-key" aria-hidden="true"></span> Request PIN
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<span class="badge bg-secondary">Joomla <?php echo $this->escape($siteInfo->joomla_version); ?></span>
|
||||
<span class="badge bg-secondary">PHP <?php echo $this->escape($siteInfo->php_version); ?></span>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -71,7 +71,14 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
|
||||
<span class="fw-bold"><?php echo htmlspecialchars($siteInfo->sitename ?? ''); ?></span>
|
||||
<span class="badge bg-primary">MokoSuite <?php echo htmlspecialchars($siteInfo->mokosuiteclient_version ?? ''); ?></span>
|
||||
<?php if (!empty($supportPin)): ?>
|
||||
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo htmlspecialchars($supportPin); ?></span>
|
||||
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Support PIN — valid for 72 hours"><span class="icon-key small me-1" aria-hidden="true"></span><?php echo htmlspecialchars($supportPin); ?></span>
|
||||
<?php elseif (!empty($supportPinAvailable)): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2" id="mokosuiteclient-request-pin"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json'); ?>"
|
||||
data-token="<?php echo Session::getFormToken(); ?>"
|
||||
style="font-size:0.75rem;" title="Request a support PIN (valid 72 hours)">
|
||||
<span class="icon-key" aria-hidden="true"></span> Request PIN
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<span class="badge bg-secondary">Joomla <?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?></span>
|
||||
<span class="badge bg-secondary">PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
|
||||
@@ -88,3 +95,37 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var btn = document.getElementById('mokosuiteclient-request-pin');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function() {
|
||||
var el = this;
|
||||
el.disabled = true;
|
||||
el.textContent = '...';
|
||||
var fd = new FormData();
|
||||
fd.append(el.dataset.token, '1');
|
||||
fetch(el.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 = 'font-family:monospace;letter-spacing:0.08em;cursor:help;';
|
||||
badge.title = 'Support PIN — valid for 72 hours';
|
||||
badge.innerHTML = '<span class="icon-key small me-1" aria-hidden="true"></span>' + d.pin;
|
||||
el.replaceWith(badge);
|
||||
} else {
|
||||
Joomla.renderMessages({error:[d.message||'Failed to generate PIN']});
|
||||
el.disabled = false;
|
||||
el.innerHTML = '<span class="icon-key" aria-hidden="true"></span> Request PIN';
|
||||
}
|
||||
})
|
||||
.catch(function(){
|
||||
Joomla.renderMessages({error:['Network error']});
|
||||
el.disabled = false;
|
||||
el.innerHTML = '<span class="icon-key" aria-hidden="true"></span> Request PIN';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user