mbkauthe 4.6.2 → 4.7.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 CHANGED
@@ -30,6 +30,7 @@
30
30
  - Session fixation prevention
31
31
  - Dynamic profile picture routing with session caching
32
32
  - Modern responsive UI with desktop two-column layout
33
+ - Dev-only DB Query Monitor with callsite, timing, and request context
33
34
 
34
35
  ## 📦 Installation
35
36
 
@@ -46,7 +47,7 @@ Copy-Item .env.example .env
46
47
  ```
47
48
  See [docs/env.md](docs/env.md).
48
49
 
49
- **2. Set Up Database**
50
+ **2. Set Up Database**
50
51
  Run [docs/db.sql](docs/db.sql) to create tables and a default SuperAdmin (`support` / `12345678`). Change the password immediately. See [docs/db.md](docs/db.md).
51
52
 
52
53
  **3. Integrate with Express**
@@ -81,6 +82,14 @@ npm run dev
81
82
  - **Combined:** `validateSessionAndRole(['SuperAdmin', 'NormalUser'])`
82
83
  - **API Token Auth:** `authenticate(process.env.API_TOKEN)`
83
84
 
85
+ ## 🧰 Diagnostics (dev only)
86
+
87
+ These are only mounted when `process.env.env === "dev"`:
88
+
89
+ - **DB Query Monitor (HTML):** `/mbkauthe/db`
90
+ - **DB Query Monitor (JSON):** `/mbkauthe/db.json`
91
+ - **SuperAdmin check:** `/mbkauthe/validate-superadmin`
92
+
84
93
  ## 🔐 Security
85
94
 
86
95
  - Rate limiting, CSRF protection, secure cookies
@@ -102,6 +111,18 @@ Enable via `MBKAUTH_TWO_FA_ENABLE=true`. Trusted devices can skip 2FA for a set
102
111
  - **Custom Views:** `views/loginmbkauthe.handlebars`, `2fa.handlebars`, `Error/dError.handlebars`
103
112
  - **Database Access:** `import { dblogin } from 'mbkauthe'; const result = await dblogin.query('SELECT * FROM "Users"');`
104
113
 
114
+ ## 📄 API Reference
115
+
116
+ - Full endpoint list and details: [docs/api.md](docs/api.md)
117
+
118
+ ## 🧰 Diagnostics (dev only)
119
+
120
+ - **DB Query Monitor (HTML):** `/mbkauthe/db`
121
+ - **DB Query Monitor (JSON):** `/mbkauthe/db.json`
122
+ - **SuperAdmin check:** `/mbkauthe/debug/validate-superadmin`
123
+
124
+ These routes are only mounted when `process.env.env === "dev"`. They expose query timing, status/error, pool stats, request context, and callsite data for troubleshooting.
125
+
105
126
  ## 🚢 Deployment
106
127
 
107
128
  Checklist for production:
package/docs/api.md CHANGED
@@ -15,6 +15,8 @@ This document provides comprehensive API documentation for MBKAuthe authenticati
15
15
  - [Protected Endpoints](#protected-endpoints)
16
16
  - [OAuth Endpoints](#oauth-endpoints)
17
17
  - [Information Endpoints](#information-endpoints)
18
+ - [Diagnostics (Dev Only)](#diagnostics-dev-only)
19
+ - [Additional Endpoints](#additional-endpoints)
18
20
  - [Middleware Reference](#middleware-reference)
19
21
  - [Code Examples](#code-examples)
20
22
 
@@ -195,6 +197,150 @@ GET /mbkauthe/login?redirect=/dashboard
195
197
 
196
198
  ---
197
199
 
200
+ #### `GET /mbkauthe/2fa`
201
+
202
+ Renders the 2FA challenge page after a login that requires TOTP.
203
+
204
+ **Rate Limit:** 5 requests per minute
205
+
206
+ ---
207
+
208
+ #### `GET /mbkauthe/accounts`
209
+
210
+ Renders the account-switch page for remembered sessions on the device.
211
+
212
+ **Rate Limit:** 8 requests per minute
213
+
214
+ ---
215
+
216
+ #### `GET /mbkauthe/test`
217
+
218
+ Renders a test page for the current session context.
219
+
220
+ **Rate Limit:** 8 requests per minute
221
+
222
+ ---
223
+
224
+ #### `POST /mbkauthe/test`
225
+
226
+ Lightweight check to verify an authenticated session.
227
+
228
+ **Response:** `{ "success": true, "message": "You are logged in" }`
229
+
230
+ ---
231
+
232
+ ## Diagnostics (Dev Only)
233
+
234
+ These endpoints are only mounted when `process.env.env === "dev"`.
235
+
236
+ #### `GET /mbkauthe/db`
237
+
238
+ Renders the DB Query Monitor page. The UI fetches data from `/mbkauthe/db.json`.
239
+
240
+ **Query Parameters:**
241
+ - `limit` (optional) - number of most recent queries to show (default: 50)
242
+ - `resetDone` (optional) - UI notification flag used after reset
243
+
244
+ ---
245
+
246
+ #### `GET /mbkauthe/db.json`
247
+
248
+ Returns recent DB query diagnostics.
249
+
250
+ **Query Parameters:**
251
+ - `limit` (optional) - number of most recent queries to return (default: 50)
252
+ - `reset` (optional) - set to `1` to clear the query log and counter
253
+
254
+ **Response Body:**
255
+ ```json
256
+ {
257
+ "queryCount": 120,
258
+ "queryLimit": 50,
259
+ "resetDone": false,
260
+ "queryLog": [
261
+ {
262
+ "time": "2026-03-19T12:00:00.000Z",
263
+ "name": "login-get-user",
264
+ "query": "SELECT ...",
265
+ "values": ["user"],
266
+ "durationMs": 3.42,
267
+ "success": true,
268
+ "error": null,
269
+ "request": {
270
+ "method": "GET",
271
+ "url": "/mbkauthe/login",
272
+ "ip": "::1",
273
+ "userId": 1,
274
+ "username": "support"
275
+ },
276
+ "pool": {
277
+ "total": 2,
278
+ "idle": 1,
279
+ "waiting": 0
280
+ },
281
+ "callsite": {
282
+ "function": "validateSession",
283
+ "file": "lib/middleware/auth.js",
284
+ "line": 197,
285
+ "column": 30
286
+ }
287
+ }
288
+ ]
289
+ }
290
+ ```
291
+
292
+ ---
293
+
294
+ #### `GET /mbkauthe/validate-superadmin`
295
+
296
+ Validates that the current session has `SuperAdmin` role and returns a JSON summary.
297
+
298
+ ---
299
+
300
+ ## Additional Endpoints
301
+
302
+ The endpoints below are active in the router but are not fully expanded above. Use this list as a reference.
303
+
304
+ **Auth & Session:**
305
+
306
+ - `POST /mbkauthe/api/verify-2fa` - Verifies TOTP and completes login.
307
+ - `POST /mbkauthe/api/logout` - Logs out the current session.
308
+ - `GET /mbkauthe/api/account-sessions` - Lists remembered accounts for the current device.
309
+ - `POST /mbkauthe/api/switch-session` - Switches active session to another remembered account.
310
+ - `POST /mbkauthe/api/logout-all` - Logs out all remembered accounts on the device.
311
+
312
+ **Session Validation:**
313
+
314
+ - `GET /mbkauthe/api/checkSession` - Checks session validity (cookie-based).
315
+ - `POST /mbkauthe/api/checkSession` - Checks session validity by sessionId (body).
316
+ - `POST /mbkauthe/api/verifySession` - Returns session details by sessionId (body).
317
+
318
+ **OAuth:**
319
+
320
+ - `GET /mbkauthe/api/github/login` - Starts GitHub OAuth login flow.
321
+ - `GET /mbkauthe/api/github/login/callback` - GitHub OAuth callback.
322
+ - `GET /mbkauthe/api/google/login` - Starts Google OAuth login flow.
323
+ - `GET /mbkauthe/api/google/login/callback` - Google OAuth callback.
324
+
325
+ **Info & UI:**
326
+
327
+ - `GET /mbkauthe/info` and `GET /mbkauthe/i` - Info page.
328
+ - `GET /mbkauthe/info.json` and `GET /mbkauthe/i.json` - Info page JSON.
329
+ - `GET /mbkauthe/ErrorCode` - Error codes page.
330
+ - `GET /mbkauthe/user/profilepic` - User profile picture proxy.
331
+
332
+ **Admin:**
333
+
334
+ - `POST /mbkauthe/api/terminateAllSessions` - Terminates all sessions (requires `Main_SECRET_TOKEN`).
335
+
336
+ **Static Assets:**
337
+
338
+ - `GET /mbkauthe/main.js`
339
+ - `GET /mbkauthe/main.css`
340
+ - `GET /mbkauthe/bg.webp`
341
+
342
+ ---
343
+
198
344
  #### `POST /mbkauthe/api/login`
199
345
 
200
346
  Authenticates a user and creates a session.
package/docs/env.md CHANGED
@@ -81,6 +81,12 @@ This document describes the environment variables MBKAuth expects and keeps brie
81
81
  - Example: `"loginRedirectURL":"/dashboard"`
82
82
  - Required: No
83
83
 
84
+ - env
85
+ - Description: Development flag to enable diagnostics (DB query monitor, debug endpoints).
86
+ - Values: `dev` to enable; any other value disables.
87
+ - Example: `env=dev`
88
+ - Required: No
89
+
84
90
  - bucket
85
91
  - Description: Optional external storage bucket name or identifier used for static assets or third-party integrations.
86
92
  - Default: an empty string `""` (no bucket configured)
package/index.js CHANGED
@@ -59,7 +59,7 @@ if (process.env.test === "dev") {
59
59
  return res.redirect("/mbkauthe/");
60
60
  });
61
61
  app.get("/dev/2fa", (req, res) => {
62
- return renderPage(req, res, "2fa", {
62
+ return renderPage(req, res, "pages/2fa.handlebars", {
63
63
  layout: false,
64
64
  pagename: "Two-Factor Authentication",
65
65
  page: "/home"
@@ -109,7 +109,8 @@ export {
109
109
  sessionConfig,
110
110
  corsMiddleware,
111
111
  sessionRestorationMiddleware,
112
- sessionCookieSyncMiddleware
112
+ sessionCookieSyncMiddleware,
113
+ requestContextMiddleware
113
114
  } from "./lib/middleware/index.js";
114
115
  export { validateTokenScope } from "./lib/middleware/scopeValidator.js";
115
116
  export { renderError, getUserContext, renderPage, proxycall } from "#response.js";
package/lib/main.js CHANGED
@@ -6,11 +6,13 @@ import {
6
6
  sessionConfig,
7
7
  corsMiddleware,
8
8
  sessionRestorationMiddleware,
9
- sessionCookieSyncMiddleware
9
+ sessionCookieSyncMiddleware,
10
+ requestContextMiddleware
10
11
  } from "./middleware/index.js";
11
12
  import authRoutes from "./routes/auth.js";
12
13
  import oauthRoutes from "./routes/oauth.js";
13
14
  import miscRoutes from "./routes/misc.js";
15
+ import dbLogsRoutes from "./routes/dbLogs.js";
14
16
  import { fileURLToPath } from "url";
15
17
  import path from "path";
16
18
 
@@ -45,6 +47,11 @@ router.use(session(sessionConfig));
45
47
  // Session restoration
46
48
  router.use(sessionRestorationMiddleware);
47
49
 
50
+ // Attach request context for DB query logging (dev only)
51
+ if (process.env.env === 'dev') {
52
+ router.use(requestContextMiddleware);
53
+ }
54
+
48
55
  // Initialize passport
49
56
  router.use(passport.initialize());
50
57
  router.use(passport.session());
@@ -57,6 +64,10 @@ router.use('/mbkauthe', authRoutes);
57
64
  router.use('/mbkauthe', oauthRoutes);
58
65
  router.use('/mbkauthe', miscRoutes);
59
66
 
67
+ if (process.env.env === 'dev') {
68
+ router.use('/mbkauthe', dbLogsRoutes);
69
+ }
70
+
60
71
  // Redirect shortcuts for login
61
72
  router.get(["/login", "/signin"], async (req, res) => {
62
73
  const queryParams = new URLSearchParams(req.query).toString();
@@ -64,7 +75,7 @@ router.get(["/login", "/signin"], async (req, res) => {
64
75
  return res.redirect(redirectUrl);
65
76
  });
66
77
 
67
- router.get(['/icon.svg',"/favicon.ico", "/icon.png"], (req, res) => {
78
+ router.get(['/icon.svg', "/favicon.ico", "/icon.png"], (req, res) => {
68
79
  res.setHeader('Cache-Control', 'public, max-age=31536000');
69
80
  res.sendFile(path.join(__dirname, '..', 'public', 'M.png'));
70
81
  });
@@ -1,7 +1,7 @@
1
1
  import session from "express-session";
2
2
  import pgSession from "connect-pg-simple";
3
3
  const PgSession = pgSession(session);
4
- import { dblogin } from "#pool.js";
4
+ import { dblogin, runWithRequestContext } from "#pool.js";
5
5
  import { mbkautheVar } from "#config.js";
6
6
  import { cachedCookieOptions, decryptSessionId, encryptSessionId } from "#cookies.js";
7
7
 
@@ -142,4 +142,9 @@ export function sessionCookieSyncMiddleware(req, res, next) {
142
142
  }
143
143
  }
144
144
  next();
145
+ }
146
+
147
+ // Request context middleware (used for DB query logging)
148
+ export function requestContextMiddleware(req, res, next) {
149
+ return runWithRequestContext(req, () => next());
145
150
  }
package/lib/pool.js CHANGED
@@ -21,7 +21,28 @@ const poolConfig = {
21
21
 
22
22
  export const dblogin = new Pool(poolConfig);
23
23
 
24
- if(process.env.env === 'dev') {
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
+
25
46
  // Simple counter for all DB requests made via this pool. This is intentionally
26
47
  // lightweight.
27
48
  let _dbQueryCount = 0;
@@ -36,29 +57,133 @@ if(process.env.env === 'dev') {
36
57
  // Track query text for debugging/metrics.
37
58
  // `pg` supports (text, values, callback) or (config, callback).
38
59
  let queryText = '';
60
+ let queryName = '';
61
+ let queryValues;
39
62
  try {
40
63
  if (typeof args[0] === 'string') {
41
64
  queryText = args[0];
65
+ queryValues = Array.isArray(args[1]) ? args[1] : undefined;
42
66
  } else if (args[0] && typeof args[0] === 'object') {
43
67
  queryText = args[0].text || '';
68
+ queryName = args[0].name || '';
69
+ queryValues = Array.isArray(args[0].values) ? args[0].values : undefined;
44
70
  }
45
71
  } catch {
46
72
  queryText = '';
47
73
  }
48
74
 
49
- if (queryText) {
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
+
50
145
  _dbQueryLog.push({
51
146
  time: new Date().toISOString(),
52
147
  query: queryText,
53
- values: Array.isArray(args[1]) ? args[1] : undefined
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
54
160
  });
55
161
 
56
162
  if (_dbQueryLog.length > _MAX_QUERY_LOG_ENTRIES) {
57
163
  _dbQueryLog.shift();
58
164
  }
59
- }
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
+ }
60
180
 
61
- return _origQuery(...args);
181
+ recordLog(true, null);
182
+ return result;
183
+ } catch (err) {
184
+ recordLog(false, err);
185
+ throw err;
186
+ }
62
187
  };
63
188
 
64
189
  // Public helpers
@@ -517,7 +517,7 @@ router.get("/2fa", csrfProtection, (req, res) => {
517
517
  redirectToUse = mbkautheVar.loginRedirectURL || '/dashboard';
518
518
  }
519
519
 
520
- res.render("2fa.handlebars", {
520
+ res.render("pages/2fa.handlebars", {
521
521
  layout: false,
522
522
  customURL: redirectToUse,
523
523
  csrfToken: req.csrfToken(),
@@ -828,7 +828,7 @@ router.post("/api/logout-all", LoginLimit, async (req, res) => {
828
828
  // GET /mbkauthe/login
829
829
  router.get("/login", LoginLimit, csrfProtection, (req, res) => {
830
830
  const lastLogin = req.cookies && typeof req.cookies.lastLoginMethod === 'string' ? req.cookies.lastLoginMethod : null;
831
- return res.render("loginmbkauthe.handlebars", {
831
+ return res.render("pages/loginmbkauthe.handlebars", {
832
832
  layout: false,
833
833
  githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
834
834
  googleLoginEnabled: mbkautheVar.GOOGLE_LOGIN_ENABLED,
@@ -853,7 +853,7 @@ router.get("/accounts", LoginLimit, csrfProtection, (req, res) => {
853
853
  ? redirectFromQuery
854
854
  : (mbkautheVar.loginRedirectURL || '/dashboard');
855
855
 
856
- return res.render("accountSwitch.handlebars", {
856
+ return res.render("pages/accountSwitch.handlebars", {
857
857
  layout: false,
858
858
  customURL: safeRedirect,
859
859
  version: packageJson.version,
@@ -0,0 +1,67 @@
1
+ import express from "express";
2
+ import { renderError } from "#response.js";
3
+ import { dblogin } from "#pool.js";
4
+ import { mbkautheVar } from "#config.js";
5
+ import rateLimit from 'express-rate-limit';
6
+
7
+ const router = express.Router();
8
+
9
+ // Rate limiter for info/test routes
10
+ const LogLimit = rateLimit({
11
+ windowMs: 1 * 60 * 1000,
12
+ max: 50,
13
+ message: { success: false, message: "Too many attempts, please try again later" },
14
+ skip: (req) => {
15
+ return !!req.session.user;
16
+ },
17
+ validate: {
18
+ trustProxy: false,
19
+ xForwardedForHeader: false
20
+ }
21
+ });
22
+
23
+ // DB stats API (JSON)
24
+ router.get(["/db.json"], LogLimit, async (req, res) => {
25
+ try {
26
+ const queryCount = typeof dblogin.getQueryCount === 'function' ? dblogin.getQueryCount() : null;
27
+ 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
+
31
+ if (resetRequested) {
32
+ if (typeof dblogin.resetQueryCount === 'function') dblogin.resetQueryCount();
33
+ if (typeof dblogin.resetQueryLog === 'function') dblogin.resetQueryLog();
34
+ }
35
+
36
+ return res.json({ queryCount, queryLimit, queryLog, resetDone: resetRequested });
37
+ } catch (err) {
38
+ console.error('[mbkauthe] /db.json route error:', err);
39
+ return res.status(500).json({ success: false, message: 'Could not fetch DB stats.' });
40
+ }
41
+ });
42
+
43
+ // DB stats page (HTML)
44
+ router.get(["/db"], LogLimit, async (req, res) => {
45
+ try {
46
+ const queryLimit = Number(req.query.limit) || 50;
47
+ const resetDone = req.query.resetDone === '1';
48
+ return res.render('pages/dbLogs.handlebars', {
49
+ layout: false,
50
+ appName: mbkautheVar.APP_NAME,
51
+ queryLimit,
52
+ resetDone
53
+ });
54
+ } catch (err) {
55
+ console.error('[mbkauthe] /db route error:', err);
56
+ return renderError(res, req, {
57
+ layout: false,
58
+ code: 500,
59
+ error: "Internal Server Error",
60
+ message: "Could not fetch DB stats.",
61
+ pagename: "DB Stats",
62
+ page: "/mbkauthe/info",
63
+ });
64
+ }
65
+ });
66
+
67
+ export default router;
@@ -3,7 +3,7 @@ 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 } from "#response.js";
6
- import { authenticate, validateSession, validateApiSession } from "../middleware/auth.js";
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
9
  import { clearSessionCookies, decryptSessionId } from "#cookies.js";
@@ -157,6 +157,28 @@ router.get('/user/profilepic', async (req, res) => {
157
157
  }
158
158
  });
159
159
 
160
+ if (process.env.env === 'dev') {
161
+ // Dev-only diagnostic endpoint to verify SuperAdmin role enforcement
162
+ router.get(['/validate-superadmin'], validateSessionAndRole("SuperAdmin"), LoginLimit, async (req, res) => {
163
+ try {
164
+ const user = req.session?.user || null;
165
+ return res.json({
166
+ success: true,
167
+ message: 'SuperAdmin access granted',
168
+ user: user ? {
169
+ id: user.id,
170
+ username: user.username,
171
+ role: user.role,
172
+ sessionId: user.sessionId
173
+ } : null
174
+ });
175
+ } catch (err) {
176
+ console.error('[mbkauthe] debug validate-superadmin error:', err);
177
+ return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
178
+ }
179
+ });
180
+ }
181
+
160
182
  // Test route
161
183
  router.get(['/test', '/'], validateSession, LoginLimit, async (req, res) => {
162
184
  const { username, fullname, role, id, sessionId, allowedApps } = req.session.user;
@@ -165,7 +187,7 @@ router.get(['/test', '/'], validateSession, LoginLimit, async (req, res) => {
165
187
  ? new Date(req.session.cookie.expires).toISOString()
166
188
  : null;
167
189
 
168
- return res.render('test.handlebars', {
190
+ return res.render('pages/test.handlebars', {
169
191
  layout: false,
170
192
  username,
171
193
  fullname: fullname || 'N/A',
@@ -483,7 +505,7 @@ router.get(["/info", "/i"], LoginLimit, async (req, res) => {
483
505
  }
484
506
 
485
507
  try {
486
- res.render("info_mbkauthe.handlebars", {
508
+ res.render("pages/info_mbkauthe.handlebars", {
487
509
  layout: false,
488
510
  mbkautheVar: safe_mbkautheVar,
489
511
  CurrentVersion: packageJson.version,
@@ -506,59 +528,6 @@ router.get(["/info", "/i"], LoginLimit, async (req, res) => {
506
528
  }
507
529
  });
508
530
 
509
- if(process.env.env === 'dev') {
510
- // DB stats page
511
- router.get(["/db", "/db.json"], LoginLimit, async (req, res) => {
512
- try {
513
- const queryCount = typeof dblogin.getQueryCount === 'function' ? dblogin.getQueryCount() : null;
514
- const queryLimit = Number(req.query.limit) || 50;
515
- const queryLog = typeof dblogin.getQueryLog === 'function' ? dblogin.getQueryLog({ limit: queryLimit }) : [];
516
-
517
- // Allow a manual reset via query string (e.g., /mbkauthe/db?reset=1)
518
- if (req.query.reset === '1') {
519
- if (typeof dblogin.resetQueryCount === 'function') dblogin.resetQueryCount();
520
- if (typeof dblogin.resetQueryLog === 'function') dblogin.resetQueryLog();
521
- }
522
-
523
- if (req.path.endsWith('.json')) {
524
- return res.json({ queryCount, queryLog });
525
- }
526
-
527
- const formattedLog = queryLog
528
- .map((entry, idx) => {
529
- const values = Array.isArray(entry.values) ? ` -- values: ${JSON.stringify(entry.values)}` : '';
530
- return `${idx + 1}. [${entry.time}] ${entry.query}${values}`;
531
- })
532
- .join('\n\n');
533
-
534
- return res.send(`<!doctype html>
535
- <html lang="en">
536
- <head>
537
- <meta charset="utf-8" />
538
- <title>mbkauthe DB Stats</title>
539
- <style>body{font-family:system-ui, sans-serif;margin:2rem;}h1{margin-bottom:1rem;}pre{background:#f7f7f7;padding:1rem;border-radius:6px;white-space:pre-wrap;word-break:break-word;}</style>
540
- </head>
541
- <body>
542
- <h1>mbkauthe DB Stats</h1>
543
- <p>Query count: <strong>${queryCount}</strong></p>
544
- <p>Showing last <strong>${queryLimit}</strong> queries (<a href="/mbkauthe/db?reset=1">reset</a>)</p>
545
- <pre>${formattedLog || '<i>No queries recorded yet.</i>'}</pre>
546
- </body>
547
- </html>`);
548
- } catch (err) {
549
- console.error('[mbkauthe] /db route error:', err);
550
- return renderError(res, req, {
551
- layout: false,
552
- code: 500,
553
- error: "Internal Server Error",
554
- message: "Could not fetch DB stats.",
555
- pagename: "DB Stats",
556
- page: "/mbkauthe/info",
557
- });
558
- }
559
- });
560
- }
561
-
562
531
  router.get(["/info.json", "/i.json"], LoginLimit, async (req, res) => {
563
532
  let latestVersion;
564
533
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "4.6.2",
3
+ "version": "4.7.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,476 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ {{> head pageTitle="DB Query Monitor" ogUrl="/mbkauthe/db" extraStyles="<style>
5
+ .db-container {
6
+ flex: 1;
7
+ display: flex;
8
+ align-items: flex-start;
9
+ justify-content: center;
10
+ padding: 120px 1.7rem 40px;
11
+ position: relative;
12
+ overflow: hidden;
13
+ background: radial-gradient(circle at top right, rgba(33, 150, 243, 0.25), transparent 55%),
14
+ radial-gradient(circle at 20% 20%, rgba(0, 184, 148, 0.18), transparent 50%),
15
+ linear-gradient(135deg, var(--darker), #0b1e25);
16
+ }
17
+
18
+ .db-panel {
19
+ background: rgba(10, 20, 20, 0.94);
20
+ border-radius: 18px;
21
+ padding: 2.25rem;
22
+ width: min(1200px, 100%);
23
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
24
+ border: 1px solid rgba(0, 184, 148, 0.18);
25
+ position: relative;
26
+ z-index: 2;
27
+ overflow: hidden;
28
+ }
29
+
30
+ .db-panel::after {
31
+ content: '';
32
+ position: absolute;
33
+ inset: 0;
34
+ background: linear-gradient(120deg, rgba(33, 150, 243, 0.06), transparent 55%);
35
+ pointer-events: none;
36
+ }
37
+
38
+ .db-header {
39
+ display: flex;
40
+ flex-wrap: wrap;
41
+ justify-content: space-between;
42
+ align-items: flex-start;
43
+ gap: 1.5rem;
44
+ margin-bottom: 1.5rem;
45
+ }
46
+
47
+ .db-title {
48
+ font-family: var(--font-display);
49
+ font-size: clamp(1.8rem, 2vw, 2.4rem);
50
+ color: var(--light);
51
+ }
52
+
53
+ .db-subtitle {
54
+ color: var(--text-light);
55
+ font-size: var(--text-size-sm);
56
+ margin-top: 0.3rem;
57
+ }
58
+
59
+ .db-actions {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: 0.6rem;
63
+ }
64
+
65
+ .db-link {
66
+ padding: 0.45rem 0.85rem;
67
+ border-radius: 0.6rem;
68
+ border: 1px solid rgba(33, 150, 243, 0.4);
69
+ background: rgba(33, 150, 243, 0.12);
70
+ color: var(--light);
71
+ text-decoration: none;
72
+ transition: var(--transition);
73
+ }
74
+
75
+ .db-link:hover {
76
+ background: rgba(33, 150, 243, 0.25);
77
+ }
78
+
79
+ .db-card {
80
+ display: grid;
81
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
82
+ gap: 1rem;
83
+ margin-bottom: 1.5rem;
84
+ }
85
+
86
+ .stat-box {
87
+ padding: 1rem 1.2rem;
88
+ border-radius: 14px;
89
+ border: 1px solid rgba(0, 184, 148, 0.2);
90
+ background: rgba(0, 0, 0, 0.25);
91
+ }
92
+
93
+ .stat-label {
94
+ font-size: 0.85rem;
95
+ color: var(--text-light);
96
+ text-transform: uppercase;
97
+ letter-spacing: 0.08em;
98
+ }
99
+
100
+ .stat-value {
101
+ font-size: 1.4rem;
102
+ font-weight: 700;
103
+ color: var(--light);
104
+ }
105
+
106
+ .db-toolbar {
107
+ display: flex;
108
+ flex-wrap: wrap;
109
+ gap: 0.75rem;
110
+ align-items: center;
111
+ margin-bottom: 1.2rem;
112
+ }
113
+
114
+ .db-toolbar label {
115
+ color: var(--text-light);
116
+ }
117
+
118
+ .db-toolbar input {
119
+ width: 5rem;
120
+ padding: 0.35rem 0.5rem;
121
+ border-radius: 0.5rem;
122
+ border: 1px solid rgba(255, 255, 255, 0.2);
123
+ background: rgba(0, 0, 0, 0.4);
124
+ color: var(--text);
125
+ }
126
+
127
+ .db-toolbar button {
128
+ padding: 0.45rem 0.85rem;
129
+ border-radius: 0.6rem;
130
+ border: 1px solid rgba(0, 184, 148, 0.4);
131
+ background: rgba(0, 184, 148, 0.12);
132
+ color: var(--light);
133
+ cursor: pointer;
134
+ transition: var(--transition);
135
+ }
136
+
137
+ .db-toolbar button:hover {
138
+ background: rgba(0, 184, 148, 0.25);
139
+ }
140
+
141
+ .db-table-wrapper {
142
+ border-radius: 16px;
143
+ border: 1px solid rgba(255, 255, 255, 0.08);
144
+ background: rgba(6, 12, 14, 0.7);
145
+ padding: 0.4rem;
146
+ overflow-x: auto;
147
+ }
148
+
149
+ .copy-row-btn {
150
+ padding: 0.25rem 0.6rem;
151
+ border-radius: 0.5rem;
152
+ border: 1px solid rgba(33, 150, 243, 0.5);
153
+ background: rgba(33, 150, 243, 0.12);
154
+ color: var(--light);
155
+ cursor: pointer;
156
+ font-size: 0.75rem;
157
+ transition: var(--transition);
158
+ }
159
+
160
+ .copy-row-btn:hover {
161
+ background: rgba(33, 150, 243, 0.25);
162
+ }
163
+
164
+ table {
165
+ width: 100%;
166
+ border-collapse: collapse;
167
+ color: var(--text);
168
+ font-size: 0.9rem;
169
+ min-width: 900px;
170
+ }
171
+
172
+ th, td {
173
+ padding: 0.6rem 0.8rem;
174
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
175
+ vertical-align: top;
176
+ }
177
+
178
+ th {
179
+ text-align: left;
180
+ font-weight: 600;
181
+ color: var(--text-light);
182
+ text-transform: uppercase;
183
+ font-size: 0.75rem;
184
+ letter-spacing: 0.08em;
185
+ }
186
+
187
+ code {
188
+ font-family: var(--font-mono);
189
+ white-space: pre-wrap;
190
+ word-break: break-word;
191
+ }
192
+
193
+ .badge {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ padding: 0.15rem 0.55rem;
197
+ border-radius: 999px;
198
+ font-size: 0.72rem;
199
+ font-weight: 700;
200
+ text-transform: uppercase;
201
+ }
202
+
203
+ .badge.ok {
204
+ background: rgba(67, 233, 123, 0.15);
205
+ color: var(--success);
206
+ border: 1px solid rgba(67, 233, 123, 0.4);
207
+ }
208
+
209
+ .badge.err {
210
+ background: rgba(255, 118, 117, 0.15);
211
+ color: var(--danger);
212
+ border: 1px solid rgba(255, 118, 117, 0.35);
213
+ }
214
+
215
+ .empty {
216
+ font-style: italic;
217
+ color: var(--text-light);
218
+ padding: 1.5rem;
219
+ text-align: center;
220
+ }
221
+
222
+ .notification {
223
+ margin-bottom: 1rem;
224
+ padding: 0.75rem 1rem;
225
+ border-radius: 0.7rem;
226
+ background: rgba(67, 233, 123, 0.12);
227
+ color: var(--success);
228
+ border: 1px solid rgba(67, 233, 123, 0.4);
229
+ }
230
+
231
+ @media (max-width: 900px) {
232
+ .db-panel {
233
+ padding: 1.5rem;
234
+ }
235
+
236
+ .db-actions {
237
+ width: 100%;
238
+ }
239
+
240
+ table {
241
+ min-width: 100%;
242
+ display: block;
243
+ }
244
+
245
+ thead {
246
+ display: none;
247
+ }
248
+
249
+ tbody,
250
+ tr,
251
+ td {
252
+ display: block;
253
+ width: 100%;
254
+ }
255
+
256
+ tr {
257
+ border: 1px solid rgba(255, 255, 255, 0.08);
258
+ border-radius: 12px;
259
+ margin-bottom: 0.9rem;
260
+ padding: 0.6rem 0.8rem;
261
+ background: rgba(0, 0, 0, 0.28);
262
+ }
263
+
264
+ td {
265
+ border: none;
266
+ padding: 0.5rem 0;
267
+ }
268
+
269
+ td::before {
270
+ content: attr(data-label);
271
+ display: block;
272
+ font-size: 0.7rem;
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.08em;
275
+ color: var(--text-light);
276
+ margin-bottom: 0.25rem;
277
+ }
278
+ }
279
+ </style>"}}
280
+
281
+ <body>
282
+ {{> header appName=appName}}
283
+
284
+ <section class="db-container">
285
+ {{> backgroundElements}}
286
+
287
+ <div class="db-panel">
288
+ <div class="db-header">
289
+ <div>
290
+ <div class="db-title">DB Query Monitor</div>
291
+ <div class="db-subtitle">Live view of recent database queries and diagnostics</div>
292
+ </div>
293
+ <div class="db-actions">
294
+ <a class="db-link" href="/mbkauthe/info">Back to info</a>
295
+ </div>
296
+ </div>
297
+
298
+ <div class="db-card">
299
+ <div class="stat-box">
300
+ <div class="stat-label">Query Count</div>
301
+ <div class="stat-value" id="query-count">-</div>
302
+ </div>
303
+ <div class="stat-box">
304
+ <div class="stat-label">Limit</div>
305
+ <div class="stat-value" id="query-limit">{{queryLimit}}</div>
306
+ </div>
307
+ </div>
308
+
309
+ <div class="db-toolbar">
310
+ <form id="limit-form" action="/mbkauthe/db" method="get">
311
+ <label for="limit">Limit</label>
312
+ <input id="limit" name="limit" type="number" min="1" max="500" value="{{queryLimit}}" />
313
+ <button type="submit">Update</button>
314
+ </form>
315
+ <button id="reset-btn" type="button">Reset</button>
316
+ <button id="copy-btn" type="button">Copy Log</button>
317
+ </div>
318
+
319
+ {{#if resetDone}}
320
+ <div class="notification">Query log and count have been reset.</div>
321
+ {{/if}}
322
+
323
+ <div id="query-log" class="db-table-wrapper">
324
+ <div class="empty">Loading query log...</div>
325
+ </div>
326
+ </div>
327
+ </section>
328
+
329
+ <script>
330
+ const limitInput = document.getElementById('limit');
331
+ const queryCountEl = document.getElementById('query-count');
332
+ const queryLimitEl = document.getElementById('query-limit');
333
+ const queryLogEl = document.getElementById('query-log');
334
+ const resetBtn = document.getElementById('reset-btn');
335
+ const copyBtn = document.getElementById('copy-btn');
336
+
337
+ const escapeHtml = (str) => String(str || '')
338
+ .replace(/[&<>"'`]/g, (s) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '`': '&#96;' })[s]);
339
+
340
+ const buildTable = (queryLog) => {
341
+ if (!Array.isArray(queryLog) || queryLog.length === 0) {
342
+ queryLogEl.innerHTML = '<div class="empty">No queries recorded yet.</div>';
343
+ return;
344
+ }
345
+
346
+ const rows = queryLog.map((entry, idx) => {
347
+ const values = Array.isArray(entry.values) ? escapeHtml(JSON.stringify(entry.values)) : '';
348
+ const callsite = entry.callsite || null;
349
+ const callsiteText = callsite
350
+ ? `${callsite.function || '(anonymous)'}\n${callsite.file}:${callsite.line}:${callsite.column}`
351
+ : '';
352
+ const durationText = typeof entry.durationMs === 'number'
353
+ ? `${entry.durationMs.toFixed(2)} ms`
354
+ : '';
355
+ const statusText = entry.success === true ? 'OK' : entry.success === false ? 'ERR' : '';
356
+ const statusBadge = statusText === 'OK'
357
+ ? '<span class="badge ok">OK</span>'
358
+ : statusText === 'ERR'
359
+ ? '<span class="badge err">ERR</span>'
360
+ : '';
361
+ const errorText = entry.error
362
+ ? `${entry.error.code ? `${entry.error.code}: ` : ''}${entry.error.message || ''}`
363
+ : '';
364
+ const requestText = entry.request
365
+ ? `${entry.request.method || ''} ${entry.request.url || ''}`.trim() +
366
+ `\nuser: ${entry.request.username || entry.request.userId || 'anonymous'}` +
367
+ `\nip: ${entry.request.ip || ''}`
368
+ : '';
369
+ const poolText = entry.pool
370
+ ? `total:${entry.pool.total} idle:${entry.pool.idle} waiting:${entry.pool.waiting}`
371
+ : '';
372
+
373
+ return `
374
+ <tr>
375
+ <td data-label="#">${idx + 1}</td>
376
+ <td data-label="Time">${escapeHtml(entry.time)}</td>
377
+ <td data-label="Name">${entry.name ? `<code>${escapeHtml(entry.name)}</code>` : ''}</td>
378
+ <td data-label="Duration">${durationText ? `<code>${escapeHtml(durationText)}</code>` : ''}</td>
379
+ <td data-label="Status">${statusBadge}</td>
380
+ <td data-label="Query"><code>${escapeHtml(entry.query)}</code></td>
381
+ <td data-label="Values">${values ? `<code>${values}</code>` : ''}</td>
382
+ <td data-label="Error">${errorText ? `<code>${escapeHtml(errorText)}</code>` : ''}</td>
383
+ <td data-label="Request">${requestText ? `<code>${escapeHtml(requestText)}</code>` : ''}</td>
384
+ <td data-label="Pool">${poolText ? `<code>${escapeHtml(poolText)}</code>` : ''}</td>
385
+ <td data-label="Callsite">${callsiteText ? `<code>${escapeHtml(callsiteText)}</code>` : ''}</td>
386
+ <td data-label="Copy"><button class="copy-row-btn" data-copy='${escapeHtml(JSON.stringify(entry))}'>Copy</button></td>
387
+ </tr>
388
+ `;
389
+ }).join('');
390
+
391
+ queryLogEl.innerHTML = `
392
+ <table>
393
+ <thead>
394
+ <tr>
395
+ <th>#</th>
396
+ <th>Time</th>
397
+ <th>Name</th>
398
+ <th>Duration</th>
399
+ <th>Status</th>
400
+ <th>Query</th>
401
+ <th>Values</th>
402
+ <th>Error</th>
403
+ <th>Request</th>
404
+ <th>Pool</th>
405
+ <th>Callsite</th>
406
+ <th>Copy</th>
407
+ </tr>
408
+ </thead>
409
+ <tbody>
410
+ ${rows}
411
+ </tbody>
412
+ </table>
413
+ `;
414
+ };
415
+
416
+ const loadLog = async (options = {}) => {
417
+ const limit = Number(limitInput.value) || 50;
418
+ queryLimitEl.textContent = String(limit);
419
+
420
+ const params = new URLSearchParams({ limit: String(limit) });
421
+ if (options.reset) params.set('reset', '1');
422
+
423
+ const response = await fetch(`/mbkauthe/db.json?${params.toString()}`);
424
+ if (!response.ok) {
425
+ queryLogEl.innerHTML = '<div class="empty">Failed to load query log.</div>';
426
+ return;
427
+ }
428
+
429
+ const data = await response.json();
430
+ queryCountEl.textContent = data.queryCount ?? '-';
431
+ buildTable(data.queryLog);
432
+
433
+ if (options.reset) {
434
+ const next = new URLSearchParams({ limit: String(limit), resetDone: '1' });
435
+ window.location.assign(`/mbkauthe/db?${next.toString()}`);
436
+ }
437
+ };
438
+
439
+ resetBtn.addEventListener('click', () => {
440
+ loadLog({ reset: true }).catch(() => {
441
+ queryLogEl.innerHTML = '<div class="empty">Failed to reset query log.</div>';
442
+ });
443
+ });
444
+
445
+ copyBtn.addEventListener('click', () => {
446
+ const text = queryLogEl.innerText;
447
+ navigator.clipboard.writeText(text).then(() => {
448
+ copyBtn.textContent = 'Copied!';
449
+ setTimeout(() => (copyBtn.textContent = 'Copy log'), 1200);
450
+ });
451
+ });
452
+
453
+ queryLogEl.addEventListener('click', (event) => {
454
+ const target = event.target;
455
+ if (!(target instanceof HTMLElement)) return;
456
+ if (!target.classList.contains('copy-row-btn')) return;
457
+
458
+ const payload = target.getAttribute('data-copy');
459
+ if (!payload) return;
460
+
461
+ navigator.clipboard.writeText(payload).then(() => {
462
+ const original = target.textContent;
463
+ target.textContent = 'Copied';
464
+ setTimeout(() => {
465
+ target.textContent = original || 'Copy';
466
+ }, 1200);
467
+ });
468
+ });
469
+
470
+ loadLog().catch(() => {
471
+ queryLogEl.innerHTML = '<div class="empty">Failed to load query log.</div>';
472
+ });
473
+ </script>
474
+ {{> versionInfo}}
475
+ </body>
476
+ </html>
@@ -210,6 +210,9 @@
210
210
  <a class="btn btn-outline" href="/mbkauthe/info">
211
211
  <i class="fas fa-info-circle"></i> Info Page
212
212
  </a>
213
+ <a class="btn btn-outline" href="/mbkauthe/validate-superadmin">
214
+ <i class="fas fa-user-shield"></i> Validate SuperAdmin
215
+ </a>
213
216
  <a class="btn btn-outline" href="/mbkauthe/login">
214
217
  <i class="fas fa-sign-in-alt"></i> Login Page
215
218
  </a>
@@ -1,5 +1,4 @@
1
1
  <style>
2
- @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap');
3
2
 
4
3
  :root {
5
4
  --primary: #2196f3;
File without changes