Skip to content

Commit 39ab3d0

Browse files
feat: Update Forum Notification Widget documentation and add plugin XML configuration
1 parent b5ac2a0 commit 39ab3d0

4 files changed

Lines changed: 114 additions & 119 deletions

File tree

docs/forum-notification-widget.md

Lines changed: 43 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -4,122 +4,58 @@
44

55
This document describes how to embed the XtremeIdiots Portal notification widget on the xtremeidiots.com Invision Community forum. The widget shows personalised notifications for logged-in forum users and a public activity feed for guests.
66

7-
## Prerequisites
7+
## Installation (2 steps)
88

9-
1. The HMAC shared secret must be configured in both:
10-
- **Portal**: via Azure App Configuration key `XtremeIdiots:ExternalWidget:HmacSecret` (auto-provisioned by portal-environments Terraform)
11-
- **Forum**: as a custom Invision Community setting (see step 2 below)
12-
13-
2. The portal must be deployed with the external notifications API (`/api/external/notifications`)
14-
15-
## Step 1: Add the HMAC Secret to Invision Community
16-
17-
1. Go to **ACP → System → Settings** (or use a custom plugin settings page)
18-
2. Add a custom setting:
19-
- **Key**: `portal_hmac_secret`
20-
- **Value**: Copy the HMAC secret from Azure Key Vault (`external-widget-hmac-secret` in the shared Key Vault)
21-
22-
> **Important**: The secret value must match exactly between the portal and forum. After the initial Terraform apply, retrieve the value from Azure Key Vault and paste it into the forum setting.
23-
24-
## Step 2: Add the Widget to the Forum Template
25-
26-
Edit the Invision Community theme template where you want the widget to appear (e.g., sidebar, footer, or a custom page block).
27-
28-
### Option A: Global Template (sidebar on all pages)
29-
30-
Edit the **globalTemplate** or **sidebar** template in your theme:
31-
32-
```html
33-
{{if member.member_id}}
34-
<?php
35-
$secret = \IPS\Settings::i()->portal_hmac_secret;
36-
$memberId = \IPS\Member::loggedIn()->member_id;
37-
$timestamp = time();
38-
$hmac = hash_hmac('sha256', "{$memberId}:{$timestamp}", $secret);
39-
$token = base64_encode("{$memberId}:{$timestamp}:{$hmac}");
40-
?>
41-
<div id="portal-notifications-widget"
42-
data-token="<?php echo $token; ?>"
43-
data-portal-url="https://portal.xtremeidiots.com">
44-
</div>
45-
{{else}}
46-
<div id="portal-notifications-widget"
47-
data-token=""
48-
data-portal-url="https://portal.xtremeidiots.com">
49-
</div>
50-
{{endif}}
51-
<script src="https://portal.xtremeidiots.com/js/forum-widget.js" defer></script>
52-
```
9+
### Step 1: Install the Settings Plugin
10+
11+
1. Upload `docs/invision-plugin/plugin.xml` via **ACP → System → Plugins → Install New Plugin**
12+
2. Click the cog icon next to the plugin to open settings
13+
3. Set **HMAC Shared Secret** — paste from Azure Key Vault (`external-widget-hmac-secret`)
14+
4. Set **Portal Base URL**`https://portal.xtremeidiots.com` (default)
15+
5. Set **Enable Widget** — Yes
16+
17+
This plugin only registers settings — it does not inject any code or hooks.
5318

54-
### Option B: Using an Invision Community Plugin Hook
19+
### Step 2: Create a Custom PHP Block
5520

56-
If you prefer not to mix PHP into templates, create a simple plugin:
21+
1. Go to **ACP → Pages → Blocks → Create New Block → Custom → PHP**
22+
2. Give it a name (e.g., "Portal Notifications")
23+
3. Paste the following as the block content:
5724

58-
**File: `hooks/portalWidget.php`**
5925
```php
60-
class portalWidget
61-
{
62-
public function globalTemplate($html)
63-
{
64-
$member = \IPS\Member::loggedIn();
65-
$token = '';
66-
67-
if ($member->member_id) {
68-
$secret = \IPS\Settings::i()->portal_hmac_secret;
69-
$memberId = $member->member_id;
70-
$timestamp = time();
71-
$hmac = hash_hmac('sha256', "{$memberId}:{$timestamp}", $secret);
72-
$token = base64_encode("{$memberId}:{$timestamp}:{$hmac}");
73-
}
74-
75-
$widget = <<<HTML
76-
<div id="portal-notifications-widget"
77-
data-token="{$token}"
78-
data-portal-url="https://portal.xtremeidiots.com">
79-
</div>
80-
<script src="https://portal.xtremeidiots.com/js/forum-widget.js" defer></script>
81-
HTML;
82-
83-
// Insert before </body>
84-
return str_replace('</body>', $widget . '</body>', $html);
26+
$portalToken = '';
27+
$portalUrl = rtrim( \IPS\Settings::i()->portal_base_url ?: 'https://portal.xtremeidiots.com', '/' );
28+
$member = \IPS\Member::loggedIn();
29+
if ( $member->member_id ) {
30+
$secret = \IPS\Settings::i()->portal_hmac_secret;
31+
if ( !empty( $secret ) ) {
32+
$memberId = (string) $member->member_id;
33+
$timestamp = (string) time();
34+
$hmac = hash_hmac( 'sha256', "{$memberId}:{$timestamp}", $secret );
35+
$portalToken = base64_encode( "{$memberId}:{$timestamp}:{$hmac}" );
8536
}
8637
}
38+
echo '<div id="portal-notifications-widget" data-token="' . htmlspecialchars( $portalToken, ENT_QUOTES, 'UTF-8' ) . '" data-portal-url="' . htmlspecialchars( $portalUrl, ENT_QUOTES, 'UTF-8' ) . '"></div>';
39+
echo '<script src="' . htmlspecialchars( $portalUrl, ENT_QUOTES, 'UTF-8' ) . '/js/forum-widget.js" defer></script>';
8740
```
8841

89-
### Option C: Custom HTML Block (simplest)
42+
4. Save the block
43+
5. Place the block via the front-end widget manager — drag it into the sidebar or wherever you want it
9044

91-
If you just want a quick test, use the **Pages** app or a **Custom Block**:
45+
## Prerequisites
9246

93-
1. Go to **ACP → Pages → Blocks → Create New Block**
94-
2. Choose **Custom HTML**
95-
3. Paste:
96-
```html
97-
<?php
98-
$token = '';
99-
$member = \IPS\Member::loggedIn();
100-
if ($member->member_id) {
101-
$secret = \IPS\Settings::i()->portal_hmac_secret;
102-
$memberId = $member->member_id;
103-
$timestamp = time();
104-
$hmac = hash_hmac('sha256', "{$memberId}:{$timestamp}", $secret);
105-
$token = base64_encode("{$memberId}:{$timestamp}:{$hmac}");
106-
}
107-
?>
108-
<div id="portal-notifications-widget"
109-
data-token="<?php echo $token; ?>"
110-
data-portal-url="https://portal.xtremeidiots.com">
111-
</div>
112-
<script src="https://portal.xtremeidiots.com/js/forum-widget.js" defer></script>
113-
```
114-
4. Place the block in your desired sidebar/widget area
47+
- The HMAC shared secret must be configured in both:
48+
- **Portal**: via Azure App Configuration key `XtremeIdiots:ExternalWidget:HmacSecret` (auto-provisioned by portal-environments Terraform)
49+
- **Forum**: via the plugin settings page (Step 1 above)
50+
- The portal must be deployed with the external notifications API (`/api/external/notifications`)
11551

11652
## How It Works
11753

118-
1. **Token generation** (forum-side): When a logged-in forum user loads a page, the PHP template generates an HMAC-SHA256 signed token containing their forum member ID and a Unix timestamp, signed with the shared secret.
54+
1. **Token generation** (forum-side): The plugin widget generates an HMAC-SHA256 signed token containing the logged-in forum member's ID and a Unix timestamp, signed with the shared secret.
11955

12056
2. **Token format**: `Base64({forumMemberId}:{timestampUnix}:{hmacHex})`
12157

122-
3. **Widget load**: The JavaScript widget (`forum-widget.js`) reads the token from the `data-token` attribute and calls the portal API.
58+
3. **Widget load**: The JavaScript widget (`forum-widget.js`, served from the portal) reads the token from the `data-token` attribute and calls the portal API.
12359

12460
4. **API response**:
12561
- **No token / invalid token**: Returns a public feed (recent admin actions)
@@ -157,7 +93,13 @@ To customise, override the styles in your forum theme CSS:
15793

15894
| Issue | Cause | Fix |
15995
|-------|-------|-----|
160-
| Widget shows "Portal URL not configured" | Missing `data-portal-url` attribute | Ensure the attribute is set in the template |
96+
| Widget shows "Portal URL not configured" | Missing portal base URL in plugin settings | Check plugin settings in ACP |
16197
| Widget shows "Unable to load notifications" | CORS or network error | Check browser console; verify portal CORS allows the forum domain |
162-
| All users see public feed only | HMAC secret mismatch | Ensure the forum `portal_hmac_secret` matches the Azure Key Vault value |
98+
| All users see public feed only | HMAC secret mismatch | Ensure the plugin's HMAC secret matches the Azure Key Vault value |
16399
| Token always expired | Server clock drift | Ensure both servers have NTP synced clocks (±1 minute tolerance) |
100+
101+
## Updating
102+
103+
To update the plugin, upload the new `plugin.xml` via ACP → System → Plugins. Settings are preserved across updates.
104+
105+
To rotate the HMAC secret: update both the Azure Key Vault value and the plugin setting in ACP. Changes take effect immediately.

docs/invision-plugin/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# XtremeIdiots Portal Notifications — Invision Community Plugin
2+
3+
## What This Plugin Does
4+
5+
Registers 3 ACP settings for the portal notification widget:
6+
- **Enable Widget** (Yes/No toggle)
7+
- **HMAC Shared Secret** (for signing auth tokens)
8+
- **Portal Base URL** (defaults to `https://portal.xtremeidiots.com`)
9+
10+
This plugin **only provides settings** — the widget itself is delivered via a custom PHP block (see below).
11+
12+
## Installation
13+
14+
1. Upload `plugin.xml` via **ACP → System → Plugins → Install New Plugin**
15+
2. Click the cog icon next to the plugin → configure the 3 settings
16+
3. Create a custom PHP block: **ACP → Pages → Blocks → Create New Block → Custom → PHP**
17+
4. Paste the PHP snippet from `docs/forum-notification-widget.md` as the block content (use `echo`, not `return`)
18+
5. Place the block via the front-end widget manager into the sidebar
19+
20+
See `docs/forum-notification-widget.md` for the full snippet and troubleshooting guide.
21+
22+
## Why a Custom Block?
23+
24+
IPS4's plugin template compiler doesn't support `hash_hmac()` and `base64_encode()` calls needed for HMAC token generation. A custom PHP block executes raw PHP directly, bypassing the template compiler.

docs/invision-plugin/plugin.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<plugin name="XtremeIdiots Portal Notifications" version_long="10004" version_human="1.0.4" author="XtremeIdiots" website="https://portal.xtremeidiots.com" update_check=""><hooks/><settings><setting><key>portal_notifications_enabled</key><default>1</default></setting><setting><key>portal_hmac_secret</key><default></default></setting><setting><key>portal_base_url</key><default>https://portal.xtremeidiots.com</default></setting></settings><settingsCode><![CDATA[//<?php
3+
4+
$form->addHeader('Portal Notifications Widget');
5+
$form->add( new \IPS\Helpers\Form\YesNo( 'portal_notifications_enabled', \IPS\Settings::i()->portal_notifications_enabled, FALSE, array(), NULL, NULL, NULL, 'portal_notifications_enabled' ) );
6+
$form->add( new \IPS\Helpers\Form\Text( 'portal_hmac_secret', \IPS\Settings::i()->portal_hmac_secret, FALSE, array(), NULL, NULL, NULL, 'portal_hmac_secret' ) );
7+
$form->add( new \IPS\Helpers\Form\Url( 'portal_base_url', \IPS\Settings::i()->portal_base_url ?: 'https://portal.xtremeidiots.com', FALSE, array(), NULL, NULL, NULL, 'portal_base_url' ) );
8+
9+
if ( $values = $form->values() )
10+
{
11+
$form->saveAsSettings();
12+
return TRUE;
13+
}
14+
15+
return $form;]]></settingsCode><tasks/><widgets/><htmlFiles/><cssFiles/><jsFiles/><resourcesFiles/><lang><word key="portal_notifications_enabled" js="0">Enable Widget</word><word key="portal_hmac_secret" js="0">HMAC Shared Secret</word><word key="portal_hmac_secret_desc" js="0">Copy from Azure Key Vault (external-widget-hmac-secret). Must match the portal value.</word><word key="portal_base_url" js="0">Portal Base URL</word><word key="portal_base_url_desc" js="0">The base URL of the XtremeIdiots Portal (no trailing slash).</word></lang><versions><version long="10004" human="1.0.4"><![CDATA[//<?php
16+
17+
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
18+
{
19+
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
20+
exit;
21+
}
22+
23+
class ips_plugins_setup_install
24+
{
25+
public function step1()
26+
{
27+
return TRUE;
28+
}
29+
}]]></version></versions></plugin>

src/XtremeIdiots.Portal.Web/wwwroot/js/forum-widget.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -219,28 +219,28 @@
219219
var style = document.createElement('style');
220220
style.id = 'xi-pw-styles';
221221
style.textContent =
222-
'.xi-portal-widget{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:13px;border:1px solid #ddd;border-radius:6px;background:#fff;overflow:hidden;max-width:400px}' +
223-
'.xi-pw-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;background:#2b3a4a;color:#fff;font-weight:600}' +
222+
'.xi-portal-widget{font-family:inherit;font-size:13px;border:1px solid #333;border-radius:4px;background:#262626;overflow:hidden;max-width:400px;color:#ccc}' +
223+
'.xi-pw-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#1a1a1a;color:#e0e0e0;font-weight:600;border-bottom:1px solid #333}' +
224224
'.xi-pw-title{display:flex;align-items:center;gap:6px}' +
225225
'.xi-pw-bell{font-size:16px}' +
226-
'.xi-pw-badge{background:#e74c3c;color:#fff;font-size:11px;padding:1px 6px;border-radius:10px;margin-left:4px}' +
227-
'.xi-pw-mark-all{color:#aec6e0;font-size:11px;text-decoration:none;white-space:nowrap}' +
228-
'.xi-pw-mark-all:hover{color:#fff}' +
226+
'.xi-pw-badge{background:#c0392b;color:#fff;font-size:11px;padding:1px 6px;border-radius:10px;margin-left:4px}' +
227+
'.xi-pw-mark-all{color:#7eaac4;font-size:11px;text-decoration:none;white-space:nowrap}' +
228+
'.xi-pw-mark-all:hover{color:#aed6f1}' +
229229
'.xi-pw-list{max-height:360px;overflow-y:auto}' +
230-
'.xi-pw-item{display:block;padding:10px 14px;border-bottom:1px solid #f0f0f0;text-decoration:none;color:#333;transition:background .15s}' +
231-
'.xi-pw-item:hover{background:#f8f9fa;text-decoration:none;color:#333}' +
232-
'.xi-pw-unread{background:#f0f7ff;border-left:3px solid #3498db}' +
230+
'.xi-pw-item{display:block;padding:8px 12px;border-bottom:1px solid #333;text-decoration:none;color:#ccc;transition:background .15s}' +
231+
'.xi-pw-item:hover{background:#2f2f2f;text-decoration:none;color:#fff}' +
232+
'.xi-pw-unread{background:#1e2a35;border-left:3px solid #3498db}' +
233233
'.xi-pw-item-header{display:flex;justify-content:space-between;align-items:baseline;gap:8px}' +
234-
'.xi-pw-item-title{font-weight:600;font-size:12px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
235-
'.xi-pw-item-time{font-size:11px;color:#999;white-space:nowrap}' +
236-
'.xi-pw-item-msg{font-size:12px;color:#666;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
237-
'.xi-pw-unclaimed{display:block;padding:8px 14px;background:#fff3cd;color:#856404;font-size:12px;text-decoration:none;border-top:1px solid #ffeaa7}' +
238-
'.xi-pw-unclaimed:hover{background:#ffe69c;text-decoration:none;color:#856404}' +
239-
'.xi-pw-footer{padding:8px 14px;text-align:center;background:#f8f9fa;border-top:1px solid #eee}' +
240-
'.xi-pw-footer a{color:#3498db;text-decoration:none;font-size:12px;font-weight:500}' +
241-
'.xi-pw-footer a:hover{text-decoration:underline}' +
242-
'.xi-pw-empty{padding:24px;text-align:center;color:#999}' +
243-
'.xi-pw-loading{padding:24px;text-align:center;color:#999}' +
234+
'.xi-pw-item-title{font-weight:600;font-size:12px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e0e0e0}' +
235+
'.xi-pw-item-time{font-size:11px;color:#777;white-space:nowrap}' +
236+
'.xi-pw-item-msg{font-size:12px;color:#999;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
237+
'.xi-pw-unclaimed{display:block;padding:8px 12px;background:#3d2f0a;color:#f0c040;font-size:12px;text-decoration:none;border-top:1px solid #4a3a10}' +
238+
'.xi-pw-unclaimed:hover{background:#4a3a10;text-decoration:none;color:#ffd54f}' +
239+
'.xi-pw-footer{padding:8px 12px;text-align:center;background:#1a1a1a;border-top:1px solid #333}' +
240+
'.xi-pw-footer a{color:#7eaac4;text-decoration:none;font-size:12px;font-weight:500}' +
241+
'.xi-pw-footer a:hover{color:#aed6f1;text-decoration:underline}' +
242+
'.xi-pw-empty{padding:24px;text-align:center;color:#777}' +
243+
'.xi-pw-loading{padding:24px;text-align:center;color:#777}' +
244244
'.xi-pw-error{padding:16px;text-align:center;color:#e74c3c;font-size:12px}';
245245
document.head.appendChild(style);
246246
}

0 commit comments

Comments
 (0)