mbkauthe 5.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "5.0.2",
3
+ "version": "5.1.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -10,8 +10,10 @@
10
10
  "create-tables": "node lib/createTable.js",
11
11
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
12
12
  "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
13
+ "tets": "npm run test",
13
14
  "render:mermaid": "npx @mermaid-js/mermaid-cli -i docs/diagrams/auth-flows.mmd -o docs/images/auth-flows.svg",
14
- "render:mermaid:png": "npx @mermaid-js/mermaid-cli -i docs/diagrams/auth-flows.mmd -o docs/images/auth-flows.png -s 20"
15
+ "render:mermaid:png": "npx @mermaid-js/mermaid-cli -i docs/diagrams/auth-flows.mmd -o docs/images/auth-flows.png -s 20",
16
+ "install-mermaid": "npm i @mermaid-js/mermaid-cli@11.15.0 --save-dev"
15
17
  },
16
18
  "imports": {
17
19
  "#pool.js": "./lib/pool.js",
@@ -29,10 +31,13 @@
29
31
  "nodejs",
30
32
  "express",
31
33
  "session",
32
- "security"
34
+ "security",
35
+ "oauth",
36
+ "2fa",
37
+ "csrf"
33
38
  ],
34
- "author": "Muhammad Bin Khalid <support@mbktech.org>",
35
- "license": "GPL-2.0",
39
+ "author": "Muhammad Bin Khalid <support@mbktech.org> <chmuhammadbinkhalid28@gmail.com>",
40
+ "license": "LGPL-3.0-only",
36
41
  "bugs": {
37
42
  "url": "https://github.com/MIbnEKhalid/mbkauthe/issues"
38
43
  },
@@ -53,7 +58,6 @@
53
58
  "registry": "https://registry.npmjs.org/"
54
59
  },
55
60
  "dependencies": {
56
- "@mermaid-js/mermaid-cli": "^11.15.0",
57
61
  "bytes": "^3.1.2",
58
62
  "connect-pg-simple": "^10.0.0",
59
63
  "content-type": "^1.0.5",
@@ -99,7 +103,6 @@
99
103
  }
100
104
  },
101
105
  "devDependencies": {
102
- "@types/express": "^5.0.6",
103
106
  "cross-env": "^7.0.3",
104
107
  "jest": "^30.2.0",
105
108
  "nodemon": "^3.1.11",
package/public/main.js CHANGED
@@ -1,115 +1,174 @@
1
- async function logout() {
2
- const confirmation = confirm("Are you sure you want to logout?");
3
- if (!confirmation) {
4
- return;
5
- }
1
+ (() => {
2
+ const SESSION_KEYS = [
3
+ 'sessionId',
4
+ 'mbkauthe.sid',
5
+ 'fullName',
6
+ '_csrf',
7
+ 'profileImageUser',
8
+ 'profileImageUrl'
9
+ ];
10
+ const LOG_PREFIX = '[mbkauthe]';
11
+ const EXPIRED_COOKIE = 'expires=Thu, 01 Jan 1970 00:00:00 GMT';
12
+ const dateFormatter = new Intl.DateTimeFormat('en-GB', {
13
+ day: '2-digit',
14
+ month: '2-digit',
15
+ year: 'numeric',
16
+ hour: 'numeric',
17
+ minute: 'numeric',
18
+ second: 'numeric',
19
+ hour12: true
20
+ });
21
+
22
+ const reloadPage = () => window.location.reload();
23
+
24
+ const getCookieDomains = () => {
25
+ const hostname = window.location.hostname;
26
+
27
+ if (!hostname) {
28
+ return [];
29
+ }
6
30
 
7
- try {
8
- // First, logout from server
9
- const response = await fetch("/mbkauthe/api/logout", {
10
- method: "POST",
11
- headers: {
12
- "Content-Type": "application/json",
13
- "Cache-Control": "no-cache"
14
- },
15
- credentials: "include"
16
- });
31
+ const domains = new Set();
32
+ const configuredDomain = window.mbkautheConfig?.cookieDomain;
17
33
 
18
- const result = await response.json();
34
+ if (configuredDomain) {
35
+ domains.add(configuredDomain);
36
+ }
19
37
 
20
- if (response.ok) {
21
- // Then clear all caches after successful logout (except rememberedUsername)
22
- await selectiveCacheClear();
23
- // selectiveCacheClear already redirects, so no need for additional redirect
24
- } else {
25
- alert(result.message);
38
+ domains.add(hostname);
39
+ if (hostname.includes('.')) {
40
+ domains.add(`.${hostname}`);
26
41
  }
27
- } catch (error) {
28
- console.error(`[mbkauthe] Error during logout:`, error);
29
- alert(`Logout failed: ${error.message}`);
30
- }
31
- }
32
-
33
- async function selectiveCacheClear() {
34
- try {
35
-
36
- const cookiesToClear = [
37
- 'sessionId',
38
- 'mbkauthe.sid',
39
- 'fullName',
40
- '_csrf',
41
- 'profileImageUser',
42
- 'profileImageUrl'
43
- ];
44
-
45
- const localStorageToClear = [
46
- 'sessionId',
47
- 'mbkauthe.sid',
48
- 'fullName',
49
- '_csrf',
50
- 'profileImageUser',
51
- 'profileImageUrl'
52
- ];
53
-
54
- // 1. Clear selected localStorage keys
55
- localStorageToClear.forEach(key => {
56
- localStorage.removeItem(key);
57
- });
58
42
 
59
- // 2. Clear selected cookies
60
- cookiesToClear.forEach(name => {
61
- document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
62
- document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${window.location.hostname}`;
63
- document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname}`;
43
+ return [...domains];
44
+ };
45
+
46
+ const clearCookie = (name) => {
47
+ document.cookie = `${name}=; ${EXPIRED_COOKIE}; path=/`;
48
+
49
+ getCookieDomains().forEach((domain) => {
50
+ document.cookie = `${name}=; ${EXPIRED_COOKIE}; path=/; domain=${domain}`;
64
51
  });
52
+ };
65
53
 
66
- // 3. Optional reload
67
- window.location.reload();
54
+ const parseJson = async (response) => {
55
+ try {
56
+ return await response.json();
57
+ } catch {
58
+ return {};
59
+ }
60
+ };
68
61
 
69
- } catch (error) {
70
- console.error(`[mbkauthe] selective cache clear failed:`, error);
71
- window.location.reload();
72
- }
73
- }
74
-
75
- async function logoutuser() {
76
- await logout();
77
- }
78
-
79
- const validateSessionInterval = 60000;
80
- // 1 minutes in milliseconds Function to check session validity by sending a request to the server
81
- function checkSession() {
82
- fetch("/api/validate-session")
83
- .then((response) => {
84
- if (!response.ok) {
85
- // Redirect or handle errors (session expired, user inactive, etc.)
86
- window.location.reload(); // Reload the page to update the session status
62
+ async function logout({ confirmLogout = true } = {}) {
63
+ if (confirmLogout && !confirm('Are you sure you want to logout?')) {
64
+ return false;
65
+ }
66
+
67
+ try {
68
+ const response = await fetch('/mbkauthe/api/logout', {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ 'Cache-Control': 'no-cache'
73
+ },
74
+ credentials: 'include',
75
+ cache: 'no-store'
76
+ });
77
+ const result = await parseJson(response);
78
+
79
+ if (response.ok) {
80
+ selectiveCacheClear();
81
+ return true;
87
82
  }
83
+
84
+ alert(result.message || 'Logout failed. Please try again.');
85
+ } catch (error) {
86
+ console.error(`${LOG_PREFIX} Error during logout:`, error);
87
+ alert(`Logout failed: ${error.message}`);
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ function selectiveCacheClear() {
94
+ try {
95
+ SESSION_KEYS.forEach((key) => localStorage.removeItem(key));
96
+ SESSION_KEYS.forEach(clearCookie);
97
+ } catch (error) {
98
+ console.error(`${LOG_PREFIX} selective cache clear failed:`, error);
99
+ } finally {
100
+ reloadPage();
101
+ }
102
+ }
103
+
104
+ async function logoutuser() {
105
+ return logout();
106
+ }
107
+
108
+ function checkSession() {
109
+ return fetch('/mbkauthe/api/checkSession', {
110
+ credentials: 'include',
111
+ cache: 'no-store'
88
112
  })
89
- .catch((error) => console.error(`[mbkauthe] Error checking session:`, error));
90
- }
91
- // Call validateSession every 2 minutes (120000 milliseconds)
92
- // setInterval(checkSession, validateSessionInterval);
93
-
94
- function getCookieValue(cookieName) {
95
- const cookies = document.cookie.split('; ');
96
- for (let cookie of cookies) {
97
- const [name, value] = cookie.split('=');
98
- if (name === cookieName) {
113
+ .then(async (response) => {
114
+ if (!response.ok) {
115
+ reloadPage();
116
+ return;
117
+ }
118
+
119
+ const session = await parseJson(response);
120
+ if (session.sessionValid === false) {
121
+ reloadPage();
122
+ }
123
+ })
124
+ .catch((error) => console.error(`${LOG_PREFIX} Error checking session:`, error));
125
+ }
126
+
127
+ function getCookieValue(cookieName) {
128
+ if (!cookieName) {
129
+ return null;
130
+ }
131
+
132
+ const prefix = `${cookieName}=`;
133
+ const cookie = document.cookie
134
+ .split('; ')
135
+ .find((entry) => entry.startsWith(prefix));
136
+
137
+ if (!cookie) {
138
+ return null;
139
+ }
140
+
141
+ const value = cookie.slice(prefix.length);
142
+
143
+ try {
99
144
  return decodeURIComponent(value);
145
+ } catch {
146
+ return value;
100
147
  }
101
148
  }
102
- return null; // Return null if the cookie is not found
103
- }
104
149
 
105
- function loadpage(url) {
106
- window.location.href = url;
107
- }
150
+ function loadpage(url) {
151
+ if (url) {
152
+ window.location.href = url;
153
+ }
154
+ }
108
155
 
109
- function formatDate(date) {
110
- return new Date(date).toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true });
111
- }
156
+ function formatDate(date) {
157
+ const parsedDate = new Date(date);
158
+ return Number.isNaN(parsedDate.getTime()) ? 'Invalid Date' : dateFormatter.format(parsedDate);
159
+ }
112
160
 
113
- function reloadPage() {
114
- window.location.reload();
115
- }
161
+ const api = {
162
+ checkSession,
163
+ formatDate,
164
+ getCookieValue,
165
+ loadpage,
166
+ logout,
167
+ logoutuser,
168
+ reloadPage,
169
+ selectiveCacheClear
170
+ };
171
+
172
+ window.mbkauthe = Object.assign(window.mbkauthe || {}, api);
173
+ Object.assign(window, api);
174
+ })();
package/test.spec.js CHANGED
@@ -14,7 +14,13 @@ process.env.dbLogs = 'true';
14
14
  process.env.dbLogsCallsite = 'false';
15
15
 
16
16
  const { default: router } = await import('./lib/main.js');
17
- const { packageJson } = await import('./lib/config/index.js');
17
+ const { packageJson, mbkautheVar } = await import('./lib/config/index.js');
18
+ const {
19
+ resolveCookieDomain,
20
+ isAllowedOriginHostname,
21
+ getCookieDomain,
22
+ cachedCookieOptions
23
+ } = await import('./lib/config/cookies.js');
18
24
  const { dblogin } = await import('./lib/pool.js');
19
25
  const {
20
26
  attachDevQueryLogger,
@@ -23,6 +29,11 @@ const {
23
29
  runWithRequestContext
24
30
  } = await import('./lib/utils/dbQueryLogger.js');
25
31
 
32
+ const { isSafeRelativeRedirect, sanitizeRelativeRedirect } = await import('./lib/utils/redirect.js');
33
+ const { isSafeFetchUrl } = await import('./lib/utils/urlSafety.js');
34
+ const { hashPassword, verifyPassword } = await import('./lib/config/index.js');
35
+ const { hashDeviceToken } = await import('./lib/config/cookies.js');
36
+
26
37
  const viewsPath = path.join(__dirname, 'views');
27
38
 
28
39
  const handlebarsHelpers = {
@@ -252,6 +263,7 @@ describe('mbkauthe Routes', () => {
252
263
 
253
264
  expect(response.status).toBe(200);
254
265
  expect(response.headers['content-type']).toContain('javascript');
266
+ expect(response.text).toMatch(/^window\.mbkautheConfig=/);
255
267
  });
256
268
 
257
269
  test('GET /icon.svg returns SVG content', async () => {
@@ -680,4 +692,64 @@ describe('mbkauthe Routes', () => {
680
692
  }
681
693
  });
682
694
  });
695
+
696
+ describe('Security hardening', () => {
697
+ test('sanitizeRelativeRedirect accepts safe paths and rejects open redirects', () => {
698
+ expect(sanitizeRelativeRedirect('/dashboard')).toBe('/dashboard');
699
+ expect(sanitizeRelativeRedirect('//evil.com')).toBeNull();
700
+ expect(sanitizeRelativeRedirect('https://evil.com')).toBeNull();
701
+ expect(isSafeRelativeRedirect('/app/settings')).toBe(true);
702
+ });
703
+
704
+ test('isSafeFetchUrl blocks localhost and allows public https URLs', () => {
705
+ expect(isSafeFetchUrl('https://avatars.githubusercontent.com/u/1')).toBe(true);
706
+ expect(isSafeFetchUrl('http://127.0.0.1/secret')).toBe(false);
707
+ expect(isSafeFetchUrl('https://localhost/admin')).toBe(false);
708
+ expect(isSafeFetchUrl('ftp://example.com/icon.png')).toBe(false);
709
+ });
710
+
711
+ test('verifyPassword validates peppered password hashes', async () => {
712
+ const username = 'test.user';
713
+ const password = 'test-password-123';
714
+ const stored = hashPassword(password, username);
715
+
716
+ expect(stored).toMatch(/^[0-9a-f]{128}$/);
717
+ expect(await verifyPassword(password, username, stored)).toBe(true);
718
+ expect(await verifyPassword('wrong-password', username, stored)).toBe(false);
719
+ });
720
+
721
+ test('hashDeviceToken returns a keyed hex digest', () => {
722
+ const token = 'a'.repeat(64);
723
+ expect(hashDeviceToken(token)).toMatch(/^[0-9a-f]{64}$/);
724
+ });
725
+
726
+ test('security headers are set on API responses', async () => {
727
+ const response = await request(app).get('/mbkauthe/main.js');
728
+ expect(response.headers['x-content-type-options']).toBe('nosniff');
729
+ expect(response.headers['x-frame-options']).toBe('SAMEORIGIN');
730
+ });
731
+ });
732
+
733
+ describe('Cross-subdomain cookie sharing', () => {
734
+ test('resolveCookieDomain returns parent domain only when deployed outside test dev', () => {
735
+ expect(resolveCookieDomain('true', 'mbktech.org', true)).toBeUndefined();
736
+ expect(resolveCookieDomain('false', 'mbktech.org', false)).toBeUndefined();
737
+ expect(resolveCookieDomain('true', 'mbktech.org', false)).toBe('.mbktech.org');
738
+ expect(resolveCookieDomain('true', '.mbktech.org', false)).toBe('.mbktech.org');
739
+ });
740
+
741
+ test('isAllowedOriginHostname accepts root and nested subdomains', () => {
742
+ expect(isAllowedOriginHostname('mbktech.org', 'mbktech.org')).toBe(true);
743
+ expect(isAllowedOriginHostname('auth.mbktech.org', 'mbktech.org')).toBe(true);
744
+ expect(isAllowedOriginHostname('app.auth.mbktech.org', 'mbktech.org')).toBe(true);
745
+ expect(isAllowedOriginHostname('notmbktech.org', 'mbktech.org')).toBe(false);
746
+ expect(isAllowedOriginHostname('mbktech.org.evil.com', 'mbktech.org')).toBe(false);
747
+ });
748
+
749
+ test('cached cookie options omit domain in test dev environment', () => {
750
+ expect(getCookieDomain()).toBeUndefined();
751
+ expect(cachedCookieOptions.domain).toBeUndefined();
752
+ expect(cachedCookieOptions.secure).toBe(false);
753
+ });
754
+ });
683
755
  });
@@ -29,4 +29,5 @@
29
29
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
30
30
  {{> sharedStyles}}
31
31
  {{#if extraStyles}}{{{extraStyles}}}{{/if}}
32
+ <script src="/mbkauthe/main.js{{cacheBuster}}" defer></script>
32
33
  </head>
@@ -340,7 +340,15 @@
340
340
  </style>
341
341
 
342
342
  <script>
343
- const redirectTarget = new URLSearchParams(window.location.search).get('redirect') || '{{customURL}}';
343
+ function sanitizeRelativeRedirect(value) {
344
+ if (typeof value !== 'string') return null;
345
+ const trimmed = value.trim();
346
+ if (!trimmed.startsWith('/') || trimmed.startsWith('//')) return null;
347
+ if (trimmed.includes('://') || trimmed.includes('\\')) return null;
348
+ return trimmed;
349
+ }
350
+
351
+ const redirectTarget = sanitizeRelativeRedirect(new URLSearchParams(window.location.search).get('redirect')) || '{{customURL}}';
344
352
 
345
353
  // Ensure login anchors include ?redirect using the redirect query param (not the current page URL)
346
354
  (function setLoginHrefFromQuery() {
@@ -485,16 +493,8 @@
485
493
  if (res.ok && data.success) {
486
494
  // Prioritize explicit ?redirect if present and valid, else fall back to server-provided redirect or template
487
495
  const params = new URLSearchParams(window.location.search);
488
- let queryRedirect = null;
489
- if (params.has('redirect')) {
490
- try {
491
- const raw = params.get('redirect');
492
- if (raw) queryRedirect = decodeURIComponent(raw);
493
- } catch (e) {
494
- queryRedirect = null;
495
- }
496
- }
497
- const finalRedirect = queryRedirect || data.redirect || redirectTarget;
496
+ const queryRedirect = sanitizeRelativeRedirect(params.get('redirect'));
497
+ const finalRedirect = data.redirect || queryRedirect || redirectTarget;
498
498
  if (finalRedirect) window.location.href = finalRedirect;
499
499
  } else {
500
500
  showMessage(data.message || 'Could not switch account', 'Switch Account');
@@ -249,6 +249,14 @@
249
249
  {{> versionInfo}}
250
250
 
251
251
  <script>
252
+ function sanitizeRelativeRedirect(value) {
253
+ if (typeof value !== 'string') return null;
254
+ const trimmed = value.trim();
255
+ if (!trimmed.startsWith('/') || trimmed.startsWith('//')) return null;
256
+ if (trimmed.includes('://') || trimmed.includes('\\')) return null;
257
+ return trimmed;
258
+ }
259
+
252
260
  // Toggle password visibility
253
261
  const togglePassword = document.getElementById('togglePassword');
254
262
  const passwordInput = document.getElementById('loginPassword');
@@ -301,7 +309,7 @@
301
309
 
302
310
  // Pass redirect query param through to server so it can be used by 2FA flow
303
311
  const urlParams = new URLSearchParams(window.location.search);
304
- const pageRedirect = urlParams.get('redirect');
312
+ const pageRedirect = sanitizeRelativeRedirect(urlParams.get('redirect'));
305
313
  fetch('/mbkauthe/api/login', {
306
314
  method: 'POST',
307
315
  credentials: 'include',
@@ -330,9 +338,8 @@
330
338
  deleteCookie('rememberedUsername');
331
339
  }
332
340
 
333
- // Redirect to the appropriate page
334
- const redirectUrl = new URLSearchParams(window.location.search).get('redirect');
335
- window.location.href = redirectUrl ? decodeURIComponent(redirectUrl) : '{{customURL}}';
341
+ // Redirect to the validated target from the server or safe query param
342
+ window.location.href = data.redirectUrl || pageRedirect || '{{customURL}}';
336
343
  }
337
344
  } else {
338
345
  // Handle errors
@@ -349,7 +356,9 @@
349
356
  });
350
357
  });
351
358
 
352
- // Cookie helper functions for cross-domain functionality
359
+ // Cookie helper functions for cross-subdomain sharing
360
+ const cookieDomain = '{{cookieDomain}}';
361
+
353
362
  function setCookie(name, value, days) {
354
363
  let expires = "";
355
364
  if (days) {
@@ -357,11 +366,11 @@
357
366
  date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
358
367
  expires = "; expires=" + date.toUTCString();
359
368
  }
360
- // Set cookie for the entire domain (works across all subdomains)
361
- const domain = window.location.hostname.includes('.') ?
362
- '.' + window.location.hostname.split('.').slice(-2).join('.') :
363
- window.location.hostname;
364
- document.cookie = name + "=" + (value || "") + expires + "; path=/; domain=" + domain + "; SameSite=Lax";
369
+ let cookieValue = name + "=" + (value || "") + expires + "; path=/; SameSite=Lax";
370
+ if (cookieDomain) {
371
+ cookieValue += "; domain=" + cookieDomain;
372
+ }
373
+ document.cookie = cookieValue;
365
374
  }
366
375
 
367
376
  function getCookie(name) {
@@ -376,19 +385,26 @@
376
385
  }
377
386
 
378
387
  function deleteCookie(name) {
379
- const domain = window.location.hostname.includes('.') ?
380
- '.' + window.location.hostname.split('.').slice(-2).join('.') :
381
- window.location.hostname;
382
- document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=" + domain;
388
+ let cookieValue = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";
389
+ if (cookieDomain) {
390
+ cookieValue += "; domain=" + cookieDomain;
391
+ }
392
+ document.cookie = cookieValue;
383
393
  }
384
394
 
385
395
  // Check for URL parameters
386
396
  document.addEventListener('DOMContentLoaded', function () {
387
397
  const urlParams = new URLSearchParams(window.location.search);
388
- const usernameFromUrl = urlParams.get('username');
389
- const passwordFromUrl = urlParams.get('password');
390
398
  const usernameInput = document.getElementById('loginUsername');
391
399
 
400
+ if (urlParams.has('password')) {
401
+ urlParams.delete('password');
402
+ const sanitizedQuery = urlParams.toString();
403
+ const nextUrl = `${window.location.pathname}${sanitizedQuery ? `?${sanitizedQuery}` : ''}${window.location.hash}`;
404
+ window.history.replaceState({}, document.title, nextUrl);
405
+ showMessage('Passwords must not be passed in the URL. Enter your password in the form instead.', 'Security Notice');
406
+ }
407
+
392
408
  // Check for remembered username in cookies
393
409
  const rememberedUsername = getCookie('rememberedUsername');
394
410
  if (rememberedUsername) {
@@ -396,14 +412,6 @@
396
412
  document.getElementById('rememberMe').checked = true;
397
413
  }
398
414
 
399
- if (usernameFromUrl) {
400
- usernameInput.value = usernameFromUrl;
401
- }
402
-
403
- if (passwordFromUrl) {
404
- document.getElementById('loginPassword').value = passwordFromUrl;
405
- }
406
-
407
415
  // Automatically focus the first empty field
408
416
  if (!usernameInput.value) {
409
417
  usernameInput.focus();
@@ -194,7 +194,7 @@
194
194
  <div>
195
195
  <div class="session-status">✅ Authentication successful</div>
196
196
  <h3 class="session-title">{{username}} <small class="session-role">· {{role}}</small></h3>
197
- <p class="session-sub">ID: {{id}} · Session: {{sessionIdShort}}…</p>
197
+ <p class="session-sub">UserId: {{userId}} · Session: {{sessionIdShort}}…</p>
198
198
  </div>
199
199
 
200
200
  <div class="session-details" aria-live="polite">
@@ -205,7 +205,7 @@
205
205
  </div>
206
206
 
207
207
  <div class="session-actions">
208
- <button class="btn btn-danger" onclick="logout()" aria-label="Log out">
208
+ <button class="btn btn-danger" onclick="mbkauthe.logout()" aria-label="Log out">
209
209
  <i class="fas fa-sign-out-alt"></i> Logout
210
210
  </button>
211
211
  <a class="btn btn-outline" href="https://portal.mbktech.org/">
@@ -233,7 +233,6 @@
233
233
  </div>
234
234
  </section>
235
235
 
236
- <script src="/mbkauthe/main.js{{cacheBuster}}"></script>
237
236
  {{#if sessionExpiry}}
238
237
  <script>
239
238
  (function () {
@@ -261,21 +261,7 @@
261
261
  logoutButton.textContent = 'Logging out...';
262
262
 
263
263
  try {
264
- const response = await fetch('/mbkauthe/api/logout', {
265
- method: 'POST',
266
- headers: {
267
- 'Content-Type': 'application/json'
268
- },
269
- credentials: 'include'
270
- });
271
-
272
- const result = await response.json().catch(() => ({}));
273
-
274
- if (response.ok) {
275
- window.location.reload();
276
- } else {
277
- alert(result.message || 'Logout failed. Please try again.');
278
- }
264
+ await mbkauthe.logout();
279
265
  } catch (error) {
280
266
  console.error(`[mbkauthe] Error during logout:`, error);
281
267
  alert('Logout failed. Please try again.');