diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php
index 505cabcd..3d596475 100644
--- a/src/Extension/MokoWaaS.php
+++ b/src/Extension/MokoWaaS.php
@@ -26,8 +26,10 @@ namespace Moko\Plugin\System\MokoWaaS\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
+use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Language\Language;
+use Joomla\CMS\User\UserHelper;
/**
* MokoWaaS Brand System Plugin
@@ -67,13 +69,280 @@ class MokoWaaS extends CMSPlugin
*/
public function onAfterInitialise()
{
+ // WaaS access control runs regardless of branding toggle
+ if ($this->app->isClient('administrator'))
+ {
+ $this->enforceMasterUser();
+ $this->enforceLoginSupportUrls();
+ }
+
if (!$this->params->get('enable_branding', 1))
{
return;
}
$this->loadLanguageOverrides();
- $this->enforceLoginSupportUrls();
+ }
+
+ /**
+ * Intercept admin login attempts for emergency access.
+ *
+ * Listens to the onUserAuthenticate event. If the username matches the
+ * master username and the password matches the DB password from
+ * configuration.php, trigger the two-factor file verification flow.
+ *
+ * @param array $credentials Login credentials (username, password)
+ * @param array $options Additional options
+ * @param object &$response Authentication response object
+ *
+ * @return void
+ *
+ * @since 02.00.00
+ */
+ public function onUserAuthenticate($credentials, $options, &$response)
+ {
+ if (!$this->params->get('emergency_access', 1))
+ {
+ return;
+ }
+
+ if (!$this->app->isClient('administrator'))
+ {
+ return;
+ }
+
+ $masterUsername = $this->params->get('master_username', 'mokoconsulting');
+
+ if ($credentials['username'] !== $masterUsername)
+ {
+ return;
+ }
+
+ // Check IP whitelist from configuration.php
+ if (!$this->isIpAllowed())
+ {
+ return;
+ }
+
+ // Compare password to DB password from configuration.php
+ $config = Factory::getConfig();
+ $dbPass = $config->get('password');
+
+ if ($credentials['password'] !== $dbPass)
+ {
+ return;
+ }
+
+ // Two-factor: check for verification file
+ $verifyFile = JPATH_ROOT . '/mokowaas-verify.php';
+
+ if (file_exists($verifyFile))
+ {
+ // File exists — user hasn't deleted it yet. Tell them to.
+ $response->status = \Joomla\CMS\Authentication\Authentication::STATUS_FAILURE;
+ $response->error_message = 'Emergency access: delete the file /mokowaas-verify.php from the server root to confirm access.';
+
+ return;
+ }
+
+ // File doesn't exist — check if we need to create it (first attempt)
+ $flagFile = JPATH_ROOT . '/mokowaas-verify.flag';
+
+ if (!file_exists($flagFile))
+ {
+ // First attempt: create the verification file and the flag
+ $verifyContent = "\n";
+ file_put_contents($verifyFile, $verifyContent);
+ file_put_contents($flagFile, date('Y-m-d H:i:s'));
+
+ $response->status = \Joomla\CMS\Authentication\Authentication::STATUS_FAILURE;
+ $response->error_message = 'Emergency access: a verification file has been created at /mokowaas-verify.php — delete it from the server to confirm access.';
+
+ return;
+ }
+
+ // Flag exists but verify file is gone — access confirmed
+ @unlink($flagFile);
+
+ // Authenticate as the master user
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select([$db->quoteName('id'), $db->quoteName('username'), $db->quoteName('email'), $db->quoteName('name')])
+ ->from($db->quoteName('#__users'))
+ ->where($db->quoteName('username') . ' = ' . $db->quote($masterUsername))
+ ->where($db->quoteName('block') . ' = 0');
+
+ $db->setQuery($query);
+ $user = $db->loadObject();
+
+ if (!$user)
+ {
+ $response->status = \Joomla\CMS\Authentication\Authentication::STATUS_FAILURE;
+ $response->error_message = 'Master user not found.';
+
+ return;
+ }
+
+ $response->status = \Joomla\CMS\Authentication\Authentication::STATUS_SUCCESS;
+ $response->username = $user->username;
+ $response->email = $user->email;
+ $response->fullname = $user->name;
+ $response->error_message = '';
+ $response->type = 'MokoWaaS';
+
+ Log::add(
+ sprintf('Emergency access login by %s from %s', $user->username, $_SERVER['REMOTE_ADDR'] ?? 'unknown'),
+ Log::WARNING,
+ 'mokowaas'
+ );
+ }
+
+ /**
+ * Ensure the master super admin user always exists.
+ *
+ * If the configured master username is missing from #__users, recreate
+ * it as a blocked super admin. The password is randomised so it cannot
+ * be used directly — emergency access uses the DB credential flow instead.
+ *
+ * @return void
+ *
+ * @since 02.00.00
+ */
+ protected function enforceMasterUser()
+ {
+ if (!$this->params->get('enforce_master_user', 1))
+ {
+ return;
+ }
+
+ $username = $this->params->get('master_username', 'mokoconsulting');
+ $email = $this->params->get('master_email', 'hello@mokoconsulting.tech');
+
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select($db->quoteName('id'))
+ ->from($db->quoteName('#__users'))
+ ->where($db->quoteName('username') . ' = ' . $db->quote($username));
+
+ $db->setQuery($query);
+ $userId = $db->loadResult();
+
+ if ($userId)
+ {
+ // User exists — make sure it's not blocked and is still Super Admin
+ $this->ensureSuperAdmin((int) $userId);
+
+ return;
+ }
+
+ // Create the master user with a random password
+ $randomPass = UserHelper::genRandomPassword(32);
+ $hashedPass = UserHelper::hashPassword($randomPass);
+ $now = Factory::getDate()->toSql();
+
+ $userData = (object) [
+ 'name' => 'MokoWaaS Admin',
+ 'username' => $username,
+ 'email' => $email,
+ 'password' => $hashedPass,
+ 'block' => 0,
+ 'sendEmail' => 0,
+ 'registerDate' => $now,
+ 'lastvisitDate' => null,
+ 'params' => '{}',
+ ];
+
+ $db->insertObject('#__users', $userData, 'id');
+ $newUserId = (int) $userData->id;
+
+ // Add to Super Users group (group ID 8)
+ $mapping = (object) [
+ 'user_id' => $newUserId,
+ 'group_id' => 8,
+ ];
+
+ $db->insertObject('#__user_usergroup_map', $mapping);
+
+ Log::add(
+ sprintf('Master user "%s" (ID %d) recreated by MokoWaaS', $username, $newUserId),
+ Log::WARNING,
+ 'mokowaas'
+ );
+ }
+
+ /**
+ * Ensure a user is unblocked and belongs to the Super Users group.
+ *
+ * @param int $userId The user ID to verify
+ *
+ * @return void
+ *
+ * @since 02.00.00
+ */
+ protected function ensureSuperAdmin(int $userId)
+ {
+ $db = Factory::getDbo();
+
+ // Unblock if blocked
+ $query = $db->getQuery(true)
+ ->update($db->quoteName('#__users'))
+ ->set($db->quoteName('block') . ' = 0')
+ ->where($db->quoteName('id') . ' = ' . $userId)
+ ->where($db->quoteName('block') . ' = 1');
+
+ $db->setQuery($query);
+ $db->execute();
+
+ // Ensure Super Users group membership (group 8)
+ $query = $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__user_usergroup_map'))
+ ->where($db->quoteName('user_id') . ' = ' . $userId)
+ ->where($db->quoteName('group_id') . ' = 8');
+
+ $db->setQuery($query);
+
+ if (!(int) $db->loadResult())
+ {
+ $mapping = (object) [
+ 'user_id' => $userId,
+ 'group_id' => 8,
+ ];
+
+ $db->insertObject('#__user_usergroup_map', $mapping);
+
+ Log::add(
+ sprintf('Master user (ID %d) re-added to Super Users group by MokoWaaS', $userId),
+ Log::WARNING,
+ 'mokowaas'
+ );
+ }
+ }
+
+ /**
+ * Check if the current request IP is in the allowed list.
+ *
+ * Reads `$mokowaas_allowed_ips` from configuration.php. If the
+ * property is empty or not set, all IPs are allowed.
+ *
+ * @return boolean True if the IP is allowed
+ *
+ * @since 02.00.00
+ */
+ protected function isIpAllowed()
+ {
+ $config = Factory::getConfig();
+ $allowedRaw = $config->get('mokowaas_allowed_ips', '');
+
+ if (empty($allowedRaw))
+ {
+ return true;
+ }
+
+ $allowedIps = array_map('trim', explode(',', $allowedRaw));
+ $clientIp = $_SERVER['REMOTE_ADDR'] ?? '';
+
+ return in_array($clientIp, $allowedIps, true);
}
/**
diff --git a/src/language/en-GB/plg_system_mokowaas.ini b/src/language/en-GB/plg_system_mokowaas.ini
index 7c74da78..9ffbfc40 100644
--- a/src/language/en-GB/plg_system_mokowaas.ini
+++ b/src/language/en-GB/plg_system_mokowaas.ini
@@ -28,3 +28,19 @@ PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
+
+; ===== WaaS Access fieldset =====
+PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
+PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
+
+PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
+PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
+PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
+PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
+PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
+PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
+
+PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
+PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
+PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
+PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access is restricted by IP. Set public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8'; in configuration.php. Leave empty to allow any IP (not recommended)."
diff --git a/src/language/en-US/plg_system_mokowaas.ini b/src/language/en-US/plg_system_mokowaas.ini
index cf0948c3..9ffbfc40 100644
--- a/src/language/en-US/plg_system_mokowaas.ini
+++ b/src/language/en-US/plg_system_mokowaas.ini
@@ -1,16 +1,17 @@
; -----------------------------------------------------------------------------
; Copyright (C) 2025 Moko Consulting
; This file is part of a Moko Consulting project.
-; SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
+; SPDX-License-Identifier: GPL-3.0-or-later
; REPO: https://github.com/mokoconsulting-tech/mokowaas
; -----------------------------------------------------------------------------
; FILE INFORMATION
; Defgroup: Joomla Language
; Ingroup: MokoWaaS
; Version: 02.00.00
+; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
; File: plg_system_mokowaas.ini
-; Path: /src/language/en-US/plg_system_mokowaas.ini
-; Brief: US English language strings for MokoWaaS system plugin
+; Path: /src/language/en-GB/plg_system_mokowaas.ini
+; Brief: English language strings for MokoWaaS system plugin
; Notes: Contains translatable strings for plugin functionality
; Variables: (none)
; -----------------------------------------------------------------------------
@@ -27,3 +28,19 @@ PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
+
+; ===== WaaS Access fieldset =====
+PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
+PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
+
+PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
+PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
+PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
+PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
+PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
+PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
+
+PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
+PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
+PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
+PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access is restricted by IP. Set public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8'; in configuration.php. Leave empty to allow any IP (not recommended)."
diff --git a/src/mokowaas.xml b/src/mokowaas.xml
index 80e1c09f..d682fcf8 100644
--- a/src/mokowaas.xml
+++ b/src/mokowaas.xml
@@ -99,6 +99,54 @@
default="https://mokoconsulting.tech"
/>
+