mbkauthe 2.1.0 → 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,20 +14,13 @@ import speakeasy from "speakeasy";
14
14
  import passport from 'passport';
15
15
  import GitHubStrategy from 'passport-github2';
16
16
 
17
- import { createRequire } from "module";
18
17
  import { fileURLToPath } from "url";
19
18
  import fs from "fs";
20
19
  import path from "path";
21
-
22
- import dotenv from "dotenv";
23
- dotenv.config();
24
- const mbkautheVar = JSON.parse(process.env.mbkautheVar);
20
+ import { mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS } from "./config.js";
25
21
 
26
22
  const router = express.Router();
27
23
 
28
- const require = createRequire(import.meta.url);
29
- const packageJson = require("../package.json");
30
-
31
24
  router.use(express.json());
32
25
  router.use(express.urlencoded({ extended: true }));
33
26
  router.use(cookieParser());
@@ -35,16 +28,27 @@ router.use(cookieParser());
35
28
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
29
 
37
30
  router.get('/mbkauthe/main.js', (req, res) => {
31
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
38
32
  res.sendFile(path.join(__dirname, '..', 'public', 'main.js'));
39
33
  });
40
34
 
41
- router.get("/mbkauthe/bg.avif", (req, res) => {
42
- const imgPath = path.join(__dirname, "..", "public", "bg.avif");
43
- res.setHeader('Content-Type', 'image/avif');
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');
44
48
  res.setHeader('Cache-Control', 'public, max-age=31536000');
45
49
  const stream = fs.createReadStream(imgPath);
46
50
  stream.on('error', (err) => {
47
- console.error('[mbkauthe] Error streaming bg.avif:', err);
51
+ console.error('[mbkauthe] Error streaming bg.webp:', err);
48
52
  res.status(404).send('Image not found');
49
53
  });
50
54
  stream.pipe(res);
@@ -128,14 +132,16 @@ router.use(session(sessionConfig));
128
132
  router.use(async (req, res, next) => {
129
133
  // Only restore session if not already present and sessionId cookie exists
130
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
+
131
144
  try {
132
- const sessionId = req.cookies.sessionId;
133
-
134
- // Validate sessionId format (should be 64 hex characters) and normalize to lowercase
135
- if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
136
- console.warn("[mbkauthe] Invalid sessionId format detected");
137
- return next();
138
- }
139
145
 
140
146
  const normalizedSessionId = sessionId.toLowerCase();
141
147
 
@@ -151,6 +157,7 @@ router.use(async (req, res, next) => {
151
157
  role: user.Role,
152
158
  Role: user.Role,
153
159
  sessionId: normalizedSessionId,
160
+ allowedApps: user.AllowedApps,
154
161
  };
155
162
  }
156
163
  } catch (err) {
@@ -160,23 +167,6 @@ router.use(async (req, res, next) => {
160
167
  next();
161
168
  });
162
169
 
163
- const getCookieOptions = () => ({
164
- maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
165
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
166
- secure: mbkautheVar.IS_DEPLOYED === 'true',
167
- sameSite: 'lax',
168
- path: '/',
169
- httpOnly: true
170
- });
171
-
172
- const getClearCookieOptions = () => ({
173
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
174
- secure: mbkautheVar.IS_DEPLOYED === 'true',
175
- sameSite: 'lax',
176
- path: '/',
177
- httpOnly: true
178
- });
179
-
180
170
  router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
181
171
  if (req.session?.user) {
182
172
  return res.send(`
@@ -190,7 +180,7 @@ router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
190
180
  }
191
181
  });
192
182
 
193
- async function completeLoginProcess(req, res, user, redirectUrl = null) {
183
+ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
194
184
  try {
195
185
  // Ensure both username formats are available for compatibility
196
186
  const username = user.username || user.UserName;
@@ -211,19 +201,21 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
211
201
  });
212
202
  });
213
203
 
214
- // Delete all old sessions for this user from session table
215
- await dblogin.query({
216
- name: 'login-delete-old-user-sessions',
217
- text: 'DELETE FROM "session" WHERE sess::text LIKE $1',
218
- values: [`%"username":"${username}"%`]
219
- });
220
-
221
- await dblogin.query({
222
- name: 'login-update-session-id', text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, values: [
223
- sessionId,
224
- user.id,
225
- ]
226
- });
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
+ ]);
227
219
 
228
220
  req.session.user = {
229
221
  id: user.id,
@@ -232,6 +224,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
232
224
  role: user.role || user.Role,
233
225
  Role: user.role || user.Role,
234
226
  sessionId,
227
+ allowedApps: user.allowedApps || user.AllowedApps,
235
228
  };
236
229
 
237
230
  if (req.session.preAuthUser) {
@@ -246,8 +239,33 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
246
239
  // avoid writing back into the session table here to reduce DB writes;
247
240
  // the pg session store will already persist the session data.
248
241
 
249
- const cookieOptions = getCookieOptions();
250
- 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
+
251
269
  console.log(`[mbkauthe] User "${username}" logged in successfully`);
252
270
 
253
271
  const responsePayload = {
@@ -270,11 +288,10 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
270
288
 
271
289
  router.use(async (req, res, next) => {
272
290
  if (req.session && req.session.user) {
273
- const cookieOptions = getCookieOptions();
274
291
  // Only set cookies if they're missing or different
275
292
  if (req.cookies.sessionId !== req.session.user.sessionId) {
276
- res.cookie("username", req.session.user.username, { ...cookieOptions, httpOnly: false });
277
- 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);
278
295
  }
279
296
  }
280
297
  next();
@@ -282,8 +299,11 @@ router.use(async (req, res, next) => {
282
299
 
283
300
  router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
284
301
  try {
285
- await dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` });
286
- 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
+ ]);
287
307
 
288
308
  req.session.destroy((err) => {
289
309
  if (err) {
@@ -291,10 +311,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
291
311
  return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
292
312
  }
293
313
 
294
- const cookieOptions = getClearCookieOptions();
295
- res.clearCookie("mbkauthe.sid", cookieOptions);
296
- res.clearCookie("sessionId", cookieOptions);
297
- res.clearCookie("username", cookieOptions);
314
+ clearSessionCookies(res);
298
315
 
299
316
  console.log("[mbkauthe] All sessions terminated successfully");
300
317
  res.status(200).json({
@@ -345,7 +362,84 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
345
362
  const trimmedUsername = username.trim();
346
363
 
347
364
  try {
348
- 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
+ `;
349
443
  const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
350
444
 
351
445
  if (userResult.rows.length === 0) {
@@ -393,21 +487,16 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
393
487
  }
394
488
  }
395
489
 
396
- if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
397
- const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
398
- const twoFAResult = await dblogin.query({ name: 'login-check-2fa-status', text: query, values: [trimmedUsername] });
399
-
400
- if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
401
- // 2FA is enabled, prompt for token on a separate page
402
- req.session.preAuthUser = {
403
- id: user.id,
404
- username: user.UserName,
405
- role: user.Role,
406
- Role: user.Role,
407
- };
408
- console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
409
- return res.json({ success: true, twoFactorRequired: true });
410
- }
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 });
411
500
  }
412
501
 
413
502
  // If 2FA is not enabled, proceed with login
@@ -416,6 +505,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
416
505
  username: user.UserName,
417
506
  role: user.Role,
418
507
  Role: user.Role,
508
+ allowedApps: user.AllowedApps,
419
509
  };
420
510
  await completeLoginProcess(req, res, userForSession);
421
511
 
@@ -431,9 +521,10 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
431
521
  }
432
522
  res.render("2fa.handlebars", {
433
523
  layout: false,
434
- customURL: mbkautheVar.loginRedirectURL || '/home',
524
+ customURL: mbkautheVar.loginRedirectURL || '/dashboard',
435
525
  csrfToken: req.csrfToken(),
436
526
  appName: mbkautheVar.APP_NAME.toLowerCase(),
527
+ DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
437
528
  });
438
529
  });
439
530
 
@@ -442,7 +533,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
442
533
  return res.status(401).json({ success: false, message: "Not authorized. Please login first." });
443
534
  }
444
535
 
445
- const { token } = req.body;
536
+ const { token, trustDevice } = req.body;
446
537
  const { username, id, role } = req.session.preAuthUser;
447
538
 
448
539
  // Validate 2FA token
@@ -456,8 +547,11 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
456
547
  return res.status(400).json({ success: false, message: "Invalid 2FA token format" });
457
548
  }
458
549
 
550
+ // Validate trustDevice parameter if provided
551
+ const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
552
+
459
553
  try {
460
- 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`;
461
555
  const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
462
556
 
463
557
  if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
@@ -465,6 +559,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
465
559
  }
466
560
 
467
561
  const sharedSecret = twoFAResult.rows[0].TwoFASecret;
562
+ const allowedApps = twoFAResult.rows[0].AllowedApps;
468
563
  const tokenValidates = speakeasy.totp.verify({
469
564
  secret: sharedSecret,
470
565
  encoding: "base32",
@@ -477,10 +572,10 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
477
572
  return res.status(401).json({ success: false, message: "Invalid 2FA code" });
478
573
  }
479
574
 
480
- // 2FA successful, complete login
481
- const userForSession = { id, username, role };
482
- const redirectUrl = mbkautheVar.loginRedirectURL || '/home';
483
- 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);
484
579
 
485
580
  } catch (err) {
486
581
  console.error("[mbkauthe] Error during 2FA verification:", err);
@@ -493,11 +588,18 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
493
588
  try {
494
589
  const { id, username } = req.session.user;
495
590
 
496
- await dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] });
497
-
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
+
498
596
  if (req.sessionID) {
499
- 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
+ );
500
600
  }
601
+
602
+ await Promise.all(operations);
501
603
 
502
604
  req.session.destroy((err) => {
503
605
  if (err) {
@@ -505,10 +607,7 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
505
607
  return res.status(500).json({ success: false, message: "Logout failed" });
506
608
  }
507
609
 
508
- const cookieOptions = getClearCookieOptions();
509
- res.clearCookie("mbkauthe.sid", cookieOptions);
510
- res.clearCookie("sessionId", cookieOptions);
511
- res.clearCookie("username", cookieOptions);
610
+ clearSessionCookies(res);
512
611
 
513
612
  console.log(`[mbkauthe] User "${username}" logged out successfully`);
514
613
  res.status(200).json({ success: true, message: "Logout successful" });
@@ -526,7 +625,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
526
625
  return res.render("loginmbkauthe.handlebars", {
527
626
  layout: false,
528
627
  githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
529
- customURL: mbkautheVar.loginRedirectURL || '/home',
628
+ customURL: mbkautheVar.loginRedirectURL || '/dashboard',
530
629
  userLoggedIn: !!req.session?.user,
531
630
  username: req.session?.user?.username || '',
532
631
  version: packageJson.version,
@@ -677,16 +776,13 @@ router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
677
776
  passport.authenticate('github-login')(req, res, next);
678
777
  }
679
778
  else {
680
- res.status(403).render('Error/dError.handlebars', {
681
- layout: false,
779
+ return res.status(403).send(renderError(res, {
682
780
  code: '403',
683
781
  error: 'GitHub Login Disabled',
684
782
  message: 'GitHub login is currently disabled. Please use your username and password to log in.',
685
783
  page: '/mbkauthe/login',
686
784
  pagename: 'Login',
687
- version: packageJson.version,
688
- app: mbkautheVar.APP_NAME
689
- });
785
+ }).render());
690
786
  }
691
787
  });
692
788
 
@@ -704,68 +800,53 @@ router.get('/mbkauthe/api/github/login/callback',
704
800
  // Map error codes to user-friendly messages
705
801
  switch (err.code) {
706
802
  case 'GITHUB_NOT_LINKED':
707
- return res.status(403).render('Error/dError.handlebars', {
708
- layout: false,
803
+ return res.status(403).send(renderError(res, {
709
804
  code: '403',
710
805
  error: 'GitHub Account Not Linked',
711
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.',
712
807
  page: '/mbkauthe/login',
713
- pagename: 'Login',
714
- version: packageJson.version,
715
- app: mbkautheVar.APP_NAME
716
- });
808
+ pagename: 'Login'
809
+ }).render());
717
810
 
718
811
  case 'ACCOUNT_INACTIVE':
719
- return res.status(403).render('Error/dError.handlebars', {
720
- layout: false,
812
+ return res.status(403).send(renderError(res, {
721
813
  code: '403',
722
814
  error: 'Account Inactive',
723
815
  message: 'Your account has been deactivated. Please contact your administrator.',
724
816
  page: '/mbkauthe/login',
725
- pagename: 'Login',
726
- version: packageJson.version,
727
- app: mbkautheVar.APP_NAME
728
- });
817
+ pagename: 'Login'
818
+ }).render());
729
819
 
730
820
  case 'NOT_AUTHORIZED':
731
- return res.status(403).render('Error/dError.handlebars', {
732
- layout: false,
821
+ return res.status(403).send(renderError(res, {
733
822
  code: '403',
734
823
  error: 'Not Authorized',
735
824
  message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
736
825
  page: '/mbkauthe/login',
737
- pagename: 'Login',
738
- version: packageJson.version,
739
- app: mbkautheVar.APP_NAME
740
- });
826
+ pagename: 'Login'
827
+ }).render());
741
828
 
742
829
  default:
743
- return res.status(500).render('Error/dError.handlebars', {
744
- layout: false,
830
+ return res.status(500).send(renderError(res, {
745
831
  code: '500',
746
832
  error: 'Authentication Error',
747
833
  message: 'An error occurred during GitHub authentication. Please try again.',
748
834
  page: '/mbkauthe/login',
749
835
  pagename: 'Login',
750
- version: packageJson.version,
751
- app: mbkautheVar.APP_NAME,
752
836
  details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
753
- });
837
+ }).render());
754
838
  }
755
839
  }
756
840
 
757
841
  if (!user) {
758
842
  console.error('[mbkauthe] GitHub callback: No user data received');
759
- return res.status(401).render('Error/dError.handlebars', {
760
- layout: false,
843
+ return res.status(401).send(renderError(res, {
761
844
  code: '401',
762
845
  error: 'Authentication Failed',
763
846
  message: 'GitHub authentication failed. Please try again.',
764
847
  page: '/mbkauthe/login',
765
- pagename: 'Login',
766
- version: packageJson.version,
767
- app: mbkautheVar.APP_NAME
768
- });
848
+ pagename: 'Login'
849
+ }).render());
769
850
  }
770
851
 
771
852
  // Authentication successful, attach user to request
@@ -777,8 +858,14 @@ router.get('/mbkauthe/api/github/login/callback',
777
858
  try {
778
859
  const githubUser = req.user;
779
860
 
780
- // Find the actual user record with named query
781
- 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
+ `;
782
869
  const userResult = await dblogin.query({
783
870
  name: 'github-callback-get-user',
784
871
  text: userQuery,
@@ -787,43 +874,31 @@ router.get('/mbkauthe/api/github/login/callback',
787
874
 
788
875
  if (userResult.rows.length === 0) {
789
876
  console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
790
- return res.status(404).render('Error/dError.handlebars', {
791
- layout: false,
877
+ return res.status(404).send(renderError(res, {
792
878
  code: '404',
793
879
  error: 'User Not Found',
794
880
  message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
795
881
  page: '/mbkauthe/login',
796
882
  pagename: 'Login',
797
- version: packageJson.version,
798
- app: mbkautheVar.APP_NAME,
799
883
  details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
800
- });
884
+ }).render());
801
885
  }
802
886
 
803
887
  const user = userResult.rows[0];
804
888
 
805
889
  // Check 2FA if enabled
806
- if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
807
- const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
808
- const twoFAResult = await dblogin.query({
809
- name: 'github-check-2fa-status',
810
- text: twoFAQuery,
811
- values: [githubUser.username]
812
- });
813
-
814
- if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
815
- // 2FA is enabled, store pre-auth user and redirect to 2FA
816
- req.session.preAuthUser = {
817
- id: user.id,
818
- username: user.UserName,
819
- UserName: user.UserName,
820
- role: user.Role,
821
- Role: user.Role,
822
- loginMethod: 'github'
823
- };
824
- console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
825
- return res.redirect('/mbkauthe/2fa');
826
- }
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');
827
902
  }
828
903
 
829
904
  // Complete login process using the shared function
@@ -832,7 +907,8 @@ router.get('/mbkauthe/api/github/login/callback',
832
907
  username: user.UserName,
833
908
  UserName: user.UserName,
834
909
  role: user.Role,
835
- Role: user.Role
910
+ Role: user.Role,
911
+ allowedApps: user.AllowedApps,
836
912
  };
837
913
 
838
914
  // For OAuth redirect flow, we need to handle redirect differently
@@ -853,7 +929,7 @@ router.get('/mbkauthe/api/github/login/callback',
853
929
  res.json = function (data) {
854
930
  if (data.success && statusCode === 200) {
855
931
  // If login successful, redirect instead of sending JSON
856
- const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/home';
932
+ const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
857
933
  console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
858
934
  // Restore original methods before redirect
859
935
  res.json = originalJson;
@@ -870,17 +946,14 @@ router.get('/mbkauthe/api/github/login/callback',
870
946
 
871
947
  } catch (err) {
872
948
  console.error('[mbkauthe] GitHub login callback error:', err);
873
- return res.status(500).render('Error/dError.handlebars', {
874
- layout: false,
949
+ return res.status(500).send(renderError(res, {
875
950
  code: '500',
876
951
  error: 'Internal Server Error',
877
952
  message: 'An error occurred during GitHub authentication. Please try again.',
878
953
  page: '/mbkauthe/login',
879
954
  pagename: 'Login',
880
- version: packageJson.version,
881
- app: mbkautheVar.APP_NAME,
882
955
  details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
883
- });
956
+ }).render());
884
957
  }
885
958
  }
886
959
  );