mbkauthe 4.7.2 → 4.8.1
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 +4 -4
- package/docs/api.md +13 -13
- package/docs/db.md +5 -3
- package/docs/db.sql +25 -1
- package/docs/env.md +18 -6
- package/index.d.ts +11 -4
- package/index.js +59 -94
- package/lib/config/index.js +8 -5
- package/lib/middleware/auth.js +101 -29
- package/lib/pool.js +13 -8
- package/lib/routes/auth.js +57 -43
- package/lib/routes/dbLogs.js +8 -4
- package/lib/routes/misc.js +21 -7
- package/lib/routes/oauth.js +361 -355
- package/lib/utils/dbQueryLogger.js +210 -133
- package/package.json +1 -1
- package/test.spec.js +14 -2
- package/views/pages/dbLogs.handlebars +56 -5
- package/views/pages/loginmbkauthe.handlebars +2 -2
package/lib/middleware/auth.js
CHANGED
|
@@ -6,6 +6,44 @@ import { ErrorCodes, createErrorResponse } from "../utils/errors.js";
|
|
|
6
6
|
import { hashApiToken } from "#config.js";
|
|
7
7
|
import { canAccessMethod } from "#config.js";
|
|
8
8
|
|
|
9
|
+
const IS_DEV = process.env.env === 'dev' || process.env.test === 'dev' || process.env.NODE_ENV === 'development';
|
|
10
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
const isUuid = (val) => typeof val === 'string' && UUID_RE.test(val);
|
|
12
|
+
|
|
13
|
+
const SQL_VALIDATE_APP_SESSION = `
|
|
14
|
+
SELECT s.expires_at, u."Active", u."Role"
|
|
15
|
+
FROM "Sessions" s
|
|
16
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
17
|
+
WHERE s.id = $1
|
|
18
|
+
LIMIT 1
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decide if the incoming request should return JSON errors instead of HTML.
|
|
23
|
+
* Non-browser clients (API calls / AJAX) should get JSON.
|
|
24
|
+
*/
|
|
25
|
+
function isJsonRequest(req) {
|
|
26
|
+
if (!req || !req.headers) return false;
|
|
27
|
+
const accept = (req.headers.accept || "").toLowerCase();
|
|
28
|
+
const xRequestedWith = (req.headers["x-requested-with"] || "").toLowerCase();
|
|
29
|
+
const userAgent = (req.headers["user-agent"] || "").toLowerCase();
|
|
30
|
+
const url = (req.originalUrl || req.url || "").toLowerCase();
|
|
31
|
+
const path = (req.path || "").toLowerCase();
|
|
32
|
+
|
|
33
|
+
const isApiPath = url.startsWith("/mbkauthe/api/") || url.startsWith("/api/") || path.startsWith("/mbkauthe/api/") || path.startsWith("/api/");
|
|
34
|
+
const isAcceptJson = accept.includes("application/json") || accept.includes("json") || accept.includes("*/*");
|
|
35
|
+
|
|
36
|
+
const nonBrowserAgent = /curl|wget|httpie|python-requests|python|go-http-client|java\/|php|node-fetch|axios|postman|insomnia|okhttp/;
|
|
37
|
+
const browserAgent = /mozilla|applewebkit|chrome|safari|firefox|edg|msie|trident|opera/;
|
|
38
|
+
|
|
39
|
+
if (isApiPath || xRequestedWith === "xmlhttprequest") return true;
|
|
40
|
+
if (isAcceptJson && !accept.includes("text/html")) return true;
|
|
41
|
+
|
|
42
|
+
if (nonBrowserAgent.test(userAgent) && !browserAgent.test(userAgent)) return true;
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
9
47
|
/**
|
|
10
48
|
* Validates a Bearer token (API Token or Session UUID)
|
|
11
49
|
* Returns a user object if valid, or null/error object
|
|
@@ -158,8 +196,13 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
158
196
|
|
|
159
197
|
// --- Fallback to Cookie Session ---
|
|
160
198
|
if (!req.session.user) {
|
|
161
|
-
|
|
162
|
-
|
|
199
|
+
if (IS_DEV) {
|
|
200
|
+
console.log("[mbkauthe] User not authenticated");
|
|
201
|
+
console.log("[mbkauthe]: ", req.session.user);
|
|
202
|
+
}
|
|
203
|
+
if (isJsonRequest(req)) {
|
|
204
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
205
|
+
}
|
|
163
206
|
return renderError(res, req, {
|
|
164
207
|
code: 401,
|
|
165
208
|
error: "Not Logged In",
|
|
@@ -170,13 +213,16 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
170
213
|
}
|
|
171
214
|
|
|
172
215
|
try {
|
|
173
|
-
const {
|
|
216
|
+
const { sessionId, role, allowedApps } = req.session.user;
|
|
174
217
|
|
|
175
218
|
// Defensive checks for sessionId and allowedApps
|
|
176
|
-
if (!sessionId) {
|
|
219
|
+
if (!sessionId || !isUuid(sessionId)) {
|
|
177
220
|
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
178
221
|
req.session.destroy();
|
|
179
222
|
clearSessionCookies(res);
|
|
223
|
+
if (isJsonRequest(req)) {
|
|
224
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
225
|
+
}
|
|
180
226
|
return renderError(res, req, {
|
|
181
227
|
code: 401,
|
|
182
228
|
error: "Session Expired",
|
|
@@ -186,20 +232,16 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
186
232
|
});
|
|
187
233
|
}
|
|
188
234
|
|
|
189
|
-
// Normalize sessionId (DB id) for consistent comparison
|
|
190
|
-
const normalizedSessionId = sessionId;
|
|
191
|
-
|
|
192
235
|
// Validate session by DB primary key id and join to user
|
|
193
|
-
const
|
|
194
|
-
FROM "Sessions" s
|
|
195
|
-
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
196
|
-
WHERE s.id = $1 LIMIT 1`;
|
|
197
|
-
const result = await dblogin.query({ name: 'validate-app-session', text: query, values: [normalizedSessionId] });
|
|
236
|
+
const result = await dblogin.query({ name: 'validate-app-session', text: SQL_VALIDATE_APP_SESSION, values: [sessionId] });
|
|
198
237
|
|
|
199
238
|
if (result.rows.length === 0) {
|
|
200
239
|
console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
|
|
201
240
|
req.session.destroy();
|
|
202
241
|
clearSessionCookies(res);
|
|
242
|
+
if (isJsonRequest(req)) {
|
|
243
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
244
|
+
}
|
|
203
245
|
return renderError(res, req, {
|
|
204
246
|
code: 401,
|
|
205
247
|
error: "Session Expired",
|
|
@@ -212,11 +254,18 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
212
254
|
const sessionRow = result.rows[0];
|
|
213
255
|
|
|
214
256
|
// Check expired
|
|
215
|
-
if (sessionRow.expires_at
|
|
257
|
+
if (sessionRow.expires_at) {
|
|
258
|
+
const expiresMs = sessionRow.expires_at instanceof Date
|
|
259
|
+
? sessionRow.expires_at.getTime()
|
|
260
|
+
: Date.parse(sessionRow.expires_at);
|
|
261
|
+
if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
|
|
216
262
|
console.log(`[mbkauthe] Session invalidated (expired) for user "${req.session.user.username}"`);
|
|
217
263
|
// destroy and clear cookies
|
|
218
264
|
req.session.destroy();
|
|
219
265
|
clearSessionCookies(res);
|
|
266
|
+
if (isJsonRequest(req)) {
|
|
267
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
268
|
+
}
|
|
220
269
|
return renderError(res, req, {
|
|
221
270
|
code: 401,
|
|
222
271
|
error: "Session Expired",
|
|
@@ -225,12 +274,16 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
225
274
|
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
226
275
|
});
|
|
227
276
|
}
|
|
277
|
+
}
|
|
228
278
|
|
|
229
279
|
|
|
230
280
|
if (!sessionRow.Active) {
|
|
231
281
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
232
282
|
req.session.destroy();
|
|
233
283
|
clearSessionCookies(res);
|
|
284
|
+
if (isJsonRequest(req)) {
|
|
285
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
286
|
+
}
|
|
234
287
|
return renderError(res, req, {
|
|
235
288
|
code: 401,
|
|
236
289
|
error: "Account Inactive",
|
|
@@ -247,6 +300,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
247
300
|
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
248
301
|
req.session.destroy();
|
|
249
302
|
clearSessionCookies(res);
|
|
303
|
+
if (isJsonRequest(req)) {
|
|
304
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
305
|
+
}
|
|
250
306
|
return renderError(res, req, {
|
|
251
307
|
code: 401,
|
|
252
308
|
error: "Unauthorized",
|
|
@@ -258,7 +314,7 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
258
314
|
}
|
|
259
315
|
|
|
260
316
|
// Store user role in request for checkRolePermission to use
|
|
261
|
-
req.userRole =
|
|
317
|
+
req.userRole = sessionRow.Role;
|
|
262
318
|
|
|
263
319
|
next();
|
|
264
320
|
} catch (err) {
|
|
@@ -277,25 +333,18 @@ async function validateApiSession(req, res, next) {
|
|
|
277
333
|
}
|
|
278
334
|
|
|
279
335
|
try {
|
|
280
|
-
const {
|
|
336
|
+
const { sessionId, role, allowedApps } = req.session.user;
|
|
281
337
|
|
|
282
338
|
// Defensive checks for sessionId and allowedApps
|
|
283
|
-
if (!sessionId) {
|
|
339
|
+
if (!sessionId || !isUuid(sessionId)) {
|
|
284
340
|
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
285
341
|
req.session.destroy();
|
|
286
342
|
clearSessionCookies(res);
|
|
287
343
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
288
344
|
}
|
|
289
345
|
|
|
290
|
-
// Normalize sessionId (DB id) for consistent comparison
|
|
291
|
-
const normalizedSessionId = sessionId;
|
|
292
|
-
|
|
293
346
|
// Validate session by DB primary key id and join to user
|
|
294
|
-
const
|
|
295
|
-
FROM "Sessions" s
|
|
296
|
-
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
297
|
-
WHERE s.id = $1 LIMIT 1`;
|
|
298
|
-
const result = await dblogin.query({ name: 'validate-app-session-for-api', text: query, values: [normalizedSessionId] });
|
|
347
|
+
const result = await dblogin.query({ name: 'validate-app-session-for-api', text: SQL_VALIDATE_APP_SESSION, values: [sessionId] });
|
|
299
348
|
|
|
300
349
|
if (result.rows.length === 0) {
|
|
301
350
|
req.session.destroy();
|
|
@@ -306,11 +355,16 @@ async function validateApiSession(req, res, next) {
|
|
|
306
355
|
const sessionRow = result.rows[0];
|
|
307
356
|
|
|
308
357
|
// Check expired
|
|
309
|
-
if (sessionRow.expires_at
|
|
358
|
+
if (sessionRow.expires_at) {
|
|
359
|
+
const expiresMs = sessionRow.expires_at instanceof Date
|
|
360
|
+
? sessionRow.expires_at.getTime()
|
|
361
|
+
: Date.parse(sessionRow.expires_at);
|
|
362
|
+
if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
|
|
310
363
|
req.session.destroy();
|
|
311
364
|
clearSessionCookies(res);
|
|
312
365
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
313
366
|
}
|
|
367
|
+
}
|
|
314
368
|
|
|
315
369
|
|
|
316
370
|
if (!result.rows[0].Active) {
|
|
@@ -332,7 +386,7 @@ async function validateApiSession(req, res, next) {
|
|
|
332
386
|
}
|
|
333
387
|
|
|
334
388
|
// Store user role in request for checkRolePermission to use
|
|
335
|
-
req.userRole =
|
|
389
|
+
req.userRole = sessionRow.Role;
|
|
336
390
|
|
|
337
391
|
next();
|
|
338
392
|
} catch (err) {
|
|
@@ -444,6 +498,9 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
444
498
|
try {
|
|
445
499
|
if (!req.session || !req.session.user || !req.session.user.id) {
|
|
446
500
|
console.log("[mbkauthe] User not authenticated");
|
|
501
|
+
if (isJsonRequest(req)) {
|
|
502
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
503
|
+
}
|
|
447
504
|
return renderError(res, req, {
|
|
448
505
|
code: 401,
|
|
449
506
|
error: "Not Logged In",
|
|
@@ -453,11 +510,19 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
453
510
|
});
|
|
454
511
|
}
|
|
455
512
|
|
|
513
|
+
// SuperAdmin bypasses all role checks
|
|
514
|
+
if(req.session.role === "SuperAdmin") {
|
|
515
|
+
return next();
|
|
516
|
+
}
|
|
517
|
+
|
|
456
518
|
// Use role from validateSession to avoid additional DB query
|
|
457
519
|
const userRole = req.userRole;
|
|
458
520
|
|
|
459
521
|
// Check notAllowed role
|
|
460
522
|
if (notAllowed && userRole === notAllowed) {
|
|
523
|
+
if (isJsonRequest(req)) {
|
|
524
|
+
return res.status(403).json(createErrorResponse(403, ErrorCodes.ROLE_NOT_ALLOWED));
|
|
525
|
+
}
|
|
461
526
|
return renderError(res, req, {
|
|
462
527
|
code: 403,
|
|
463
528
|
error: "Access Denied",
|
|
@@ -471,12 +536,15 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
471
536
|
const rolesArray = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
|
|
472
537
|
|
|
473
538
|
// Check for "Any" or "any" role
|
|
474
|
-
if (rolesArray.includes("Any") || rolesArray.includes("any")) {
|
|
539
|
+
if (rolesArray.includes("Any") || rolesArray.includes("any") || rolesArray.includes("*")) {
|
|
475
540
|
return next();
|
|
476
541
|
}
|
|
477
542
|
|
|
478
543
|
// Check if user role is in allowed roles
|
|
479
544
|
if (!rolesArray.includes(userRole)) {
|
|
545
|
+
if (isJsonRequest(req)) {
|
|
546
|
+
return res.status(403).json(createErrorResponse(403, ErrorCodes.INSUFFICIENT_PERMISSIONS));
|
|
547
|
+
}
|
|
480
548
|
return renderError(res, req, {
|
|
481
549
|
code: 403,
|
|
482
550
|
error: "Access Denied",
|
|
@@ -502,7 +570,6 @@ const validateSessionAndRole = (requiredRole, notAllowed, strictTokenValidation
|
|
|
502
570
|
};
|
|
503
571
|
};
|
|
504
572
|
|
|
505
|
-
|
|
506
573
|
const authenticate = (authentication) => {
|
|
507
574
|
return (req, res, next) => {
|
|
508
575
|
const token = req.headers["authorization"];
|
|
@@ -520,8 +587,13 @@ const authenticate = (authentication) => {
|
|
|
520
587
|
const strictValidateSession = (req, res, next) => validateSession(req, res, next, true);
|
|
521
588
|
const strictValidateSessionAndRole = (requiredRole, notAllowed) => validateSessionAndRole(requiredRole, notAllowed, true);
|
|
522
589
|
|
|
590
|
+
// Short aliases for convenience
|
|
591
|
+
const sessVal = validateSession;
|
|
592
|
+
const sessRole = validateSessionAndRole;
|
|
593
|
+
|
|
523
594
|
export {
|
|
524
595
|
validateSession, validateApiSession, checkRolePermission,
|
|
525
596
|
validateSessionAndRole, authenticate, reloadSessionUser,
|
|
526
|
-
strictValidateSession, strictValidateSessionAndRole
|
|
597
|
+
strictValidateSession, strictValidateSessionAndRole,
|
|
598
|
+
sessVal, sessRole
|
|
527
599
|
}
|
package/lib/pool.js
CHANGED
|
@@ -12,14 +12,9 @@ const poolConfig = {
|
|
|
12
12
|
ssl: {
|
|
13
13
|
rejectUnauthorized: true,
|
|
14
14
|
},
|
|
15
|
-
|
|
16
|
-
// Connection pool tuning for serverless/ephemeral environments (Vercel)
|
|
17
|
-
// - keep max small to avoid exhausting DB connections
|
|
18
|
-
// - reduce idle time so connections are returned sooner
|
|
19
|
-
// - set a short connection timeout to fail fast
|
|
20
|
-
max: 10,
|
|
15
|
+
max: 3,
|
|
21
16
|
idleTimeoutMillis: 10000,
|
|
22
|
-
connectionTimeoutMillis:
|
|
17
|
+
connectionTimeoutMillis: 10000,
|
|
23
18
|
};
|
|
24
19
|
|
|
25
20
|
export const dblogin = new Pool(poolConfig);
|
|
@@ -27,11 +22,21 @@ export const dblogin = new Pool(poolConfig);
|
|
|
27
22
|
// Keep pool.js focused on pool setup; attach dev-only query logger from dedicated module.
|
|
28
23
|
attachDevQueryLogger(dblogin);
|
|
29
24
|
|
|
25
|
+
/*
|
|
26
|
+
attachDevQueryLogger([
|
|
27
|
+
{ pool: dblogin, name: "loginDB" },
|
|
28
|
+
{ pool: dblogin1, name: "loginDB1" },
|
|
29
|
+
]);
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/*
|
|
30
33
|
(async () => {
|
|
31
34
|
try {
|
|
32
35
|
const client = await dblogin.connect();
|
|
33
36
|
client.release();
|
|
37
|
+
console.log("[mbkauthe] Database connection pool established successfully.");
|
|
34
38
|
} catch (err) {
|
|
35
39
|
console.error("[mbkauthe] Database connection error (pool):", err);
|
|
36
40
|
}
|
|
37
|
-
})();
|
|
41
|
+
})();
|
|
42
|
+
*/
|
package/lib/routes/auth.js
CHANGED
|
@@ -110,11 +110,17 @@ export async function checkTrustedDevice(req, username) {
|
|
|
110
110
|
try {
|
|
111
111
|
// Hash the provided device token before querying DB (we store token hashes in DB)
|
|
112
112
|
const deviceTokenHash = hashDeviceToken(deviceToken);
|
|
113
|
+
// Single round-trip: validate trusted device AND refresh LastUsed.
|
|
113
114
|
const deviceQuery = `
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
WHERE td."DeviceToken" = $1
|
|
115
|
+
UPDATE "TrustedDevices" td
|
|
116
|
+
SET "LastUsed" = NOW()
|
|
117
|
+
FROM "Users" u
|
|
118
|
+
WHERE td."DeviceToken" = $1
|
|
119
|
+
AND td."UserName" = $2
|
|
120
|
+
AND td."ExpiresAt" > NOW()
|
|
121
|
+
AND u."UserName" = td."UserName"
|
|
122
|
+
AND u."Active" = TRUE
|
|
123
|
+
RETURNING td."UserName", td."ExpiresAt", u."id" as id, u."Active", u."Role", u."AllowedApps"
|
|
118
124
|
`;
|
|
119
125
|
const deviceResult = await dblogin.query({
|
|
120
126
|
name: 'check-trusted-device',
|
|
@@ -138,13 +144,6 @@ export async function checkTrustedDevice(req, username) {
|
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
// Update last used timestamp
|
|
142
|
-
await dblogin.query({
|
|
143
|
-
name: 'update-device-last-used',
|
|
144
|
-
text: 'UPDATE "TrustedDevices" SET "LastUsed" = NOW() WHERE "DeviceToken" = $1',
|
|
145
|
-
values: [deviceTokenHash]
|
|
146
|
-
});
|
|
147
|
-
|
|
148
147
|
console.log(`[mbkauthe] Trusted device validated for user: ${username}`);
|
|
149
148
|
return {
|
|
150
149
|
id: deviceUser.id,
|
|
@@ -193,21 +192,24 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
193
192
|
const configuredMax = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
|
|
194
193
|
const MAX_SESSIONS = Number.isInteger(configuredMax) && configuredMax > 0 ? configuredMax : 5;
|
|
195
194
|
|
|
196
|
-
// Clean up expired sessions
|
|
197
|
-
await dblogin.query({
|
|
198
|
-
name: 'cleanup-expired-sessions',
|
|
199
|
-
text: `DELETE FROM "Sessions" WHERE "UserName" = $1 AND expires_at IS NOT NULL AND expires_at <= NOW()`,
|
|
200
|
-
values: [username]
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Count active sessions for this user (by username)
|
|
195
|
+
// Clean up expired sessions + count active sessions in a single round-trip.
|
|
204
196
|
const countRes = await dblogin.query({
|
|
205
|
-
name: 'count-user-sessions',
|
|
206
|
-
text: `
|
|
197
|
+
name: 'cleanup-and-count-user-sessions',
|
|
198
|
+
text: `
|
|
199
|
+
WITH deleted AS (
|
|
200
|
+
DELETE FROM "Sessions"
|
|
201
|
+
WHERE "UserName" = $1
|
|
202
|
+
AND expires_at IS NOT NULL
|
|
203
|
+
AND expires_at <= NOW()
|
|
204
|
+
)
|
|
205
|
+
SELECT COUNT(*)::int AS count
|
|
206
|
+
FROM "Sessions"
|
|
207
|
+
WHERE "UserName" = $1
|
|
208
|
+
`,
|
|
207
209
|
values: [username]
|
|
208
210
|
});
|
|
209
211
|
|
|
210
|
-
const currentSessions = countRes.rows
|
|
212
|
+
const currentSessions = Number(countRes.rows?.[0]?.count ?? 0);
|
|
211
213
|
// If we have MAX_SESSIONS or more, delete oldest to make room for exactly 1 new session
|
|
212
214
|
if (currentSessions >= MAX_SESSIONS) {
|
|
213
215
|
const sessionsToDelete = currentSessions - MAX_SESSIONS + 1; // +1 to make room for new session
|
|
@@ -215,15 +217,15 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
215
217
|
|
|
216
218
|
await dblogin.query({
|
|
217
219
|
name: 'prune-oldest-user-session',
|
|
218
|
-
|
|
220
|
+
// Expired sessions were already removed above; remaining rows are active.
|
|
221
|
+
text: `DELETE FROM "Sessions" WHERE id IN (SELECT id FROM "Sessions" WHERE "UserName" = $1 ORDER BY created_at ASC LIMIT $2)`,
|
|
219
222
|
values: [username, sessionsToDelete]
|
|
220
223
|
});
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
|
|
224
227
|
|
|
225
|
-
// Insert new session record for the user
|
|
226
|
-
const newSessionId = crypto.randomUUID();
|
|
228
|
+
// Insert new session record for the user.
|
|
227
229
|
let dbSessionId;
|
|
228
230
|
try {
|
|
229
231
|
const insertRes = await dblogin.query({
|
|
@@ -237,12 +239,18 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
237
239
|
throw insertErr;
|
|
238
240
|
}
|
|
239
241
|
|
|
240
|
-
// Update last_login
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
// Update last_login and fetch FullName/Image in a single query.
|
|
243
|
+
let profileRow = null;
|
|
244
|
+
try {
|
|
245
|
+
const profUpdateRes = await dblogin.query({
|
|
246
|
+
name: 'login-update-last-login-return-profile',
|
|
247
|
+
text: `UPDATE "Users" SET "last_login" = NOW() WHERE "id" = $1 RETURNING "FullName", "Image"`,
|
|
248
|
+
values: [user.id]
|
|
249
|
+
});
|
|
250
|
+
profileRow = profUpdateRes.rows?.[0] || null;
|
|
251
|
+
} catch (profileUpdateErr) {
|
|
252
|
+
console.error('[mbkauthe] Error updating last_login/returning profile:', profileUpdateErr);
|
|
253
|
+
}
|
|
246
254
|
|
|
247
255
|
req.session.user = {
|
|
248
256
|
id: user.id,
|
|
@@ -255,20 +263,26 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
255
263
|
// Clear profile picture cache to fetch fresh data
|
|
256
264
|
clearProfilePicCache(req, username);
|
|
257
265
|
|
|
258
|
-
//
|
|
266
|
+
// Store FullName/Image in session and cache cookie values.
|
|
259
267
|
let loginProfileImage = null;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
if (profileRow) {
|
|
269
|
+
if (profileRow.FullName) req.session.user.fullname = profileRow.FullName;
|
|
270
|
+
if (typeof profileRow.Image === 'string' && profileRow.Image.trim() !== '') loginProfileImage = profileRow.Image;
|
|
271
|
+
} else {
|
|
272
|
+
// Fallback: try a read query if UPDATE...RETURNING failed unexpectedly.
|
|
273
|
+
try {
|
|
274
|
+
const profileResult = await dblogin.query({
|
|
275
|
+
name: 'login-get-fullname-and-image',
|
|
276
|
+
text: 'SELECT "FullName", "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
|
|
277
|
+
values: [username]
|
|
278
|
+
});
|
|
279
|
+
if (profileResult.rows.length > 0) {
|
|
280
|
+
if (profileResult.rows[0].FullName) req.session.user.fullname = profileResult.rows[0].FullName;
|
|
281
|
+
if (profileResult.rows[0].Image && profileResult.rows[0].Image.trim() !== '') loginProfileImage = profileResult.rows[0].Image;
|
|
282
|
+
}
|
|
283
|
+
} catch (profileErr) {
|
|
284
|
+
console.error("[mbkauthe] Error fetching FullName/Image for user:", profileErr);
|
|
269
285
|
}
|
|
270
|
-
} catch (profileErr) {
|
|
271
|
-
console.error("[mbkauthe] Error fetching FullName/Image for user:", profileErr);
|
|
272
286
|
}
|
|
273
287
|
|
|
274
288
|
if (req.session.preAuthUser) {
|
package/lib/routes/dbLogs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { renderError } from "#response.js";
|
|
3
3
|
import { dblogin } from "#pool.js";
|
|
4
|
+
import { getQueryCount, getQueryLog, resetQueryCount, resetQueryLog } from "../utils/dbQueryLogger.js";
|
|
4
5
|
import { mbkautheVar } from "#config.js";
|
|
5
6
|
import rateLimit from 'express-rate-limit';
|
|
6
7
|
|
|
@@ -39,8 +40,8 @@ router.get(["/db.json"], LogLimit, async (req, res) => {
|
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const queryCount = typeof dblogin.getQueryCount === 'function' ? dblogin.getQueryCount() :
|
|
43
|
-
const queryLog = typeof dblogin.getQueryLog === 'function' ? dblogin.getQueryLog({ limit: queryLimit }) : [];
|
|
43
|
+
const queryCount = typeof getQueryCount === 'function' ? getQueryCount() : (typeof dblogin.getQueryCount === 'function' ? dblogin.getQueryCount() : 0);
|
|
44
|
+
const queryLog = typeof getQueryLog === 'function' ? getQueryLog({ limit: queryLimit }) : (typeof dblogin.getQueryLog === 'function' ? dblogin.getQueryLog({ limit: queryLimit }) : []);
|
|
44
45
|
|
|
45
46
|
return res.json({ queryCount, queryLimit, queryLog, isDev });
|
|
46
47
|
} catch (err) {
|
|
@@ -61,8 +62,11 @@ router.post(["/db/reset"], LogLimit, async (req, res) => {
|
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
if (typeof
|
|
65
|
-
if (typeof dblogin.
|
|
65
|
+
if (typeof resetQueryCount === 'function') resetQueryCount();
|
|
66
|
+
else if (typeof dblogin.resetQueryCount === 'function') dblogin.resetQueryCount();
|
|
67
|
+
|
|
68
|
+
if (typeof resetQueryLog === 'function') resetQueryLog();
|
|
69
|
+
else if (typeof dblogin.resetQueryLog === 'function') dblogin.resetQueryLog();
|
|
66
70
|
|
|
67
71
|
return res.json({ success: true, message: 'Query log and count have been reset.' });
|
|
68
72
|
} catch (err) {
|
package/lib/routes/misc.js
CHANGED
|
@@ -228,7 +228,24 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
|
|
|
228
228
|
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
// Single round-trip: fetch app-session expiry and (if needed) connect-pg-simple expiry.
|
|
232
|
+
const result = await dblogin.query({
|
|
233
|
+
name: 'check-session-validity',
|
|
234
|
+
text: `
|
|
235
|
+
SELECT
|
|
236
|
+
s.expires_at,
|
|
237
|
+
u."Active",
|
|
238
|
+
CASE
|
|
239
|
+
WHEN s.expires_at IS NULL THEN (SELECT expire FROM "session" WHERE sid = $2)
|
|
240
|
+
ELSE NULL
|
|
241
|
+
END AS connect_expire
|
|
242
|
+
FROM "Sessions" s
|
|
243
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
244
|
+
WHERE s.id = $1
|
|
245
|
+
LIMIT 1
|
|
246
|
+
`,
|
|
247
|
+
values: [sessionId, req.sessionID]
|
|
248
|
+
});
|
|
232
249
|
|
|
233
250
|
if (result.rows.length === 0) {
|
|
234
251
|
req.session.destroy(() => { });
|
|
@@ -243,12 +260,9 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
|
|
|
243
260
|
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
244
261
|
}
|
|
245
262
|
|
|
246
|
-
// Determine expiry: prefer application session expiry if present else fallback to connect-pg-simple
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const sessResult = await dblogin.query({ name: 'get-session-expiry', text: 'SELECT expire FROM "session" WHERE sid = $1', values: [req.sessionID] });
|
|
250
|
-
expiry = sessResult.rows.length > 0 && sessResult.rows[0].expire ? new Date(sessResult.rows[0].expire).toISOString() : null;
|
|
251
|
-
}
|
|
263
|
+
// Determine expiry: prefer application session expiry if present else fallback to connect-pg-simple expiry.
|
|
264
|
+
const expirySource = row.expires_at || row.connect_expire || null;
|
|
265
|
+
const expiry = expirySource ? new Date(expirySource).toISOString() : null;
|
|
252
266
|
|
|
253
267
|
return res.status(200).json({ sessionValid: true, expiry });
|
|
254
268
|
} catch (err) {
|