mbkauthe 4.8.4 → 4.9.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.
@@ -4,6 +4,7 @@ const PgSession = pgSession(session);
4
4
  import { dblogin, runWithRequestContext } from "#pool.js";
5
5
  import { mbkautheVar } from "#config.js";
6
6
  import { cachedCookieOptions, decryptSessionId, encryptSessionId } from "#cookies.js";
7
+ import { AuthRepository } from "../db/AuthRepository.js";
7
8
 
8
9
  // Session configuration
9
10
  export const sessionConfig = {
@@ -33,6 +34,8 @@ export const sessionConfig = {
33
34
  name: 'mbkauthe.sid'
34
35
  };
35
36
 
37
+ const authRepo = new AuthRepository({ db: dblogin });
38
+
36
39
  // CORS middleware
37
40
  export function corsMiddleware(req, res, next) {
38
41
  const origin = req.headers.origin;
@@ -77,14 +80,9 @@ export async function sessionRestorationMiddleware(req, res, next) {
77
80
 
78
81
  try {
79
82
  // Validate session by DB primary key id and join to user
80
- const query = `SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps", s.expires_at
81
- FROM "Sessions" s
82
- JOIN "Users" u ON s."UserName" = u."UserName"
83
- WHERE s.id = $1 LIMIT 1`;
84
- const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [sessionId] });
83
+ const row = await authRepo.getSessionWithUserById(sessionId, 'restore-user-session');
85
84
 
86
- if (result.rows.length > 0) {
87
- const row = result.rows[0];
85
+ if (row) {
88
86
 
89
87
  // Reject expired sessions or inactive users
90
88
  if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
@@ -105,13 +103,9 @@ export async function sessionRestorationMiddleware(req, res, next) {
105
103
  } else {
106
104
  // Fallback: attempt to fetch FullName from Users to populate session
107
105
  try {
108
- const profileRes = await dblogin.query({
109
- name: 'restore-get-fullname',
110
- text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
111
- values: [row.UserName]
112
- });
113
- if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {
114
- req.session.user.fullname = profileRes.rows[0].FullName;
106
+ const profileRes = await authRepo.getUserFullNameByUsername(row.UserName, 'restore-get-fullname');
107
+ if (profileRes && profileRes.FullName) {
108
+ req.session.user.fullname = profileRes.FullName;
115
109
  }
116
110
  } catch (profileErr) {
117
111
  console.error(`[mbkauthe] Error fetching FullName during session restore:`, profileErr);
@@ -14,8 +14,10 @@ import {
14
14
  import { packageJson } from "#config.js";
15
15
  import { hashPassword, hashApiToken } from "#config.js";
16
16
  import { ErrorCodes, createErrorResponse, logError } from "../utils/errors.js";
17
+ import { AuthRepository } from "../db/AuthRepository.js";
17
18
 
18
19
  const router = express.Router();
20
+ const authRepo = new AuthRepository({ db: dblogin });
19
21
 
20
22
  // Helper function to clear profile picture cache
21
23
  function clearProfilePicCache(req, username) {
@@ -68,13 +70,8 @@ const csrfProtection = csurf({ cookie: true });
68
70
  // Helper: load a session by DB id and validate basics
69
71
  async function fetchActiveSession(sessionId) {
70
72
  if (!sessionId || typeof sessionId !== 'string') return null;
71
- const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
72
- FROM "Sessions" s
73
- JOIN "Users" u ON s."UserName" = u."UserName"
74
- WHERE s.id = $1 LIMIT 1`;
75
- const result = await dblogin.query({ name: 'multi-session-fetch', text: query, values: [sessionId] });
76
- if (result.rows.length === 0) return null;
77
- const row = result.rows[0];
73
+ const row = await authRepo.fetchActiveSession(sessionId);
74
+ if (!row) return null;
78
75
  if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) return null;
79
76
  if (row.Role !== 'SuperAdmin') {
80
77
  const allowedApps = row.AllowedApps;
@@ -91,7 +88,7 @@ const isUuid = (val) => typeof val === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-
91
88
  async function invalidateDbSession(sessionId) {
92
89
  if (!isUuid(sessionId)) return;
93
90
  try {
94
- await dblogin.query({ name: 'invalidate-app-session', text: 'DELETE FROM "Sessions" WHERE id = $1', values: [sessionId] });
91
+ await authRepo.deleteAppSessionById(sessionId);
95
92
  } catch (err) {
96
93
  console.error(`[mbkauthe] Error invalidating session:`, err);
97
94
  }
@@ -111,25 +108,9 @@ export async function checkTrustedDevice(req, username) {
111
108
  // Hash the provided device token before querying DB (we store token hashes in DB)
112
109
  const deviceTokenHash = hashDeviceToken(deviceToken);
113
110
  // Single round-trip: validate trusted device AND refresh LastUsed.
114
- const deviceQuery = `
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"
124
- `;
125
- const deviceResult = await dblogin.query({
126
- name: 'check-trusted-device',
127
- text: deviceQuery,
128
- values: [deviceTokenHash, username]
129
- });
111
+ const deviceUser = await authRepo.touchTrustedDevice(deviceTokenHash, username);
130
112
 
131
- if (deviceResult.rows.length > 0) {
132
- const deviceUser = deviceResult.rows[0];
113
+ if (deviceUser) {
133
114
 
134
115
  if (!deviceUser.Active) {
135
116
  console.log(`[mbkauthe] Trusted device check: inactive account for username: ${username}`);
@@ -174,11 +155,7 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
174
155
  const oldSessionId = req.sessionID;
175
156
 
176
157
  // Delete old session first to prevent session fixation attacks
177
- await dblogin.query({
178
- name: 'login-delete-old-session-before-regen',
179
- text: 'DELETE FROM "session" WHERE sid = $1',
180
- values: [oldSessionId]
181
- });
158
+ await authRepo.deleteSessionBySid(oldSessionId);
182
159
 
183
160
  // Now regenerate with new session ID (timing window closed)
184
161
  await new Promise((resolve, reject) => {
@@ -193,67 +170,40 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
193
170
  const configuredMax = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
194
171
  const MAX_SESSIONS = Number.isInteger(configuredMax) && configuredMax > 0 ? configuredMax : 5;
195
172
 
196
- const dbClient = await dblogin.connect();
197
173
  let dbSessionId;
198
174
  try {
199
- await dbClient.query('BEGIN');
200
- await dbClient.query('LOCK TABLE "Sessions" IN SHARE ROW EXCLUSIVE MODE');
201
-
202
- // Clean up expired sessions + count active sessions in a single round-trip.
203
- const countRes = await dbClient.query({
204
- name: 'cleanup-and-count-user-sessions',
205
- text: `
206
- WITH deleted AS (
207
- DELETE FROM "Sessions"
208
- WHERE "UserName" = $1
209
- AND expires_at IS NOT NULL
210
- AND expires_at <= NOW()
211
- )
212
- SELECT COUNT(*)::int AS count
213
- FROM "Sessions"
214
- WHERE "UserName" = $1
215
- `,
216
- values: [username]
217
- });
175
+ await authRepo.withTransaction(async (txRepo) => {
176
+ await txRepo.lockTable("Sessions", "SHARE ROW EXCLUSIVE");
177
+ const currentSessions = await txRepo.cleanupAndCountUserSessions(username);
178
+ if (currentSessions >= MAX_SESSIONS) {
179
+ const sessionsToDelete = currentSessions - MAX_SESSIONS + 1; // +1 to make room for new session
180
+ console.log(`[mbkauthe] User "${username}" has ${currentSessions} active sessions, exceeding max of ${MAX_SESSIONS}. Deleting ${sessionsToDelete} oldest sessions.`);
181
+
182
+ await txRepo.deleteOldestSessionsForUser(username, sessionsToDelete, "prune-oldest-user-session");
183
+ }
218
184
 
219
- const currentSessions = Number(countRes.rows?.[0]?.count ?? 0);
220
- if (currentSessions >= MAX_SESSIONS) {
221
- const sessionsToDelete = currentSessions - MAX_SESSIONS + 1; // +1 to make room for new session
222
- console.log(`[mbkauthe] User "${username}" has ${currentSessions} active sessions, exceeding max of ${MAX_SESSIONS}. Deleting ${sessionsToDelete} oldest sessions.`);
185
+ const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
186
+ const insertedSession = await txRepo.insertAppSession(
187
+ username,
188
+ expiresAt,
189
+ JSON.stringify({ ip: req.ip, ua: req.headers['user-agent'] || null })
190
+ );
223
191
 
224
- await dbClient.query({
225
- name: 'prune-oldest-user-session',
226
- text: `DELETE FROM "Sessions" WHERE id IN (SELECT id FROM "Sessions" WHERE "UserName" = $1 ORDER BY created_at ASC LIMIT $2)`,
227
- values: [username, sessionsToDelete]
228
- });
229
- }
192
+ if (!insertedSession?.id) {
193
+ throw new Error('Failed to insert app session');
194
+ }
230
195
 
231
- const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
232
- const insertRes = await dbClient.query({
233
- name: 'insert-app-session',
234
- text: `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`,
235
- values: [username, expiresAt, JSON.stringify({ ip: req.ip, ua: req.headers['user-agent'] || null })]
196
+ dbSessionId = insertedSession.id;
236
197
  });
237
- dbSessionId = insertRes.rows[0].id;
238
-
239
- await dbClient.query('COMMIT');
240
198
  } catch (err) {
241
- await dbClient.query('ROLLBACK').catch(() => {});
242
199
  console.error(`[mbkauthe] Error enforcing session limit or inserting app session:`, err);
243
200
  throw err;
244
- } finally {
245
- dbClient.release();
246
201
  }
247
202
 
248
203
  // Update last_login and fetch FullName/Image in a single query.
249
204
  let profileRow = null;
250
205
  try {
251
- const profUpdateRes = await dblogin.query({
252
- name: 'login-update-last-login-return-profile',
253
- text: `UPDATE "Users" SET "last_login" = NOW() WHERE "id" = $1 RETURNING "FullName", "Image"`,
254
- values: [user.id]
255
- });
256
- profileRow = profUpdateRes.rows?.[0] || null;
206
+ profileRow = await authRepo.updateLastLoginReturnProfile(user.id);
257
207
  } catch (profileUpdateErr) {
258
208
  console.error(`[mbkauthe] Error updating last_login/returning profile:`, profileUpdateErr);
259
209
  }
@@ -277,14 +227,10 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
277
227
  } else {
278
228
  // Fallback: try a read query if UPDATE...RETURNING failed unexpectedly.
279
229
  try {
280
- const profileResult = await dblogin.query({
281
- name: 'login-get-fullname-and-image',
282
- text: 'SELECT "FullName", "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
283
- values: [username]
284
- });
285
- if (profileResult.rows.length > 0) {
286
- if (profileResult.rows[0].FullName) req.session.user.fullname = profileResult.rows[0].FullName;
287
- if (profileResult.rows[0].Image && profileResult.rows[0].Image.trim() !== '') loginProfileImage = profileResult.rows[0].Image;
230
+ const profileResult = await authRepo.getUserProfileByUsername(username);
231
+ if (profileResult) {
232
+ if (profileResult.FullName) req.session.user.fullname = profileResult.FullName;
233
+ if (profileResult.Image && profileResult.Image.trim() !== '') loginProfileImage = profileResult.Image;
288
234
  }
289
235
  } catch (profileErr) {
290
236
  console.error(`[mbkauthe] Error fetching FullName/Image for user:`, profileErr);
@@ -340,11 +286,13 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
340
286
 
341
287
  // Store only the HASH of the device token in DB; send the raw token to the client (httpOnly cookie)
342
288
  const deviceTokenHash = hashDeviceToken(deviceToken);
343
- await dblogin.query({
344
- name: 'insert-trusted-device',
345
- text: `INSERT INTO "TrustedDevices" ("UserName", "DeviceToken", "DeviceName", "UserAgent", "IpAddress", "ExpiresAt")
346
- VALUES ($1, $2, $3, $4, $5, $6)`,
347
- values: [username, deviceTokenHash, deviceName, userAgent, ipAddress, expiresAt]
289
+ await authRepo.insertTrustedDevice({
290
+ username,
291
+ deviceTokenHash,
292
+ deviceName,
293
+ userAgent,
294
+ ipAddress,
295
+ expiresAt
348
296
  });
349
297
 
350
298
  // Send raw token to client as httpOnly cookie only
@@ -414,24 +362,15 @@ router.post("/api/login", LoginLimit, async (req, res) => {
414
362
 
415
363
  try {
416
364
  // Combined query: fetch user data and 2FA status in one query
417
- const userQuery = `
418
- SELECT u.id, u."UserName", u."PasswordEnc", u."Active", u."Role", u."AllowedApps",
419
- tfa."TwoFAStatus"
420
- FROM "Users" u
421
- LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
422
- WHERE u."UserName" = $1
423
- `;
424
- const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
425
-
426
- if (userResult.rows.length === 0) {
365
+ const user = await authRepo.getUserWithTwoFA(trimmedUsername);
366
+
367
+ if (!user) {
427
368
  logError('Login attempt', ErrorCodes.USER_NOT_FOUND, { username: trimmedUsername });
428
369
  return res.status(401).json(
429
370
  createErrorResponse(401, ErrorCodes.INVALID_CREDENTIALS)
430
371
  );
431
372
  }
432
373
 
433
- const user = userResult.rows[0];
434
-
435
374
  // Password verification (hash-only). We never read/compare plaintext passwords.
436
375
  let passwordMatches = false;
437
376
  if (user.PasswordEnc) {
@@ -575,16 +514,15 @@ router.post("/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
575
514
  const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
576
515
 
577
516
  try {
578
- const query = `SELECT tfa."TwoFASecret" FROM "TwoFA" tfa WHERE tfa."UserName" = $1`;
579
- const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
517
+ const twoFARecord = await authRepo.getTwoFASecret(username);
580
518
 
581
- if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
519
+ if (!twoFARecord || !twoFARecord.TwoFASecret) {
582
520
  return res.status(500).json(
583
521
  createErrorResponse(500, ErrorCodes.TWO_FA_NOT_CONFIGURED)
584
522
  );
585
523
  }
586
524
 
587
- const sharedSecret = twoFAResult.rows[0].TwoFASecret;
525
+ const sharedSecret = twoFARecord.TwoFASecret;
588
526
  const allowedApps = req.session.preAuthUser?.allowedApps;
589
527
  const tokenValidates = speakeasy.totp.verify({
590
528
  secret: sharedSecret,
@@ -632,11 +570,11 @@ router.post("/api/logout", LogoutLimit, async (req, res) => {
632
570
  // Remove the application session record for this token (if present)
633
571
  const operations = [];
634
572
  if (req.session && req.session.user && req.session.user.sessionId) {
635
- operations.push(dblogin.query({ name: 'logout-delete-app-session', text: 'DELETE FROM "Sessions" WHERE id = $1', values: [req.session.user.sessionId] }));
573
+ operations.push(authRepo.deleteAppSessionById(req.session.user.sessionId, "logout-delete-app-session"));
636
574
  }
637
575
 
638
576
  if (req.sessionID) {
639
- operations.push(dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] }));
577
+ operations.push(authRepo.deleteSessionBySid(req.sessionID, "logout-delete-session"));
640
578
  }
641
579
 
642
580
  await Promise.all(operations);
@@ -694,14 +632,10 @@ router.get("/api/account-sessions", LoginLimit, async (req, res) => {
694
632
  let image = acct.image || null;
695
633
  if (!acct.fullName || !acct.image) {
696
634
  try {
697
- const prof = await dblogin.query({
698
- name: 'multi-session-fullname-image',
699
- text: 'SELECT "FullName", "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
700
- values: [row.UserName]
701
- });
702
- if (prof.rows.length > 0) {
703
- if (!acct.fullName && prof.rows[0].FullName) fullName = prof.rows[0].FullName;
704
- if (!acct.image && prof.rows[0].Image && prof.rows[0].Image.trim() !== '') image = prof.rows[0].Image;
635
+ const prof = await authRepo.getUserProfileByUsername(row.UserName, "multi-session-fullname-image");
636
+ if (prof) {
637
+ if (!acct.fullName && prof.FullName) fullName = prof.FullName;
638
+ if (!acct.image && prof.Image && prof.Image.trim() !== '') image = prof.Image;
705
639
  }
706
640
  } catch (profileErr) {
707
641
  console.error(`[mbkauthe] Error fetching fullname/image for account list:`, profileErr);
@@ -748,14 +682,10 @@ router.post("/api/switch-session", LoginLimit, async (req, res) => {
748
682
  let fullName = row.UserName;
749
683
  let switchProfileImage = null;
750
684
  try {
751
- const prof = await dblogin.query({
752
- name: 'multi-session-switch-fullname-image',
753
- text: 'SELECT "FullName", "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
754
- values: [row.UserName]
755
- });
756
- if (prof.rows.length > 0) {
757
- if (prof.rows[0].FullName) fullName = prof.rows[0].FullName;
758
- if (prof.rows[0].Image && prof.rows[0].Image.trim() !== '') switchProfileImage = prof.rows[0].Image;
685
+ const prof = await authRepo.getUserProfileByUsername(row.UserName, "multi-session-switch-fullname-image");
686
+ if (prof) {
687
+ if (prof.FullName) fullName = prof.FullName;
688
+ if (prof.Image && prof.Image.trim() !== '') switchProfileImage = prof.Image;
759
689
  }
760
690
  } catch (profileErr) {
761
691
  console.error(`[mbkauthe] Error fetching fullname/image during switch:`, profileErr);
@@ -816,15 +746,11 @@ router.post("/api/logout-all", LoginLimit, async (req, res) => {
816
746
  if (currentSessionId) sessionIds.push(currentSessionId);
817
747
 
818
748
  if (sessionIds.length) {
819
- await dblogin.query({
820
- name: 'logout-all-app-sessions',
821
- text: 'DELETE FROM "Sessions" WHERE id = ANY($1)',
822
- values: [sessionIds]
823
- });
749
+ await authRepo.deleteSessionsByIds(sessionIds, "logout-all-app-sessions");
824
750
  }
825
751
 
826
752
  if (req.sessionID) {
827
- await dblogin.query({ name: 'logout-all-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] });
753
+ await authRepo.deleteSessionBySid(req.sessionID, "logout-all-delete-session");
828
754
  }
829
755
 
830
756
  clearAccountListCookie(res);
@@ -842,6 +768,9 @@ router.post("/api/logout-all", LoginLimit, async (req, res) => {
842
768
  // GET /mbkauthe/login
843
769
  router.get("/login", LoginLimit, csrfProtection, (req, res) => {
844
770
  const lastLogin = req.cookies && typeof req.cookies.lastLoginMethod === 'string' ? req.cookies.lastLoginMethod : null;
771
+ const reason = req.query.reason;
772
+ const redirectTarget = req.query.redirect || null;
773
+
845
774
  return res.render("pages/loginmbkauthe.handlebars", {
846
775
  layout: false,
847
776
  githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
@@ -856,7 +785,9 @@ router.get("/login", LoginLimit, csrfProtection, (req, res) => {
856
785
  lastLoginMethod: lastLogin,
857
786
  lastLoginPassword: lastLogin === 'password',
858
787
  lastLoginGithub: lastLogin === 'github',
859
- lastLoginGoogle: lastLogin === 'google'
788
+ lastLoginGoogle: lastLogin === 'google',
789
+ showLoggedOutMessage: reason === 'logged_out',
790
+ redirectTarget: redirectTarget
860
791
  });
861
792
  });
862
793
 
@@ -7,6 +7,7 @@ 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";
@@ -18,6 +19,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
19
 
19
20
 
20
21
  const router = express.Router();
22
+ const authRepo = new AuthRepository({ db: dblogin });
21
23
  // Rate limiter for info/test routes
22
24
  const LoginLimit = rateLimit({
23
25
  windowMs: 1 * 60 * 1000,
@@ -100,14 +102,10 @@ router.get('/user/profilepic', async (req, res) => {
100
102
 
101
103
  // If not in cache, fetch from DB
102
104
  if (!imageUrl) {
103
- const result = await dblogin.query({
104
- name: 'get-user-profile-pic',
105
- text: 'SELECT "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
106
- values: [username]
107
- });
105
+ const profile = await authRepo.getUserImageByUsername(username, 'get-user-profile-pic');
108
106
 
109
- if (result.rows.length > 0 && result.rows[0].Image && result.rows[0].Image.trim() !== '') {
110
- imageUrl = result.rows[0].Image;
107
+ if (profile && profile.Image && profile.Image.trim() !== '') {
108
+ imageUrl = profile.Image;
111
109
  } else {
112
110
  imageUrl = 'default';
113
111
  }
@@ -229,31 +227,13 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
229
227
  }
230
228
 
231
229
  // 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
- });
230
+ const row = await authRepo.getSessionValidity(sessionId, req.sessionID, 'check-session-validity');
249
231
 
250
- if (result.rows.length === 0) {
232
+ if (!row) {
251
233
  req.session.destroy(() => { });
252
234
  clearSessionCookies(res);
253
235
  return res.status(200).json({ sessionValid: false, expiry: null });
254
236
  }
255
-
256
- const row = result.rows[0];
257
237
  if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
258
238
  req.session.destroy(() => { });
259
239
  clearSessionCookies(res);
@@ -298,17 +278,8 @@ function normalizeSessionIdFromBody(body = {}) {
298
278
  }
299
279
 
300
280
  async function getSessionValidationRow(sessionId, queryName = 'check-session-validity-by-id') {
301
- const result = await dblogin.query({
302
- name: queryName,
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];
281
+ const row = await authRepo.getSessionValidationRow(sessionId, queryName);
282
+ return row || null;
312
283
  }
313
284
 
314
285
  function isSessionRowValid(row) {
@@ -567,14 +538,8 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
567
538
  try {
568
539
  // Run both operations in parallel for better performance
569
540
  await Promise.all([
570
- dblogin.query({
571
- name: 'terminate-all-app-sessions',
572
- text: 'DELETE FROM "Sessions"'
573
- }),
574
- dblogin.query({
575
- name: 'terminate-all-db-sessions',
576
- text: 'DELETE FROM "session" WHERE expire > NOW()'
577
- })
541
+ authRepo.deleteAllAppSessions('terminate-all-app-sessions'),
542
+ authRepo.deleteActiveSessionStoreRows('terminate-all-db-sessions')
578
543
  ]);
579
544
 
580
545
  req.session.destroy((err) => {
@@ -8,8 +8,10 @@ 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";
11
12
 
12
13
  const router = express.Router();
14
+ const authRepo = new AuthRepository({ db: dblogin });
13
15
 
14
16
  // CSRF protection middleware
15
17
  const csrfProtection = csurf({ cookie: true });
@@ -42,25 +44,16 @@ const createOAuthStrategy = async (provider, profile, done) => {
42
44
  console.log(`[mbkauthe] ${provider} OAuth callback for user: ${profile.emails?.[0]?.value || profile.id}`);
43
45
 
44
46
  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
47
 
49
48
  // Check if this OAuth account is linked to any user
50
- const oauthUser = await dblogin.query({
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
- });
49
+ const user = await authRepo.getOAuthUserByProviderId(provider, profile.id);
55
50
 
56
- if (oauthUser.rows.length === 0) {
51
+ if (!user) {
57
52
  const error = new Error(`${provider} account not linked to any user`);
58
53
  error.code = `${provider.toUpperCase()}_NOT_LINKED`;
59
54
  return done(error);
60
55
  }
61
56
 
62
- const user = oauthUser.rows[0];
63
-
64
57
  // Check if the user account is active
65
58
  if (!user.Active) {
66
59
  const error = new Error('Account is inactive');
@@ -300,21 +293,9 @@ const validateOAuthCallback = (req, res) => {
300
293
  };
301
294
 
302
295
  const finishProviderLogin = async (req, res, provider, username, detailValue = '') => {
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) {
296
+ const user = await authRepo.getUserWithTwoFA(username, `${provider.toLowerCase()}-callback-get-user`);
297
+
298
+ if (!user) {
318
299
  console.error(`[mbkauthe] ${provider} login: User not found: ${username}`);
319
300
  return renderError(res, req, {
320
301
  code: 404,
@@ -326,8 +307,6 @@ const finishProviderLogin = async (req, res, provider, username, detailValue = '
326
307
  });
327
308
  }
328
309
 
329
- const user = userResult.rows[0];
330
-
331
310
  // Check for trusted device
332
311
  const trustedDeviceUser = await checkTrustedDevice(req, user.UserName);
333
312
  if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "4.8.4",
3
+ "version": "4.9.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/public/main.css CHANGED
@@ -286,6 +286,14 @@ header {
286
286
  transition: var(--transition);
287
287
  }
288
288
 
289
+ .icon-button {
290
+ appearance: none;
291
+ border: 0;
292
+ background: transparent;
293
+ padding: 0;
294
+ font: inherit;
295
+ }
296
+
289
297
  .input-icon:hover {
290
298
  color: var(--accent);
291
299
  }
@@ -304,6 +312,14 @@ header {
304
312
  box-shadow: var(--box-shadow);
305
313
  }
306
314
 
315
+ .btn-social,
316
+ .swi {
317
+ appearance: none;
318
+ border: 0.13rem solid;
319
+ font: inherit;
320
+ cursor: pointer;
321
+ }
322
+
307
323
  .swi {
308
324
  display: flex;
309
325
  align-items: center;
@@ -315,7 +331,6 @@ header {
315
331
  font-size: var(--text-size-sm);
316
332
  text-decoration: none;
317
333
  transition: var(--transition);
318
- border: 0.13rem solid;
319
334
  position: relative;
320
335
  z-index: 1;
321
336
  overflow: hidden;
@@ -423,6 +438,25 @@ header {
423
438
  text-decoration: none;
424
439
  }
425
440
 
441
+ .btn-message-action {
442
+ appearance: none;
443
+ border: 0;
444
+ border-radius: var(--border-radius);
445
+ background: var(--accent);
446
+ color: var(--dark);
447
+ font: inherit;
448
+ font-weight: 700;
449
+ cursor: pointer;
450
+ padding: 0.7rem 1rem;
451
+ transition: var(--transition);
452
+ }
453
+
454
+ .btn-message-action:hover,
455
+ .btn-message-action:focus-visible {
456
+ background: var(--surface-1);
457
+ color: var(--accent);
458
+ }
459
+
426
460
  .token-container {
427
461
  animation: fadeInUp 0.4s ease-out;
428
462
  }
@@ -776,7 +810,6 @@ header {
776
810
  font-size: 0.9rem;
777
811
  text-decoration: none;
778
812
  transition: var(--transition);
779
- border: 0.13rem solid;
780
813
  position: relative;
781
814
  z-index: 1;
782
815
  overflow: hidden;
@@ -3,7 +3,7 @@
3
3
  <a class="logo">
4
4
  <img src="/icon.svg" alt="Logo" class="logo-image"/>
5
5
  <span class="logo-text">BK <span>Tech</span></span>
6
- <span class="logo-comp">{{#if appName}}{{appName}}{{else}}{{#if app}}{{app}}{{else}}mbkauthe{{/if}}{{/if}}
6
+ <span class="logo-comp">{{#if appName}}{{appName}}{{else}}{{#if app}}{{app}}{{else}}mbkauthe{{/if}}{{/if}}</span>
7
7
  </a>
8
8
 
9
9
  {{> profilemenu}}