mbkauthe 4.9.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,21 @@
1
1
  import { dblogin } from "#pool.js";
2
2
  import { mbkautheVar } from "#config.js";
3
3
  import { renderError } from "#response.js";
4
- import { clearSessionCookies, cachedCookieOptions, readAccountListFromCookie, encryptSessionId } from "#cookies.js";
4
+ import { clearSessionCookies, cachedCookieOptions, encryptSessionId } from "#cookies.js";
5
5
  import { ErrorCodes, createErrorResponse } from "../utils/errors.js";
6
6
  import { hashApiToken } from "#config.js";
7
7
  import { canAccessMethod } from "#config.js";
8
8
  import { extractAuthorizationToken, timingSafeTokenMatch } from "../utils/timingSafeToken.js";
9
9
  import { AuthRepository } from "../db/AuthRepository.js";
10
+ import { createLogger } from "../utils/logger.js";
10
11
 
11
12
  const IS_DEV = process.env.env === 'dev' || process.env.test === 'dev' || process.env.NODE_ENV === 'development';
12
13
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
14
  const isUuid = (val) => typeof val === 'string' && UUID_RE.test(val);
15
+ const MAX_API_TOKEN_LENGTH = 4096;
16
+ const API_TOKEN_SESSION_RESTORE = Symbol('mbkauthe.apiTokenSessionRestore');
14
17
  const authRepo = new AuthRepository({ db: dblogin });
18
+ const logAuth = createLogger("auth");
15
19
 
16
20
  /**
17
21
  * Decide if the incoming request should return JSON errors instead of HTML.
@@ -57,6 +61,8 @@ async function validateTokenAuthentication(req) {
57
61
 
58
62
  // 1. Check for API Token (mbk_)
59
63
  if (token.startsWith('mbk_')) {
64
+ if (token.length > MAX_API_TOKEN_LENGTH) return { error: 'INVALID_TOKEN' };
65
+
60
66
  const tokenHash = hashApiToken(token);
61
67
  const row = await authRepo.getApiTokenByHash(tokenHash);
62
68
 
@@ -74,7 +80,7 @@ async function validateTokenAuthentication(req) {
74
80
  allowedApps = tokenAllowedApps;
75
81
  }
76
82
 
77
- // Update usage
83
+ // Update usage opportunistically, but not on every request.
78
84
  authRepo.updateApiTokenLastUsed(row.id).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
79
85
 
80
86
  return {
@@ -93,104 +99,98 @@ async function validateTokenAuthentication(req) {
93
99
  return null;
94
100
  }
95
101
 
96
- async function validateSession(req, res, next, strictTokenValidation = false) {
97
- // --- Check for API Token Header first ---
98
- if (req.headers.authorization) {
99
- // If strict validation is enabled, reject token-based authentication
100
- if (strictTokenValidation) {
101
- return res.status(401).json(createErrorResponse(401, ErrorCodes.INVALID_AUTH_TOKEN, {
102
- message: 'Token-based authentication not allowed for this endpoint',
103
- hint: 'Use session-based authentication (cookies) instead'
104
- }));
105
- }
106
-
107
- try {
108
- const tokenUser = await validateTokenAuthentication(req);
109
-
110
- if (tokenUser && !tokenUser.error) {
111
- if (!tokenUser.active) {
112
- return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
113
- }
114
-
115
- // SuperAdmin bypasses app permission checks
116
- if (tokenUser.role !== "SuperAdmin") {
117
- // API tokens must respect their app restrictions for non-SuperAdmin users
118
- const allowedApps = tokenUser.allowedApps;
119
- const userAllowedApps = tokenUser.userAllowedApps;
102
+ function attachApiTokenUser(req, res, tokenUser) {
103
+ const user = {
104
+ id: tokenUser.id,
105
+ username: tokenUser.username,
106
+ fullname: tokenUser.fullname,
107
+ role: tokenUser.role,
108
+ sessionId: tokenUser.sessionId,
109
+ allowedApps: tokenUser.allowedApps,
110
+ tokenScope: tokenUser.tokenScope || null,
111
+ };
120
112
 
121
- // allowedApps should always be an array (never null at this point)
122
- // If token had null allowedApps, it was already replaced with user's apps in validateTokenAuthentication
123
- if (!Array.isArray(allowedApps) || allowedApps.length === 0) {
124
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
125
- }
113
+ req.auth = {
114
+ type: 'api-token',
115
+ user,
116
+ tokenScope: user.tokenScope,
117
+ allowedApps: user.allowedApps,
118
+ };
126
119
 
127
- // Check if token has access to current app
128
- const hasWildcard = allowedApps.includes('*');
129
- const hasSpecificApp = allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
120
+ // Backwards compatibility: existing protected routes commonly read
121
+ // req.session.user. For API tokens this must be request-local so the
122
+ // Postgres session store is not dirtied/saved on every token request.
123
+ if (req.session) {
124
+ const originalDescriptor = Object.getOwnPropertyDescriptor(req.session, 'user');
125
+
126
+ Object.defineProperty(req.session, 'user', {
127
+ value: user,
128
+ enumerable: false,
129
+ configurable: true,
130
+ writable: true,
131
+ });
130
132
 
131
- // If wildcard, check against user's allowed apps (wildcard means "all user's apps", not "all apps")
132
- if (hasWildcard) {
133
- const userHasApp = userAllowedApps && Array.isArray(userAllowedApps) &&
134
- userAllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
135
- if (!userHasApp) {
136
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
137
- }
138
- } else if (!hasSpecificApp) {
139
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
133
+ if (res && !req.session[API_TOKEN_SESSION_RESTORE]) {
134
+ req.session[API_TOKEN_SESSION_RESTORE] = true;
135
+ const originalEnd = res.end;
136
+ let restored = false;
137
+
138
+ res.end = function apiTokenSessionEnd(...args) {
139
+ if (!restored) {
140
+ restored = true;
141
+ if (originalDescriptor) {
142
+ Object.defineProperty(req.session, 'user', originalDescriptor);
143
+ } else {
144
+ delete req.session.user;
140
145
  }
146
+ delete req.session[API_TOKEN_SESSION_RESTORE];
141
147
  }
148
+ return originalEnd.apply(this, args);
149
+ };
150
+ }
151
+ }
142
152
 
143
- // Populate session for downstream
144
- req.session.user = {
145
- id: tokenUser.id,
146
- username: tokenUser.username,
147
- fullname: tokenUser.fullname,
148
- role: tokenUser.role,
149
- sessionId: tokenUser.sessionId,
150
- allowedApps: tokenUser.allowedApps,
151
- tokenScope: tokenUser.tokenScope || null, // Add scope for token-based auth
152
- };
153
- req.userRole = tokenUser.role;
154
-
155
- // Validate token scope for API token requests
156
- if (tokenUser.tokenScope) {
157
- const requestMethod = req.method;
158
- if (!canAccessMethod(tokenUser.tokenScope, requestMethod)) {
159
- return res.status(403).json(createErrorResponse(403, ErrorCodes.TOKEN_SCOPE_INSUFFICIENT, {
160
- message: `Token scope '${tokenUser.tokenScope}' does not allow ${requestMethod} requests`,
161
- tokenScope: tokenUser.tokenScope,
162
- requestedMethod: requestMethod,
163
- hint: 'Use a token with write scope for write operations'
164
- }));
165
- }
166
- }
153
+ req.user = user;
154
+ req.userRole = tokenUser.role;
155
+ return user;
156
+ }
167
157
 
168
- return next();
169
- }
158
+ function hasAppAccess(role, allowedApps) {
159
+ if (role === "SuperAdmin") return true;
160
+ return Array.isArray(allowedApps)
161
+ && allowedApps.length > 0
162
+ && allowedApps.some((app) => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
163
+ }
170
164
 
171
- // Token provided but invalid (or null if format incorrect)
172
- let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
173
- if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
174
- errorCode = ErrorCodes.API_TOKEN_EXPIRED;
175
- }
176
- return res.status(401).json(createErrorResponse(401, errorCode));
165
+ function destroySessionCookies(req, res) {
166
+ req.session?.destroy?.(() => { });
167
+ clearSessionCookies(res);
168
+ }
177
169
 
178
- } catch (err) {
179
- console.error(`[mbkauthe] Token validation error:`, err);
180
- return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
181
- }
170
+ function respondSessionFailure(req, res, { prefersJson, code, errorCode, error, message, page, pagename = "Login" }) {
171
+ destroySessionCookies(req, res);
172
+ if (prefersJson) {
173
+ return res.status(code).json(createErrorResponse(code, errorCode));
182
174
  }
175
+ return renderError(res, req, {
176
+ code,
177
+ error,
178
+ message,
179
+ pagename,
180
+ page,
181
+ });
182
+ }
183
183
 
184
- // --- Fallback to Cookie Session ---
184
+ async function validateCookieSession(req, res, next, { prefersJson }) {
185
185
  if (!req.session.user) {
186
186
  if (IS_DEV) {
187
- console.log(`[mbkauthe] User not authenticated`);
188
- console.log(`[mbkauthe] req.session.user:`, req.session.user);
187
+ logAuth(`User not authenticated`);
188
+ logAuth(`req.session.user: %O`, req.session.user);
189
189
  }
190
- if (isJsonRequest(req)) {
190
+ if (prefersJson) {
191
191
  return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
192
192
  }
193
- // Use OAuth 2.0 style redirect to the login page for browser navigation
193
+
194
194
  const redirectParams = new URLSearchParams({
195
195
  redirect: req.originalUrl,
196
196
  reason: 'logged_out'
@@ -199,77 +199,61 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
199
199
  }
200
200
 
201
201
  try {
202
- const { sessionId, role, allowedApps } = req.session.user;
202
+ const { sessionId } = req.session.user;
203
203
 
204
- // Defensive checks for sessionId and allowedApps
205
204
  if (!sessionId || !isUuid(sessionId)) {
206
205
  console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
207
- req.session.destroy();
208
- clearSessionCookies(res);
209
- if (isJsonRequest(req)) {
210
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
211
- }
212
- return renderError(res, req, {
206
+ return respondSessionFailure(req, res, {
207
+ prefersJson,
213
208
  code: 401,
209
+ errorCode: ErrorCodes.SESSION_EXPIRED,
214
210
  error: "Session Expired",
215
211
  message: "Your Session Has Expired. Please Log In Again.",
216
- pagename: "Login",
217
212
  page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
218
213
  });
219
214
  }
220
215
 
221
- // Validate session by DB primary key id and join to user
222
- const sessionRow = await authRepo.getSessionAuthData(sessionId, 'validate-app-session');
216
+ const sessionRow = await authRepo.getSessionAuthData(
217
+ sessionId,
218
+ prefersJson ? 'validate-app-session-for-api' : 'validate-app-session'
219
+ );
223
220
 
224
221
  if (!sessionRow) {
225
- console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
226
- req.session.destroy();
227
- clearSessionCookies(res);
228
- if (isJsonRequest(req)) {
229
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
230
- }
231
- return renderError(res, req, {
222
+ logAuth(`Session not found for user "${req.session.user.username}"`);
223
+ return respondSessionFailure(req, res, {
224
+ prefersJson,
232
225
  code: 401,
226
+ errorCode: prefersJson ? ErrorCodes.SESSION_INVALID : ErrorCodes.SESSION_EXPIRED,
233
227
  error: "Session Expired",
234
228
  message: "Your Session Has Expired. Please Log In Again.",
235
- pagename: "Login",
236
229
  page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
237
230
  });
238
231
  }
239
232
 
240
- // Check expired
241
233
  if (sessionRow.expires_at) {
242
234
  const expiresMs = sessionRow.expires_at instanceof Date
243
235
  ? sessionRow.expires_at.getTime()
244
236
  : Date.parse(sessionRow.expires_at);
237
+
245
238
  if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
246
- console.log(`[mbkauthe] Session invalidated (expired) for user "${req.session.user.username}"`);
247
- // destroy and clear cookies
248
- req.session.destroy();
249
- clearSessionCookies(res);
250
- if (isJsonRequest(req)) {
251
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
239
+ logAuth(`Session invalidated (expired) for user "${sessionRow.UserName || req.session.user.username}"`);
240
+ return respondSessionFailure(req, res, {
241
+ prefersJson,
242
+ code: 401,
243
+ errorCode: ErrorCodes.SESSION_EXPIRED,
244
+ error: "Session Expired",
245
+ message: "Your Session Has Expired. Please Log In Again.",
246
+ page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
247
+ });
252
248
  }
253
- return renderError(res, req, {
254
- code: 401,
255
- error: "Session Expired",
256
- message: "Your Session Has Expired. Please Log In Again.",
257
- pagename: "Login",
258
- page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
259
- });
260
249
  }
261
- }
262
-
263
250
 
264
251
  if (!sessionRow.Active) {
265
- console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
266
- req.session.destroy();
267
- clearSessionCookies(res);
268
- if (isJsonRequest(req)) {
269
- return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
270
- }
271
- return renderError(res, req, {
252
+ logAuth(`Account is inactive for user "${sessionRow.UserName || req.session.user.username}"`);
253
+ return respondSessionFailure(req, res, {
254
+ prefersJson,
272
255
  code: 401,
256
+ errorCode: ErrorCodes.ACCOUNT_INACTIVE,
273
257
  error: "Account Inactive",
274
258
  message: "Your Account Is Inactive. Please Contact Support.",
275
259
  pagename: "Support",
@@ -277,104 +261,106 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
277
261
  });
278
262
  }
279
263
 
280
- if (role !== "SuperAdmin") {
281
- // If allowedApps is not provided or not an array, treat as no access
282
- const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
283
- if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
284
- console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
285
- req.session.destroy();
286
- clearSessionCookies(res);
287
- if (isJsonRequest(req)) {
288
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
289
- }
290
- return renderError(res, req, {
291
- code: 401,
292
- error: "Unauthorized",
293
- message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"`,
294
- pagename: "Home",
295
- page: `/${mbkautheVar.loginRedirectURL}`
296
- });
297
- }
264
+ if (!hasAppAccess(sessionRow.Role, sessionRow.AllowedApps)) {
265
+ console.warn(`[mbkauthe] User "${sessionRow.UserName || req.session.user.username}" is not authorized to use the application "${mbkautheVar.APP_NAME}"`);
266
+ return respondSessionFailure(req, res, {
267
+ prefersJson,
268
+ code: 401,
269
+ errorCode: ErrorCodes.APP_NOT_AUTHORIZED,
270
+ error: "Unauthorized",
271
+ message: `You Are Not Authorized To Use The Application "${mbkautheVar.APP_NAME}"`,
272
+ pagename: "Home",
273
+ page: `/${mbkautheVar.loginRedirectURL}`
274
+ });
298
275
  }
299
276
 
300
- // Store user role in request for checkRolePermission to use
301
277
  req.userRole = sessionRow.Role;
302
-
303
- next();
278
+ return next();
304
279
  } catch (err) {
305
280
  console.error(`[mbkauthe] Session validation error:`, err);
306
- res.status(500).json({ success: false, message: "Internal Server Error" });
281
+ return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
307
282
  }
308
283
  }
309
284
 
310
- /**
311
- * API-friendly session validation middleware
312
- * Returns JSON error responses instead of rendering pages
313
- */
314
- async function validateApiSession(req, res, next) {
315
- if (!req.session.user) {
316
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
317
- }
285
+ async function validateSession(req, res, next, strictTokenValidation = false) {
286
+ if (req.headers.authorization) {
287
+ if (strictTokenValidation) {
288
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.INVALID_AUTH_TOKEN, {
289
+ message: 'Token-based authentication not allowed for this endpoint',
290
+ hint: 'Use session-based authentication (cookies) instead'
291
+ }));
292
+ }
318
293
 
319
- try {
320
- const { sessionId, role, allowedApps } = req.session.user;
294
+ try {
295
+ const tokenUser = await validateTokenAuthentication(req);
321
296
 
322
- // Defensive checks for sessionId and allowedApps
323
- if (!sessionId || !isUuid(sessionId)) {
324
- console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
325
- req.session.destroy();
326
- clearSessionCookies(res);
327
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
328
- }
297
+ if (tokenUser && !tokenUser.error) {
298
+ if (!tokenUser.active) {
299
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
300
+ }
329
301
 
330
- // Validate session by DB primary key id and join to user
331
- const sessionRow = await authRepo.getSessionAuthData(sessionId, 'validate-app-session-for-api');
302
+ if (tokenUser.role !== "SuperAdmin") {
303
+ const allowedApps = tokenUser.allowedApps;
304
+ const userAllowedApps = tokenUser.userAllowedApps;
332
305
 
333
- if (!sessionRow) {
334
- req.session.destroy();
335
- clearSessionCookies(res);
336
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
337
- }
306
+ if (!Array.isArray(allowedApps) || allowedApps.length === 0) {
307
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
308
+ }
338
309
 
339
- // Check expired
340
- if (sessionRow.expires_at) {
341
- const expiresMs = sessionRow.expires_at instanceof Date
342
- ? sessionRow.expires_at.getTime()
343
- : Date.parse(sessionRow.expires_at);
344
- if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
345
- req.session.destroy();
346
- clearSessionCookies(res);
347
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
348
- }
349
- }
310
+ const hasWildcard = allowedApps.includes('*');
311
+ const hasSpecificApp = allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
350
312
 
313
+ if (hasWildcard) {
314
+ const userHasApp = Array.isArray(userAllowedApps)
315
+ && userAllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
316
+ if (!userHasApp) {
317
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
318
+ }
319
+ } else if (!hasSpecificApp) {
320
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
321
+ }
322
+ }
351
323
 
352
- if (!sessionRow.Active) {
353
- console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
354
- req.session.destroy();
355
- clearSessionCookies(res);
356
- return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
357
- }
324
+ attachApiTokenUser(req, res, tokenUser);
358
325
 
359
- if (role !== "SuperAdmin") {
360
- // If allowedApps is not provided or not an array, treat as no access
361
- const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
362
- if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
363
- console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
364
- req.session.destroy();
365
- clearSessionCookies(res);
366
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
326
+ if (tokenUser.tokenScope) {
327
+ const requestMethod = req.method;
328
+ if (!canAccessMethod(tokenUser.tokenScope, requestMethod)) {
329
+ return res.status(403).json(createErrorResponse(403, ErrorCodes.TOKEN_SCOPE_INSUFFICIENT, {
330
+ message: `Token scope '${tokenUser.tokenScope}' does not allow ${requestMethod} requests`,
331
+ tokenScope: tokenUser.tokenScope,
332
+ requestedMethod: requestMethod,
333
+ hint: 'Use a token with write scope for write operations'
334
+ }));
335
+ }
336
+ }
337
+
338
+ return next();
339
+ }
340
+
341
+ let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
342
+ if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
343
+ errorCode = ErrorCodes.API_TOKEN_EXPIRED;
367
344
  }
345
+ return res.status(401).json(createErrorResponse(401, errorCode));
346
+ } catch (err) {
347
+ console.error(`[mbkauthe] Token validation error:`, err);
348
+ return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
368
349
  }
350
+ }
369
351
 
370
- // Store user role in request for checkRolePermission to use
371
- req.userRole = sessionRow.Role;
352
+ return validateCookieSession(req, res, next, { prefersJson: isJsonRequest(req) });
353
+ }
372
354
 
373
- next();
374
- } catch (err) {
375
- console.error(`[mbkauthe] API session validation error:`, err);
376
- return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
355
+ /**
356
+ * API-friendly session validation middleware
357
+ * Returns JSON error responses instead of rendering pages
358
+ */
359
+ async function validateApiSession(req, res, next) {
360
+ if (req.headers.authorization) {
361
+ return validateSession(req, res, next);
377
362
  }
363
+ return validateCookieSession(req, res, next, { prefersJson: true });
378
364
  }
379
365
 
380
366
  /**
@@ -388,7 +374,7 @@ async function validateApiSession(req, res, next) {
388
374
  async function reloadSessionUser(req, res) {
389
375
  if (!req.session || !req.session.user || !req.session.user.id) return false;
390
376
  try {
391
- const { id, sessionId: currentSessionId } = req.session.user;
377
+ const { sessionId: currentSessionId } = req.session.user;
392
378
 
393
379
  if (!currentSessionId) {
394
380
  req.session.destroy(() => { });
@@ -437,15 +423,10 @@ async function reloadSessionUser(req, res) {
437
423
  req.session.user.allowedApps = row.AllowedApps;
438
424
 
439
425
  // Obtain fullname from client cookie cache when present else DB
440
- if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
426
+ if (typeof row.FullName === 'string' && row.FullName.trim() !== '') {
427
+ req.session.user.fullname = row.FullName;
428
+ } else if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
441
429
  req.session.user.fullname = req.cookies.fullName;
442
- } else {
443
- try {
444
- const prof = await authRepo.getUserFullNameByUsername(row.UserName, 'reload-get-fullname');
445
- if (prof && prof.FullName) req.session.user.fullname = prof.FullName;
446
- } catch (profileErr) {
447
- console.error(`[mbkauthe] Error fetching fullname during reload:`, profileErr);
448
- }
449
430
  }
450
431
 
451
432
  // Persist session changes
@@ -472,8 +453,10 @@ async function reloadSessionUser(req, res) {
472
453
  const checkRolePermission = (requiredRoles, notAllowed) => {
473
454
  return async (req, res, next) => {
474
455
  try {
475
- if (!req.session || !req.session.user || !req.session.user.id) {
476
- console.log(`[mbkauthe] User not authenticated`);
456
+ const authUser = req.auth?.user || req.session?.user;
457
+
458
+ if (!authUser || !authUser.id) {
459
+ logAuth(`User not authenticated`);
477
460
  if (isJsonRequest(req)) {
478
461
  return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
479
462
  }
@@ -487,10 +470,10 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
487
470
  }
488
471
 
489
472
  // Use role from validateSession to avoid additional DB query
490
- const userRole = req.userRole;
473
+ const userRole = req.userRole || authUser.role;
491
474
 
492
475
  // SuperAdmin bypasses all role checks
493
- if(req.session.user?.role === "SuperAdmin" || userRole === "SuperAdmin") {
476
+ if(authUser.role === "SuperAdmin" || userRole === "SuperAdmin") {
494
477
  return next();
495
478
  }
496
479
 
@@ -550,10 +533,10 @@ const authenticate = (authentication) => {
550
533
  return (req, res, next) => {
551
534
  const token = extractAuthorizationToken(req.headers?.authorization ?? req.headers?.["authorization"]);
552
535
  if (timingSafeTokenMatch(token, authentication)) {
553
- console.log(`[mbkauthe] Authentication successful`);
536
+ logAuth(`Authentication successful`);
554
537
  next();
555
538
  } else {
556
- console.log(`[mbkauthe] Authentication failed`);
539
+ logAuth(`Authentication failed`);
557
540
  res.status(401).send("Unauthorized");
558
541
  }
559
542
  };
@@ -35,6 +35,7 @@ export const sessionConfig = {
35
35
  };
36
36
 
37
37
  const authRepo = new AuthRepository({ db: dblogin });
38
+ const hasAuthorizationHeader = (req) => typeof req.headers?.authorization === 'string' && req.headers.authorization.trim().length > 0;
38
39
 
39
40
  // CORS middleware
40
41
  export function corsMiddleware(req, res, next) {
@@ -60,6 +61,10 @@ export function corsMiddleware(req, res, next) {
60
61
 
61
62
  // Session restoration middleware
62
63
  export async function sessionRestorationMiddleware(req, res, next) {
64
+ if (hasAuthorizationHeader(req)) {
65
+ return next();
66
+ }
67
+
63
68
  // Only restore session if not already present and sessionId cookie exists
64
69
  if (!req.session.user && req.cookies.sessionId) {
65
70
  // Decrypt the sessionId from cookie
@@ -83,33 +88,23 @@ export async function sessionRestorationMiddleware(req, res, next) {
83
88
  const row = await authRepo.getSessionWithUserById(sessionId, 'restore-user-session');
84
89
 
85
90
  if (row) {
86
-
87
91
  // Reject expired sessions or inactive users
88
92
  if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
89
93
  // leave cookies cleared and don't restore session
90
94
  } else {
91
95
  const normalizedSessionId = String(sessionId);
92
96
  req.session.user = {
93
- id: row.id,
97
+ id: row.uid,
94
98
  username: row.UserName,
95
99
  role: row.Role,
96
100
  sessionId: normalizedSessionId,
97
101
  allowedApps: row.AllowedApps,
98
102
  };
99
103
 
100
- // Use cached FullName from client cookie when available to avoid extra DB queries
101
- if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
104
+ if (typeof row.FullName === 'string' && row.FullName.trim() !== '') {
105
+ req.session.user.fullname = row.FullName;
106
+ } else if (req.cookies && typeof req.cookies.fullName === 'string') {
102
107
  req.session.user.fullname = req.cookies.fullName;
103
- } else {
104
- // Fallback: attempt to fetch FullName from Users to populate session
105
- try {
106
- const profileRes = await authRepo.getUserFullNameByUsername(row.UserName, 'restore-get-fullname');
107
- if (profileRes && profileRes.FullName) {
108
- req.session.user.fullname = profileRes.FullName;
109
- }
110
- } catch (profileErr) {
111
- console.error(`[mbkauthe] Error fetching FullName during session restore:`, profileErr);
112
- }
113
108
  }
114
109
  }
115
110
  }
@@ -122,6 +117,10 @@ export async function sessionRestorationMiddleware(req, res, next) {
122
117
 
123
118
  // Session cookie sync middleware
124
119
  export function sessionCookieSyncMiddleware(req, res, next) {
120
+ if (hasAuthorizationHeader(req) || req.auth?.type === 'api-token') {
121
+ return next();
122
+ }
123
+
125
124
  if (req.session && req.session.user) {
126
125
  // Decrypt existing cookie to compare with session
127
126
  const currentDecryptedId = decryptSessionId(req.cookies.sessionId);