mbkauthe 4.8.1 → 4.8.3

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.
@@ -510,14 +510,14 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
510
510
  });
511
511
  }
512
512
 
513
+ // Use role from validateSession to avoid additional DB query
514
+ const userRole = req.userRole;
515
+
513
516
  // SuperAdmin bypasses all role checks
514
- if(req.session.role === "SuperAdmin") {
517
+ if(req.session.role === "SuperAdmin" || userRole === "SuperAdmin") {
515
518
  return next();
516
519
  }
517
520
 
518
- // Use role from validateSession to avoid additional DB query
519
- const userRole = req.userRole;
520
-
521
521
  // Check notAllowed role
522
522
  if (notAllowed && userRole === notAllowed) {
523
523
  if (isJsonRequest(req)) {
@@ -188,55 +188,61 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
188
188
  });
189
189
  });
190
190
 
191
- // Enforce max sessions per user (configurable via mbkautheVar.MAX_SESSIONS_PER_USER) and persist a new application session record (keyed by username)
191
+ // Enforce max sessions per user (configurable via mbkautheVar.MAX_SESSIONS_PER_USER)
192
+ // Use a transaction and lock the Sessions table to prevent concurrent logins from exceeding the configured limit.
192
193
  const configuredMax = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
193
194
  const MAX_SESSIONS = Number.isInteger(configuredMax) && configuredMax > 0 ? configuredMax : 5;
194
195
 
195
- // Clean up expired sessions + count active sessions in a single round-trip.
196
- const countRes = await dblogin.query({
197
- name: 'cleanup-and-count-user-sessions',
198
- text: `
199
- WITH deleted AS (
200
- DELETE FROM "Sessions"
196
+ const dbClient = await dblogin.connect();
197
+ let dbSessionId;
198
+ 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"
201
214
  WHERE "UserName" = $1
202
- AND expires_at IS NOT NULL
203
- AND expires_at <= NOW()
204
- )
205
- SELECT COUNT(*)::int AS count
206
- FROM "Sessions"
207
- WHERE "UserName" = $1
208
- `,
209
- values: [username]
210
- });
211
-
212
- const currentSessions = Number(countRes.rows?.[0]?.count ?? 0);
213
- // If we have MAX_SESSIONS or more, delete oldest to make room for exactly 1 new session
214
- if (currentSessions >= MAX_SESSIONS) {
215
- const sessionsToDelete = currentSessions - MAX_SESSIONS + 1; // +1 to make room for new session
216
- console.log(`[mbkauthe] User "${username}" has ${currentSessions} active sessions, exceeding max of ${MAX_SESSIONS}. Deleting ${sessionsToDelete} oldest sessions.`);
217
-
218
- await dblogin.query({
219
- name: 'prune-oldest-user-session',
220
- // Expired sessions were already removed above; remaining rows are active.
221
- text: `DELETE FROM "Sessions" WHERE id IN (SELECT id FROM "Sessions" WHERE "UserName" = $1 ORDER BY created_at ASC LIMIT $2)`,
222
- values: [username, sessionsToDelete]
215
+ `,
216
+ values: [username]
223
217
  });
224
- }
225
218
 
226
- const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
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.`);
227
223
 
228
- // Insert new session record for the user.
229
- let dbSessionId;
230
- try {
231
- const insertRes = await dblogin.query({
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
+ }
230
+
231
+ const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
232
+ const insertRes = await dbClient.query({
232
233
  name: 'insert-app-session',
233
234
  text: `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`,
234
235
  values: [username, expiresAt, JSON.stringify({ ip: req.ip, ua: req.headers['user-agent'] || null })]
235
236
  });
236
237
  dbSessionId = insertRes.rows[0].id;
237
- } catch (insertErr) {
238
- console.error('[mbkauthe] Error inserting app session:', insertErr);
239
- throw insertErr;
238
+
239
+ await dbClient.query('COMMIT');
240
+ } catch (err) {
241
+ await dbClient.query('ROLLBACK').catch(() => {});
242
+ console.error('[mbkauthe] Error enforcing session limit or inserting app session:', err);
243
+ throw err;
244
+ } finally {
245
+ dbClient.release();
240
246
  }
241
247
 
242
248
  // Update last_login and fetch FullName/Image in a single query.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "4.8.1",
3
+ "version": "4.8.3",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -84,6 +84,7 @@
84
84
  try {
85
85
  const response = await fetch('/mbkauthe/api/verify-2fa', {
86
86
  method: 'POST',
87
+ credentials: 'include',
87
88
  headers: {
88
89
  'Content-Type': 'application/json'
89
90
  },
@@ -287,6 +287,7 @@
287
287
  const pageRedirect = new URLSearchParams(window.location.search).get('redirect');
288
288
  fetch('/mbkauthe/api/login', {
289
289
  method: 'POST',
290
+ credentials: 'include',
290
291
  headers: {
291
292
  'Content-Type': 'application/json'
292
293
  },