mbkauthe 4.8.4 → 5.0.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 +1 -0
- package/docs/api.md +29 -178
- package/docs/db.md +1 -1
- package/docs/db.sql +305 -253
- package/index.js +6 -3
- package/lib/config/cookies.js +84 -18
- package/lib/config/index.js +3 -1
- package/lib/config/tokenScopes.js +1 -1
- package/lib/createTable.js +95 -8
- package/lib/db/AuthRepository.js +336 -0
- package/lib/db/BaseRepository.js +193 -0
- package/lib/db/dialects/postgres.js +18 -0
- package/lib/main.js +5 -5
- package/lib/middleware/auth.js +213 -259
- package/lib/middleware/index.js +18 -25
- package/lib/middleware/scopeValidator.js +8 -3
- package/lib/pool.js +5 -6
- package/lib/routes/auth.js +95 -169
- package/lib/routes/dbLogs.js +247 -29
- package/lib/routes/misc.js +17 -50
- package/lib/routes/oauth.js +23 -48
- package/lib/utils/dbQueryLogger.js +485 -80
- package/lib/utils/errors.js +1 -1
- package/lib/utils/logger.js +12 -0
- package/lib/utils/timingSafeToken.js +1 -1
- package/package.json +1 -1
- package/public/main.css +36 -3
- package/test.spec.js +515 -48
- package/views/header.handlebars +1 -1
- package/views/pages/2fa.handlebars +9 -5
- package/views/pages/dbLogs.handlebars +618 -420
- package/views/pages/loginmbkauthe.handlebars +42 -25
- package/views/showmessage.handlebars +2 -2
package/lib/routes/dbLogs.js
CHANGED
|
@@ -1,15 +1,213 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
+
import rateLimit from "express-rate-limit";
|
|
2
3
|
import { renderError } from "#response.js";
|
|
3
4
|
import { dblogin } from "#pool.js";
|
|
4
5
|
import { getQueryCount, getQueryLog, resetQueryCount, resetQueryLog } from "../utils/dbQueryLogger.js";
|
|
5
6
|
import { mbkautheVar } from "#config.js";
|
|
6
|
-
import rateLimit from 'express-rate-limit';
|
|
7
7
|
|
|
8
8
|
const router = express.Router();
|
|
9
9
|
|
|
10
10
|
const isDbLogsEnabled = () => process.env.env === "dev" && process.env.dbLogs === "true";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const clampLimit = (value, fallback = 50, max = 500) => {
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
return Math.min(max, Math.floor(parsed));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const normalizeStringFilter = (value) => {
|
|
21
|
+
if (typeof value !== "string") return "";
|
|
22
|
+
return value.trim();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const parseSuccessFilter = (value) => {
|
|
26
|
+
if (value === true || value === "true") return true;
|
|
27
|
+
if (value === false || value === "false") return false;
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getRawQueryLog = () => {
|
|
32
|
+
if (typeof getQueryLog === "function") return getQueryLog();
|
|
33
|
+
if (typeof dblogin.getQueryLog === "function") return dblogin.getQueryLog();
|
|
34
|
+
return [];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const filterQueryLog = (queryLog, filters) => {
|
|
38
|
+
const poolName = normalizeStringFilter(filters.pool);
|
|
39
|
+
const username = normalizeStringFilter(filters.username).toLowerCase();
|
|
40
|
+
const url = normalizeStringFilter(filters.url).toLowerCase();
|
|
41
|
+
const success = parseSuccessFilter(filters.success);
|
|
42
|
+
|
|
43
|
+
return queryLog.filter((entry) => {
|
|
44
|
+
if (poolName && (entry?.pool?.name || "") !== poolName) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (username) {
|
|
49
|
+
const candidate = String(entry?.request?.username || entry?.request?.userId || "").toLowerCase();
|
|
50
|
+
if (!candidate.includes(username)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (url) {
|
|
56
|
+
const candidateUrl = String(entry?.request?.url || "").toLowerCase();
|
|
57
|
+
if (!candidateUrl.includes(url)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (success !== null && Boolean(entry?.success) !== success) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const sortQueryLogNewestFirst = (queryLog) =>
|
|
71
|
+
[...queryLog].sort((a, b) => {
|
|
72
|
+
const left = Date.parse(b?.time || "") || 0;
|
|
73
|
+
const right = Date.parse(a?.time || "") || 0;
|
|
74
|
+
return left - right;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const average = (numbers) => {
|
|
78
|
+
if (!numbers.length) return 0;
|
|
79
|
+
return numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildSummary = (queryLog) => {
|
|
83
|
+
const durations = queryLog
|
|
84
|
+
.map((entry) => Number(entry?.durationMs))
|
|
85
|
+
.filter((value) => Number.isFinite(value));
|
|
86
|
+
const executionDurations = queryLog
|
|
87
|
+
.map((entry) => Number(entry?.executionDurationMs))
|
|
88
|
+
.filter((value) => Number.isFinite(value));
|
|
89
|
+
const waitDurations = queryLog
|
|
90
|
+
.map((entry) => Number(entry?.poolWait?.waitMs))
|
|
91
|
+
.filter((value) => Number.isFinite(value));
|
|
92
|
+
const errorCount = queryLog.filter((entry) => entry?.success === false).length;
|
|
93
|
+
const pressuredQueries = queryLog.filter((entry) => entry?.poolWait?.hadPoolPressure).length;
|
|
94
|
+
|
|
95
|
+
const slowestQueries = [...queryLog]
|
|
96
|
+
.sort((a, b) => (Number(b?.durationMs) || 0) - (Number(a?.durationMs) || 0))
|
|
97
|
+
.slice(0, 5)
|
|
98
|
+
.map((entry) => ({
|
|
99
|
+
time: entry.time,
|
|
100
|
+
query: entry.query,
|
|
101
|
+
name: entry.name,
|
|
102
|
+
fingerprint: entry.fingerprint,
|
|
103
|
+
durationMs: entry.durationMs,
|
|
104
|
+
executionDurationMs: entry.executionDurationMs,
|
|
105
|
+
waitMs: entry.poolWait?.waitMs || 0,
|
|
106
|
+
success: entry.success,
|
|
107
|
+
request: entry.request,
|
|
108
|
+
pool: entry.pool,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const repeatedGroupsMap = new Map();
|
|
112
|
+
for (const entry of queryLog) {
|
|
113
|
+
const key = entry?.fingerprint || entry?.normalizedQuery || entry?.query;
|
|
114
|
+
if (!key) continue;
|
|
115
|
+
|
|
116
|
+
const existing = repeatedGroupsMap.get(key);
|
|
117
|
+
if (existing) {
|
|
118
|
+
existing.count += 1;
|
|
119
|
+
existing.totalDurationMs += Number(entry?.durationMs) || 0;
|
|
120
|
+
existing.totalExecutionDurationMs += Number(entry?.executionDurationMs) || 0;
|
|
121
|
+
existing.totalWaitMs += Number(entry?.poolWait?.waitMs) || 0;
|
|
122
|
+
existing.errorCount += entry?.success === false ? 1 : 0;
|
|
123
|
+
if ((Date.parse(entry?.time || "") || 0) > (Date.parse(existing.lastSeen || "") || 0)) {
|
|
124
|
+
existing.lastSeen = entry.time;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
repeatedGroupsMap.set(key, {
|
|
130
|
+
fingerprint: entry.fingerprint,
|
|
131
|
+
normalizedQuery: entry.normalizedQuery,
|
|
132
|
+
sampleQuery: entry.query,
|
|
133
|
+
sampleName: entry.name,
|
|
134
|
+
poolName: entry?.pool?.name || null,
|
|
135
|
+
requestUrl: entry?.request?.url || null,
|
|
136
|
+
count: 1,
|
|
137
|
+
totalDurationMs: Number(entry?.durationMs) || 0,
|
|
138
|
+
totalExecutionDurationMs: Number(entry?.executionDurationMs) || 0,
|
|
139
|
+
totalWaitMs: Number(entry?.poolWait?.waitMs) || 0,
|
|
140
|
+
errorCount: entry?.success === false ? 1 : 0,
|
|
141
|
+
lastSeen: entry.time,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const repeatedGroups = [...repeatedGroupsMap.values()]
|
|
146
|
+
.filter((group) => group.count > 1)
|
|
147
|
+
.sort((a, b) => {
|
|
148
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
149
|
+
return b.totalDurationMs - a.totalDurationMs;
|
|
150
|
+
})
|
|
151
|
+
.slice(0, 8)
|
|
152
|
+
.map((group) => ({
|
|
153
|
+
fingerprint: group.fingerprint,
|
|
154
|
+
normalizedQuery: group.normalizedQuery,
|
|
155
|
+
sampleQuery: group.sampleQuery,
|
|
156
|
+
sampleName: group.sampleName,
|
|
157
|
+
poolName: group.poolName,
|
|
158
|
+
requestUrl: group.requestUrl,
|
|
159
|
+
count: group.count,
|
|
160
|
+
avgDurationMs: group.totalDurationMs / group.count,
|
|
161
|
+
avgExecutionDurationMs: group.totalExecutionDurationMs / group.count,
|
|
162
|
+
avgWaitMs: group.totalWaitMs / group.count,
|
|
163
|
+
errorCount: group.errorCount,
|
|
164
|
+
lastSeen: group.lastSeen,
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
totalVisible: queryLog.length,
|
|
169
|
+
avgDurationMs: average(durations),
|
|
170
|
+
avgExecutionDurationMs: average(executionDurations),
|
|
171
|
+
avgWaitMs: average(waitDurations),
|
|
172
|
+
errorCount,
|
|
173
|
+
pressuredQueries,
|
|
174
|
+
slowestQueries,
|
|
175
|
+
repeatedGroups,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const buildResponsePayload = (req) => {
|
|
180
|
+
const queryCount = typeof getQueryCount === "function"
|
|
181
|
+
? getQueryCount()
|
|
182
|
+
: typeof dblogin.getQueryCount === "function"
|
|
183
|
+
? dblogin.getQueryCount()
|
|
184
|
+
: 0;
|
|
185
|
+
const queryLimit = clampLimit(req.query.limit);
|
|
186
|
+
const filters = {
|
|
187
|
+
pool: normalizeStringFilter(req.query.pool),
|
|
188
|
+
username: normalizeStringFilter(req.query.username),
|
|
189
|
+
url: normalizeStringFilter(req.query.url),
|
|
190
|
+
success: parseSuccessFilter(req.query.success),
|
|
191
|
+
};
|
|
192
|
+
const filtered = filterQueryLog(getRawQueryLog(), filters);
|
|
193
|
+
const ordered = sortQueryLogNewestFirst(filtered);
|
|
194
|
+
const queryLog = ordered.slice(0, queryLimit);
|
|
195
|
+
const summary = buildSummary(queryLog);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
queryCount,
|
|
199
|
+
queryLimit,
|
|
200
|
+
filters: {
|
|
201
|
+
pool: filters.pool,
|
|
202
|
+
username: filters.username,
|
|
203
|
+
url: filters.url,
|
|
204
|
+
success: filters.success,
|
|
205
|
+
},
|
|
206
|
+
summary,
|
|
207
|
+
queryLog,
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
13
211
|
const LogLimit = rateLimit({
|
|
14
212
|
windowMs: 1 * 60 * 1000,
|
|
15
213
|
max: 50,
|
|
@@ -19,15 +217,14 @@ const LogLimit = rateLimit({
|
|
|
19
217
|
},
|
|
20
218
|
validate: {
|
|
21
219
|
trustProxy: false,
|
|
22
|
-
xForwardedForHeader: false
|
|
23
|
-
}
|
|
220
|
+
xForwardedForHeader: false,
|
|
221
|
+
},
|
|
24
222
|
});
|
|
25
223
|
|
|
26
|
-
// DB stats API (JSON)
|
|
27
224
|
router.get(["/db.json"], LogLimit, async (req, res) => {
|
|
28
225
|
try {
|
|
29
226
|
const isDev = isDbLogsEnabled();
|
|
30
|
-
const queryLimit =
|
|
227
|
+
const queryLimit = clampLimit(req.query.limit);
|
|
31
228
|
|
|
32
229
|
if (!isDev) {
|
|
33
230
|
return res.status(403).json({
|
|
@@ -36,21 +233,33 @@ router.get(["/db.json"], LogLimit, async (req, res) => {
|
|
|
36
233
|
isDev,
|
|
37
234
|
queryCount: 0,
|
|
38
235
|
queryLimit,
|
|
39
|
-
|
|
236
|
+
filters: {
|
|
237
|
+
pool: normalizeStringFilter(req.query.pool),
|
|
238
|
+
username: normalizeStringFilter(req.query.username),
|
|
239
|
+
url: normalizeStringFilter(req.query.url),
|
|
240
|
+
success: parseSuccessFilter(req.query.success),
|
|
241
|
+
},
|
|
242
|
+
summary: {
|
|
243
|
+
totalVisible: 0,
|
|
244
|
+
avgDurationMs: 0,
|
|
245
|
+
avgExecutionDurationMs: 0,
|
|
246
|
+
avgWaitMs: 0,
|
|
247
|
+
errorCount: 0,
|
|
248
|
+
pressuredQueries: 0,
|
|
249
|
+
slowestQueries: [],
|
|
250
|
+
repeatedGroups: [],
|
|
251
|
+
},
|
|
252
|
+
queryLog: [],
|
|
40
253
|
});
|
|
41
254
|
}
|
|
42
255
|
|
|
43
|
-
|
|
44
|
-
const queryLog = typeof getQueryLog === 'function' ? getQueryLog({ limit: queryLimit }) : (typeof dblogin.getQueryLog === 'function' ? dblogin.getQueryLog({ limit: queryLimit }) : []);
|
|
45
|
-
|
|
46
|
-
return res.json({ queryCount, queryLimit, queryLog, isDev });
|
|
256
|
+
return res.json({ ...buildResponsePayload(req), isDev });
|
|
47
257
|
} catch (err) {
|
|
48
|
-
console.error(
|
|
49
|
-
return res.status(500).json({ success: false, message:
|
|
258
|
+
console.error("[mbkauthe] /db.json route error:", err);
|
|
259
|
+
return res.status(500).json({ success: false, message: "Could not fetch DB stats." });
|
|
50
260
|
}
|
|
51
261
|
});
|
|
52
262
|
|
|
53
|
-
// Dedicated reset API for DB logs and counters
|
|
54
263
|
router.post(["/db/reset"], LogLimit, async (req, res) => {
|
|
55
264
|
try {
|
|
56
265
|
const isDev = isDbLogsEnabled();
|
|
@@ -58,39 +267,48 @@ router.post(["/db/reset"], LogLimit, async (req, res) => {
|
|
|
58
267
|
return res.status(403).json({
|
|
59
268
|
success: false,
|
|
60
269
|
message: "DB logs are disabled.",
|
|
61
|
-
isDev
|
|
270
|
+
isDev,
|
|
62
271
|
});
|
|
63
272
|
}
|
|
64
273
|
|
|
65
|
-
if (typeof resetQueryCount ===
|
|
66
|
-
else if (typeof dblogin.resetQueryCount ===
|
|
274
|
+
if (typeof resetQueryCount === "function") resetQueryCount();
|
|
275
|
+
else if (typeof dblogin.resetQueryCount === "function") dblogin.resetQueryCount();
|
|
67
276
|
|
|
68
|
-
if (typeof resetQueryLog ===
|
|
69
|
-
else if (typeof dblogin.resetQueryLog ===
|
|
277
|
+
if (typeof resetQueryLog === "function") resetQueryLog();
|
|
278
|
+
else if (typeof dblogin.resetQueryLog === "function") dblogin.resetQueryLog();
|
|
70
279
|
|
|
71
|
-
return res.json({ success: true, message:
|
|
280
|
+
return res.json({ success: true, message: "Query log and count have been reset." });
|
|
72
281
|
} catch (err) {
|
|
73
|
-
console.error(
|
|
74
|
-
return res.status(500).json({ success: false, message:
|
|
282
|
+
console.error("[mbkauthe] /db/reset route error:", err);
|
|
283
|
+
return res.status(500).json({ success: false, message: "Could not reset DB stats." });
|
|
75
284
|
}
|
|
76
285
|
});
|
|
77
286
|
|
|
78
|
-
// DB stats page (HTML)
|
|
79
287
|
router.get(["/db"], LogLimit, async (req, res) => {
|
|
80
288
|
try {
|
|
81
289
|
const isDev = isDbLogsEnabled();
|
|
82
|
-
const queryLimit =
|
|
83
|
-
const resetDone = req.query.resetDone ===
|
|
84
|
-
|
|
290
|
+
const queryLimit = clampLimit(req.query.limit);
|
|
291
|
+
const resetDone = req.query.resetDone === "1";
|
|
292
|
+
const successFilter = parseSuccessFilter(req.query.success);
|
|
293
|
+
|
|
294
|
+
return res.render("pages/dbLogs.handlebars", {
|
|
85
295
|
layout: false,
|
|
86
296
|
appName: mbkautheVar.APP_NAME,
|
|
87
297
|
queryLimit,
|
|
88
298
|
resetDone,
|
|
89
299
|
isDev,
|
|
90
|
-
|
|
300
|
+
filters: {
|
|
301
|
+
pool: normalizeStringFilter(req.query.pool),
|
|
302
|
+
username: normalizeStringFilter(req.query.username),
|
|
303
|
+
url: normalizeStringFilter(req.query.url),
|
|
304
|
+
successAny: successFilter === null,
|
|
305
|
+
successTrue: successFilter === true,
|
|
306
|
+
successFalse: successFilter === false,
|
|
307
|
+
},
|
|
308
|
+
disabledMessage: isDev ? null : "DB logs are disabled.",
|
|
91
309
|
});
|
|
92
310
|
} catch (err) {
|
|
93
|
-
console.error(
|
|
311
|
+
console.error("[mbkauthe] /db route error:", err);
|
|
94
312
|
return renderError(res, req, {
|
|
95
313
|
layout: false,
|
|
96
314
|
code: 500,
|
|
@@ -102,4 +320,4 @@ router.get(["/db"], LogLimit, async (req, res) => {
|
|
|
102
320
|
}
|
|
103
321
|
});
|
|
104
322
|
|
|
105
|
-
export default router;
|
|
323
|
+
export default router;
|
package/lib/routes/misc.js
CHANGED
|
@@ -7,10 +7,12 @@ import { authenticate, sessVal, sessRole } 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, cachedCookieOptions } from "#cookies.js";
|
|
10
|
+
import { AuthRepository } from "../db/AuthRepository.js";
|
|
10
11
|
import { fileURLToPath } from "url";
|
|
11
12
|
import path from "path";
|
|
12
13
|
import fs from "fs";
|
|
13
14
|
import dotenv from "dotenv";
|
|
15
|
+
import { createLogger } from "../utils/logger.js";
|
|
14
16
|
|
|
15
17
|
dotenv.config();
|
|
16
18
|
|
|
@@ -18,6 +20,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
const router = express.Router();
|
|
23
|
+
const authRepo = new AuthRepository({ db: dblogin });
|
|
24
|
+
const logMisc = createLogger("misc");
|
|
21
25
|
// Rate limiter for info/test routes
|
|
22
26
|
const LoginLimit = rateLimit({
|
|
23
27
|
windowMs: 1 * 60 * 1000,
|
|
@@ -100,14 +104,10 @@ router.get('/user/profilepic', async (req, res) => {
|
|
|
100
104
|
|
|
101
105
|
// If not in cache, fetch from DB
|
|
102
106
|
if (!imageUrl) {
|
|
103
|
-
const
|
|
104
|
-
name: 'get-user-profile-pic',
|
|
105
|
-
text: 'SELECT "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
|
|
106
|
-
values: [username]
|
|
107
|
-
});
|
|
107
|
+
const profile = await authRepo.getUserImageByUsername(username, 'get-user-profile-pic');
|
|
108
108
|
|
|
109
|
-
if (
|
|
110
|
-
imageUrl =
|
|
109
|
+
if (profile && profile.Image && profile.Image.trim() !== '') {
|
|
110
|
+
imageUrl = profile.Image;
|
|
111
111
|
} else {
|
|
112
112
|
imageUrl = 'default';
|
|
113
113
|
}
|
|
@@ -229,31 +229,13 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// Single round-trip: fetch app-session expiry and (if needed) connect-pg-simple expiry.
|
|
232
|
-
const
|
|
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
|
+
const row = await authRepo.getSessionValidity(sessionId, req.sessionID, 'check-session-validity');
|
|
249
233
|
|
|
250
|
-
if (
|
|
234
|
+
if (!row) {
|
|
251
235
|
req.session.destroy(() => { });
|
|
252
236
|
clearSessionCookies(res);
|
|
253
237
|
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
254
238
|
}
|
|
255
|
-
|
|
256
|
-
const row = result.rows[0];
|
|
257
239
|
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
258
240
|
req.session.destroy(() => { });
|
|
259
241
|
clearSessionCookies(res);
|
|
@@ -298,17 +280,8 @@ function normalizeSessionIdFromBody(body = {}) {
|
|
|
298
280
|
}
|
|
299
281
|
|
|
300
282
|
async function getSessionValidationRow(sessionId, queryName = 'check-session-validity-by-id') {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
text: `SELECT s.expires_at, u."Active", u."UserName", u."Role" FROM "Sessions" s JOIN "Users" u ON s."UserName" = u."UserName" WHERE s.id = $1 LIMIT 1`,
|
|
304
|
-
values: [sessionId]
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
if (result.rows.length === 0) {
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return result.rows[0];
|
|
283
|
+
const row = await authRepo.getSessionValidationRow(sessionId, queryName);
|
|
284
|
+
return row || null;
|
|
312
285
|
}
|
|
313
286
|
|
|
314
287
|
function isSessionRowValid(row) {
|
|
@@ -501,9 +474,9 @@ export async function checkVersion() {
|
|
|
501
474
|
if (hasValidLatest && latestVersion !== packageJson.version) {
|
|
502
475
|
console.warn(`[mbkauthe] Current version (${packageJson.version}) is outdated. Latest version: ${latestVersion}. Consider updating mbkauthe.`);
|
|
503
476
|
} else if (hasValidLatest) {
|
|
504
|
-
|
|
477
|
+
logMisc(`Running latest version (${packageJson.version}).`);
|
|
505
478
|
} else {
|
|
506
|
-
|
|
479
|
+
logMisc(`Skipped version check warning: latest version unavailable.`);
|
|
507
480
|
}
|
|
508
481
|
} catch (error) {
|
|
509
482
|
console.warn(`[mbkauthe] Failed to check for updates: ${error.message}`);
|
|
@@ -567,25 +540,19 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
|
|
|
567
540
|
try {
|
|
568
541
|
// Run both operations in parallel for better performance
|
|
569
542
|
await Promise.all([
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
text: 'DELETE FROM "Sessions"'
|
|
573
|
-
}),
|
|
574
|
-
dblogin.query({
|
|
575
|
-
name: 'terminate-all-db-sessions',
|
|
576
|
-
text: 'DELETE FROM "session" WHERE expire > NOW()'
|
|
577
|
-
})
|
|
543
|
+
authRepo.deleteAllAppSessions('terminate-all-app-sessions'),
|
|
544
|
+
authRepo.deleteActiveSessionStoreRows('terminate-all-db-sessions')
|
|
578
545
|
]);
|
|
579
546
|
|
|
580
547
|
req.session.destroy((err) => {
|
|
581
548
|
if (err) {
|
|
582
|
-
console.
|
|
549
|
+
console.error(`[mbkauthe] Error destroying session:`, err);
|
|
583
550
|
return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
|
|
584
551
|
}
|
|
585
552
|
|
|
586
553
|
clearSessionCookies(res);
|
|
587
554
|
|
|
588
|
-
|
|
555
|
+
logMisc(`All sessions terminated successfully`);
|
|
589
556
|
res.status(200).json({
|
|
590
557
|
success: true,
|
|
591
558
|
message: "All sessions terminated successfully",
|
package/lib/routes/oauth.js
CHANGED
|
@@ -8,8 +8,12 @@ import { dblogin } from "#pool.js";
|
|
|
8
8
|
import { mbkautheVar } from "#config.js";
|
|
9
9
|
import { renderError } from "../utils/response.js";
|
|
10
10
|
import { checkTrustedDevice, completeLoginProcess } from "./auth.js";
|
|
11
|
+
import { AuthRepository } from "../db/AuthRepository.js";
|
|
12
|
+
import { createLogger } from "../utils/logger.js";
|
|
11
13
|
|
|
12
14
|
const router = express.Router();
|
|
15
|
+
const authRepo = new AuthRepository({ db: dblogin });
|
|
16
|
+
const logOAuth = createLogger("oauth");
|
|
13
17
|
|
|
14
18
|
// CSRF protection middleware
|
|
15
19
|
const csrfProtection = csurf({ cookie: true });
|
|
@@ -39,28 +43,19 @@ const githubClientSecret = mbkautheVar.GITHUB_APP_CLIENT_SECRET || mbkautheVar.G
|
|
|
39
43
|
// Common OAuth strategy handler
|
|
40
44
|
const createOAuthStrategy = async (provider, profile, done) => {
|
|
41
45
|
try {
|
|
42
|
-
|
|
46
|
+
logOAuth(`${provider} OAuth callback for user: ${profile.emails?.[0]?.value || profile.id}`);
|
|
43
47
|
|
|
44
48
|
const isGitHub = provider === 'GitHub';
|
|
45
|
-
const tableName = isGitHub ? 'user_github' : 'user_google';
|
|
46
|
-
const idField = isGitHub ? 'github_id' : 'google_id';
|
|
47
|
-
const queryName = isGitHub ? 'github-login-get-user' : 'google-login-get-user';
|
|
48
49
|
|
|
49
50
|
// Check if this OAuth account is linked to any user
|
|
50
|
-
const
|
|
51
|
-
name: queryName,
|
|
52
|
-
text: `SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id" FROM ${tableName} ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.${idField} = $1`,
|
|
53
|
-
values: [profile.id]
|
|
54
|
-
});
|
|
51
|
+
const user = await authRepo.getOAuthUserByProviderId(provider, profile.id);
|
|
55
52
|
|
|
56
|
-
if (
|
|
53
|
+
if (!user) {
|
|
57
54
|
const error = new Error(`${provider} account not linked to any user`);
|
|
58
55
|
error.code = `${provider.toUpperCase()}_NOT_LINKED`;
|
|
59
56
|
return done(error);
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
const user = oauthUser.rows[0];
|
|
63
|
-
|
|
64
59
|
// Check if the user account is active
|
|
65
60
|
if (!user.Active) {
|
|
66
61
|
const error = new Error('Account is inactive');
|
|
@@ -83,6 +78,8 @@ const createOAuthStrategy = async (provider, profile, done) => {
|
|
|
83
78
|
id: user.id,
|
|
84
79
|
username: user.UserName,
|
|
85
80
|
role: user.Role,
|
|
81
|
+
allowedApps: user.AllowedApps,
|
|
82
|
+
TwoFAStatus: user.TwoFAStatus,
|
|
86
83
|
};
|
|
87
84
|
|
|
88
85
|
if (isGitHub) {
|
|
@@ -149,7 +146,7 @@ if ((mbkautheVar.GOOGLE_LOGIN_ENABLED || "").toLowerCase() === "true") {
|
|
|
149
146
|
|
|
150
147
|
// Print consolidated OAuth summary
|
|
151
148
|
if (enabledProviders.length > 0) {
|
|
152
|
-
|
|
149
|
+
logOAuth(`Social providers: ${enabledProviders.join(', ')}`);
|
|
153
150
|
}
|
|
154
151
|
|
|
155
152
|
// Serialize/Deserialize user for OAuth login
|
|
@@ -182,7 +179,7 @@ const createOAuthInitiation = (provider, enabledFlag, clientIdFlag, clientSecret
|
|
|
182
179
|
// Store CSRF token for validation on callback
|
|
183
180
|
const csrfToken = req.csrfToken();
|
|
184
181
|
req.session.oauthCsrfToken = csrfToken;
|
|
185
|
-
|
|
182
|
+
logOAuth(`${provider} OAuth initiation started`);
|
|
186
183
|
|
|
187
184
|
// Store redirect parameter in session before OAuth flow
|
|
188
185
|
const redirect = req.query.redirect;
|
|
@@ -207,7 +204,7 @@ const createOAuthInitiation = (provider, enabledFlag, clientIdFlag, clientSecret
|
|
|
207
204
|
pagename: 'Login'
|
|
208
205
|
});
|
|
209
206
|
}
|
|
210
|
-
|
|
207
|
+
logOAuth(`${provider} OAuth session saved successfully`);
|
|
211
208
|
passport.authenticate(`${provider.toLowerCase()}-login`, { state: csrfToken })(req, res, next);
|
|
212
209
|
});
|
|
213
210
|
} else {
|
|
@@ -299,39 +296,11 @@ const validateOAuthCallback = (req, res) => {
|
|
|
299
296
|
return true;
|
|
300
297
|
};
|
|
301
298
|
|
|
302
|
-
const finishProviderLogin = async (req, res, provider,
|
|
303
|
-
const userQuery = `
|
|
304
|
-
SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps",
|
|
305
|
-
tfa."TwoFAStatus"
|
|
306
|
-
FROM "Users" u
|
|
307
|
-
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
308
|
-
WHERE u."UserName" = $1
|
|
309
|
-
`;
|
|
310
|
-
|
|
311
|
-
const userResult = await dblogin.query({
|
|
312
|
-
name: `${provider.toLowerCase()}-callback-get-user`,
|
|
313
|
-
text: userQuery,
|
|
314
|
-
values: [username]
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
if (userResult.rows.length === 0) {
|
|
318
|
-
console.error(`[mbkauthe] ${provider} login: User not found: ${username}`);
|
|
319
|
-
return renderError(res, req, {
|
|
320
|
-
code: 404,
|
|
321
|
-
error: 'User Not Found',
|
|
322
|
-
message: `Your ${provider} account is linked, but the user account no longer exists in our system.`,
|
|
323
|
-
page: '/mbkauthe/login',
|
|
324
|
-
pagename: 'Login',
|
|
325
|
-
details: `${provider} identifier: ${detailValue}\nPlease contact your administrator.`
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const user = userResult.rows[0];
|
|
330
|
-
|
|
299
|
+
const finishProviderLogin = async (req, res, provider, user, detailValue = '') => {
|
|
331
300
|
// Check for trusted device
|
|
332
301
|
const trustedDeviceUser = await checkTrustedDevice(req, user.UserName);
|
|
333
302
|
if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
334
|
-
|
|
303
|
+
logOAuth(`${provider} trusted device login for user: ${user.UserName}, skipping 2FA only`);
|
|
335
304
|
return await handleOAuthRedirect(req, res, user, 'trusted', provider.toLowerCase());
|
|
336
305
|
}
|
|
337
306
|
|
|
@@ -347,7 +316,7 @@ const finishProviderLogin = async (req, res, provider, username, detailValue = '
|
|
|
347
316
|
loginMethod: provider.toLowerCase(),
|
|
348
317
|
redirectUrl: oauthRedirect || null
|
|
349
318
|
};
|
|
350
|
-
|
|
319
|
+
logOAuth(`${provider} login: 2FA required for user: ${user.UserName}`);
|
|
351
320
|
return res.redirect('/mbkauthe/2fa');
|
|
352
321
|
}
|
|
353
322
|
|
|
@@ -394,7 +363,13 @@ const createOAuthCallback = (provider, strategy) => {
|
|
|
394
363
|
req,
|
|
395
364
|
res,
|
|
396
365
|
provider,
|
|
397
|
-
|
|
366
|
+
{
|
|
367
|
+
id: oauthUser.id,
|
|
368
|
+
UserName: oauthUser.username,
|
|
369
|
+
Role: oauthUser.role,
|
|
370
|
+
AllowedApps: oauthUser.allowedApps,
|
|
371
|
+
TwoFAStatus: oauthUser.TwoFAStatus
|
|
372
|
+
},
|
|
398
373
|
provider === 'GitHub' ? (oauthUser.githubUsername || oauthUser.username) : (oauthUser.googleEmail || oauthUser.username)
|
|
399
374
|
);
|
|
400
375
|
} catch (err) {
|
|
@@ -436,7 +411,7 @@ const handleOAuthRedirect = async (req, res, user, type, method = null) => {
|
|
|
436
411
|
res.json = function (data) {
|
|
437
412
|
if (data.success && statusCode === 200) {
|
|
438
413
|
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
439
|
-
|
|
414
|
+
logOAuth(`${method || 'social'} ${type} login: Redirecting to ${redirectUrl}`);
|
|
440
415
|
res.json = originalJson;
|
|
441
416
|
res.status = originalStatus;
|
|
442
417
|
return res.redirect(redirectUrl);
|