mbkauthe 4.9.0 → 5.0.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/docs/api.md +29 -178
- package/docs/db.md +1 -1
- package/docs/db.sql +305 -253
- package/index.js +5 -3
- package/lib/config/cookies.js +84 -18
- package/lib/config/index.js +3 -1
- package/lib/config/tokenScopes.js +1 -1
- package/lib/createTable.js +95 -8
- package/lib/db/AuthRepository.js +57 -16
- package/lib/db/BaseRepository.js +9 -1
- package/lib/db/dialects/postgres.js +1 -1
- package/lib/main.js +5 -5
- package/lib/middleware/auth.js +201 -218
- package/lib/middleware/index.js +13 -14
- package/lib/middleware/scopeValidator.js +8 -3
- package/lib/pool.js +5 -6
- package/lib/routes/auth.js +42 -47
- package/lib/routes/dbLogs.js +247 -29
- package/lib/routes/misc.js +6 -4
- package/lib/routes/oauth.js +19 -23
- package/lib/utils/dbQueryLogger.js +485 -80
- package/lib/utils/errors.js +1 -1
- package/lib/utils/logger.js +12 -0
- package/lib/utils/timingSafeToken.js +1 -1
- package/package.json +1 -1
- package/public/main.css +1 -1
- package/test.spec.js +515 -48
- package/views/pages/dbLogs.handlebars +618 -420
|
@@ -13,8 +13,13 @@ import { ErrorCodes, createErrorResponse } from '../utils/errors.js';
|
|
|
13
13
|
export function validateTokenScope(req, res, next) {
|
|
14
14
|
// Only validate for API token requests (not cookie-based sessions)
|
|
15
15
|
// Check if this request was authenticated via API token
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const tokenScope = req.auth?.type === 'api-token'
|
|
17
|
+
? req.auth.tokenScope
|
|
18
|
+
: req.session?.user?.sessionId === 'api-token-session'
|
|
19
|
+
? req.session.user.tokenScope
|
|
20
|
+
: null;
|
|
21
|
+
|
|
22
|
+
if (tokenScope) {
|
|
18
23
|
const requestMethod = req.method;
|
|
19
24
|
|
|
20
25
|
// Check if scope allows this HTTP method
|
|
@@ -29,4 +34,4 @@ export function validateTokenScope(req, res, next) {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
next();
|
|
32
|
-
}
|
|
37
|
+
}
|
package/lib/pool.js
CHANGED
|
@@ -9,12 +9,11 @@ export { runWithRequestContext, getRequestContext };
|
|
|
9
9
|
|
|
10
10
|
const poolConfig = {
|
|
11
11
|
connectionString: mbkautheVar.LOGIN_DB,
|
|
12
|
-
ssl: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
connectionTimeoutMillis: 10000,
|
|
12
|
+
ssl: { rejectUnauthorized: true },
|
|
13
|
+
max: 10,
|
|
14
|
+
idleTimeoutMillis: 30000,
|
|
15
|
+
connectionTimeoutMillis: 5000,
|
|
16
|
+
application_name: `${mbkautheVar.APP_NAME}-mbkauthe-app`,
|
|
18
17
|
};
|
|
19
18
|
|
|
20
19
|
export const dblogin = new Pool(poolConfig);
|
package/lib/routes/auth.js
CHANGED
|
@@ -12,12 +12,14 @@ import {
|
|
|
12
12
|
encryptSessionId
|
|
13
13
|
} from "#cookies.js";
|
|
14
14
|
import { packageJson } from "#config.js";
|
|
15
|
-
import { hashPassword
|
|
15
|
+
import { hashPassword } from "#config.js";
|
|
16
16
|
import { ErrorCodes, createErrorResponse, logError } from "../utils/errors.js";
|
|
17
17
|
import { AuthRepository } from "../db/AuthRepository.js";
|
|
18
|
+
import { createLogger } from "../utils/logger.js";
|
|
18
19
|
|
|
19
20
|
const router = express.Router();
|
|
20
21
|
const authRepo = new AuthRepository({ db: dblogin });
|
|
22
|
+
const logAuth = createLogger("auth");
|
|
21
23
|
|
|
22
24
|
// Helper function to clear profile picture cache
|
|
23
25
|
function clearProfilePicCache(req, username) {
|
|
@@ -113,7 +115,7 @@ export async function checkTrustedDevice(req, username) {
|
|
|
113
115
|
if (deviceUser) {
|
|
114
116
|
|
|
115
117
|
if (!deviceUser.Active) {
|
|
116
|
-
|
|
118
|
+
logAuth(`Trusted device check: inactive account for username: ${username}`);
|
|
117
119
|
return null;
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -125,7 +127,7 @@ export async function checkTrustedDevice(req, username) {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
logAuth(`Trusted device validated for user: ${username}`);
|
|
129
131
|
return {
|
|
130
132
|
id: deviceUser.id,
|
|
131
133
|
username: username,
|
|
@@ -173,11 +175,11 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
173
175
|
let dbSessionId;
|
|
174
176
|
try {
|
|
175
177
|
await authRepo.withTransaction(async (txRepo) => {
|
|
176
|
-
await txRepo.
|
|
178
|
+
await txRepo.advisoryTransactionLock(`sessions:${username}`, "lock-user-sessions");
|
|
177
179
|
const currentSessions = await txRepo.cleanupAndCountUserSessions(username);
|
|
178
180
|
if (currentSessions >= MAX_SESSIONS) {
|
|
179
181
|
const sessionsToDelete = currentSessions - MAX_SESSIONS + 1; // +1 to make room for new session
|
|
180
|
-
|
|
182
|
+
logAuth(`User "${username}" has ${currentSessions} active sessions, exceeding max of ${MAX_SESSIONS}. Deleting ${sessionsToDelete} oldest sessions.`);
|
|
181
183
|
|
|
182
184
|
await txRepo.deleteOldestSessionsForUser(username, sessionsToDelete, "prune-oldest-user-session");
|
|
183
185
|
}
|
|
@@ -297,14 +299,14 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
297
299
|
|
|
298
300
|
// Send raw token to client as httpOnly cookie only
|
|
299
301
|
res.cookie("device_token", deviceToken, getDeviceTokenCookieOptions());
|
|
300
|
-
|
|
302
|
+
logAuth(`Trusted device token created for user: ${username}`);
|
|
301
303
|
} catch (deviceErr) {
|
|
302
304
|
console.error(`[mbkauthe] Error creating trusted device:`, deviceErr);
|
|
303
305
|
// Continue with login even if device trust fails
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
|
|
309
|
+
logAuth(`User "${username}" logged in successfully (last_login updated)`);
|
|
308
310
|
|
|
309
311
|
const responsePayload = {
|
|
310
312
|
success: true,
|
|
@@ -326,7 +328,7 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
326
328
|
|
|
327
329
|
// POST /mbkauthe/api/login
|
|
328
330
|
router.post("/api/login", LoginLimit, async (req, res) => {
|
|
329
|
-
|
|
331
|
+
logAuth(`Login request received`);
|
|
330
332
|
|
|
331
333
|
const { username, password, redirect } = req.body;
|
|
332
334
|
|
|
@@ -356,7 +358,7 @@ router.post("/api/login", LoginLimit, async (req, res) => {
|
|
|
356
358
|
);
|
|
357
359
|
}
|
|
358
360
|
|
|
359
|
-
|
|
361
|
+
logAuth(`Login attempt for username: ${username.trim()}`);
|
|
360
362
|
|
|
361
363
|
const trimmedUsername = username.trim();
|
|
362
364
|
|
|
@@ -413,7 +415,7 @@ router.post("/api/login", LoginLimit, async (req, res) => {
|
|
|
413
415
|
// Check for trusted device AFTER password validation
|
|
414
416
|
const trustedDeviceUser = await checkTrustedDevice(req, trimmedUsername);
|
|
415
417
|
if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
416
|
-
|
|
418
|
+
logAuth(`Trusted device login for user: ${trimmedUsername}, skipping 2FA only`);
|
|
417
419
|
|
|
418
420
|
const userForSession = {
|
|
419
421
|
id: user.id,
|
|
@@ -435,7 +437,7 @@ router.post("/api/login", LoginLimit, async (req, res) => {
|
|
|
435
437
|
allowedApps: user.AllowedApps,
|
|
436
438
|
redirectUrl: requestedRedirect
|
|
437
439
|
};
|
|
438
|
-
|
|
440
|
+
logAuth(`2FA required for user: ${trimmedUsername}`);
|
|
439
441
|
return res.json({ success: true, twoFactorRequired: true, redirectUrl: requestedRedirect });
|
|
440
442
|
}
|
|
441
443
|
|
|
@@ -592,7 +594,7 @@ router.post("/api/logout", LogoutLimit, async (req, res) => {
|
|
|
592
594
|
|
|
593
595
|
clearSessionCookies(res);
|
|
594
596
|
|
|
595
|
-
|
|
597
|
+
logAuth(`User "${username}" logged out successfully`);
|
|
596
598
|
res.status(200).json({ success: true, message: "Logout successful" });
|
|
597
599
|
});
|
|
598
600
|
} catch (err) {
|
|
@@ -611,50 +613,52 @@ router.get("/api/account-sessions", LoginLimit, async (req, res) => {
|
|
|
611
613
|
return res.json({ accounts: [], currentSessionId: req.session?.user?.sessionId || null });
|
|
612
614
|
}
|
|
613
615
|
|
|
614
|
-
const validated = [];
|
|
615
616
|
const currentSessionId = req.session?.user?.sessionId || null;
|
|
616
617
|
|
|
618
|
+
const validAccountEntries = [];
|
|
617
619
|
for (const acct of storedAccounts) {
|
|
618
620
|
if (!isUuid(acct.sessionId)) {
|
|
619
621
|
removeAccountFromCookie(req, res, acct.sessionId);
|
|
620
622
|
continue;
|
|
621
623
|
}
|
|
624
|
+
validAccountEntries.push(acct);
|
|
625
|
+
}
|
|
622
626
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
627
|
+
const sessionIds = validAccountEntries.map((acct) => acct.sessionId);
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
const sessionRows = await authRepo.getSessionsWithUsersByIds(sessionIds, "multi-session-fetch-many");
|
|
631
|
+
const sessionMap = new Map(sessionRows.map((row) => [row.sid, row]));
|
|
632
|
+
const validated = [];
|
|
633
|
+
|
|
634
|
+
for (const acct of validAccountEntries) {
|
|
635
|
+
const row = sessionMap.get(acct.sessionId);
|
|
636
|
+
const expired = row?.expires_at && new Date(row.expires_at) <= new Date();
|
|
637
|
+
const authorized = row && row.Active && (
|
|
638
|
+
row.Role === "SuperAdmin" ||
|
|
639
|
+
(Array.isArray(row.AllowedApps) && row.AllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase()))
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (!row || expired || !authorized) {
|
|
626
643
|
await invalidateDbSession(acct.sessionId);
|
|
627
644
|
removeAccountFromCookie(req, res, acct.sessionId);
|
|
628
645
|
continue;
|
|
629
646
|
}
|
|
630
647
|
|
|
631
|
-
let fullName = acct.fullName || acct.username;
|
|
632
|
-
let image = acct.image || null;
|
|
633
|
-
if (!acct.fullName || !acct.image) {
|
|
634
|
-
try {
|
|
635
|
-
const prof = await authRepo.getUserProfileByUsername(row.UserName, "multi-session-fullname-image");
|
|
636
|
-
if (prof) {
|
|
637
|
-
if (!acct.fullName && prof.FullName) fullName = prof.FullName;
|
|
638
|
-
if (!acct.image && prof.Image && prof.Image.trim() !== '') image = prof.Image;
|
|
639
|
-
}
|
|
640
|
-
} catch (profileErr) {
|
|
641
|
-
console.error(`[mbkauthe] Error fetching fullname/image for account list:`, profileErr);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
648
|
validated.push({
|
|
646
649
|
sessionId: row.sid,
|
|
647
650
|
username: row.UserName,
|
|
648
|
-
fullName,
|
|
649
|
-
image,
|
|
651
|
+
fullName: acct.fullName || row.FullName || acct.username || row.UserName,
|
|
652
|
+
image: acct.image || (typeof row.Image === 'string' && row.Image.trim() !== '' ? row.Image : null),
|
|
650
653
|
isCurrent: currentSessionId && row.sid === currentSessionId
|
|
651
654
|
});
|
|
652
|
-
} catch (err) {
|
|
653
|
-
console.error(`[mbkauthe] Error validating remembered account:`, err);
|
|
654
655
|
}
|
|
655
|
-
}
|
|
656
656
|
|
|
657
|
-
|
|
657
|
+
return res.json({ accounts: validated, currentSessionId });
|
|
658
|
+
} catch (err) {
|
|
659
|
+
console.error(`[mbkauthe] Error validating remembered accounts:`, err);
|
|
660
|
+
return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
|
|
661
|
+
}
|
|
658
662
|
});
|
|
659
663
|
|
|
660
664
|
// Switch active session to another remembered account
|
|
@@ -679,17 +683,8 @@ router.post("/api/switch-session", LoginLimit, async (req, res) => {
|
|
|
679
683
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
680
684
|
}
|
|
681
685
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
try {
|
|
685
|
-
const prof = await authRepo.getUserProfileByUsername(row.UserName, "multi-session-switch-fullname-image");
|
|
686
|
-
if (prof) {
|
|
687
|
-
if (prof.FullName) fullName = prof.FullName;
|
|
688
|
-
if (prof.Image && prof.Image.trim() !== '') switchProfileImage = prof.Image;
|
|
689
|
-
}
|
|
690
|
-
} catch (profileErr) {
|
|
691
|
-
console.error(`[mbkauthe] Error fetching fullname/image during switch:`, profileErr);
|
|
692
|
-
}
|
|
686
|
+
const fullName = row.FullName || row.UserName;
|
|
687
|
+
const switchProfileImage = typeof row.Image === 'string' && row.Image.trim() !== '' ? row.Image : null;
|
|
693
688
|
|
|
694
689
|
// Regenerate session to avoid fixation
|
|
695
690
|
await new Promise((resolve, reject) => {
|
package/lib/routes/dbLogs.js
CHANGED
|
@@ -1,15 +1,213 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
+
import rateLimit from "express-rate-limit";
|
|
2
3
|
import { renderError } from "#response.js";
|
|
3
4
|
import { dblogin } from "#pool.js";
|
|
4
5
|
import { getQueryCount, getQueryLog, resetQueryCount, resetQueryLog } from "../utils/dbQueryLogger.js";
|
|
5
6
|
import { mbkautheVar } from "#config.js";
|
|
6
|
-
import rateLimit from 'express-rate-limit';
|
|
7
7
|
|
|
8
8
|
const router = express.Router();
|
|
9
9
|
|
|
10
10
|
const isDbLogsEnabled = () => process.env.env === "dev" && process.env.dbLogs === "true";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const clampLimit = (value, fallback = 50, max = 500) => {
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
return Math.min(max, Math.floor(parsed));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const normalizeStringFilter = (value) => {
|
|
21
|
+
if (typeof value !== "string") return "";
|
|
22
|
+
return value.trim();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const parseSuccessFilter = (value) => {
|
|
26
|
+
if (value === true || value === "true") return true;
|
|
27
|
+
if (value === false || value === "false") return false;
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getRawQueryLog = () => {
|
|
32
|
+
if (typeof getQueryLog === "function") return getQueryLog();
|
|
33
|
+
if (typeof dblogin.getQueryLog === "function") return dblogin.getQueryLog();
|
|
34
|
+
return [];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const filterQueryLog = (queryLog, filters) => {
|
|
38
|
+
const poolName = normalizeStringFilter(filters.pool);
|
|
39
|
+
const username = normalizeStringFilter(filters.username).toLowerCase();
|
|
40
|
+
const url = normalizeStringFilter(filters.url).toLowerCase();
|
|
41
|
+
const success = parseSuccessFilter(filters.success);
|
|
42
|
+
|
|
43
|
+
return queryLog.filter((entry) => {
|
|
44
|
+
if (poolName && (entry?.pool?.name || "") !== poolName) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (username) {
|
|
49
|
+
const candidate = String(entry?.request?.username || entry?.request?.userId || "").toLowerCase();
|
|
50
|
+
if (!candidate.includes(username)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (url) {
|
|
56
|
+
const candidateUrl = String(entry?.request?.url || "").toLowerCase();
|
|
57
|
+
if (!candidateUrl.includes(url)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (success !== null && Boolean(entry?.success) !== success) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const sortQueryLogNewestFirst = (queryLog) =>
|
|
71
|
+
[...queryLog].sort((a, b) => {
|
|
72
|
+
const left = Date.parse(b?.time || "") || 0;
|
|
73
|
+
const right = Date.parse(a?.time || "") || 0;
|
|
74
|
+
return left - right;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const average = (numbers) => {
|
|
78
|
+
if (!numbers.length) return 0;
|
|
79
|
+
return numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildSummary = (queryLog) => {
|
|
83
|
+
const durations = queryLog
|
|
84
|
+
.map((entry) => Number(entry?.durationMs))
|
|
85
|
+
.filter((value) => Number.isFinite(value));
|
|
86
|
+
const executionDurations = queryLog
|
|
87
|
+
.map((entry) => Number(entry?.executionDurationMs))
|
|
88
|
+
.filter((value) => Number.isFinite(value));
|
|
89
|
+
const waitDurations = queryLog
|
|
90
|
+
.map((entry) => Number(entry?.poolWait?.waitMs))
|
|
91
|
+
.filter((value) => Number.isFinite(value));
|
|
92
|
+
const errorCount = queryLog.filter((entry) => entry?.success === false).length;
|
|
93
|
+
const pressuredQueries = queryLog.filter((entry) => entry?.poolWait?.hadPoolPressure).length;
|
|
94
|
+
|
|
95
|
+
const slowestQueries = [...queryLog]
|
|
96
|
+
.sort((a, b) => (Number(b?.durationMs) || 0) - (Number(a?.durationMs) || 0))
|
|
97
|
+
.slice(0, 5)
|
|
98
|
+
.map((entry) => ({
|
|
99
|
+
time: entry.time,
|
|
100
|
+
query: entry.query,
|
|
101
|
+
name: entry.name,
|
|
102
|
+
fingerprint: entry.fingerprint,
|
|
103
|
+
durationMs: entry.durationMs,
|
|
104
|
+
executionDurationMs: entry.executionDurationMs,
|
|
105
|
+
waitMs: entry.poolWait?.waitMs || 0,
|
|
106
|
+
success: entry.success,
|
|
107
|
+
request: entry.request,
|
|
108
|
+
pool: entry.pool,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const repeatedGroupsMap = new Map();
|
|
112
|
+
for (const entry of queryLog) {
|
|
113
|
+
const key = entry?.fingerprint || entry?.normalizedQuery || entry?.query;
|
|
114
|
+
if (!key) continue;
|
|
115
|
+
|
|
116
|
+
const existing = repeatedGroupsMap.get(key);
|
|
117
|
+
if (existing) {
|
|
118
|
+
existing.count += 1;
|
|
119
|
+
existing.totalDurationMs += Number(entry?.durationMs) || 0;
|
|
120
|
+
existing.totalExecutionDurationMs += Number(entry?.executionDurationMs) || 0;
|
|
121
|
+
existing.totalWaitMs += Number(entry?.poolWait?.waitMs) || 0;
|
|
122
|
+
existing.errorCount += entry?.success === false ? 1 : 0;
|
|
123
|
+
if ((Date.parse(entry?.time || "") || 0) > (Date.parse(existing.lastSeen || "") || 0)) {
|
|
124
|
+
existing.lastSeen = entry.time;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
repeatedGroupsMap.set(key, {
|
|
130
|
+
fingerprint: entry.fingerprint,
|
|
131
|
+
normalizedQuery: entry.normalizedQuery,
|
|
132
|
+
sampleQuery: entry.query,
|
|
133
|
+
sampleName: entry.name,
|
|
134
|
+
poolName: entry?.pool?.name || null,
|
|
135
|
+
requestUrl: entry?.request?.url || null,
|
|
136
|
+
count: 1,
|
|
137
|
+
totalDurationMs: Number(entry?.durationMs) || 0,
|
|
138
|
+
totalExecutionDurationMs: Number(entry?.executionDurationMs) || 0,
|
|
139
|
+
totalWaitMs: Number(entry?.poolWait?.waitMs) || 0,
|
|
140
|
+
errorCount: entry?.success === false ? 1 : 0,
|
|
141
|
+
lastSeen: entry.time,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const repeatedGroups = [...repeatedGroupsMap.values()]
|
|
146
|
+
.filter((group) => group.count > 1)
|
|
147
|
+
.sort((a, b) => {
|
|
148
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
149
|
+
return b.totalDurationMs - a.totalDurationMs;
|
|
150
|
+
})
|
|
151
|
+
.slice(0, 8)
|
|
152
|
+
.map((group) => ({
|
|
153
|
+
fingerprint: group.fingerprint,
|
|
154
|
+
normalizedQuery: group.normalizedQuery,
|
|
155
|
+
sampleQuery: group.sampleQuery,
|
|
156
|
+
sampleName: group.sampleName,
|
|
157
|
+
poolName: group.poolName,
|
|
158
|
+
requestUrl: group.requestUrl,
|
|
159
|
+
count: group.count,
|
|
160
|
+
avgDurationMs: group.totalDurationMs / group.count,
|
|
161
|
+
avgExecutionDurationMs: group.totalExecutionDurationMs / group.count,
|
|
162
|
+
avgWaitMs: group.totalWaitMs / group.count,
|
|
163
|
+
errorCount: group.errorCount,
|
|
164
|
+
lastSeen: group.lastSeen,
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
totalVisible: queryLog.length,
|
|
169
|
+
avgDurationMs: average(durations),
|
|
170
|
+
avgExecutionDurationMs: average(executionDurations),
|
|
171
|
+
avgWaitMs: average(waitDurations),
|
|
172
|
+
errorCount,
|
|
173
|
+
pressuredQueries,
|
|
174
|
+
slowestQueries,
|
|
175
|
+
repeatedGroups,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const buildResponsePayload = (req) => {
|
|
180
|
+
const queryCount = typeof getQueryCount === "function"
|
|
181
|
+
? getQueryCount()
|
|
182
|
+
: typeof dblogin.getQueryCount === "function"
|
|
183
|
+
? dblogin.getQueryCount()
|
|
184
|
+
: 0;
|
|
185
|
+
const queryLimit = clampLimit(req.query.limit);
|
|
186
|
+
const filters = {
|
|
187
|
+
pool: normalizeStringFilter(req.query.pool),
|
|
188
|
+
username: normalizeStringFilter(req.query.username),
|
|
189
|
+
url: normalizeStringFilter(req.query.url),
|
|
190
|
+
success: parseSuccessFilter(req.query.success),
|
|
191
|
+
};
|
|
192
|
+
const filtered = filterQueryLog(getRawQueryLog(), filters);
|
|
193
|
+
const ordered = sortQueryLogNewestFirst(filtered);
|
|
194
|
+
const queryLog = ordered.slice(0, queryLimit);
|
|
195
|
+
const summary = buildSummary(queryLog);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
queryCount,
|
|
199
|
+
queryLimit,
|
|
200
|
+
filters: {
|
|
201
|
+
pool: filters.pool,
|
|
202
|
+
username: filters.username,
|
|
203
|
+
url: filters.url,
|
|
204
|
+
success: filters.success,
|
|
205
|
+
},
|
|
206
|
+
summary,
|
|
207
|
+
queryLog,
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
13
211
|
const LogLimit = rateLimit({
|
|
14
212
|
windowMs: 1 * 60 * 1000,
|
|
15
213
|
max: 50,
|
|
@@ -19,15 +217,14 @@ const LogLimit = rateLimit({
|
|
|
19
217
|
},
|
|
20
218
|
validate: {
|
|
21
219
|
trustProxy: false,
|
|
22
|
-
xForwardedForHeader: false
|
|
23
|
-
}
|
|
220
|
+
xForwardedForHeader: false,
|
|
221
|
+
},
|
|
24
222
|
});
|
|
25
223
|
|
|
26
|
-
// DB stats API (JSON)
|
|
27
224
|
router.get(["/db.json"], LogLimit, async (req, res) => {
|
|
28
225
|
try {
|
|
29
226
|
const isDev = isDbLogsEnabled();
|
|
30
|
-
const queryLimit =
|
|
227
|
+
const queryLimit = clampLimit(req.query.limit);
|
|
31
228
|
|
|
32
229
|
if (!isDev) {
|
|
33
230
|
return res.status(403).json({
|
|
@@ -36,21 +233,33 @@ router.get(["/db.json"], LogLimit, async (req, res) => {
|
|
|
36
233
|
isDev,
|
|
37
234
|
queryCount: 0,
|
|
38
235
|
queryLimit,
|
|
39
|
-
|
|
236
|
+
filters: {
|
|
237
|
+
pool: normalizeStringFilter(req.query.pool),
|
|
238
|
+
username: normalizeStringFilter(req.query.username),
|
|
239
|
+
url: normalizeStringFilter(req.query.url),
|
|
240
|
+
success: parseSuccessFilter(req.query.success),
|
|
241
|
+
},
|
|
242
|
+
summary: {
|
|
243
|
+
totalVisible: 0,
|
|
244
|
+
avgDurationMs: 0,
|
|
245
|
+
avgExecutionDurationMs: 0,
|
|
246
|
+
avgWaitMs: 0,
|
|
247
|
+
errorCount: 0,
|
|
248
|
+
pressuredQueries: 0,
|
|
249
|
+
slowestQueries: [],
|
|
250
|
+
repeatedGroups: [],
|
|
251
|
+
},
|
|
252
|
+
queryLog: [],
|
|
40
253
|
});
|
|
41
254
|
}
|
|
42
255
|
|
|
43
|
-
|
|
44
|
-
const queryLog = typeof getQueryLog === 'function' ? getQueryLog({ limit: queryLimit }) : (typeof dblogin.getQueryLog === 'function' ? dblogin.getQueryLog({ limit: queryLimit }) : []);
|
|
45
|
-
|
|
46
|
-
return res.json({ queryCount, queryLimit, queryLog, isDev });
|
|
256
|
+
return res.json({ ...buildResponsePayload(req), isDev });
|
|
47
257
|
} catch (err) {
|
|
48
|
-
console.error(
|
|
49
|
-
return res.status(500).json({ success: false, message:
|
|
258
|
+
console.error("[mbkauthe] /db.json route error:", err);
|
|
259
|
+
return res.status(500).json({ success: false, message: "Could not fetch DB stats." });
|
|
50
260
|
}
|
|
51
261
|
});
|
|
52
262
|
|
|
53
|
-
// Dedicated reset API for DB logs and counters
|
|
54
263
|
router.post(["/db/reset"], LogLimit, async (req, res) => {
|
|
55
264
|
try {
|
|
56
265
|
const isDev = isDbLogsEnabled();
|
|
@@ -58,39 +267,48 @@ router.post(["/db/reset"], LogLimit, async (req, res) => {
|
|
|
58
267
|
return res.status(403).json({
|
|
59
268
|
success: false,
|
|
60
269
|
message: "DB logs are disabled.",
|
|
61
|
-
isDev
|
|
270
|
+
isDev,
|
|
62
271
|
});
|
|
63
272
|
}
|
|
64
273
|
|
|
65
|
-
if (typeof resetQueryCount ===
|
|
66
|
-
else if (typeof dblogin.resetQueryCount ===
|
|
274
|
+
if (typeof resetQueryCount === "function") resetQueryCount();
|
|
275
|
+
else if (typeof dblogin.resetQueryCount === "function") dblogin.resetQueryCount();
|
|
67
276
|
|
|
68
|
-
if (typeof resetQueryLog ===
|
|
69
|
-
else if (typeof dblogin.resetQueryLog ===
|
|
277
|
+
if (typeof resetQueryLog === "function") resetQueryLog();
|
|
278
|
+
else if (typeof dblogin.resetQueryLog === "function") dblogin.resetQueryLog();
|
|
70
279
|
|
|
71
|
-
return res.json({ success: true, message:
|
|
280
|
+
return res.json({ success: true, message: "Query log and count have been reset." });
|
|
72
281
|
} catch (err) {
|
|
73
|
-
console.error(
|
|
74
|
-
return res.status(500).json({ success: false, message:
|
|
282
|
+
console.error("[mbkauthe] /db/reset route error:", err);
|
|
283
|
+
return res.status(500).json({ success: false, message: "Could not reset DB stats." });
|
|
75
284
|
}
|
|
76
285
|
});
|
|
77
286
|
|
|
78
|
-
// DB stats page (HTML)
|
|
79
287
|
router.get(["/db"], LogLimit, async (req, res) => {
|
|
80
288
|
try {
|
|
81
289
|
const isDev = isDbLogsEnabled();
|
|
82
|
-
const queryLimit =
|
|
83
|
-
const resetDone = req.query.resetDone ===
|
|
84
|
-
|
|
290
|
+
const queryLimit = clampLimit(req.query.limit);
|
|
291
|
+
const resetDone = req.query.resetDone === "1";
|
|
292
|
+
const successFilter = parseSuccessFilter(req.query.success);
|
|
293
|
+
|
|
294
|
+
return res.render("pages/dbLogs.handlebars", {
|
|
85
295
|
layout: false,
|
|
86
296
|
appName: mbkautheVar.APP_NAME,
|
|
87
297
|
queryLimit,
|
|
88
298
|
resetDone,
|
|
89
299
|
isDev,
|
|
90
|
-
|
|
300
|
+
filters: {
|
|
301
|
+
pool: normalizeStringFilter(req.query.pool),
|
|
302
|
+
username: normalizeStringFilter(req.query.username),
|
|
303
|
+
url: normalizeStringFilter(req.query.url),
|
|
304
|
+
successAny: successFilter === null,
|
|
305
|
+
successTrue: successFilter === true,
|
|
306
|
+
successFalse: successFilter === false,
|
|
307
|
+
},
|
|
308
|
+
disabledMessage: isDev ? null : "DB logs are disabled.",
|
|
91
309
|
});
|
|
92
310
|
} catch (err) {
|
|
93
|
-
console.error(
|
|
311
|
+
console.error("[mbkauthe] /db route error:", err);
|
|
94
312
|
return renderError(res, req, {
|
|
95
313
|
layout: false,
|
|
96
314
|
code: 500,
|
|
@@ -102,4 +320,4 @@ router.get(["/db"], LogLimit, async (req, res) => {
|
|
|
102
320
|
}
|
|
103
321
|
});
|
|
104
322
|
|
|
105
|
-
export default router;
|
|
323
|
+
export default router;
|
package/lib/routes/misc.js
CHANGED
|
@@ -12,6 +12,7 @@ import { fileURLToPath } from "url";
|
|
|
12
12
|
import path from "path";
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import dotenv from "dotenv";
|
|
15
|
+
import { createLogger } from "../utils/logger.js";
|
|
15
16
|
|
|
16
17
|
dotenv.config();
|
|
17
18
|
|
|
@@ -20,6 +21,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
20
21
|
|
|
21
22
|
const router = express.Router();
|
|
22
23
|
const authRepo = new AuthRepository({ db: dblogin });
|
|
24
|
+
const logMisc = createLogger("misc");
|
|
23
25
|
// Rate limiter for info/test routes
|
|
24
26
|
const LoginLimit = rateLimit({
|
|
25
27
|
windowMs: 1 * 60 * 1000,
|
|
@@ -472,9 +474,9 @@ export async function checkVersion() {
|
|
|
472
474
|
if (hasValidLatest && latestVersion !== packageJson.version) {
|
|
473
475
|
console.warn(`[mbkauthe] Current version (${packageJson.version}) is outdated. Latest version: ${latestVersion}. Consider updating mbkauthe.`);
|
|
474
476
|
} else if (hasValidLatest) {
|
|
475
|
-
|
|
477
|
+
logMisc(`Running latest version (${packageJson.version}).`);
|
|
476
478
|
} else {
|
|
477
|
-
|
|
479
|
+
logMisc(`Skipped version check warning: latest version unavailable.`);
|
|
478
480
|
}
|
|
479
481
|
} catch (error) {
|
|
480
482
|
console.warn(`[mbkauthe] Failed to check for updates: ${error.message}`);
|
|
@@ -544,13 +546,13 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
|
|
|
544
546
|
|
|
545
547
|
req.session.destroy((err) => {
|
|
546
548
|
if (err) {
|
|
547
|
-
console.
|
|
549
|
+
console.error(`[mbkauthe] Error destroying session:`, err);
|
|
548
550
|
return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
|
|
549
551
|
}
|
|
550
552
|
|
|
551
553
|
clearSessionCookies(res);
|
|
552
554
|
|
|
553
|
-
|
|
555
|
+
logMisc(`All sessions terminated successfully`);
|
|
554
556
|
res.status(200).json({
|
|
555
557
|
success: true,
|
|
556
558
|
message: "All sessions terminated successfully",
|