mbkauthe 4.7.0 → 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/docs/db.sql CHANGED
@@ -138,7 +138,10 @@ INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount"
138
138
  VALUES ('support', '12345678', 'SuperAdmin', true, false, 'Support User')
139
139
  ON CONFLICT ("UserName") DO NOTHING;
140
140
 
141
- SELECT * FROM "Users" WHERE "UserName" = 'support';
141
+ INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "FullName")
142
+ VALUES ('admin', '12345678', 'SuperAdmin', true, false, 'Admin User')
143
+ ON CONFLICT ("UserName") DO NOTHING;
144
+
142
145
 
143
146
  -- API Tokens for persistent programmatic access
144
147
  CREATE TABLE IF NOT EXISTS "ApiTokens" (
package/index.js CHANGED
@@ -5,6 +5,7 @@ import { engine } from "express-handlebars";
5
5
  import path from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { renderError, renderPage } from "#response.js";
8
+ import { packageJson } from "#config.js";
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
@@ -43,6 +44,9 @@ app.engine("handlebars", engine({
43
44
  return []; // Return an empty array if obj is undefined, null, or not an object
44
45
  }
45
46
  return Object.entries(obj).map(([key, value]) => ({ key, value }));
47
+ },
48
+ cacheBuster: function () {
49
+ return "?v=" + packageJson.version;
46
50
  }
47
51
  }
48
52
 
@@ -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 {
@@ -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.session && username) {
23
- const cacheKey = `profilepic_${username}`;
24
- delete req.session[cacheKey];
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 = cachedAllowedApps;
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);
@@ -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 queryCount = typeof dblogin.getQueryCount === 'function' ? dblogin.getQueryCount() : null;
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 (resetRequested) {
32
- if (typeof dblogin.resetQueryCount === 'function') dblogin.resetQueryCount();
33
- if (typeof dblogin.resetQueryLog === 'function') dblogin.resetQueryLog();
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
- return res.json({ queryCount, queryLimit, queryLog, resetDone: resetRequested });
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);
@@ -2,11 +2,11 @@ import express from "express";
2
2
  import fetch from 'node-fetch';
3
3
  import rateLimit from 'express-rate-limit';
4
4
  import { mbkautheVar, packageJson, appVersion } from "#config.js";
5
- import { renderError } from "#response.js";
5
+ 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
- const cacheKey = `profilepic_${username}`;
95
- let imageUrl = req.session[cacheKey];
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
- req.session[cacheKey] = imageUrl;
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
- req.session[cacheKey] = 'default';
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
- req.session[cacheKey] = 'default';
156
+ res.cookie('profileImageUrl', 'default', { ...cachedCookieOptions, httpOnly: false });
157
+ res.cookie('profileImageUser', username, { ...cachedCookieOptions, httpOnly: false });
151
158
  return serveDefaultIcon();
152
159
  }
153
160
 
@@ -187,8 +194,7 @@ router.get(['/test', '/'], validateSession, LoginLimit, async (req, res) => {
187
194
  ? new Date(req.session.cookie.expires).toISOString()
188
195
  : null;
189
196
 
190
- return res.render('pages/test.handlebars', {
191
- layout: false,
197
+ return renderPage(req, res, 'pages/test.handlebars', false, {
192
198
  username,
193
199
  fullname: fullname || 'N/A',
194
200
  role,
@@ -439,8 +445,7 @@ router.get("/ErrorCode", (req, res) => {
439
445
  }))
440
446
  })).filter(category => category.errors.length > 0); // Remove empty categories
441
447
 
442
- res.render("errorCodes.handlebars", {
443
- layout: false,
448
+ return renderPage(req, res, "pages/errorCodes.handlebars", false, {
444
449
  pageTitle: 'Error Codes',
445
450
  appName: mbkautheVar.APP_NAME,
446
451
  errorCategories: categoriesWithErrors
@@ -505,8 +510,7 @@ router.get(["/info", "/i"], LoginLimit, async (req, res) => {
505
510
  }
506
511
 
507
512
  try {
508
- res.render("pages/info_mbkauthe.handlebars", {
509
- layout: false,
513
+ renderPage(req, res, "pages/info_mbkauthe.handlebars", false, {
510
514
  mbkautheVar: safe_mbkautheVar,
511
515
  CurrentVersion: packageJson.version,
512
516
  APP_VERSION: appVersion,