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 +22 -1
- package/docs/api.md +146 -0
- package/docs/env.md +6 -0
- package/index.js +3 -2
- package/lib/main.js +13 -2
- package/lib/middleware/index.js +6 -1
- package/lib/pool.js +130 -5
- package/lib/routes/auth.js +3 -3
- package/lib/routes/dbLogs.js +67 -0
- package/lib/routes/misc.js +25 -56
- package/package.json +1 -1
- package/views/pages/dbLogs.handlebars +476 -0
- package/views/{test.handlebars → pages/test.handlebars} +3 -0
- package/views/sharedStyles.handlebars +0 -1
- /package/views/{2fa.handlebars → pages/2fa.handlebars} +0 -0
- /package/views/{accountSwitch.handlebars → pages/accountSwitch.handlebars} +0 -0
- /package/views/{info_mbkauthe.handlebars → pages/info_mbkauthe.handlebars} +0 -0
- /package/views/{loginmbkauthe.handlebars → pages/loginmbkauthe.handlebars} +0 -0
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
|
});
|
package/lib/middleware/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/lib/routes/auth.js
CHANGED
|
@@ -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;
|
package/lib/routes/misc.js
CHANGED
|
@@ -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,
|
|
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
|
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' })[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>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|