mbkauthe 4.8.3 → 4.9.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.
@@ -3,10 +3,11 @@ import fetch from 'node-fetch';
3
3
  import rateLimit from 'express-rate-limit';
4
4
  import { mbkautheVar, packageJson, appVersion } from "#config.js";
5
5
  import { renderError, renderPage } from "#response.js";
6
- import { authenticate, validateSession, validateSessionAndRole } from "../middleware/auth.js";
6
+ import { authenticate, sessVal, sessRole } from "../middleware/auth.js";
7
7
  import { ErrorCodes, ErrorMessages, createErrorResponse } from "../utils/errors.js";
8
8
  import { dblogin } from "#pool.js";
9
9
  import { clearSessionCookies, decryptSessionId, cachedCookieOptions } from "#cookies.js";
10
+ import { AuthRepository } from "../db/AuthRepository.js";
10
11
  import { fileURLToPath } from "url";
11
12
  import path from "path";
12
13
  import fs from "fs";
@@ -18,6 +19,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
19
 
19
20
 
20
21
  const router = express.Router();
22
+ const authRepo = new AuthRepository({ db: dblogin });
21
23
  // Rate limiter for info/test routes
22
24
  const LoginLimit = rateLimit({
23
25
  windowMs: 1 * 60 * 1000,
@@ -60,7 +62,7 @@ router.get("/bg.webp", (req, res) => {
60
62
  res.setHeader('Cache-Control', 'public, max-age=31536000');
61
63
  const stream = fs.createReadStream(imgPath);
62
64
  stream.on('error', (err) => {
63
- console.error('[mbkauthe] Error streaming bg.webp:', err);
65
+ console.error(`[mbkauthe] Error streaming bg.webp:`, err);
64
66
  res.status(404).send('Image not found');
65
67
  });
66
68
  stream.pipe(res);
@@ -78,7 +80,7 @@ router.get('/user/profilepic', async (req, res) => {
78
80
  }
79
81
  const stream = fs.createReadStream(iconPath);
80
82
  stream.on('error', (err) => {
81
- console.error('[mbkauthe] Error streaming icon.svg:', err);
83
+ console.error(`[mbkauthe] Error streaming icon.svg:`, err);
82
84
  res.status(404).send('Icon not found');
83
85
  });
84
86
  stream.pipe(res);
@@ -100,14 +102,10 @@ router.get('/user/profilepic', async (req, res) => {
100
102
 
101
103
  // If not in cache, fetch from DB
102
104
  if (!imageUrl) {
103
- const result = await dblogin.query({
104
- name: 'get-user-profile-pic',
105
- text: 'SELECT "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
106
- values: [username]
107
- });
105
+ const profile = await authRepo.getUserImageByUsername(username, 'get-user-profile-pic');
108
106
 
109
- if (result.rows.length > 0 && result.rows[0].Image && result.rows[0].Image.trim() !== '') {
110
- imageUrl = result.rows[0].Image;
107
+ if (profile && profile.Image && profile.Image.trim() !== '') {
108
+ imageUrl = profile.Image;
111
109
  } else {
112
110
  imageUrl = 'default';
113
111
  }
@@ -152,21 +150,21 @@ router.get('/user/profilepic', async (req, res) => {
152
150
 
153
151
  imageResponse.body.pipe(res);
154
152
  } catch (fetchErr) {
155
- console.error('[mbkauthe] Error fetching external profile picture:', fetchErr);
153
+ console.error(`[mbkauthe] Error fetching external profile picture:`, fetchErr);
156
154
  res.cookie('profileImageUrl', 'default', { ...cachedCookieOptions, httpOnly: false });
157
155
  res.cookie('profileImageUser', username, { ...cachedCookieOptions, httpOnly: false });
158
156
  return serveDefaultIcon();
159
157
  }
160
158
 
161
159
  } catch (err) {
162
- console.error('[mbkauthe] Error fetching profile picture:', err);
160
+ console.error(`[mbkauthe] Error fetching profile picture:`, err);
163
161
  return serveDefaultIcon();
164
162
  }
165
163
  });
166
164
 
167
165
  if (process.env.env === 'dev') {
168
166
  // Dev-only diagnostic endpoint to verify SuperAdmin role enforcement
169
- router.get(['/validate-superadmin'], validateSessionAndRole("SuperAdmin"), LoginLimit, async (req, res) => {
167
+ router.get(['/validate-superadmin'], sessRole("SuperAdmin"), LoginLimit, async (req, res) => {
170
168
  try {
171
169
  const user = req.session?.user || null;
172
170
  return res.json({
@@ -180,14 +178,14 @@ if (process.env.env === 'dev') {
180
178
  } : null
181
179
  });
182
180
  } catch (err) {
183
- console.error('[mbkauthe] debug validate-superadmin error:', err);
181
+ console.error(`[mbkauthe] debug validate-superadmin error:`, err);
184
182
  return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
185
183
  }
186
184
  });
187
185
  }
188
186
 
189
187
  // Test route
190
- router.get(['/test', '/'], validateSession, LoginLimit, async (req, res) => {
188
+ router.get(['/test', '/'], sessVal, LoginLimit, async (req, res) => {
191
189
  const { username, fullname, role, id, sessionId, allowedApps } = req.session.user;
192
190
 
193
191
  const sessionExpiry = req.session.cookie?.expires
@@ -208,7 +206,7 @@ router.get(['/test', '/'], validateSession, LoginLimit, async (req, res) => {
208
206
  });
209
207
  });
210
208
 
211
- router.post('/test', validateSession, LoginLimit, async (req, res) => {
209
+ router.post('/test', sessVal, LoginLimit, async (req, res) => {
212
210
  if (req.session?.user) {
213
211
  return res.json({ success: true, message: "You are logged in" });
214
212
  }
@@ -229,31 +227,13 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
229
227
  }
230
228
 
231
229
  // Single round-trip: fetch app-session expiry and (if needed) connect-pg-simple expiry.
232
- const result = await dblogin.query({
233
- name: 'check-session-validity',
234
- text: `
235
- SELECT
236
- s.expires_at,
237
- u."Active",
238
- CASE
239
- WHEN s.expires_at IS NULL THEN (SELECT expire FROM "session" WHERE sid = $2)
240
- ELSE NULL
241
- END AS connect_expire
242
- FROM "Sessions" s
243
- JOIN "Users" u ON s."UserName" = u."UserName"
244
- WHERE s.id = $1
245
- LIMIT 1
246
- `,
247
- values: [sessionId, req.sessionID]
248
- });
230
+ const row = await authRepo.getSessionValidity(sessionId, req.sessionID, 'check-session-validity');
249
231
 
250
- if (result.rows.length === 0) {
232
+ if (!row) {
251
233
  req.session.destroy(() => { });
252
234
  clearSessionCookies(res);
253
235
  return res.status(200).json({ sessionValid: false, expiry: null });
254
236
  }
255
-
256
- const row = result.rows[0];
257
237
  if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
258
238
  req.session.destroy(() => { });
259
239
  clearSessionCookies(res);
@@ -266,7 +246,7 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
266
246
 
267
247
  return res.status(200).json({ sessionValid: true, expiry });
268
248
  } catch (err) {
269
- console.error('[mbkauthe] checkSession error:', err);
249
+ console.error(`[mbkauthe] checkSession error:`, err);
270
250
  return res.status(200).json({ sessionValid: false, expiry: null });
271
251
  }
272
252
  });
@@ -298,17 +278,8 @@ function normalizeSessionIdFromBody(body = {}) {
298
278
  }
299
279
 
300
280
  async function getSessionValidationRow(sessionId, queryName = 'check-session-validity-by-id') {
301
- const result = await dblogin.query({
302
- name: queryName,
303
- text: `SELECT s.expires_at, u."Active", u."UserName", u."Role" FROM "Sessions" s JOIN "Users" u ON s."UserName" = u."UserName" WHERE s.id = $1 LIMIT 1`,
304
- values: [sessionId]
305
- });
306
-
307
- if (result.rows.length === 0) {
308
- return null;
309
- }
310
-
311
- return result.rows[0];
281
+ const row = await authRepo.getSessionValidationRow(sessionId, queryName);
282
+ return row || null;
312
283
  }
313
284
 
314
285
  function isSessionRowValid(row) {
@@ -342,7 +313,7 @@ router.post('/api/checkSession', LoginLimit, async (req, res) => {
342
313
  const expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
343
314
  return res.status(200).json({ sessionValid: true, expiry });
344
315
  } catch (err) {
345
- console.error('[mbkauthe] checkSession (body) error:', err);
316
+ console.error(`[mbkauthe] checkSession (body) error:`, err);
346
317
  return res.status(200).json({ sessionValid: false, expiry: null });
347
318
  }
348
319
  });
@@ -374,7 +345,7 @@ router.post('/api/verifySession', LoginLimit, async (req, res) => {
374
345
  const expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
375
346
  return res.status(200).json({ valid: true, expiry, username: row.UserName, role: row.Role });
376
347
  } catch (err) {
377
- console.error('[mbkauthe] verifySession error:', err);
348
+ console.error(`[mbkauthe] verifySession error:`, err);
378
349
  return res.status(200).json({ valid: false, expiry: null });
379
350
  }
380
351
  });
@@ -465,7 +436,7 @@ router.get("/ErrorCode", (req, res) => {
465
436
  errorCategories: categoriesWithErrors
466
437
  });
467
438
  } catch (err) {
468
- console.error("[mbkauthe] Error rendering error codes page:", err);
439
+ console.error(`[mbkauthe] Error rendering error codes page:`, err);
469
440
  return renderError(res, req, {
470
441
  layout: false,
471
442
  code: 500,
@@ -488,7 +459,7 @@ export async function getLatestVersion() {
488
459
  const latestPackageJson = await response.json();
489
460
  return typeof latestPackageJson.version === 'string' ? latestPackageJson.version : null;
490
461
  } catch (error) {
491
- console.error('[mbkauthe] Error fetching latest version from GitHub');
462
+ console.error(`[mbkauthe] Error fetching latest version from GitHub`, error);
492
463
  return null;
493
464
  }
494
465
  }
@@ -503,7 +474,7 @@ export async function checkVersion() {
503
474
  } else if (hasValidLatest) {
504
475
  console.info(`[mbkauthe] Running latest version (${packageJson.version}).`);
505
476
  } else {
506
- console.info('[mbkauthe] Skipped version check warning: latest version unavailable.');
477
+ console.info(`[mbkauthe] Skipped version check warning: latest version unavailable.`);
507
478
  }
508
479
  } catch (error) {
509
480
  console.warn(`[mbkauthe] Failed to check for updates: ${error.message}`);
@@ -520,7 +491,7 @@ router.get(["/info", "/i"], LoginLimit, async (req, res) => {
520
491
  try {
521
492
  latestVersion = await getLatestVersion();
522
493
  } catch (err) {
523
- console.error("[mbkauthe] Error fetching package-lock.json:", err);
494
+ console.error(`[mbkauthe] Error fetching package-lock.json:`, err);
524
495
  }
525
496
 
526
497
  try {
@@ -531,7 +502,7 @@ router.get(["/info", "/i"], LoginLimit, async (req, res) => {
531
502
  latestVersion
532
503
  });
533
504
  } catch (err) {
534
- console.error("[mbkauthe] Error fetching version information:", err);
505
+ console.error(`[mbkauthe] Error fetching version information:`, err);
535
506
  res.status(500).send(`
536
507
  <html>
537
508
  <head>
@@ -551,13 +522,13 @@ router.get(["/info.json", "/i.json"], LoginLimit, async (req, res) => {
551
522
  try {
552
523
  latestVersion = await getLatestVersion();
553
524
  } catch (err) {
554
- console.error("[mbkauthe] Error fetching package-lock.json:", err);
525
+ console.error(`[mbkauthe] Error fetching package-lock.json:`, err);
555
526
  }
556
527
 
557
528
  try {
558
529
  res.json({ mbkautheVar: safe_mbkautheVar, CurrentVersion: packageJson.version, APP_VERSION: appVersion, latestVersion });
559
530
  } catch (err) {
560
- console.error("[mbkauthe] Error fetching version information:", err);
531
+ console.error(`[mbkauthe] Error fetching version information:`, err);
561
532
  res.status(500).json({ success: false, message: "Failed to fetch version information" });
562
533
  }
563
534
  });
@@ -567,32 +538,26 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
567
538
  try {
568
539
  // Run both operations in parallel for better performance
569
540
  await Promise.all([
570
- dblogin.query({
571
- name: 'terminate-all-app-sessions',
572
- text: 'DELETE FROM "Sessions"'
573
- }),
574
- dblogin.query({
575
- name: 'terminate-all-db-sessions',
576
- text: 'DELETE FROM "session" WHERE expire > NOW()'
577
- })
541
+ authRepo.deleteAllAppSessions('terminate-all-app-sessions'),
542
+ authRepo.deleteActiveSessionStoreRows('terminate-all-db-sessions')
578
543
  ]);
579
544
 
580
545
  req.session.destroy((err) => {
581
546
  if (err) {
582
- console.log("[mbkauthe] Error destroying session:", err);
547
+ console.log(`[mbkauthe] Error destroying session:`, err);
583
548
  return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
584
549
  }
585
550
 
586
551
  clearSessionCookies(res);
587
552
 
588
- console.log("[mbkauthe] All sessions terminated successfully");
553
+ console.log(`[mbkauthe] All sessions terminated successfully`);
589
554
  res.status(200).json({
590
555
  success: true,
591
556
  message: "All sessions terminated successfully",
592
557
  });
593
558
  });
594
559
  } catch (err) {
595
- console.error("[mbkauthe] Database query error during session termination:", err);
560
+ console.error(`[mbkauthe] Database query error during session termination:`, err);
596
561
  res.status(500).json({ success: false, message: "Internal Server Error" });
597
562
  }
598
563
  });
@@ -8,8 +8,10 @@ import { dblogin } from "#pool.js";
8
8
  import { mbkautheVar } from "#config.js";
9
9
  import { renderError } from "../utils/response.js";
10
10
  import { checkTrustedDevice, completeLoginProcess } from "./auth.js";
11
+ import { AuthRepository } from "../db/AuthRepository.js";
11
12
 
12
13
  const router = express.Router();
14
+ const authRepo = new AuthRepository({ db: dblogin });
13
15
 
14
16
  // CSRF protection middleware
15
17
  const csrfProtection = csurf({ cookie: true });
@@ -42,25 +44,16 @@ const createOAuthStrategy = async (provider, profile, done) => {
42
44
  console.log(`[mbkauthe] ${provider} OAuth callback for user: ${profile.emails?.[0]?.value || profile.id}`);
43
45
 
44
46
  const isGitHub = provider === 'GitHub';
45
- const tableName = isGitHub ? 'user_github' : 'user_google';
46
- const idField = isGitHub ? 'github_id' : 'google_id';
47
- const queryName = isGitHub ? 'github-login-get-user' : 'google-login-get-user';
48
47
 
49
48
  // Check if this OAuth account is linked to any user
50
- const oauthUser = await dblogin.query({
51
- name: queryName,
52
- text: `SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id" FROM ${tableName} ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.${idField} = $1`,
53
- values: [profile.id]
54
- });
49
+ const user = await authRepo.getOAuthUserByProviderId(provider, profile.id);
55
50
 
56
- if (oauthUser.rows.length === 0) {
51
+ if (!user) {
57
52
  const error = new Error(`${provider} account not linked to any user`);
58
53
  error.code = `${provider.toUpperCase()}_NOT_LINKED`;
59
54
  return done(error);
60
55
  }
61
56
 
62
- const user = oauthUser.rows[0];
63
-
64
57
  // Check if the user account is active
65
58
  if (!user.Active) {
66
59
  const error = new Error('Account is inactive');
@@ -300,21 +293,9 @@ const validateOAuthCallback = (req, res) => {
300
293
  };
301
294
 
302
295
  const finishProviderLogin = async (req, res, provider, username, detailValue = '') => {
303
- const userQuery = `
304
- SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps",
305
- tfa."TwoFAStatus"
306
- FROM "Users" u
307
- LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
308
- WHERE u."UserName" = $1
309
- `;
310
-
311
- const userResult = await dblogin.query({
312
- name: `${provider.toLowerCase()}-callback-get-user`,
313
- text: userQuery,
314
- values: [username]
315
- });
316
-
317
- if (userResult.rows.length === 0) {
296
+ const user = await authRepo.getUserWithTwoFA(username, `${provider.toLowerCase()}-callback-get-user`);
297
+
298
+ if (!user) {
318
299
  console.error(`[mbkauthe] ${provider} login: User not found: ${username}`);
319
300
  return renderError(res, req, {
320
301
  code: 404,
@@ -326,8 +307,6 @@ const finishProviderLogin = async (req, res, provider, username, detailValue = '
326
307
  });
327
308
  }
328
309
 
329
- const user = userResult.rows[0];
330
-
331
310
  // Check for trusted device
332
311
  const trustedDeviceUser = await checkTrustedDevice(req, user.UserName);
333
312
  if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
@@ -0,0 +1,35 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+
3
+ export function extractAuthorizationToken(authorizationHeader) {
4
+ if (typeof authorizationHeader !== "string") return "";
5
+
6
+ const raw = authorizationHeader.trim();
7
+ if (!raw) return "";
8
+
9
+ const bearerMatch = /^bearer\s+(.+)$/i.exec(raw);
10
+ if (bearerMatch) return bearerMatch[1].trim();
11
+
12
+ return raw;
13
+ }
14
+
15
+ function sha256Buffer(value) {
16
+ return createHash("sha256").update(value, "utf8").digest();
17
+ }
18
+
19
+ /**
20
+ * Constant-time comparison of two strings by hashing them first.
21
+ *
22
+ * Notes:
23
+ * - Always computes both hashes (32 bytes each) and uses timingSafeEqual.
24
+ * - Returns false when expectedToken is empty/unset.
25
+ */
26
+ export function timingSafeTokenMatch(providedToken, expectedToken) {
27
+ const provided = typeof providedToken === "string" ? providedToken : "";
28
+ const expected = typeof expectedToken === "string" ? expectedToken : "";
29
+
30
+ const providedHash = sha256Buffer(provided);
31
+ const expectedHash = sha256Buffer(expected);
32
+
33
+ const matches = timingSafeEqual(providedHash, expectedHash);
34
+ return matches && expected.length > 0;
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "4.8.3",
3
+ "version": "4.9.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/public/main.css CHANGED
@@ -286,6 +286,14 @@ header {
286
286
  transition: var(--transition);
287
287
  }
288
288
 
289
+ .icon-button {
290
+ appearance: none;
291
+ border: 0;
292
+ background: transparent;
293
+ padding: 0;
294
+ font: inherit;
295
+ }
296
+
289
297
  .input-icon:hover {
290
298
  color: var(--accent);
291
299
  }
@@ -304,6 +312,14 @@ header {
304
312
  box-shadow: var(--box-shadow);
305
313
  }
306
314
 
315
+ .btn-social,
316
+ .swi {
317
+ appearance: none;
318
+ border: 0.13rem solid;
319
+ font: inherit;
320
+ cursor: pointer;
321
+ }
322
+
307
323
  .swi {
308
324
  display: flex;
309
325
  align-items: center;
@@ -315,7 +331,6 @@ header {
315
331
  font-size: var(--text-size-sm);
316
332
  text-decoration: none;
317
333
  transition: var(--transition);
318
- border: 0.13rem solid;
319
334
  position: relative;
320
335
  z-index: 1;
321
336
  overflow: hidden;
@@ -423,6 +438,25 @@ header {
423
438
  text-decoration: none;
424
439
  }
425
440
 
441
+ .btn-message-action {
442
+ appearance: none;
443
+ border: 0;
444
+ border-radius: var(--border-radius);
445
+ background: var(--accent);
446
+ color: var(--dark);
447
+ font: inherit;
448
+ font-weight: 700;
449
+ cursor: pointer;
450
+ padding: 0.7rem 1rem;
451
+ transition: var(--transition);
452
+ }
453
+
454
+ .btn-message-action:hover,
455
+ .btn-message-action:focus-visible {
456
+ background: var(--surface-1);
457
+ color: var(--accent);
458
+ }
459
+
426
460
  .token-container {
427
461
  animation: fadeInUp 0.4s ease-out;
428
462
  }
@@ -776,7 +810,6 @@ header {
776
810
  font-size: 0.9rem;
777
811
  text-decoration: none;
778
812
  transition: var(--transition);
779
- border: 0.13rem solid;
780
813
  position: relative;
781
814
  z-index: 1;
782
815
  overflow: hidden;
package/public/main.js CHANGED
@@ -25,7 +25,7 @@ async function logout() {
25
25
  alert(result.message);
26
26
  }
27
27
  } catch (error) {
28
- console.error("[mbkauthe] Error during logout:", error);
28
+ console.error(`[mbkauthe] Error during logout:`, error);
29
29
  alert(`Logout failed: ${error.message}`);
30
30
  }
31
31
  }
@@ -67,7 +67,7 @@ async function selectiveCacheClear() {
67
67
  window.location.reload();
68
68
 
69
69
  } catch (error) {
70
- console.error('[mbkauthe] selective cache clear failed:', error);
70
+ console.error(`[mbkauthe] selective cache clear failed:`, error);
71
71
  window.location.reload();
72
72
  }
73
73
  }
@@ -86,7 +86,7 @@ function checkSession() {
86
86
  window.location.reload(); // Reload the page to update the session status
87
87
  }
88
88
  })
89
- .catch((error) => console.error("[mbkauthe] Error checking session:", error));
89
+ .catch((error) => console.error(`[mbkauthe] Error checking session:`, error));
90
90
  }
91
91
  // Call validateSession every 2 minutes (120000 milliseconds)
92
92
  // setInterval(checkSession, validateSessionInterval);
@@ -312,7 +312,7 @@
312
312
  setTimeout(() => { copyBtn.innerHTML = orig; }, 2000);
313
313
  }
314
314
  } catch (err) {
315
- console.error('[mbkauthe] Failed to copy: ', err);
315
+ console.error(`[mbkauthe] Failed to copy:`, err);
316
316
  }
317
317
  });
318
318
  }
@@ -3,7 +3,7 @@
3
3
  <a class="logo">
4
4
  <img src="/icon.svg" alt="Logo" class="logo-image"/>
5
5
  <span class="logo-text">BK <span>Tech</span></span>
6
- <span class="logo-comp">{{#if appName}}{{appName}}{{else}}{{#if app}}{{app}}{{else}}mbkauthe{{/if}}{{/if}}
6
+ <span class="logo-comp">{{#if appName}}{{appName}}{{else}}{{#if app}}{{app}}{{else}}mbkauthe{{/if}}{{/if}}</span>
7
7
  </a>
8
8
 
9
9
  {{> profilemenu}}
@@ -31,7 +31,9 @@
31
31
  title="Token must be exactly 6 digits" maxlength="6" minlength="6" autocomplete="off" autofocus
32
32
  required />
33
33
  <label class="form-label">2FA Token</label>
34
- <i class="fas fa-info-circle input-icon" onclick="tokeninfo()"></i>
34
+ <button type="button" class="icon-button input-icon" onclick="tokeninfo()" aria-label="What is the 2FA token?">
35
+ <i class="fas fa-info-circle"></i>
36
+ </button>
35
37
  </div>
36
38
 
37
39
  <div class="trust-device-container">
@@ -40,8 +42,10 @@
40
42
  <span class="checkbox-custom"></span>
41
43
  <span class="checkbox-text">Trust this device for {{DEVICE_TRUST_DURATION_DAYS}} days</span>
42
44
  </label>
43
- <i class="fas fa-info-circle trust-device-info" onclick="trustDeviceInfo()"
44
- title="Learn more about trusted devices"></i>
45
+ <button type="button" class="icon-button trust-device-info" onclick="trustDeviceInfo()"
46
+ title="Learn more about trusted devices" aria-label="Learn more about trusted devices">
47
+ <i class="fas fa-info-circle"></i>
48
+ </button>
45
49
  </div>
46
50
 
47
51
  <button type="submit" class="btn-login" id="loginButton">
@@ -50,10 +54,10 @@
50
54
 
51
55
  <p class="terms-info">
52
56
  By logging in, you agree to our
53
- <a href="https://mbktech.org/PrivacyPolicy" target="_blank" class="terms-link">Terms &
57
+ <a href="https://mbktech.org/PrivacyPolicy" target="_blank" rel="noopener noreferrer" class="terms-link">Terms &
54
58
  Conditions</a>
55
59
  and
56
- <a href="https://mbktech.org/PrivacyPolicy" target="_blank" class="terms-link">Privacy Policy</a>.
60
+ <a href="https://mbktech.org/PrivacyPolicy" target="_blank" rel="noopener noreferrer" class="terms-link">Privacy Policy</a>.
57
61
  </p>
58
62
  </form>
59
63
  </div>
@@ -362,7 +362,7 @@
362
362
  emptyStateLink.href = `/mbkauthe/login${encoded ? `?redirect=${encoded}` : ''}`;
363
363
  }
364
364
  } catch (err) {
365
- console.error('[mbkauthe] Failed to set login hrefs with redirect query param:', err);
365
+ console.error(`[mbkauthe] Failed to set login hrefs with redirect query param:`, err);
366
366
  }
367
367
  })();
368
368
 
@@ -468,7 +468,7 @@
468
468
  });
469
469
 
470
470
  } catch (err) {
471
- console.error('[mbkauthe] Failed to load accounts:', err);
471
+ console.error(`[mbkauthe] Failed to load accounts:`, err);
472
472
  list.innerHTML = '<div class="empty-state">Failed to load accounts.</div>';
473
473
  }
474
474
  }
@@ -501,7 +501,7 @@
501
501
  loadAccounts();
502
502
  }
503
503
  } catch (err) {
504
- console.error('[mbkauthe] Switch failed:', err);
504
+ console.error(`[mbkauthe] Switch failed:`, err);
505
505
  showMessage('Could not switch account right now. Please try again.', 'Switch Account');
506
506
  }
507
507
  }
@@ -521,7 +521,7 @@
521
521
  showMessage(data.message || 'Could not sign out all accounts', 'Sign out');
522
522
  }
523
523
  } catch (err) {
524
- console.error('[mbkauthe] Sign-out-all failed:', err);
524
+ console.error(`[mbkauthe] Sign-out-all failed:`, err);
525
525
  showMessage('Could not sign out all accounts right now. Please try again.', 'Sign out');
526
526
  }
527
527
  }