mbkauthe 4.7.1 → 4.7.2
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/index.js +1 -1
- package/lib/config/cookies.js +2 -0
- package/lib/pool.js +5 -184
- package/lib/routes/auth.js +14 -8
- package/lib/routes/dbLogs.js +42 -8
- package/lib/routes/misc.js +14 -8
- package/lib/utils/dbQueryLogger.js +247 -0
- package/package.json +1 -1
- package/public/main.css +109 -28
- package/public/main.js +6 -2
- package/views/head.handlebars +12 -0
- package/views/pages/accountSwitch.handlebars +37 -16
- package/views/pages/dbLogs.handlebars +379 -152
- package/views/pages/errorCodes.handlebars +30 -26
- package/views/pages/info_mbkauthe.handlebars +15 -15
- package/views/pages/loginmbkauthe.handlebars +13 -9
- package/views/pages/test.handlebars +27 -15
- package/views/profilemenu.handlebars +53 -57
- package/views/sharedStyles.handlebars +1 -1
- package/views/showmessage.handlebars +52 -30
package/index.js
CHANGED
package/lib/config/cookies.js
CHANGED
|
@@ -149,6 +149,8 @@ export const clearSessionCookies = (res) => {
|
|
|
149
149
|
res.clearCookie("mbkauthe.sid", cachedClearCookieOptions);
|
|
150
150
|
res.clearCookie("sessionId", cachedClearCookieOptions);
|
|
151
151
|
res.clearCookie("fullName", cachedClearCookieOptions);
|
|
152
|
+
res.clearCookie("profileImageUrl", cachedClearCookieOptions);
|
|
153
|
+
res.clearCookie("profileImageUser", cachedClearCookieOptions);
|
|
152
154
|
res.clearCookie("device_token", cachedClearCookieOptions);
|
|
153
155
|
};
|
|
154
156
|
|
package/lib/pool.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import pkg from "pg";
|
|
2
2
|
const { Pool } = pkg;
|
|
3
3
|
import { mbkautheVar } from "#config.js";
|
|
4
|
+
import { attachDevQueryLogger, runWithRequestContext, getRequestContext } from "./utils/dbQueryLogger.js";
|
|
4
5
|
import dotenv from "dotenv";
|
|
5
6
|
dotenv.config();
|
|
6
7
|
|
|
8
|
+
export { runWithRequestContext, getRequestContext };
|
|
9
|
+
|
|
7
10
|
const poolConfig = {
|
|
8
11
|
connectionString: mbkautheVar.LOGIN_DB,
|
|
9
12
|
ssl: {
|
|
@@ -21,190 +24,8 @@ const poolConfig = {
|
|
|
21
24
|
|
|
22
25
|
export const dblogin = new Pool(poolConfig);
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// For logging and testing purposes, we can track query counts and logs in development mode.
|
|
27
|
-
|
|
28
|
-
import path from "path";
|
|
29
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
30
|
-
|
|
31
|
-
const isDev = process.env.env === 'dev';
|
|
32
|
-
const requestContext = isDev ? new AsyncLocalStorage() : null;
|
|
33
|
-
|
|
34
|
-
export const runWithRequestContext = (req, fn) => {
|
|
35
|
-
if (!isDev || !requestContext) return fn();
|
|
36
|
-
return requestContext.run({ req }, fn);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const getRequestContext = () => {
|
|
40
|
-
if (!isDev || !requestContext) return undefined;
|
|
41
|
-
return requestContext.getStore();
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
if (isDev) {
|
|
45
|
-
|
|
46
|
-
// Simple counter for all DB requests made via this pool. This is intentionally
|
|
47
|
-
// lightweight.
|
|
48
|
-
let _dbQueryCount = 0;
|
|
49
|
-
const _dbQueryLog = [];
|
|
50
|
-
const _MAX_QUERY_LOG_ENTRIES = 1000;
|
|
51
|
-
|
|
52
|
-
const _origQuery = dblogin.query.bind(dblogin);
|
|
53
|
-
|
|
54
|
-
dblogin.query = (...args) => {
|
|
55
|
-
_dbQueryCount++;
|
|
56
|
-
|
|
57
|
-
// Track query text for debugging/metrics.
|
|
58
|
-
// `pg` supports (text, values, callback) or (config, callback).
|
|
59
|
-
let queryText = '';
|
|
60
|
-
let queryName = '';
|
|
61
|
-
let queryValues;
|
|
62
|
-
try {
|
|
63
|
-
if (typeof args[0] === 'string') {
|
|
64
|
-
queryText = args[0];
|
|
65
|
-
queryValues = Array.isArray(args[1]) ? args[1] : undefined;
|
|
66
|
-
} else if (args[0] && typeof args[0] === 'object') {
|
|
67
|
-
queryText = args[0].text || '';
|
|
68
|
-
queryName = args[0].name || '';
|
|
69
|
-
queryValues = Array.isArray(args[0].values) ? args[0].values : undefined;
|
|
70
|
-
}
|
|
71
|
-
} catch {
|
|
72
|
-
queryText = '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!queryText) {
|
|
76
|
-
return _origQuery(...args);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const startTime = process.hrtime.bigint();
|
|
80
|
-
const toWorkspacePath = (filePath) => {
|
|
81
|
-
const rel = path.relative(process.cwd(), filePath) || filePath;
|
|
82
|
-
return rel.replace(/\\/g, '/');
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const buildCallsite = () => {
|
|
86
|
-
try {
|
|
87
|
-
const stack = new Error().stack || '';
|
|
88
|
-
const lines = stack.split('\n').map(l => l.trim());
|
|
89
|
-
// Skip frames from this wrapper and node internals; pick first app frame.
|
|
90
|
-
const frame = lines.find((line) =>
|
|
91
|
-
line.startsWith('at ') &&
|
|
92
|
-
!line.includes('/lib/pool.js') &&
|
|
93
|
-
!line.includes('node:internal') &&
|
|
94
|
-
!line.includes('internal/process')
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
if (!frame) return null;
|
|
98
|
-
|
|
99
|
-
const withFunc = /^at\s+([^\s(]+)\s+\((.+):([0-9]+):([0-9]+)\)$/.exec(frame);
|
|
100
|
-
const noFunc = /^at\s+(.+):([0-9]+):([0-9]+)$/.exec(frame);
|
|
101
|
-
|
|
102
|
-
if (withFunc) {
|
|
103
|
-
return {
|
|
104
|
-
function: withFunc[1],
|
|
105
|
-
file: toWorkspacePath(withFunc[2]),
|
|
106
|
-
line: Number(withFunc[3]),
|
|
107
|
-
column: Number(withFunc[4])
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
if (noFunc) {
|
|
111
|
-
return {
|
|
112
|
-
function: null,
|
|
113
|
-
file: toWorkspacePath(noFunc[1]),
|
|
114
|
-
line: Number(noFunc[2]),
|
|
115
|
-
column: Number(noFunc[3])
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const buildRequestContext = () => {
|
|
125
|
-
const store = getRequestContext();
|
|
126
|
-
const req = store?.req;
|
|
127
|
-
if (!req) return null;
|
|
128
|
-
|
|
129
|
-
const user = req.session?.user || null;
|
|
130
|
-
return {
|
|
131
|
-
method: req.method,
|
|
132
|
-
url: req.originalUrl || req.url,
|
|
133
|
-
ip: req.ip,
|
|
134
|
-
userId: user?.id || null,
|
|
135
|
-
username: user?.username || null
|
|
136
|
-
};
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const callsiteSnapshot = buildCallsite();
|
|
140
|
-
|
|
141
|
-
const recordLog = (success, error) => {
|
|
142
|
-
const durationMs = Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
143
|
-
const request = buildRequestContext();
|
|
144
|
-
|
|
145
|
-
_dbQueryLog.push({
|
|
146
|
-
time: new Date().toISOString(),
|
|
147
|
-
query: queryText,
|
|
148
|
-
name: queryName || undefined,
|
|
149
|
-
values: queryValues,
|
|
150
|
-
durationMs,
|
|
151
|
-
success,
|
|
152
|
-
error: error ? { message: error.message, code: error.code } : undefined,
|
|
153
|
-
request,
|
|
154
|
-
pool: {
|
|
155
|
-
total: dblogin.totalCount,
|
|
156
|
-
idle: dblogin.idleCount,
|
|
157
|
-
waiting: dblogin.waitingCount
|
|
158
|
-
},
|
|
159
|
-
callsite: callsiteSnapshot
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (_dbQueryLog.length > _MAX_QUERY_LOG_ENTRIES) {
|
|
163
|
-
_dbQueryLog.shift();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const result = _origQuery(...args);
|
|
169
|
-
if (result && typeof result.then === 'function') {
|
|
170
|
-
return result
|
|
171
|
-
.then((res) => {
|
|
172
|
-
recordLog(true, null);
|
|
173
|
-
return res;
|
|
174
|
-
})
|
|
175
|
-
.catch((err) => {
|
|
176
|
-
recordLog(false, err);
|
|
177
|
-
throw err;
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
recordLog(true, null);
|
|
182
|
-
return result;
|
|
183
|
-
} catch (err) {
|
|
184
|
-
recordLog(false, err);
|
|
185
|
-
throw err;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Public helpers
|
|
190
|
-
|
|
191
|
-
dblogin.getQueryCount = () => _dbQueryCount;
|
|
192
|
-
dblogin.resetQueryCount = () => {
|
|
193
|
-
_dbQueryCount = 0;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
dblogin.getQueryLog = (options = {}) => {
|
|
197
|
-
const { limit } = options;
|
|
198
|
-
if (typeof limit === 'number') {
|
|
199
|
-
return _dbQueryLog.slice(-limit);
|
|
200
|
-
}
|
|
201
|
-
return [..._dbQueryLog];
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
dblogin.resetQueryLog = () => {
|
|
205
|
-
_dbQueryLog.length = 0;
|
|
206
|
-
};
|
|
207
|
-
}
|
|
27
|
+
// Keep pool.js focused on pool setup; attach dev-only query logger from dedicated module.
|
|
28
|
+
attachDevQueryLogger(dblogin);
|
|
208
29
|
|
|
209
30
|
(async () => {
|
|
210
31
|
try {
|
package/lib/routes/auth.js
CHANGED
|
@@ -19,10 +19,13 @@ const router = express.Router();
|
|
|
19
19
|
|
|
20
20
|
// Helper function to clear profile picture cache
|
|
21
21
|
function clearProfilePicCache(req, username) {
|
|
22
|
-
if (req.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (!req || !req.res || !username) return;
|
|
23
|
+
|
|
24
|
+
const cookieUsername = req.cookies?.profileImageUser;
|
|
25
|
+
if (cookieUsername && cookieUsername !== username) return;
|
|
26
|
+
|
|
27
|
+
req.res.clearCookie('profileImageUrl', cachedClearCookieOptions);
|
|
28
|
+
req.res.clearCookie('profileImageUser', cachedClearCookieOptions);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
// Rate limiters for auth routes
|
|
@@ -285,6 +288,9 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
285
288
|
}
|
|
286
289
|
// Cache display name client-side to avoid extra DB lookups
|
|
287
290
|
res.cookie("fullName", req.session.user.fullname || username, { ...cachedCookieOptions, httpOnly: false });
|
|
291
|
+
const profileImageForCookie = loginProfileImage && typeof loginProfileImage === 'string' ? loginProfileImage : 'default';
|
|
292
|
+
res.cookie('profileImageUrl', profileImageForCookie, { ...cachedCookieOptions, httpOnly: false });
|
|
293
|
+
res.cookie('profileImageUser', username, { ...cachedCookieOptions, httpOnly: false });
|
|
288
294
|
// Record which method was used to login (client-visible badge)
|
|
289
295
|
if (method && typeof method === 'string') {
|
|
290
296
|
try {
|
|
@@ -561,9 +567,6 @@ router.post("/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
|
|
|
561
567
|
const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
|
|
562
568
|
|
|
563
569
|
try {
|
|
564
|
-
// Use cached allowedApps from preAuthUser to avoid extra database join
|
|
565
|
-
const cachedAllowedApps = req.session.preAuthUser?.allowedApps;
|
|
566
|
-
|
|
567
570
|
const query = `SELECT tfa."TwoFASecret" FROM "TwoFA" tfa WHERE tfa."UserName" = $1`;
|
|
568
571
|
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
569
572
|
|
|
@@ -574,7 +577,7 @@ router.post("/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
|
|
|
574
577
|
}
|
|
575
578
|
|
|
576
579
|
const sharedSecret = twoFAResult.rows[0].TwoFASecret;
|
|
577
|
-
const allowedApps =
|
|
580
|
+
const allowedApps = req.session.preAuthUser?.allowedApps;
|
|
578
581
|
const tokenValidates = speakeasy.totp.verify({
|
|
579
582
|
secret: sharedSecret,
|
|
580
583
|
encoding: "base32",
|
|
@@ -771,6 +774,9 @@ router.post("/api/switch-session", LoginLimit, async (req, res) => {
|
|
|
771
774
|
|
|
772
775
|
// Sync sessionId cookie and remember list
|
|
773
776
|
res.cookie('fullName', fullName, { ...cachedCookieOptions, httpOnly: false });
|
|
777
|
+
const switchProfileForCookie = switchProfileImage && typeof switchProfileImage === 'string' ? switchProfileImage : 'default';
|
|
778
|
+
res.cookie('profileImageUrl', switchProfileForCookie, { ...cachedCookieOptions, httpOnly: false });
|
|
779
|
+
res.cookie('profileImageUser', row.UserName, { ...cachedCookieOptions, httpOnly: false });
|
|
774
780
|
const encryptedSid = encryptSessionId(row.sid);
|
|
775
781
|
if (encryptedSid) {
|
|
776
782
|
res.cookie('sessionId', encryptedSid, cachedCookieOptions);
|
package/lib/routes/dbLogs.js
CHANGED
|
@@ -6,6 +6,8 @@ import rateLimit from 'express-rate-limit';
|
|
|
6
6
|
|
|
7
7
|
const router = express.Router();
|
|
8
8
|
|
|
9
|
+
const isDbLogsEnabled = () => process.env.env === "dev" && process.env.dbLogs === "true";
|
|
10
|
+
|
|
9
11
|
// Rate limiter for info/test routes
|
|
10
12
|
const LogLimit = rateLimit({
|
|
11
13
|
windowMs: 1 * 60 * 1000,
|
|
@@ -23,33 +25,65 @@ const LogLimit = rateLimit({
|
|
|
23
25
|
// DB stats API (JSON)
|
|
24
26
|
router.get(["/db.json"], LogLimit, async (req, res) => {
|
|
25
27
|
try {
|
|
26
|
-
const
|
|
28
|
+
const isDev = isDbLogsEnabled();
|
|
27
29
|
const queryLimit = Number(req.query.limit) || 50;
|
|
28
|
-
const queryLog = typeof dblogin.getQueryLog === 'function' ? dblogin.getQueryLog({ limit: queryLimit }) : [];
|
|
29
|
-
const resetRequested = req.query.reset === '1';
|
|
30
30
|
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
if (!isDev) {
|
|
32
|
+
return res.status(403).json({
|
|
33
|
+
success: false,
|
|
34
|
+
message: "DB logs are disabled.",
|
|
35
|
+
isDev,
|
|
36
|
+
queryCount: 0,
|
|
37
|
+
queryLimit,
|
|
38
|
+
queryLog: []
|
|
39
|
+
});
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
const queryCount = typeof dblogin.getQueryCount === 'function' ? dblogin.getQueryCount() : null;
|
|
43
|
+
const queryLog = typeof dblogin.getQueryLog === 'function' ? dblogin.getQueryLog({ limit: queryLimit }) : [];
|
|
44
|
+
|
|
45
|
+
return res.json({ queryCount, queryLimit, queryLog, isDev });
|
|
37
46
|
} catch (err) {
|
|
38
47
|
console.error('[mbkauthe] /db.json route error:', err);
|
|
39
48
|
return res.status(500).json({ success: false, message: 'Could not fetch DB stats.' });
|
|
40
49
|
}
|
|
41
50
|
});
|
|
42
51
|
|
|
52
|
+
// Dedicated reset API for DB logs and counters
|
|
53
|
+
router.post(["/db/reset"], LogLimit, async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const isDev = isDbLogsEnabled();
|
|
56
|
+
if (!isDev) {
|
|
57
|
+
return res.status(403).json({
|
|
58
|
+
success: false,
|
|
59
|
+
message: "DB logs are disabled.",
|
|
60
|
+
isDev
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof dblogin.resetQueryCount === 'function') dblogin.resetQueryCount();
|
|
65
|
+
if (typeof dblogin.resetQueryLog === 'function') dblogin.resetQueryLog();
|
|
66
|
+
|
|
67
|
+
return res.json({ success: true, message: 'Query log and count have been reset.' });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('[mbkauthe] /db/reset route error:', err);
|
|
70
|
+
return res.status(500).json({ success: false, message: 'Could not reset DB stats.' });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
43
74
|
// DB stats page (HTML)
|
|
44
75
|
router.get(["/db"], LogLimit, async (req, res) => {
|
|
45
76
|
try {
|
|
77
|
+
const isDev = isDbLogsEnabled();
|
|
46
78
|
const queryLimit = Number(req.query.limit) || 50;
|
|
47
79
|
const resetDone = req.query.resetDone === '1';
|
|
48
80
|
return res.render('pages/dbLogs.handlebars', {
|
|
49
81
|
layout: false,
|
|
50
82
|
appName: mbkautheVar.APP_NAME,
|
|
51
83
|
queryLimit,
|
|
52
|
-
resetDone
|
|
84
|
+
resetDone,
|
|
85
|
+
isDev,
|
|
86
|
+
disabledMessage: isDev ? null : 'DB logs are disabled.'
|
|
53
87
|
});
|
|
54
88
|
} catch (err) {
|
|
55
89
|
console.error('[mbkauthe] /db route error:', err);
|
package/lib/routes/misc.js
CHANGED
|
@@ -6,7 +6,7 @@ import { renderError, renderPage } from "#response.js";
|
|
|
6
6
|
import { authenticate, validateSession, validateSessionAndRole } from "../middleware/auth.js";
|
|
7
7
|
import { ErrorCodes, ErrorMessages, createErrorResponse } from "../utils/errors.js";
|
|
8
8
|
import { dblogin } from "#pool.js";
|
|
9
|
-
import { clearSessionCookies, decryptSessionId } from "#cookies.js";
|
|
9
|
+
import { clearSessionCookies, decryptSessionId, cachedCookieOptions } from "#cookies.js";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
import path from "path";
|
|
12
12
|
import fs from "fs";
|
|
@@ -91,8 +91,12 @@ router.get('/user/profilepic', async (req, res) => {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const username = req.session.user.username;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
let imageUrl = null;
|
|
95
|
+
const cookieUser = req.cookies?.profileImageUser;
|
|
96
|
+
const cookieImageUrl = req.cookies?.profileImageUrl;
|
|
97
|
+
if (cookieUser === username && typeof cookieImageUrl === 'string' && cookieImageUrl.length > 0) {
|
|
98
|
+
imageUrl = cookieImageUrl;
|
|
99
|
+
}
|
|
96
100
|
|
|
97
101
|
// If not in cache, fetch from DB
|
|
98
102
|
if (!imageUrl) {
|
|
@@ -107,7 +111,8 @@ router.get('/user/profilepic', async (req, res) => {
|
|
|
107
111
|
} else {
|
|
108
112
|
imageUrl = 'default';
|
|
109
113
|
}
|
|
110
|
-
|
|
114
|
+
res.cookie('profileImageUrl', imageUrl, { ...cachedCookieOptions, httpOnly: false });
|
|
115
|
+
res.cookie('profileImageUser', username, { ...cachedCookieOptions, httpOnly: false });
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
// Generate ETag based on username and image URL
|
|
@@ -137,7 +142,8 @@ router.get('/user/profilepic', async (req, res) => {
|
|
|
137
142
|
|
|
138
143
|
if (!imageResponse.ok) {
|
|
139
144
|
console.warn(`[mbkauthe] Failed to fetch profile pic from ${imageUrl}, status: ${imageResponse.status}`);
|
|
140
|
-
|
|
145
|
+
res.cookie('profileImageUrl', 'default', { ...cachedCookieOptions, httpOnly: false });
|
|
146
|
+
res.cookie('profileImageUser', username, { ...cachedCookieOptions, httpOnly: false });
|
|
141
147
|
return serveDefaultIcon();
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -147,7 +153,8 @@ router.get('/user/profilepic', async (req, res) => {
|
|
|
147
153
|
imageResponse.body.pipe(res);
|
|
148
154
|
} catch (fetchErr) {
|
|
149
155
|
console.error('[mbkauthe] Error fetching external profile picture:', fetchErr);
|
|
150
|
-
|
|
156
|
+
res.cookie('profileImageUrl', 'default', { ...cachedCookieOptions, httpOnly: false });
|
|
157
|
+
res.cookie('profileImageUser', username, { ...cachedCookieOptions, httpOnly: false });
|
|
151
158
|
return serveDefaultIcon();
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -503,8 +510,7 @@ router.get(["/info", "/i"], LoginLimit, async (req, res) => {
|
|
|
503
510
|
}
|
|
504
511
|
|
|
505
512
|
try {
|
|
506
|
-
res
|
|
507
|
-
layout: false,
|
|
513
|
+
renderPage(req, res, "pages/info_mbkauthe.handlebars", false, {
|
|
508
514
|
mbkautheVar: safe_mbkautheVar,
|
|
509
515
|
CurrentVersion: packageJson.version,
|
|
510
516
|
APP_VERSION: appVersion,
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3
|
+
|
|
4
|
+
const isDev = process.env.env === "dev" && process.env.dbLogs === "true";
|
|
5
|
+
const requestContext = isDev ? new AsyncLocalStorage() : null;
|
|
6
|
+
|
|
7
|
+
export const runWithRequestContext = (req, fn) => {
|
|
8
|
+
if (!isDev || !requestContext) return fn();
|
|
9
|
+
return requestContext.run({ req }, fn);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getRequestContext = () => {
|
|
13
|
+
if (!isDev || !requestContext) return undefined;
|
|
14
|
+
return requestContext.getStore();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const attachDevQueryLogger = (pool) => {
|
|
18
|
+
if (!isDev || !pool || pool.__mbkQueryLoggerInstalled) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pool.__mbkQueryLoggerInstalled = true;
|
|
23
|
+
|
|
24
|
+
// Simple counter for all DB requests made via this pool. This is intentionally lightweight.
|
|
25
|
+
let dbQueryCount = 0;
|
|
26
|
+
const dbQueryLog = [];
|
|
27
|
+
const MAX_QUERY_LOG_ENTRIES = 1000;
|
|
28
|
+
|
|
29
|
+
const originalQuery = pool.query.bind(pool);
|
|
30
|
+
|
|
31
|
+
const safeValue = (value, depth = 0, seen = new WeakSet()) => {
|
|
32
|
+
if (value == null) return value;
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
return value.length > 300 ? `${value.slice(0, 300)}...` : value;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
37
|
+
if (typeof value === "bigint") return value.toString();
|
|
38
|
+
if (value instanceof Date) return value.toISOString();
|
|
39
|
+
if (Buffer.isBuffer(value)) return `[buffer:${value.length}]`;
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
if (depth >= 4) return `[array:${value.length}]`;
|
|
43
|
+
const sample = value.slice(0, 8).map((v) => safeValue(v, depth + 1, seen));
|
|
44
|
+
if (value.length > 8) sample.push(`...(${value.length - 8} more)`);
|
|
45
|
+
return sample;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof value === "object") {
|
|
49
|
+
if (seen.has(value)) return "[circular]";
|
|
50
|
+
seen.add(value);
|
|
51
|
+
|
|
52
|
+
const keys = Object.keys(value);
|
|
53
|
+
if (depth >= 4) {
|
|
54
|
+
const head = keys.slice(0, 5).join(", ");
|
|
55
|
+
return keys.length > 5 ? `[object:${head}, ...]` : `[object:${head}]`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const out = {};
|
|
59
|
+
const entries = Object.entries(value).slice(0, 20);
|
|
60
|
+
for (const [k, v] of entries) {
|
|
61
|
+
out[k] = safeValue(v, depth + 1, seen);
|
|
62
|
+
}
|
|
63
|
+
if (keys.length > 20) {
|
|
64
|
+
out.__truncated = `${keys.length - 20} more keys`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
seen.delete(value);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return String(value);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const buildReturnValue = (result) => {
|
|
75
|
+
if (!result || typeof result !== "object") return undefined;
|
|
76
|
+
|
|
77
|
+
const returnValue = {
|
|
78
|
+
command: result.command || undefined,
|
|
79
|
+
rowCount: typeof result.rowCount === "number" ? result.rowCount : undefined,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(result.rows)) {
|
|
83
|
+
const previewSize = 3;
|
|
84
|
+
returnValue.returnedRows = result.rows.length;
|
|
85
|
+
returnValue.rowsPreview = result.rows.slice(0, previewSize).map((row) => safeValue(row));
|
|
86
|
+
if (result.rows.length > previewSize) {
|
|
87
|
+
returnValue.rowsTruncated = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return returnValue;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
pool.query = (...args) => {
|
|
95
|
+
dbQueryCount++;
|
|
96
|
+
|
|
97
|
+
// `pg` supports (text, values, callback) or (config, callback).
|
|
98
|
+
let queryText = "";
|
|
99
|
+
let queryName = "";
|
|
100
|
+
let queryValues;
|
|
101
|
+
try {
|
|
102
|
+
if (typeof args[0] === "string") {
|
|
103
|
+
queryText = args[0];
|
|
104
|
+
queryValues = Array.isArray(args[1]) ? args[1] : undefined;
|
|
105
|
+
} else if (args[0] && typeof args[0] === "object") {
|
|
106
|
+
queryText = args[0].text || "";
|
|
107
|
+
queryName = args[0].name || "";
|
|
108
|
+
queryValues = Array.isArray(args[0].values) ? args[0].values : undefined;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
queryText = "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!queryText) {
|
|
115
|
+
return originalQuery(...args);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const startTime = process.hrtime.bigint();
|
|
119
|
+
const toWorkspacePath = (filePath) => {
|
|
120
|
+
const rel = path.relative(process.cwd(), filePath) || filePath;
|
|
121
|
+
return rel.replace(/\\/g, "/");
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const buildCallsite = () => {
|
|
125
|
+
try {
|
|
126
|
+
const stack = new Error().stack || "";
|
|
127
|
+
const lines = stack.split("\n").map((l) => l.trim());
|
|
128
|
+
const frame = lines.find(
|
|
129
|
+
(line) =>
|
|
130
|
+
line.startsWith("at ") &&
|
|
131
|
+
!line.includes("/lib/utils/dbQueryLogger.js") &&
|
|
132
|
+
!line.includes("node:internal") &&
|
|
133
|
+
!line.includes("internal/process")
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!frame) return null;
|
|
137
|
+
|
|
138
|
+
const withFunc = /^at\s+([^\s(]+)\s+\((.+):([0-9]+):([0-9]+)\)$/.exec(frame);
|
|
139
|
+
const noFunc = /^at\s+(.+):([0-9]+):([0-9]+)$/.exec(frame);
|
|
140
|
+
|
|
141
|
+
if (withFunc) {
|
|
142
|
+
return {
|
|
143
|
+
function: withFunc[1],
|
|
144
|
+
file: toWorkspacePath(withFunc[2]),
|
|
145
|
+
line: Number(withFunc[3]),
|
|
146
|
+
column: Number(withFunc[4]),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (noFunc) {
|
|
151
|
+
return {
|
|
152
|
+
function: null,
|
|
153
|
+
file: toWorkspacePath(noFunc[1]),
|
|
154
|
+
line: Number(noFunc[2]),
|
|
155
|
+
column: Number(noFunc[3]),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const buildRequestContext = () => {
|
|
165
|
+
const store = getRequestContext();
|
|
166
|
+
const req = store?.req;
|
|
167
|
+
if (!req) return null;
|
|
168
|
+
|
|
169
|
+
const user = req.session?.user || null;
|
|
170
|
+
return {
|
|
171
|
+
method: req.method,
|
|
172
|
+
url: req.originalUrl || req.url,
|
|
173
|
+
ip: req.ip,
|
|
174
|
+
userId: user?.id || null,
|
|
175
|
+
username: user?.username || null,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const callsiteSnapshot = buildCallsite();
|
|
180
|
+
|
|
181
|
+
const recordLog = (success, error, result) => {
|
|
182
|
+
const durationMs = Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
183
|
+
const request = buildRequestContext();
|
|
184
|
+
const returnValue = buildReturnValue(result);
|
|
185
|
+
|
|
186
|
+
dbQueryLog.push({
|
|
187
|
+
time: new Date().toISOString(),
|
|
188
|
+
query: queryText,
|
|
189
|
+
name: queryName || undefined,
|
|
190
|
+
values: queryValues,
|
|
191
|
+
durationMs,
|
|
192
|
+
success,
|
|
193
|
+
error: error ? { message: error.message, code: error.code } : undefined,
|
|
194
|
+
returnValue,
|
|
195
|
+
request,
|
|
196
|
+
pool: {
|
|
197
|
+
total: pool.totalCount,
|
|
198
|
+
idle: pool.idleCount,
|
|
199
|
+
waiting: pool.waitingCount,
|
|
200
|
+
},
|
|
201
|
+
callsite: callsiteSnapshot,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (dbQueryLog.length > MAX_QUERY_LOG_ENTRIES) {
|
|
205
|
+
dbQueryLog.shift();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const result = originalQuery(...args);
|
|
211
|
+
if (result && typeof result.then === "function") {
|
|
212
|
+
return result
|
|
213
|
+
.then((res) => {
|
|
214
|
+
recordLog(true, null, res);
|
|
215
|
+
return res;
|
|
216
|
+
})
|
|
217
|
+
.catch((err) => {
|
|
218
|
+
recordLog(false, err);
|
|
219
|
+
throw err;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
recordLog(true, null, result);
|
|
224
|
+
return result;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
recordLog(false, err);
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
pool.getQueryCount = () => dbQueryCount;
|
|
232
|
+
pool.resetQueryCount = () => {
|
|
233
|
+
dbQueryCount = 0;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
pool.getQueryLog = (options = {}) => {
|
|
237
|
+
const { limit } = options;
|
|
238
|
+
if (typeof limit === "number") {
|
|
239
|
+
return dbQueryLog.slice(-limit);
|
|
240
|
+
}
|
|
241
|
+
return [...dbQueryLog];
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
pool.resetQueryLog = () => {
|
|
245
|
+
dbQueryLog.length = 0;
|
|
246
|
+
};
|
|
247
|
+
};
|