diff --git a/source/packages/com_mokowaas/api/src/Controller/ProvisionController.php b/source/packages/com_mokowaas/api/src/Controller/ProvisionController.php
new file mode 100644
index 00000000..8f66a1c5
--- /dev/null
+++ b/source/packages/com_mokowaas/api/src/Controller/ProvisionController.php
@@ -0,0 +1,236 @@
+getIdentity();
+
+ if (!$user->authorise('core.manage', 'com_mokowaas'))
+ {
+ $this->sendJson(403, ['error' => 'Not authorized']);
+
+ return;
+ }
+
+ if ($app->input->getMethod() !== 'POST')
+ {
+ $this->sendJson(405, ['error' => 'POST required']);
+
+ return;
+ }
+
+ $db = Factory::getDbo();
+ $results = [];
+
+ // 1. Reset article hit counters
+ try
+ {
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__content'))
+ ->set($db->quoteName('hits') . ' = 0')
+ )->execute();
+ $results['hits_reset'] = $db->getAffectedRows();
+ }
+ catch (\Throwable $e)
+ {
+ $results['hits_reset'] = 'error: ' . $e->getMessage();
+ }
+
+ // 2. Delete content version history
+ try
+ {
+ $db->setQuery(
+ $db->getQuery(true)->delete($db->quoteName('#__history'))
+ )->execute();
+ $results['versions_deleted'] = $db->getAffectedRows();
+ }
+ catch (\Throwable $e)
+ {
+ $results['versions_deleted'] = 'error: ' . $e->getMessage();
+ }
+
+ // 3. Regenerate heartbeat token if requested
+ $input = $app->getInput()->json;
+ $resetToken = (bool) ($input->get('reset_token', false, 'BOOLEAN'));
+
+ if ($resetToken)
+ {
+ try
+ {
+ $newToken = bin2hex(random_bytes(32));
+
+ $plugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokowaas');
+
+ if ($plugin)
+ {
+ $pluginParams = new \Joomla\Registry\Registry($plugin->params);
+ $pluginParams->set('health_api_token', $newToken);
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('params') . ' = ' . $db->quote($pluginParams->toString()))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
+ )->execute();
+
+ $results['token_regenerated'] = true;
+ $results['new_token'] = $newToken;
+ }
+ }
+ catch (\Throwable $e)
+ {
+ $results['token_regenerated'] = 'error: ' . $e->getMessage();
+ }
+ }
+
+ // 4. Reset all user API tokens if requested
+ $resetApiTokens = (bool) ($input->get('reset_api_tokens', false, 'BOOLEAN'));
+
+ if ($resetApiTokens)
+ {
+ try
+ {
+ // Get users who have API tokens before deleting
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select('DISTINCT ' . $db->quoteName('user_id'))
+ ->from($db->quoteName('#__user_keys'))
+ ->where($db->quoteName('series') . ' LIKE ' . $db->quote('api-%'))
+ );
+ $affectedUserIds = $db->loadColumn() ?: [];
+
+ $db->setQuery(
+ $db->getQuery(true)->delete($db->quoteName('#__user_keys'))
+ ->where($db->quoteName('series') . ' LIKE ' . $db->quote('api-%'))
+ )->execute();
+ $results['api_tokens_revoked'] = $db->getAffectedRows();
+
+ // Notify affected users
+ if (!empty($affectedUserIds))
+ {
+ $this->notifyTokenReset($db, $affectedUserIds);
+ $results['users_notified'] = \count($affectedUserIds);
+ }
+ }
+ catch (\Throwable $e)
+ {
+ $results['api_tokens_revoked'] = 'error: ' . $e->getMessage();
+ }
+ }
+
+ // 5. Flag site for fresh client info setup
+ try
+ {
+ // Write a flag file that the core plugin checks on next admin load
+ $flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
+ file_put_contents($flagFile, json_encode([
+ 'created' => gmdate('Y-m-d\TH:i:s\Z'),
+ 'reason' => 'provision-reset',
+ 'remote_ip' => $_SERVER['REMOTE_ADDR'] ?? '',
+ ]));
+ $results['setup_flag'] = true;
+ }
+ catch (\Throwable $e)
+ {
+ $results['setup_flag'] = 'error: ' . $e->getMessage();
+ }
+
+ $this->sendJson(200, [
+ 'status' => 'ok',
+ 'message' => 'Site provisioned for new client.',
+ 'results' => $results,
+ ]);
+ }
+
+ /**
+ * Notify users that their API tokens have been revoked.
+ */
+ private function notifyTokenReset($db, array $userIds): void
+ {
+ try
+ {
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select([$db->quoteName('name'), $db->quoteName('email')])
+ ->from($db->quoteName('#__users'))
+ ->whereIn($db->quoteName('id'), $userIds)
+ ->where($db->quoteName('block') . ' = 0')
+ );
+ $users = $db->loadObjectList() ?: [];
+
+ $config = Factory::getConfig();
+ $siteName = $config->get('sitename', 'Joomla');
+ $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
+
+ $mailer = Factory::getMailer();
+
+ foreach ($users as $u)
+ {
+ try
+ {
+ $mailer->clearAllRecipients();
+ $mailer->addRecipient($u->email, $u->name);
+ $mailer->setSubject($siteName . ' — API tokens have been reset');
+ $mailer->setBody(
+ "Hello {$u->name},\n\n"
+ . "Your API access tokens on {$siteName} have been revoked by an administrator.\n\n"
+ . "If you use API integrations, please log in and generate a new token:\n"
+ . "{$siteUrl}/administrator/\n\n"
+ . "— {$siteName}"
+ );
+ $mailer->send();
+ }
+ catch (\Throwable $e)
+ {
+ // Non-critical
+ }
+ }
+ }
+ catch (\Throwable $e)
+ {
+ // Non-critical
+ }
+ }
+
+ private function sendJson(int $code, array $data): void
+ {
+ http_response_code($code);
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($data, JSON_UNESCAPED_SLASHES);
+ Factory::getApplication()->close();
+ }
+}
diff --git a/source/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/source/packages/plg_system_mokowaas/Extension/MokoWaaS.php
index c61e0b48..55918988 100644
--- a/source/packages/plg_system_mokowaas/Extension/MokoWaaS.php
+++ b/source/packages/plg_system_mokowaas/Extension/MokoWaaS.php
@@ -167,10 +167,11 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
$this->handleMokoApi($mokoAction);
}
- // One-time remote login (admin only)
+ // Admin-only features
if ($this->app->isClient('administrator'))
{
$this->handleOneTimeLogin();
+ $this->checkSetupRequired();
$this->preserveDownloadKeys();
}
}
@@ -242,7 +243,14 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
// Grafana auto-provisioning
$this->handleGrafanaProvisioning($params, $app);
- // NOTE: reset_hits and delete_versions now handled by devtools plugin
+ // Clear setup-required flag on save (new client setup complete)
+ $flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
+
+ if (file_exists($flagFile))
+ {
+ @unlink($flagFile);
+ $app->enqueueMessage('Client setup complete — setup flag cleared.', 'message');
+ }
if ($changed)
{
@@ -2053,6 +2061,72 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
return $this->masterNames;
}
+ // ------------------------------------------------------------------
+ // Setup Required Check
+ // ------------------------------------------------------------------
+
+ /**
+ * Check if the site has been provisioned for a new client and needs
+ * fresh setup information (company name, contact details).
+ *
+ * Shows a persistent admin banner until the setup flag is cleared
+ * by saving the core plugin settings.
+ *
+ * @return void
+ *
+ * @since 02.35.00
+ */
+ protected function checkSetupRequired(): void
+ {
+ $flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
+
+ if (!file_exists($flagFile))
+ {
+ return;
+ }
+
+ $this->app->enqueueMessage(
+ 'New client setup required. This site has been provisioned for a new client. '
+ . 'Please update the site name, contact details, and save the MokoWaaS plugin settings to complete setup. '
+ . 'Open Settings',
+ 'warning'
+ );
+ }
+
+ /**
+ * Get this plugin's extension_id.
+ */
+ private function getPluginExtensionId(): int
+ {
+ static $id = null;
+
+ if ($id !== null)
+ {
+ return $id;
+ }
+
+ try
+ {
+ $db = Factory::getDbo();
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select($db->quoteName('extension_id'))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
+ );
+ $id = (int) $db->loadResult();
+ }
+ catch (\Throwable $e)
+ {
+ $id = 0;
+ }
+
+ return $id;
+ }
+
// ------------------------------------------------------------------
// One-Time Remote Login
// ------------------------------------------------------------------
diff --git a/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php b/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php
index d929dd97..37235506 100644
--- a/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php
+++ b/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php
@@ -118,5 +118,11 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface
'remotelogin',
['component' => 'com_mokowaas']
);
+
+ $router->createCRUDRoutes(
+ 'v1/mokowaas/provision-reset',
+ 'provision',
+ ['component' => 'com_mokowaas']
+ );
}
}