mbkauthe 4.8.4 → 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,23 +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
+ import { AuthRepository } from "../db/AuthRepository.js";
10
+ import { createLogger } from "../utils/logger.js";
9
11
 
10
12
  const IS_DEV = process.env.env === 'dev' || process.env.test === 'dev' || process.env.NODE_ENV === 'development';
11
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;
12
14
  const isUuid = (val) => typeof val === 'string' && UUID_RE.test(val);
13
-
14
- const SQL_VALIDATE_APP_SESSION = `
15
- SELECT s.expires_at, u."Active", u."Role"
16
- FROM "Sessions" s
17
- JOIN "Users" u ON s."UserName" = u."UserName"
18
- WHERE s.id = $1
19
- LIMIT 1
20
- `;
15
+ const MAX_API_TOKEN_LENGTH = 4096;
16
+ const API_TOKEN_SESSION_RESTORE = Symbol('mbkauthe.apiTokenSessionRestore');
17
+ const authRepo = new AuthRepository({ db: dblogin });
18
+ const logAuth = createLogger("auth");
21
19
 
22
20
  /**
23
21
  * Decide if the incoming request should return JSON errors instead of HTML.
@@ -63,19 +61,12 @@ async function validateTokenAuthentication(req) {
63
61
 
64
62
  // 1. Check for API Token (mbk_)
65
63
  if (token.startsWith('mbk_')) {
64
+ if (token.length > MAX_API_TOKEN_LENGTH) return { error: 'INVALID_TOKEN' };
65
+
66
66
  const tokenHash = hashApiToken(token);
67
- const tokenQuery = `
68
- SELECT t.id, t."UserName", t."ExpiresAt", t."Permissions",
69
- u.id as uid, u."Active", u."Role", u."AllowedApps" as user_allowed_apps, u."FullName"
70
- FROM "ApiTokens" t
71
- JOIN "Users" u ON t."UserName" = u."UserName"
72
- WHERE t."TokenHash" = $1 LIMIT 1
73
- `;
74
- const tokenResult = await dblogin.query({ name: 'validate-api-token', text: tokenQuery, values: [tokenHash] });
75
-
76
- if (tokenResult.rows.length === 0) return { error: 'INVALID_TOKEN' };
77
-
78
- const row = tokenResult.rows[0];
67
+ const row = await authRepo.getApiTokenByHash(tokenHash);
68
+
69
+ if (!row) return { error: 'INVALID_TOKEN' };
79
70
  if (row.ExpiresAt && new Date(row.ExpiresAt) <= new Date()) return { error: 'TOKEN_EXPIRED' };
80
71
 
81
72
  // Parse permissions from JSONB
@@ -89,11 +80,8 @@ async function validateTokenAuthentication(req) {
89
80
  allowedApps = tokenAllowedApps;
90
81
  }
91
82
 
92
- // Update usage
93
- dblogin.query({
94
- text: 'UPDATE "ApiTokens" SET "LastUsed" = NOW() WHERE id = $1',
95
- values: [row.id]
96
- }).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
83
+ // Update usage opportunistically, but not on every request.
84
+ authRepo.updateApiTokenLastUsed(row.id).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
97
85
 
98
86
  return {
99
87
  id: row.uid,
@@ -111,186 +99,161 @@ async function validateTokenAuthentication(req) {
111
99
  return null;
112
100
  }
113
101
 
114
- async function validateSession(req, res, next, strictTokenValidation = false) {
115
- // --- Check for API Token Header first ---
116
- if (req.headers.authorization) {
117
- // If strict validation is enabled, reject token-based authentication
118
- if (strictTokenValidation) {
119
- return res.status(401).json(createErrorResponse(401, ErrorCodes.INVALID_AUTH_TOKEN, {
120
- message: 'Token-based authentication not allowed for this endpoint',
121
- hint: 'Use session-based authentication (cookies) instead'
122
- }));
123
- }
124
-
125
- try {
126
- const tokenUser = await validateTokenAuthentication(req);
127
-
128
- if (tokenUser && !tokenUser.error) {
129
- if (!tokenUser.active) {
130
- return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
131
- }
132
-
133
- // SuperAdmin bypasses app permission checks
134
- if (tokenUser.role !== "SuperAdmin") {
135
- // API tokens must respect their app restrictions for non-SuperAdmin users
136
- const allowedApps = tokenUser.allowedApps;
137
- 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
+ };
138
112
 
139
- // allowedApps should always be an array (never null at this point)
140
- // If token had null allowedApps, it was already replaced with user's apps in validateTokenAuthentication
141
- if (!Array.isArray(allowedApps) || allowedApps.length === 0) {
142
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
143
- }
113
+ req.auth = {
114
+ type: 'api-token',
115
+ user,
116
+ tokenScope: user.tokenScope,
117
+ allowedApps: user.allowedApps,
118
+ };
144
119
 
145
- // Check if token has access to current app
146
- const hasWildcard = allowedApps.includes('*');
147
- 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
+ });
148
132
 
149
- // If wildcard, check against user's allowed apps (wildcard means "all user's apps", not "all apps")
150
- if (hasWildcard) {
151
- const userHasApp = userAllowedApps && Array.isArray(userAllowedApps) &&
152
- userAllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
153
- if (!userHasApp) {
154
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
155
- }
156
- } else if (!hasSpecificApp) {
157
- 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;
158
145
  }
146
+ delete req.session[API_TOKEN_SESSION_RESTORE];
159
147
  }
148
+ return originalEnd.apply(this, args);
149
+ };
150
+ }
151
+ }
160
152
 
161
- // Populate session for downstream
162
- req.session.user = {
163
- id: tokenUser.id,
164
- username: tokenUser.username,
165
- fullname: tokenUser.fullname,
166
- role: tokenUser.role,
167
- sessionId: tokenUser.sessionId,
168
- allowedApps: tokenUser.allowedApps,
169
- tokenScope: tokenUser.tokenScope || null, // Add scope for token-based auth
170
- };
171
- req.userRole = tokenUser.role;
172
-
173
- // Validate token scope for API token requests
174
- if (tokenUser.tokenScope) {
175
- const requestMethod = req.method;
176
- if (!canAccessMethod(tokenUser.tokenScope, requestMethod)) {
177
- return res.status(403).json(createErrorResponse(403, ErrorCodes.TOKEN_SCOPE_INSUFFICIENT, {
178
- message: `Token scope '${tokenUser.tokenScope}' does not allow ${requestMethod} requests`,
179
- tokenScope: tokenUser.tokenScope,
180
- requestedMethod: requestMethod,
181
- hint: 'Use a token with write scope for write operations'
182
- }));
183
- }
184
- }
153
+ req.user = user;
154
+ req.userRole = tokenUser.role;
155
+ return user;
156
+ }
185
157
 
186
- return next();
187
- }
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
+ }
188
164
 
189
- // Token provided but invalid (or null if format incorrect)
190
- let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
191
- if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
192
- errorCode = ErrorCodes.API_TOKEN_EXPIRED;
193
- }
194
- return res.status(401).json(createErrorResponse(401, errorCode));
165
+ function destroySessionCookies(req, res) {
166
+ req.session?.destroy?.(() => { });
167
+ clearSessionCookies(res);
168
+ }
195
169
 
196
- } catch (err) {
197
- console.error(`[mbkauthe] Token validation error:`, err);
198
- return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
199
- }
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));
200
174
  }
175
+ return renderError(res, req, {
176
+ code,
177
+ error,
178
+ message,
179
+ pagename,
180
+ page,
181
+ });
182
+ }
201
183
 
202
- // --- Fallback to Cookie Session ---
184
+ async function validateCookieSession(req, res, next, { prefersJson }) {
203
185
  if (!req.session.user) {
204
186
  if (IS_DEV) {
205
- console.log(`[mbkauthe] User not authenticated`);
206
- console.log(`[mbkauthe] req.session.user:`, req.session.user);
187
+ logAuth(`User not authenticated`);
188
+ logAuth(`req.session.user: %O`, req.session.user);
207
189
  }
208
- if (isJsonRequest(req)) {
190
+ if (prefersJson) {
209
191
  return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
210
192
  }
211
- return renderError(res, req, {
212
- code: 401,
213
- error: "Not Logged In",
214
- message: "You Are Not Logged In. Please Log In To Continue.",
215
- pagename: "Login",
216
- page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
193
+
194
+ const redirectParams = new URLSearchParams({
195
+ redirect: req.originalUrl,
196
+ reason: 'logged_out'
217
197
  });
198
+ return res.redirect(302, `/mbkauthe/login?${redirectParams.toString()}`);
218
199
  }
219
200
 
220
201
  try {
221
- const { sessionId, role, allowedApps } = req.session.user;
202
+ const { sessionId } = req.session.user;
222
203
 
223
- // Defensive checks for sessionId and allowedApps
224
204
  if (!sessionId || !isUuid(sessionId)) {
225
205
  console.warn(`[mbkauthe] Missing sessionId 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, {
206
+ return respondSessionFailure(req, res, {
207
+ prefersJson,
232
208
  code: 401,
209
+ errorCode: ErrorCodes.SESSION_EXPIRED,
233
210
  error: "Session Expired",
234
211
  message: "Your Session Has Expired. Please Log In Again.",
235
- pagename: "Login",
236
212
  page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
237
213
  });
238
214
  }
239
215
 
240
- // Validate session by DB primary key id and join to user
241
- const result = await dblogin.query({ name: 'validate-app-session', text: SQL_VALIDATE_APP_SESSION, values: [sessionId] });
216
+ const sessionRow = await authRepo.getSessionAuthData(
217
+ sessionId,
218
+ prefersJson ? 'validate-app-session-for-api' : 'validate-app-session'
219
+ );
242
220
 
243
- if (result.rows.length === 0) {
244
- console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
245
- req.session.destroy();
246
- clearSessionCookies(res);
247
- if (isJsonRequest(req)) {
248
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
249
- }
250
- return renderError(res, req, {
221
+ if (!sessionRow) {
222
+ logAuth(`Session not found for user "${req.session.user.username}"`);
223
+ return respondSessionFailure(req, res, {
224
+ prefersJson,
251
225
  code: 401,
226
+ errorCode: prefersJson ? ErrorCodes.SESSION_INVALID : ErrorCodes.SESSION_EXPIRED,
252
227
  error: "Session Expired",
253
228
  message: "Your Session Has Expired. Please Log In Again.",
254
- pagename: "Login",
255
229
  page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
256
230
  });
257
231
  }
258
232
 
259
- const sessionRow = result.rows[0];
260
-
261
- // Check expired
262
233
  if (sessionRow.expires_at) {
263
234
  const expiresMs = sessionRow.expires_at instanceof Date
264
235
  ? sessionRow.expires_at.getTime()
265
236
  : Date.parse(sessionRow.expires_at);
237
+
266
238
  if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
267
- console.log(`[mbkauthe] Session invalidated (expired) for user "${req.session.user.username}"`);
268
- // destroy and clear cookies
269
- req.session.destroy();
270
- clearSessionCookies(res);
271
- if (isJsonRequest(req)) {
272
- 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
+ });
273
248
  }
274
- return renderError(res, req, {
275
- code: 401,
276
- error: "Session Expired",
277
- message: "Your Session Has Expired. Please Log In Again.",
278
- pagename: "Login",
279
- page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
280
- });
281
- }
282
249
  }
283
250
 
284
-
285
251
  if (!sessionRow.Active) {
286
- console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
287
- req.session.destroy();
288
- clearSessionCookies(res);
289
- if (isJsonRequest(req)) {
290
- return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
291
- }
292
- return renderError(res, req, {
252
+ logAuth(`Account is inactive for user "${sessionRow.UserName || req.session.user.username}"`);
253
+ return respondSessionFailure(req, res, {
254
+ prefersJson,
293
255
  code: 401,
256
+ errorCode: ErrorCodes.ACCOUNT_INACTIVE,
294
257
  error: "Account Inactive",
295
258
  message: "Your Account Is Inactive. Please Contact Support.",
296
259
  pagename: "Support",
@@ -298,106 +261,106 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
298
261
  });
299
262
  }
300
263
 
301
- if (role !== "SuperAdmin") {
302
- // If allowedApps is not provided or not an array, treat as no access
303
- const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
304
- if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
305
- console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
306
- req.session.destroy();
307
- clearSessionCookies(res);
308
- if (isJsonRequest(req)) {
309
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
310
- }
311
- return renderError(res, req, {
312
- code: 401,
313
- error: "Unauthorized",
314
- message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"`,
315
- pagename: "Home",
316
- page: `/${mbkautheVar.loginRedirectURL}`
317
- });
318
- }
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
+ });
319
275
  }
320
276
 
321
- // Store user role in request for checkRolePermission to use
322
277
  req.userRole = sessionRow.Role;
323
-
324
- next();
278
+ return next();
325
279
  } catch (err) {
326
280
  console.error(`[mbkauthe] Session validation error:`, err);
327
- res.status(500).json({ success: false, message: "Internal Server Error" });
281
+ return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
328
282
  }
329
283
  }
330
284
 
331
- /**
332
- * API-friendly session validation middleware
333
- * Returns JSON error responses instead of rendering pages
334
- */
335
- async function validateApiSession(req, res, next) {
336
- if (!req.session.user) {
337
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
338
- }
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
+ }
339
293
 
340
- try {
341
- const { sessionId, role, allowedApps } = req.session.user;
294
+ try {
295
+ const tokenUser = await validateTokenAuthentication(req);
342
296
 
343
- // Defensive checks for sessionId and allowedApps
344
- if (!sessionId || !isUuid(sessionId)) {
345
- console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
346
- req.session.destroy();
347
- clearSessionCookies(res);
348
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
349
- }
297
+ if (tokenUser && !tokenUser.error) {
298
+ if (!tokenUser.active) {
299
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
300
+ }
350
301
 
351
- // Validate session by DB primary key id and join to user
352
- const result = await dblogin.query({ name: 'validate-app-session-for-api', text: SQL_VALIDATE_APP_SESSION, values: [sessionId] });
302
+ if (tokenUser.role !== "SuperAdmin") {
303
+ const allowedApps = tokenUser.allowedApps;
304
+ const userAllowedApps = tokenUser.userAllowedApps;
353
305
 
354
- if (result.rows.length === 0) {
355
- req.session.destroy();
356
- clearSessionCookies(res);
357
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
358
- }
306
+ if (!Array.isArray(allowedApps) || allowedApps.length === 0) {
307
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
308
+ }
359
309
 
360
- const sessionRow = result.rows[0];
310
+ const hasWildcard = allowedApps.includes('*');
311
+ const hasSpecificApp = allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
361
312
 
362
- // Check expired
363
- if (sessionRow.expires_at) {
364
- const expiresMs = sessionRow.expires_at instanceof Date
365
- ? sessionRow.expires_at.getTime()
366
- : Date.parse(sessionRow.expires_at);
367
- if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
368
- req.session.destroy();
369
- clearSessionCookies(res);
370
- return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
371
- }
372
- }
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
+ }
373
323
 
324
+ attachApiTokenUser(req, res, tokenUser);
374
325
 
375
- if (!result.rows[0].Active) {
376
- console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
377
- req.session.destroy();
378
- clearSessionCookies(res);
379
- return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
380
- }
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
+ }
381
337
 
382
- if (role !== "SuperAdmin") {
383
- // If allowedApps is not provided or not an array, treat as no access
384
- const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
385
- if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
386
- console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
387
- req.session.destroy();
388
- clearSessionCookies(res);
389
- return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
338
+ return next();
390
339
  }
340
+
341
+ let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
342
+ if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
343
+ errorCode = ErrorCodes.API_TOKEN_EXPIRED;
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));
391
349
  }
350
+ }
392
351
 
393
- // Store user role in request for checkRolePermission to use
394
- req.userRole = sessionRow.Role;
352
+ return validateCookieSession(req, res, next, { prefersJson: isJsonRequest(req) });
353
+ }
395
354
 
396
- next();
397
- } catch (err) {
398
- console.error(`[mbkauthe] API session validation error:`, err);
399
- 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);
400
362
  }
363
+ return validateCookieSession(req, res, next, { prefersJson: true });
401
364
  }
402
365
 
403
366
  /**
@@ -411,7 +374,7 @@ async function validateApiSession(req, res, next) {
411
374
  async function reloadSessionUser(req, res) {
412
375
  if (!req.session || !req.session.user || !req.session.user.id) return false;
413
376
  try {
414
- const { id, sessionId: currentSessionId } = req.session.user;
377
+ const { sessionId: currentSessionId } = req.session.user;
415
378
 
416
379
  if (!currentSessionId) {
417
380
  req.session.destroy(() => { });
@@ -420,21 +383,15 @@ async function reloadSessionUser(req, res) {
420
383
  }
421
384
 
422
385
  const normalizedSessionId = String(currentSessionId);
423
- const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
424
- FROM "Sessions" s
425
- JOIN "Users" u ON s."UserName" = u."UserName"
426
- WHERE s.id = $1 LIMIT 1`;
427
- const result = await dblogin.query({ name: 'reload-session-user', text: query, values: [normalizedSessionId] });
386
+ const row = await authRepo.getSessionWithUserForReload(normalizedSessionId, 'reload-session-user');
428
387
 
429
- if (result.rows.length === 0) {
388
+ if (!row) {
430
389
  // Session not found — invalidate session
431
390
  req.session.destroy(() => { });
432
391
  clearSessionCookies(res);
433
392
  return false;
434
393
  }
435
394
 
436
- const row = result.rows[0];
437
-
438
395
  // Check expired
439
396
  if (row.expires_at && new Date(row.expires_at) <= new Date()) {
440
397
  req.session.destroy(() => { });
@@ -466,15 +423,10 @@ async function reloadSessionUser(req, res) {
466
423
  req.session.user.allowedApps = row.AllowedApps;
467
424
 
468
425
  // Obtain fullname from client cookie cache when present else DB
469
- 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') {
470
429
  req.session.user.fullname = req.cookies.fullName;
471
- } else {
472
- try {
473
- const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
474
- if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
475
- } catch (profileErr) {
476
- console.error(`[mbkauthe] Error fetching fullname during reload:`, profileErr);
477
- }
478
430
  }
479
431
 
480
432
  // Persist session changes
@@ -501,8 +453,10 @@ async function reloadSessionUser(req, res) {
501
453
  const checkRolePermission = (requiredRoles, notAllowed) => {
502
454
  return async (req, res, next) => {
503
455
  try {
504
- if (!req.session || !req.session.user || !req.session.user.id) {
505
- 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`);
506
460
  if (isJsonRequest(req)) {
507
461
  return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
508
462
  }
@@ -516,10 +470,10 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
516
470
  }
517
471
 
518
472
  // Use role from validateSession to avoid additional DB query
519
- const userRole = req.userRole;
473
+ const userRole = req.userRole || authUser.role;
520
474
 
521
475
  // SuperAdmin bypasses all role checks
522
- if(req.session.user?.role === "SuperAdmin" || userRole === "SuperAdmin") {
476
+ if(authUser.role === "SuperAdmin" || userRole === "SuperAdmin") {
523
477
  return next();
524
478
  }
525
479
 
@@ -579,10 +533,10 @@ const authenticate = (authentication) => {
579
533
  return (req, res, next) => {
580
534
  const token = extractAuthorizationToken(req.headers?.authorization ?? req.headers?.["authorization"]);
581
535
  if (timingSafeTokenMatch(token, authentication)) {
582
- console.log(`[mbkauthe] Authentication successful`);
536
+ logAuth(`Authentication successful`);
583
537
  next();
584
538
  } else {
585
- console.log(`[mbkauthe] Authentication failed`);
539
+ logAuth(`Authentication failed`);
586
540
  res.status(401).send("Unauthorized");
587
541
  }
588
542
  };