feat: HQ config sync, menu language fix, catalog cleanup
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 54s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled

Client stores HQ-configured support_pin_hours from heartbeat response.
PIN TTL now configurable from HQ instead of hardcoded 72h. Admin sidebar
menu loads component-local language files fixing untranslated keys for
MokoSuiteCross. Removed MokoSuiteHQ from extension catalog.
This commit is contained in:
2026-06-25 10:24:52 -05:00
parent 4b21c43e56
commit 81a95e6e23
5 changed files with 77 additions and 18 deletions
+6
View File
@@ -28,6 +28,12 @@
- **Frontend link in status bar** — cache/temp module now has 4 buttons: Site (frontend link), PIN, Cache, Temp
- **Help buttons** — all admin views link to Gitea wiki pages via toolbar help button
- **Support PIN in heartbeat** — core system plugin includes current PIN in heartbeat payload to HQ
- **HQ config sync** — client stores HQ-configured `support_pin_hours` from heartbeat response, PIN TTL now configurable from HQ
### Changed
- Admin sidebar menu module now loads component-local language files (fixes untranslated keys for MokoSuiteCross and other components)
- Support PIN TTL is now configurable via HQ global options instead of hardcoded 72 hours
- Removed MokoSuiteHQ from extension catalog (internal app, not for client sites)
- **SupportPinHelper** — shared helper centralises PIN generation across dashboard, cpanel module, cache module, and AJAX controller
- **Current IP display** — firewall plugin settings show admin's IP with copy button
- **Heartbeat monitor** — consolidated into core plugin from retired monitor plugin, with diagnostic logging on all bail-out points
@@ -20,15 +20,6 @@
<protected>true</protected>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteHQ</name>
<element>pkg_mokosuitehq</element>
<type>package</type>
<description>Centralized control panel for managing all MokoSuite client installations.</description>
<icon>icon-tachometer-alt</icon>
<category>Platform</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHQ/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteBackup</name>
<element>pkg_mokosuitebackup</element>
@@ -23,8 +23,8 @@ use Joomla\Database\DatabaseInterface;
*/
class SupportPinHelper
{
/** @var int PIN validity window in seconds (72 hours) */
public const PIN_TTL = 72 * 3600;
/** @var int Default PIN validity window in seconds (72 hours) */
public const PIN_TTL_DEFAULT = 72 * 3600;
/**
* Load core plugin params and return PIN state.
@@ -74,11 +74,12 @@ class SupportPinHelper
$result['available'] = true;
$pinTtl = (int) ($params['support_pin_hours'] ?? 0) * 3600 ?: self::PIN_TTL_DEFAULT;
$requestedAt = (int) ($params['support_pin_requested_at'] ?? 0);
if ($requestedAt && (time() - $requestedAt) < self::PIN_TTL)
if ($requestedAt && (time() - $requestedAt) < $pinTtl)
{
$result['pin'] = self::generate($token, $requestedAt);
$result['pin'] = self::generate($token, $requestedAt, $pinTtl);
}
}
catch (\Throwable $e)
@@ -94,12 +95,14 @@ class SupportPinHelper
*
* @param string $token Health API token (HMAC key).
* @param int $timestamp The request timestamp.
* @param int $ttl PIN validity window in seconds.
*
* @return string e.g. "MOKO-A1B2-C3D4"
*/
public static function generate(string $token, int $timestamp): string
public static function generate(string $token, int $timestamp, int $ttl = 0): string
{
$window = floor($timestamp / self::PIN_TTL);
$ttl = $ttl ?: self::PIN_TTL_DEFAULT;
$window = floor($timestamp / $ttl);
$hash = hash_hmac('sha256', (string) $window, $token);
return 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
@@ -121,7 +124,7 @@ class SupportPinHelper
return ['success' => false, 'message' => 'Health token not configured.'];
}
$now = time();
$now = time();
$params = $state['params'];
$params['support_pin_requested_at'] = $now;
@@ -132,8 +135,10 @@ class SupportPinHelper
$db->setQuery($query)->execute();
$pin = self::generate($state['token'], $now);
$pinHours = (int) ($params['support_pin_hours'] ?? 0) ?: (int) (self::PIN_TTL_DEFAULT / 3600);
$pinTtl = $pinHours * 3600;
$pin = self::generate($state['token'], $now, $pinTtl);
return ['success' => true, 'pin' => $pin, 'message' => 'PIN generated — valid for 72 hours.'];
return ['success' => true, 'pin' => $pin, 'message' => 'PIN generated — valid for ' . $pinHours . ' hours.'];
}
}
@@ -65,6 +65,15 @@ try
{
$lang->load($m->element . '.sys', JPATH_ADMINISTRATOR);
$lang->load($m->element, JPATH_ADMINISTRATOR);
// Also try component-local language path (Joomla 5/6 pattern)
$compLangPath = JPATH_ADMINISTRATOR . '/components/' . $m->element;
if (is_dir($compLangPath . '/language'))
{
$lang->load($m->element . '.sys', $compLangPath);
$lang->load($m->element, $compLangPath);
}
$loadedLangs[$m->element] = true;
}
}
@@ -2743,6 +2743,13 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
if ($response->code >= 200 && $response->code < 300)
{
$body = json_decode($response->body, true);
if (!empty($body['config']['support_pin_hours']))
{
$this->syncHqConfig('support_pin_hours', (int) $body['config']['support_pin_hours']);
}
$this->app->enqueueMessage('MokoSuiteHQ heartbeat: site registered successfully.', 'message');
}
else
@@ -2763,6 +2770,47 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
}
}
private function syncHqConfig(string $key, $value): void
{
try
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$query = $db->getQuery(true)
->select($db->quoteName(['extension_id', '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->setQuery($query)->loadObject();
if (!$ext)
{
return;
}
$params = json_decode($ext->params, true) ?: [];
if (($params[$key] ?? null) === $value)
{
return;
}
$params[$key] = $value;
$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();
}
catch (\Throwable $e)
{
// Config sync is non-critical
}
}
private function signHeartbeatRequest(string $domain, int $timestamp, string $token): ?string
{
$signingKeyB64 = $this->params->get('monitor_signing_key', '');