diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b35f20..fe399ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/source/packages/com_mokosuiteclient/admin/catalog.xml b/source/packages/com_mokosuiteclient/admin/catalog.xml index b1598f85..266125d1 100644 --- a/source/packages/com_mokosuiteclient/admin/catalog.xml +++ b/source/packages/com_mokosuiteclient/admin/catalog.xml @@ -20,15 +20,6 @@ true https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/updates.xml - - MokoSuiteHQ - pkg_mokosuitehq - package - Centralized control panel for managing all MokoSuite client installations. - icon-tachometer-alt - Platform - https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHQ/raw/branch/main/updates.xml - MokoSuiteBackup pkg_mokosuitebackup diff --git a/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php b/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php index 8571c1fd..3d1569e3 100644 --- a/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php +++ b/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php @@ -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.']; } } diff --git a/source/packages/mod_mokosuiteclient_menu/tmpl/default.php b/source/packages/mod_mokosuiteclient_menu/tmpl/default.php index 26d0157a..b6fef286 100644 --- a/source/packages/mod_mokosuiteclient_menu/tmpl/default.php +++ b/source/packages/mod_mokosuiteclient_menu/tmpl/default.php @@ -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; } } diff --git a/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php b/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php index 93f5e0ba..4eb75cbf 100644 --- a/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php +++ b/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php @@ -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', '');