mbkauthe 2.0.1 → 2.2.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/lib/main.js CHANGED
@@ -14,25 +14,44 @@ import speakeasy from "speakeasy";
14
14
  import passport from 'passport';
15
15
  import GitHubStrategy from 'passport-github2';
16
16
 
17
- import { createRequire } from "module";
17
+ import { fileURLToPath } from "url";
18
18
  import fs from "fs";
19
19
  import path from "path";
20
-
21
- import dotenv from "dotenv";
22
- dotenv.config();
23
- const mbkautheVar = JSON.parse(process.env.mbkautheVar);
20
+ import { mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS } from "./config.js";
24
21
 
25
22
  const router = express.Router();
26
23
 
27
- const require = createRequire(import.meta.url);
28
- const packageJson = require("../package.json");
29
-
30
24
  router.use(express.json());
31
25
  router.use(express.urlencoded({ extended: true }));
32
26
  router.use(cookieParser());
33
27
 
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+
34
30
  router.get('/mbkauthe/main.js', (req, res) => {
35
- res.sendFile(path.join(process.cwd(), 'public', 'main.js'));
31
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
32
+ res.sendFile(path.join(__dirname, '..', 'public', 'main.js'));
33
+ });
34
+
35
+ router.get('/icon.svg', (req, res) => {
36
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
37
+ res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
38
+ });
39
+
40
+ router.get(['/favicon.ico','/icon.ico'], (req, res) => {
41
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
42
+ res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
43
+ });
44
+
45
+ router.get("/mbkauthe/bg.webp", (req, res) => {
46
+ const imgPath = path.join(__dirname, "..", "public", "bg.webp");
47
+ res.setHeader('Content-Type', 'image/webp');
48
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
49
+ const stream = fs.createReadStream(imgPath);
50
+ stream.on('error', (err) => {
51
+ console.error('[mbkauthe] Error streaming bg.webp:', err);
52
+ res.status(404).send('Image not found');
53
+ });
54
+ stream.pipe(res);
36
55
  });
37
56
 
38
57
  // CSRF protection middleware
@@ -113,14 +132,16 @@ router.use(session(sessionConfig));
113
132
  router.use(async (req, res, next) => {
114
133
  // Only restore session if not already present and sessionId cookie exists
115
134
  if (!req.session.user && req.cookies.sessionId) {
135
+ const sessionId = req.cookies.sessionId;
136
+
137
+ // Early validation to avoid unnecessary processing
138
+ if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
139
+ // Clear invalid cookie to prevent repeated attempts
140
+ res.clearCookie('sessionId', cachedClearCookieOptions);
141
+ return next();
142
+ }
143
+
116
144
  try {
117
- const sessionId = req.cookies.sessionId;
118
-
119
- // Validate sessionId format (should be 64 hex characters) and normalize to lowercase
120
- if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
121
- console.warn("[mbkauthe] Invalid sessionId format detected");
122
- return next();
123
- }
124
145
 
125
146
  const normalizedSessionId = sessionId.toLowerCase();
126
147
 
@@ -136,6 +157,7 @@ router.use(async (req, res, next) => {
136
157
  role: user.Role,
137
158
  Role: user.Role,
138
159
  sessionId: normalizedSessionId,
160
+ allowedApps: user.AllowedApps,
139
161
  };
140
162
  }
141
163
  } catch (err) {
@@ -145,23 +167,6 @@ router.use(async (req, res, next) => {
145
167
  next();
146
168
  });
147
169
 
148
- const getCookieOptions = () => ({
149
- maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
150
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
151
- secure: mbkautheVar.IS_DEPLOYED === 'true',
152
- sameSite: 'lax',
153
- path: '/',
154
- httpOnly: true
155
- });
156
-
157
- const getClearCookieOptions = () => ({
158
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
159
- secure: mbkautheVar.IS_DEPLOYED === 'true',
160
- sameSite: 'lax',
161
- path: '/',
162
- httpOnly: true
163
- });
164
-
165
170
  router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
166
171
  if (req.session?.user) {
167
172
  return res.send(`
@@ -175,7 +180,7 @@ router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
175
180
  }
176
181
  });
177
182
 
178
- async function completeLoginProcess(req, res, user, redirectUrl = null) {
183
+ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
179
184
  try {
180
185
  // Ensure both username formats are available for compatibility
181
186
  const username = user.username || user.UserName;
@@ -196,19 +201,21 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
196
201
  });
197
202
  });
198
203
 
199
- // Delete all old sessions for this user from session table
200
- await dblogin.query({
201
- name: 'login-delete-old-user-sessions',
202
- text: 'DELETE FROM "session" WHERE sess::text LIKE $1',
203
- values: [`%"username":"${username}"%`]
204
- });
205
-
206
- await dblogin.query({
207
- name: 'login-update-session-id', text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, values: [
208
- sessionId,
209
- user.id,
210
- ]
211
- });
204
+ // Run both queries in parallel for better performance
205
+ await Promise.all([
206
+ // Delete old sessions using indexed lookup on sess->'user'->>'id'
207
+ dblogin.query({
208
+ name: 'login-delete-old-user-sessions',
209
+ text: 'DELETE FROM "session" WHERE (sess->\'user\'->>\'id\')::int = $1',
210
+ values: [user.id]
211
+ }),
212
+ // Update session ID in Users table
213
+ dblogin.query({
214
+ name: 'login-update-session-id',
215
+ text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`,
216
+ values: [sessionId, user.id]
217
+ })
218
+ ]);
212
219
 
213
220
  req.session.user = {
214
221
  id: user.id,
@@ -217,6 +224,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
217
224
  role: user.role || user.Role,
218
225
  Role: user.role || user.Role,
219
226
  sessionId,
227
+ allowedApps: user.allowedApps || user.AllowedApps,
220
228
  };
221
229
 
222
230
  if (req.session.preAuthUser) {
@@ -231,8 +239,33 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
231
239
  // avoid writing back into the session table here to reduce DB writes;
232
240
  // the pg session store will already persist the session data.
233
241
 
234
- const cookieOptions = getCookieOptions();
235
- res.cookie("sessionId", sessionId, cookieOptions);
242
+ res.cookie("sessionId", sessionId, cachedCookieOptions);
243
+
244
+ // Handle trusted device if requested
245
+ if (trustDevice) {
246
+ try {
247
+ const deviceToken = generateDeviceToken();
248
+ const deviceName = req.headers['user-agent'] ?
249
+ req.headers['user-agent'].substring(0, 255) : 'Unknown Device';
250
+ const userAgent = req.headers['user-agent'] || 'Unknown';
251
+ const ipAddress = req.ip || req.connection.remoteAddress || 'Unknown';
252
+ const expiresAt = new Date(Date.now() + DEVICE_TRUST_DURATION_MS);
253
+
254
+ await dblogin.query({
255
+ name: 'insert-trusted-device',
256
+ text: `INSERT INTO "TrustedDevices" ("UserName", "DeviceToken", "DeviceName", "UserAgent", "IpAddress", "ExpiresAt")
257
+ VALUES ($1, $2, $3, $4, $5, $6)`,
258
+ values: [username, deviceToken, deviceName, userAgent, ipAddress, expiresAt]
259
+ });
260
+
261
+ res.cookie("device_token", deviceToken, getDeviceTokenCookieOptions());
262
+ console.log(`[mbkauthe] Trusted device token created for user: ${username}`);
263
+ } catch (deviceErr) {
264
+ console.error("[mbkauthe] Error creating trusted device:", deviceErr);
265
+ // Continue with login even if device trust fails
266
+ }
267
+ }
268
+
236
269
  console.log(`[mbkauthe] User "${username}" logged in successfully`);
237
270
 
238
271
  const responsePayload = {
@@ -255,11 +288,10 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
255
288
 
256
289
  router.use(async (req, res, next) => {
257
290
  if (req.session && req.session.user) {
258
- const cookieOptions = getCookieOptions();
259
291
  // Only set cookies if they're missing or different
260
292
  if (req.cookies.sessionId !== req.session.user.sessionId) {
261
- res.cookie("username", req.session.user.username, { ...cookieOptions, httpOnly: false });
262
- res.cookie("sessionId", req.session.user.sessionId, cookieOptions);
293
+ res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
294
+ res.cookie("sessionId", req.session.user.sessionId, cachedCookieOptions);
263
295
  }
264
296
  }
265
297
  next();
@@ -267,8 +299,11 @@ router.use(async (req, res, next) => {
267
299
 
268
300
  router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
269
301
  try {
270
- await dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` });
271
- await dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' });
302
+ // Run both operations in parallel for better performance
303
+ await Promise.all([
304
+ dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` }),
305
+ dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' })
306
+ ]);
272
307
 
273
308
  req.session.destroy((err) => {
274
309
  if (err) {
@@ -276,10 +311,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
276
311
  return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
277
312
  }
278
313
 
279
- const cookieOptions = getClearCookieOptions();
280
- res.clearCookie("mbkauthe.sid", cookieOptions);
281
- res.clearCookie("sessionId", cookieOptions);
282
- res.clearCookie("username", cookieOptions);
314
+ clearSessionCookies(res);
283
315
 
284
316
  console.log("[mbkauthe] All sessions terminated successfully");
285
317
  res.status(200).json({
@@ -330,7 +362,84 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
330
362
  const trimmedUsername = username.trim();
331
363
 
332
364
  try {
333
- const userQuery = `SELECT id, "UserName", "Password", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
365
+ // Check for trusted device token first
366
+ const deviceToken = req.cookies.device_token;
367
+ if (deviceToken && typeof deviceToken === 'string') {
368
+ try {
369
+ const deviceQuery = `
370
+ SELECT td."UserName", td."LastUsed", td."ExpiresAt", u."id", u."Password", u."Active", u."Role", u."AllowedApps"
371
+ FROM "TrustedDevices" td
372
+ JOIN "Users" u ON td."UserName" = u."UserName"
373
+ WHERE td."DeviceToken" = $1 AND td."UserName" = $2 AND td."ExpiresAt" > NOW()
374
+ `;
375
+ const deviceResult = await dblogin.query({
376
+ name: 'login-check-trusted-device',
377
+ text: deviceQuery,
378
+ values: [deviceToken, trimmedUsername]
379
+ });
380
+
381
+ if (deviceResult.rows.length > 0) {
382
+ const deviceUser = deviceResult.rows[0];
383
+
384
+ // Validate password even with trusted device
385
+ let passwordValid = false;
386
+ if (mbkautheVar.EncryptedPassword === "true") {
387
+ passwordValid = await bcrypt.compare(password, deviceUser.Password);
388
+ } else {
389
+ passwordValid = deviceUser.Password === password;
390
+ }
391
+
392
+ if (!passwordValid) {
393
+ console.log("[mbkauthe] Login failed: invalid credentials (trusted device)");
394
+ return res.status(401).json({ success: false, message: "Invalid credentials" });
395
+ }
396
+
397
+ if (!deviceUser.Active) {
398
+ console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
399
+ return res.status(403).json({ success: false, message: "Account is inactive" });
400
+ }
401
+
402
+ if (deviceUser.Role !== "SuperAdmin") {
403
+ const allowedApps = deviceUser.AllowedApps;
404
+ if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
405
+ console.warn(`[mbkauthe] User \"${trimmedUsername}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
406
+ return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
407
+ }
408
+ }
409
+
410
+ // Update last used timestamp
411
+ await dblogin.query({
412
+ name: 'login-update-device-last-used',
413
+ text: 'UPDATE "TrustedDevices" SET "LastUsed" = NOW() WHERE "DeviceToken" = $1',
414
+ values: [deviceToken]
415
+ });
416
+
417
+ console.log(`[mbkauthe] Trusted device login for user: ${trimmedUsername}, skipping 2FA`);
418
+
419
+ // Skip 2FA and complete login
420
+ const userForSession = {
421
+ id: deviceUser.id,
422
+ username: trimmedUsername,
423
+ role: deviceUser.Role,
424
+ Role: deviceUser.Role,
425
+ allowedApps: deviceUser.AllowedApps,
426
+ };
427
+ return await completeLoginProcess(req, res, userForSession);
428
+ }
429
+ } catch (deviceErr) {
430
+ console.error("[mbkauthe] Error checking trusted device:", deviceErr);
431
+ // Continue with normal login flow if device check fails
432
+ }
433
+ }
434
+
435
+ // Combined query: fetch user data and 2FA status in one query
436
+ const userQuery = `
437
+ SELECT u.id, u."UserName", u."Password", u."Active", u."Role", u."AllowedApps",
438
+ tfa."TwoFAStatus"
439
+ FROM "Users" u
440
+ LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
441
+ WHERE u."UserName" = $1
442
+ `;
334
443
  const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
335
444
 
336
445
  if (userResult.rows.length === 0) {
@@ -378,21 +487,16 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
378
487
  }
379
488
  }
380
489
 
381
- if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
382
- const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
383
- const twoFAResult = await dblogin.query({ name: 'login-check-2fa-status', text: query, values: [trimmedUsername] });
384
-
385
- if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
386
- // 2FA is enabled, prompt for token on a separate page
387
- req.session.preAuthUser = {
388
- id: user.id,
389
- username: user.UserName,
390
- role: user.Role,
391
- Role: user.Role,
392
- };
393
- console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
394
- return res.json({ success: true, twoFactorRequired: true });
395
- }
490
+ if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
491
+ // 2FA is enabled, prompt for token on a separate page
492
+ req.session.preAuthUser = {
493
+ id: user.id,
494
+ username: user.UserName,
495
+ role: user.Role,
496
+ Role: user.Role,
497
+ };
498
+ console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
499
+ return res.json({ success: true, twoFactorRequired: true });
396
500
  }
397
501
 
398
502
  // If 2FA is not enabled, proceed with login
@@ -401,6 +505,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
401
505
  username: user.UserName,
402
506
  role: user.Role,
403
507
  Role: user.Role,
508
+ allowedApps: user.AllowedApps,
404
509
  };
405
510
  await completeLoginProcess(req, res, userForSession);
406
511
 
@@ -416,9 +521,10 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
416
521
  }
417
522
  res.render("2fa.handlebars", {
418
523
  layout: false,
419
- customURL: mbkautheVar.loginRedirectURL || '/home',
524
+ customURL: mbkautheVar.loginRedirectURL || '/dashboard',
420
525
  csrfToken: req.csrfToken(),
421
526
  appName: mbkautheVar.APP_NAME.toLowerCase(),
527
+ DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
422
528
  });
423
529
  });
424
530
 
@@ -427,7 +533,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
427
533
  return res.status(401).json({ success: false, message: "Not authorized. Please login first." });
428
534
  }
429
535
 
430
- const { token } = req.body;
536
+ const { token, trustDevice } = req.body;
431
537
  const { username, id, role } = req.session.preAuthUser;
432
538
 
433
539
  // Validate 2FA token
@@ -441,8 +547,11 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
441
547
  return res.status(400).json({ success: false, message: "Invalid 2FA token format" });
442
548
  }
443
549
 
550
+ // Validate trustDevice parameter if provided
551
+ const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
552
+
444
553
  try {
445
- const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
554
+ const query = `SELECT tfa."TwoFASecret", u."AllowedApps" FROM "TwoFA" tfa JOIN "Users" u ON tfa."UserName" = u."UserName" WHERE tfa."UserName" = $1`;
446
555
  const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
447
556
 
448
557
  if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
@@ -450,6 +559,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
450
559
  }
451
560
 
452
561
  const sharedSecret = twoFAResult.rows[0].TwoFASecret;
562
+ const allowedApps = twoFAResult.rows[0].AllowedApps;
453
563
  const tokenValidates = speakeasy.totp.verify({
454
564
  secret: sharedSecret,
455
565
  encoding: "base32",
@@ -462,10 +572,10 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
462
572
  return res.status(401).json({ success: false, message: "Invalid 2FA code" });
463
573
  }
464
574
 
465
- // 2FA successful, complete login
466
- const userForSession = { id, username, role };
467
- const redirectUrl = mbkautheVar.loginRedirectURL || '/home';
468
- await completeLoginProcess(req, res, userForSession, redirectUrl);
575
+ // 2FA successful, complete login with optional device trust
576
+ const userForSession = { id, username, role, allowedApps };
577
+ const redirectUrl = mbkautheVar.loginRedirectURL || '/dashboard';
578
+ await completeLoginProcess(req, res, userForSession, redirectUrl, shouldTrustDevice);
469
579
 
470
580
  } catch (err) {
471
581
  console.error("[mbkauthe] Error during 2FA verification:", err);
@@ -478,11 +588,18 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
478
588
  try {
479
589
  const { id, username } = req.session.user;
480
590
 
481
- await dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] });
482
-
591
+ // Run both database operations in parallel
592
+ const operations = [
593
+ dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
594
+ ];
595
+
483
596
  if (req.sessionID) {
484
- await dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] });
597
+ operations.push(
598
+ dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
599
+ );
485
600
  }
601
+
602
+ await Promise.all(operations);
486
603
 
487
604
  req.session.destroy((err) => {
488
605
  if (err) {
@@ -490,10 +607,7 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
490
607
  return res.status(500).json({ success: false, message: "Logout failed" });
491
608
  }
492
609
 
493
- const cookieOptions = getClearCookieOptions();
494
- res.clearCookie("mbkauthe.sid", cookieOptions);
495
- res.clearCookie("sessionId", cookieOptions);
496
- res.clearCookie("username", cookieOptions);
610
+ clearSessionCookies(res);
497
611
 
498
612
  console.log(`[mbkauthe] User "${username}" logged out successfully`);
499
613
  res.status(200).json({ success: true, message: "Logout successful" });
@@ -511,7 +625,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
511
625
  return res.render("loginmbkauthe.handlebars", {
512
626
  layout: false,
513
627
  githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
514
- customURL: mbkautheVar.loginRedirectURL || '/home',
628
+ customURL: mbkautheVar.loginRedirectURL || '/dashboard',
515
629
  userLoggedIn: !!req.session?.user,
516
630
  username: req.session?.user?.username || '',
517
631
  version: packageJson.version,
@@ -535,10 +649,6 @@ async function getLatestVersion() {
535
649
  }
536
650
  }
537
651
 
538
- router.get("/mbkauthe/bg.avif", (req, res) => {
539
- res.sendFile("bg.avif", { root: path.join(process.cwd(), "public") });
540
- });
541
-
542
652
  router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
543
653
  let latestVersion;
544
654
  const parameters = req.query;
@@ -666,16 +776,13 @@ router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
666
776
  passport.authenticate('github-login')(req, res, next);
667
777
  }
668
778
  else {
669
- res.status(403).render('Error/dError.handlebars', {
670
- layout: false,
779
+ return res.status(403).send(renderError(res, {
671
780
  code: '403',
672
781
  error: 'GitHub Login Disabled',
673
782
  message: 'GitHub login is currently disabled. Please use your username and password to log in.',
674
783
  page: '/mbkauthe/login',
675
784
  pagename: 'Login',
676
- version: packageJson.version,
677
- app: mbkautheVar.APP_NAME
678
- });
785
+ }).render());
679
786
  }
680
787
  });
681
788
 
@@ -693,68 +800,53 @@ router.get('/mbkauthe/api/github/login/callback',
693
800
  // Map error codes to user-friendly messages
694
801
  switch (err.code) {
695
802
  case 'GITHUB_NOT_LINKED':
696
- return res.status(403).render('Error/dError.handlebars', {
697
- layout: false,
803
+ return res.status(403).send(renderError(res, {
698
804
  code: '403',
699
805
  error: 'GitHub Account Not Linked',
700
806
  message: 'Your GitHub account is not linked to any user in our system. To link your GitHub account, a User must connect their GitHub account to mbktech account through the user settings.',
701
807
  page: '/mbkauthe/login',
702
- pagename: 'Login',
703
- version: packageJson.version,
704
- app: mbkautheVar.APP_NAME
705
- });
808
+ pagename: 'Login'
809
+ }).render());
706
810
 
707
811
  case 'ACCOUNT_INACTIVE':
708
- return res.status(403).render('Error/dError.handlebars', {
709
- layout: false,
812
+ return res.status(403).send(renderError(res, {
710
813
  code: '403',
711
814
  error: 'Account Inactive',
712
815
  message: 'Your account has been deactivated. Please contact your administrator.',
713
816
  page: '/mbkauthe/login',
714
- pagename: 'Login',
715
- version: packageJson.version,
716
- app: mbkautheVar.APP_NAME
717
- });
817
+ pagename: 'Login'
818
+ }).render());
718
819
 
719
820
  case 'NOT_AUTHORIZED':
720
- return res.status(403).render('Error/dError.handlebars', {
721
- layout: false,
821
+ return res.status(403).send(renderError(res, {
722
822
  code: '403',
723
823
  error: 'Not Authorized',
724
824
  message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
725
825
  page: '/mbkauthe/login',
726
- pagename: 'Login',
727
- version: packageJson.version,
728
- app: mbkautheVar.APP_NAME
729
- });
826
+ pagename: 'Login'
827
+ }).render());
730
828
 
731
829
  default:
732
- return res.status(500).render('Error/dError.handlebars', {
733
- layout: false,
830
+ return res.status(500).send(renderError(res, {
734
831
  code: '500',
735
832
  error: 'Authentication Error',
736
833
  message: 'An error occurred during GitHub authentication. Please try again.',
737
834
  page: '/mbkauthe/login',
738
835
  pagename: 'Login',
739
- version: packageJson.version,
740
- app: mbkautheVar.APP_NAME,
741
836
  details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
742
- });
837
+ }).render());
743
838
  }
744
839
  }
745
840
 
746
841
  if (!user) {
747
842
  console.error('[mbkauthe] GitHub callback: No user data received');
748
- return res.status(401).render('Error/dError.handlebars', {
749
- layout: false,
843
+ return res.status(401).send(renderError(res, {
750
844
  code: '401',
751
845
  error: 'Authentication Failed',
752
846
  message: 'GitHub authentication failed. Please try again.',
753
847
  page: '/mbkauthe/login',
754
- pagename: 'Login',
755
- version: packageJson.version,
756
- app: mbkautheVar.APP_NAME
757
- });
848
+ pagename: 'Login'
849
+ }).render());
758
850
  }
759
851
 
760
852
  // Authentication successful, attach user to request
@@ -766,8 +858,14 @@ router.get('/mbkauthe/api/github/login/callback',
766
858
  try {
767
859
  const githubUser = req.user;
768
860
 
769
- // Find the actual user record with named query
770
- const userQuery = `SELECT id, "UserName", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
861
+ // Combined query: fetch user data and 2FA status in one query
862
+ const userQuery = `
863
+ SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps",
864
+ tfa."TwoFAStatus"
865
+ FROM "Users" u
866
+ LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
867
+ WHERE u."UserName" = $1
868
+ `;
771
869
  const userResult = await dblogin.query({
772
870
  name: 'github-callback-get-user',
773
871
  text: userQuery,
@@ -776,43 +874,31 @@ router.get('/mbkauthe/api/github/login/callback',
776
874
 
777
875
  if (userResult.rows.length === 0) {
778
876
  console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
779
- return res.status(404).render('Error/dError.handlebars', {
780
- layout: false,
877
+ return res.status(404).send(renderError(res, {
781
878
  code: '404',
782
879
  error: 'User Not Found',
783
880
  message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
784
881
  page: '/mbkauthe/login',
785
882
  pagename: 'Login',
786
- version: packageJson.version,
787
- app: mbkautheVar.APP_NAME,
788
883
  details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
789
- });
884
+ }).render());
790
885
  }
791
886
 
792
887
  const user = userResult.rows[0];
793
888
 
794
889
  // Check 2FA if enabled
795
- if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
796
- const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
797
- const twoFAResult = await dblogin.query({
798
- name: 'github-check-2fa-status',
799
- text: twoFAQuery,
800
- values: [githubUser.username]
801
- });
802
-
803
- if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
804
- // 2FA is enabled, store pre-auth user and redirect to 2FA
805
- req.session.preAuthUser = {
806
- id: user.id,
807
- username: user.UserName,
808
- UserName: user.UserName,
809
- role: user.Role,
810
- Role: user.Role,
811
- loginMethod: 'github'
812
- };
813
- console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
814
- return res.redirect('/mbkauthe/2fa');
815
- }
890
+ if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
891
+ // 2FA is enabled, store pre-auth user and redirect to 2FA
892
+ req.session.preAuthUser = {
893
+ id: user.id,
894
+ username: user.UserName,
895
+ UserName: user.UserName,
896
+ role: user.Role,
897
+ Role: user.Role,
898
+ loginMethod: 'github'
899
+ };
900
+ console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
901
+ return res.redirect('/mbkauthe/2fa');
816
902
  }
817
903
 
818
904
  // Complete login process using the shared function
@@ -821,7 +907,8 @@ router.get('/mbkauthe/api/github/login/callback',
821
907
  username: user.UserName,
822
908
  UserName: user.UserName,
823
909
  role: user.Role,
824
- Role: user.Role
910
+ Role: user.Role,
911
+ allowedApps: user.AllowedApps,
825
912
  };
826
913
 
827
914
  // For OAuth redirect flow, we need to handle redirect differently
@@ -842,7 +929,7 @@ router.get('/mbkauthe/api/github/login/callback',
842
929
  res.json = function (data) {
843
930
  if (data.success && statusCode === 200) {
844
931
  // If login successful, redirect instead of sending JSON
845
- const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/home';
932
+ const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
846
933
  console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
847
934
  // Restore original methods before redirect
848
935
  res.json = originalJson;
@@ -859,17 +946,14 @@ router.get('/mbkauthe/api/github/login/callback',
859
946
 
860
947
  } catch (err) {
861
948
  console.error('[mbkauthe] GitHub login callback error:', err);
862
- return res.status(500).render('Error/dError.handlebars', {
863
- layout: false,
949
+ return res.status(500).send(renderError(res, {
864
950
  code: '500',
865
951
  error: 'Internal Server Error',
866
952
  message: 'An error occurred during GitHub authentication. Please try again.',
867
953
  page: '/mbkauthe/login',
868
954
  pagename: 'Login',
869
- version: packageJson.version,
870
- app: mbkautheVar.APP_NAME,
871
955
  details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
872
- });
956
+ }).render());
873
957
  }
874
958
  }
875
959
  );