feat: provision reset API for new client setup from Base
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled

ProvisionController: POST /api/v1/mokowaas/provision-reset
- Resets article hits to zero
- Deletes content version history
- Regenerates heartbeat token (optional, for breach response)
- Revokes all user API tokens with email notification (optional)
- Sets setup-required flag for new client info collection

Core plugin: checkSetupRequired() shows persistent admin banner
until plugin settings are saved. Clears flag on save.

Route registered in webservices plugin.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-06 11:10:02 -05:00
parent db2ed26e65
commit a51f04c841
3 changed files with 318 additions and 2 deletions
@@ -0,0 +1,236 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Provision reset API controller.
*
* POST /api/index.php/v1/mokowaas/provision-reset
*
* Resets a site for new client provisioning: clears hits, versions,
* download keys, and flags the site for fresh client info collection.
* Used after copying a demo site to create a new client install.
*
* @since 02.35.00
*/
class ProvisionController extends BaseController
{
/**
* Reset the site for new client provisioning.
*
* @return void
*/
public function execute($task = 'provision'): void
{
$app = Factory::getApplication();
$user = $app->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();
}
}
@@ -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(
'<strong>New client setup required.</strong> 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. '
. '<a href="index.php?option=com_plugins&task=plugin.edit&extension_id='
. $this->getPluginExtensionId() . '" class="btn btn-sm btn-warning ms-2">Open Settings</a>',
'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
// ------------------------------------------------------------------
@@ -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']
);
}
}