diff --git a/README.md b/README.md
index 3f0fb40..bf35bfe 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
OIDC SSO, TOTP and passkey second-factor authentication for **Bludit 3.22.0**.
-Current version: **1.0.1**.
+Current version: **1.0.2**.
## Features
@@ -10,6 +10,7 @@ Current version: **1.0.1**.
- Adds a full-width, slate-grey **Sign in with XYZ** button with a configurable provider name.
- Automatically opens the browser passkey prompt after password verification when a passkey is enabled. Cancellation does not trigger a prompt loop; an explicit retry remains available.
- Integrates **Security** into the native admin dashboard, including its sidebar, shield icon and Bootstrap controls. Sign out through the existing **Log out** sidebar entry.
+- Gives authors, editors and administrators access to their own Security page through desktop and mobile navigation, with an additional shortcut in their own Profile → Security tab.
- Shows SSO link status, provider, SSO username and SSO email in the administrator's native **Users** list and the user's **Security** page.
- Lets each user register TOTP, multiple named passkeys and an OIDC identity, then choose their own second-factor policy: disabled, TOTP, passkey, or either method.
- Optionally requires the local second factor after SSO as well.
@@ -55,9 +56,9 @@ For `bludit/docker`, copy the extracted plugin into `/usr/share/nginx/html/bl-pl
docker cp bludit:/usr/share/nginx/html/bl-plugins/bludit-secure \
"/tmp/bludit-secure-backup-$(date +%Y%m%d-%H%M%S)"
-unzip -o /tmp/bludit-secure-1.0.1.zip -d /tmp/bludit-secure-1.0.1
+unzip -o /tmp/bludit-secure-1.0.2.zip -d /tmp/bludit-secure-1.0.2
-docker cp /tmp/bludit-secure-1.0.1/bludit-secure/. \
+docker cp /tmp/bludit-secure-1.0.2/bludit-secure/. \
bludit:/usr/share/nginx/html/bl-plugins/bludit-secure/
docker restart bludit
@@ -91,6 +92,8 @@ Logging out ends the Bludit session only. Provider-wide logout and OIDC back-cha
In **Security**:
+Every signed-in author, editor and administrator can manage their own methods. Open **Security** in desktop or mobile navigation, or **Profile → Security → Manage authentication methods**. The direct page is `/admin/plugin/pluginbluditsecure`, adjusted for your configured admin path. Global SSO configuration and the Users list remain administrator-only.
+
- **TOTP:** select **Set up TOTP**, copy the `otpauth://` setup URI into your authenticator app and confirm a code. TOTP uses SHA-1, six digits and a 30-second period. Previously accepted codes cannot be reused.
- **Passkeys:** select **Add passkey**, enter a name and choose your password manager or authenticator in the browser. Multiple passkeys are supported, with user verification required. The browser selects the authenticator; the plugin does not force a particular manager.
- **Second factor:** choose the required method and whether it also applies after SSO. **TOTP or passkey** requires both methods to be registered.
@@ -118,6 +121,12 @@ The plugin protects **web sessions under `/admin`**. It does not change authenti
JavaScript URLs include a content hash so updated passkey behavior is not hidden by stale asset caches.
+## Changes in 1.0.2
+
+- Makes personal Security navigation available to authors, whose native plugin sidebar is hidden by Bludit.
+- Adds the personal Security link to mobile navigation for authors and editors.
+- Adds an authentication-management shortcut to the user's own Profile → Security tab.
+
## Changes in 1.0.1
- Applies native form styling and spacing to the passkey name input.
diff --git a/assets/navigation.js b/assets/navigation.js
new file mode 100644
index 0000000..6434ddd
--- /dev/null
+++ b/assets/navigation.js
@@ -0,0 +1,43 @@
+'use strict';
+(() => {
+ const data = document.getElementById('bs-security-navigation');
+ if (!data) return;
+ const config = JSON.parse(data.textContent);
+ const account = new URL(config.account, document.baseURI).href;
+ const logout = new URL(config.logout, document.baseURI).href;
+ const href = link => new URL(link.getAttribute('href'), document.baseURI).href;
+ const makeLink = () => {
+ const link = document.createElement('a');
+ link.href = account;
+ link.className = 'nav-link';
+ const icon = document.createElement('span');
+ icon.className = 'fa fa-shield';
+ icon.setAttribute('aria-hidden', 'true');
+ link.append(icon, document.createTextNode('Security'));
+ return link;
+ };
+ // Native themes hide plugin sidebar hooks from authors and mobile editors.
+ for (const nav of document.querySelectorAll('.sidebar .nav, #navbarNav .navbar-nav')) {
+ if (Array.from(nav.querySelectorAll('a[href]')).some(link => href(link) === account)) continue;
+ const item = document.createElement('li');
+ item.className = 'nav-item';
+ item.appendChild(makeLink());
+ const logoutItem = Array.from(nav.children).find(child => Array.from(child.querySelectorAll('a[href]')).some(link => href(link) === logout));
+ nav.insertBefore(item, logoutItem || null);
+ }
+ if (config.ownProfile) {
+ const section = document.querySelector('#security.tab-pane');
+ if (!section || section.querySelector('[data-bs-security-profile]')) return;
+ const box = document.createElement('div');
+ box.className = 'mb-4';
+ box.setAttribute('data-bs-security-profile', 'true');
+ const description = document.createElement('p');
+ description.textContent = 'Manage TOTP, passkeys, SSO and recovery codes for your account.';
+ const link = document.createElement('a');
+ link.href = account;
+ link.className = 'btn btn-outline-primary';
+ link.textContent = 'Manage authentication methods';
+ box.append(description, link);
+ section.prepend(box);
+ }
+})();
diff --git a/metadata.json b/metadata.json
index c9f5755..6f4bfd5 100644
--- a/metadata.json
+++ b/metadata.json
@@ -1 +1 @@
-{"author":"Interlope","version":"1.0.1","releaseDate":"2026-11-03","license":"MIT","compatible":"3.22.0","notes":"Per-user OIDC SSO, TOTP, passkeys and recovery codes. Requires PHP 8.2+, HTTPS, sodium, curl, mbstring and openssl."}
+{"author":"Interlope","version":"1.0.2","releaseDate":"2026-11-03","license":"MIT","compatible":"3.22.0","notes":"Per-user OIDC SSO, TOTP, passkeys and recovery codes. Requires PHP 8.2+, HTTPS, sodium, curl, mbstring and openssl."}
diff --git a/plugin.php b/plugin.php
index c19fce0..7e90462 100644
--- a/plugin.php
+++ b/plugin.php
@@ -77,6 +77,7 @@ class pluginBluditSecure extends Plugin
return 'Security';
}
public function adminBodyEnd() {
- return (new \BluditSecure\App($this))->usersViewEnhancement();
+ $app = new \BluditSecure\App($this);
+ return $app->navigationView().$app->usersViewEnhancement();
}
}
diff --git a/src/App.php b/src/App.php
index 6242001..9326c8a 100644
--- a/src/App.php
+++ b/src/App.php
@@ -397,6 +397,15 @@ final class App
'emailVerified'=>$oidc->getVerifiedClaims('email_verified') === true
];
}
+ public function navigationView(): string {
+ global $layout, $user;
+ $name = $this->authenticated();
+ if ($name === null) { return ''; }
+ $ownProfile = ($layout['view'] ?? '') === 'edit-user' && isset($user) && is_object($user) && method_exists($user, 'username') && $user->username() === $name;
+ $data = json_encode(['account'=>$this->accountUrl(), 'logout'=>DOMAIN_ADMIN.'logout', 'ownProfile'=>$ownProfile], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR);
+ $version = substr(hash_file('sha256', dirname(__DIR__).'/assets/navigation.js'), 0, 16);
+ return '';
+ }
public function usersViewEnhancement(): string {
global $layout, $users;
if (($layout['view'] ?? '') !== 'users') { return ''; }