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.
- package/README.md +26 -6
- package/docs/api.md +45 -27
- package/docs/auth-flows.mmd +9 -9
- package/docs/auth-processes.mmd +71 -0
- package/docs/db.md +36 -276
- package/docs/db.sql +6 -9
- package/docs/env.md +2 -8
- package/docs/error-messages.md +3 -3
- package/docs/images/auth-flows.svg +1 -1
- package/docs/images/auth-process.svg +1 -0
- package/index.d.ts +1 -2
- package/index.js +2 -1
- package/lib/config/cookies.js +6 -6
- package/lib/config/index.js +4 -10
- package/lib/createTable.js +5 -5
- package/lib/db/AuthRepository.js +295 -0
- package/lib/db/BaseRepository.js +185 -0
- package/lib/db/dialects/postgres.js +18 -0
- package/lib/middleware/auth.js +46 -65
- package/lib/middleware/index.js +10 -16
- package/lib/pool.js +2 -2
- package/lib/routes/auth.js +88 -169
- package/lib/routes/dbLogs.js +3 -3
- package/lib/routes/misc.js +33 -68
- package/lib/routes/oauth.js +7 -28
- package/lib/utils/timingSafeToken.js +35 -0
- package/package.json +1 -1
- package/public/main.css +35 -2
- package/public/main.js +3 -3
- package/views/Error/dError.handlebars +1 -1
- package/views/header.handlebars +1 -1
- package/views/pages/2fa.handlebars +9 -5
- package/views/pages/accountSwitch.handlebars +4 -4
- package/views/pages/loginmbkauthe.handlebars +43 -26
- package/views/profilemenu.handlebars +2 -2
- package/views/showmessage.handlebars +2 -2
package/lib/routes/misc.js
CHANGED
|
@@ -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,
|
|
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(
|
|
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(
|
|
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
|
|
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 (
|
|
110
|
-
imageUrl =
|
|
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(
|
|
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(
|
|
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'],
|
|
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(
|
|
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', '/'],
|
|
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',
|
|
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
|
|
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 (
|
|
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(
|
|
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
|
|
302
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
571
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
});
|
package/lib/routes/oauth.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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
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(
|
|
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(
|
|
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(
|
|
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);
|
package/views/header.handlebars
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
44
|
-
title="Learn more about trusted devices"
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|