mbkauthe 5.0.1 → 5.0.4

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.
@@ -5,140 +5,181 @@ import { createLogger } from "../utils/logger.js";
5
5
  dotenv.config();
6
6
  const logConfig = createLogger("config");
7
7
 
8
- // Comprehensive validation function
9
- function validateConfiguration() {
10
- const errors = [];
8
+ const CONFIG_KEYS = [
9
+ "APP_NAME", "DEVICE_TRUST_DURATION_DAYS", "Main_SECRET_TOKEN", "SESSION_SECRET_KEY",
10
+ "IS_DEPLOYED", "LOGIN_DB", "MBKAUTH_TWO_FA_ENABLE", "COOKIE_EXPIRE_TIME", "DOMAIN", "loginRedirectURL",
11
+ "GITHUB_LOGIN_ENABLED", "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID", "GITHUB_APP_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "GOOGLE_LOGIN_ENABLED", "GOOGLE_CLIENT_ID",
12
+ "GOOGLE_CLIENT_SECRET", "MAX_SESSIONS_PER_USER"
13
+ ];
14
+
15
+ const DEFAULT_CONFIG = {
16
+ DEVICE_TRUST_DURATION_DAYS: 7,
17
+ IS_DEPLOYED: 'false',
18
+ MBKAUTH_TWO_FA_ENABLE: 'false',
19
+ COOKIE_EXPIRE_TIME: 2,
20
+ loginRedirectURL: '/dashboard',
21
+ GITHUB_LOGIN_ENABLED: 'false',
22
+ GOOGLE_LOGIN_ENABLED: 'false',
23
+ MAX_SESSIONS_PER_USER: 5
24
+ };
25
+
26
+ const BOOLEAN_KEYS = ['GITHUB_LOGIN_ENABLED', 'GOOGLE_LOGIN_ENABLED', 'MBKAUTH_TWO_FA_ENABLE', 'IS_DEPLOYED'];
27
+ const STRING_KEYS = [
28
+ "APP_NAME", "Main_SECRET_TOKEN", "SESSION_SECRET_KEY", "LOGIN_DB", "DOMAIN", "loginRedirectURL",
29
+ "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID", "GITHUB_APP_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET",
30
+ "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"
31
+ ];
32
+ const REQUIRED_KEYS = ["APP_NAME", "Main_SECRET_TOKEN", "SESSION_SECRET_KEY", "IS_DEPLOYED", "LOGIN_DB",
33
+ "MBKAUTH_TWO_FA_ENABLE", "DOMAIN"];
34
+
35
+ const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
36
+ const isBlank = (value) => value === undefined || value === null || (typeof value === 'string' && value.trim() === '');
37
+ const hasValue = (value) => !isBlank(value);
38
+
39
+ function parseJsonEnv(name, { required = false } = {}) {
40
+ const raw = process.env[name];
41
+ if (isBlank(raw)) {
42
+ if (required) {
43
+ throw new Error(`[mbkauthe] Configuration Error:\n - process.env.${name} is not defined`);
44
+ }
45
+ return null;
46
+ }
11
47
 
12
- // Parse and validate mbkautheVar
13
- let mbkautheVar;
14
48
  try {
15
- if (!process.env.mbkautheVar) {
16
- errors.push("process.env.mbkautheVar is not defined");
17
- throw new Error("Configuration validation failed");
49
+ const parsed = JSON.parse(raw);
50
+ if (!isPlainObject(parsed)) {
51
+ throw new Error(`${name} must be a valid object`);
18
52
  }
19
- mbkautheVar = JSON.parse(process.env.mbkautheVar);
53
+ return parsed;
20
54
  } catch (error) {
21
- if (error.message === "Configuration validation failed") {
22
- throw new Error(`[mbkauthe] Configuration Error:\n - ${errors.join('\n - ')}`);
55
+ const message = error.message === `${name} must be a valid object`
56
+ ? error.message
57
+ : `Invalid JSON in process.env.${name}`;
58
+ if (required) {
59
+ throw new Error(`[mbkauthe] Configuration Error:\n - ${message}`);
23
60
  }
24
- errors.push("Invalid JSON in process.env.mbkautheVar");
25
- throw new Error(`[mbkauthe] Configuration Error:\n - ${errors.join('\n - ')}`);
61
+ console.warn(`[mbkauthe] ${message}, ignoring it`);
62
+ return null;
26
63
  }
64
+ }
27
65
 
28
- if (!mbkautheVar || typeof mbkautheVar !== 'object') {
29
- errors.push("mbkautheVar must be a valid object");
30
- throw new Error(`[mbkauthe] Configuration Error:\n - ${errors.join('\n - ')}`);
31
- }
66
+ function applySharedFallbacks(config, sharedConfig, usedFromShared) {
67
+ if (!sharedConfig) return;
32
68
 
33
- // Parse and validate mbkauthShared (optional fallback for shared settings)
34
- let mbkauthShared = null;
35
- try {
36
- if (process.env.mbkauthShared) {
37
- mbkauthShared = JSON.parse(process.env.mbkauthShared);
38
- if (mbkauthShared && typeof mbkauthShared !== 'object') {
39
- console.warn(`[mbkauthe] mbkauthShared is not a valid object, ignoring it`);
40
- mbkauthShared = null;
41
- }
42
- }
43
- } catch (error) {
44
- console.warn(`[mbkauthe] Invalid JSON in process.env.mbkauthShared, ignoring it`);
45
- mbkauthShared = null;
46
- }
47
-
48
- // Merge fallback settings: for any key missing or empty in mbkautheVar, check mbkauthShared
49
- const usedFromShared = [];
50
- const usedDefaults = [];
51
- const applyFallback = (source, sourceName) => {
52
- if (!source) return;
53
- Object.keys(source).forEach(key => {
54
- const val = source[key];
55
- if ((mbkautheVar[key] === undefined || (typeof mbkautheVar[key] === 'string' && mbkautheVar[key].trim() === '')) &&
56
- val !== undefined && !(typeof val === 'string' && val.trim() === '')) {
57
- mbkautheVar[key] = val;
58
- if (sourceName === 'mbkauthShared') usedFromShared.push(key);
59
- }
60
- });
61
- };
62
-
63
- applyFallback(mbkauthShared, 'mbkauthShared');
64
-
65
- // Ensure specific keys are checked in mbkautheVar first, then mbkauthShared, then apply config defaults
66
- const keysToCheck = [
67
- "APP_NAME", "DEVICE_TRUST_DURATION_DAYS", "Main_SECRET_TOKEN", "SESSION_SECRET_KEY",
68
- "IS_DEPLOYED", "LOGIN_DB", "MBKAUTH_TWO_FA_ENABLE", "COOKIE_EXPIRE_TIME", "DOMAIN", "loginRedirectURL",
69
- "GITHUB_LOGIN_ENABLED", "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID", "GITHUB_APP_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "GOOGLE_LOGIN_ENABLED", "GOOGLE_CLIENT_ID",
70
- "GOOGLE_CLIENT_SECRET", "MAX_SESSIONS_PER_USER"
71
- ];
72
-
73
- const defaults = {
74
- DEVICE_TRUST_DURATION_DAYS: 7,
75
- IS_DEPLOYED: 'false',
76
- MBKAUTH_TWO_FA_ENABLE: 'false',
77
- COOKIE_EXPIRE_TIME: 2,
78
- loginRedirectURL: '/dashboard',
79
- GITHUB_LOGIN_ENABLED: 'false',
80
- GOOGLE_LOGIN_ENABLED: 'false',
81
- MAX_SESSIONS_PER_USER: 5
82
- };
83
-
84
- keysToCheck.forEach(key => {
85
- const current = mbkautheVar[key];
86
- const isEmpty = current === undefined || (typeof current === 'string' && current.trim() === '');
87
- if (isEmpty) {
88
- if (mbkauthShared && mbkauthShared[key] !== undefined && !(typeof mbkauthShared[key] === 'string' && mbkauthShared[key].trim() === '')) {
89
- mbkautheVar[key] = mbkauthShared[key];
90
- if (!usedFromShared.includes(key)) usedFromShared.push(key);
91
- } else if (defaults[key] !== undefined) {
92
- mbkautheVar[key] = defaults[key];
93
- usedDefaults.push(key);
94
- }
69
+ Object.entries(sharedConfig).forEach(([key, value]) => {
70
+ if (isBlank(config[key]) && hasValue(value)) {
71
+ config[key] = value;
72
+ usedFromShared.add(key);
95
73
  }
96
74
  });
75
+ }
97
76
 
98
- // Normalize boolean-like values to consistent lowercase 'true'/'false' strings
99
- ['GITHUB_LOGIN_ENABLED', 'GOOGLE_LOGIN_ENABLED', 'MBKAUTH_TWO_FA_ENABLE', 'IS_DEPLOYED'].forEach(k => {
100
- const val = mbkautheVar[k];
101
- if (typeof val === 'boolean') {
102
- mbkautheVar[k] = val ? 'true' : 'false';
103
- } else if (typeof val === 'string') {
104
- const norm = val.trim().toLowerCase();
105
- // Accept 'f' as shorthand for false but normalize it to 'false'
106
- mbkautheVar[k] = (norm === 'f') ? 'false' : norm;
77
+ function applyDefaults(config, usedDefaults) {
78
+ CONFIG_KEYS.forEach((key) => {
79
+ if (isBlank(config[key]) && DEFAULT_CONFIG[key] !== undefined) {
80
+ config[key] = DEFAULT_CONFIG[key];
81
+ usedDefaults.add(key);
107
82
  }
108
83
  });
84
+ }
109
85
 
110
- // Validate required keys
111
- // COOKIE_EXPIRE_TIME is not required but if provided must be valid, COOKIE_EXPIRE_TIME by default is 2 days
112
- // loginRedirectURL is not required but if provided must be valid, loginRedirectURL by default is /dashboard
113
- const requiredKeys = ["APP_NAME", "Main_SECRET_TOKEN", "SESSION_SECRET_KEY", "IS_DEPLOYED", "LOGIN_DB",
114
- "MBKAUTH_TWO_FA_ENABLE", "DOMAIN"];
86
+ function normalizeBooleanFlag(config, key, errors) {
87
+ const value = config[key];
88
+ if (typeof value === 'boolean') {
89
+ config[key] = value ? 'true' : 'false';
90
+ return;
91
+ }
115
92
 
116
- requiredKeys.forEach(key => {
117
- if (!mbkautheVar[key] || (typeof mbkautheVar[key] === 'string' && mbkautheVar[key].trim() === '')) {
118
- errors.push(`mbkautheVar.${key} is required and cannot be empty`);
119
- }
120
- });
93
+ const normalized = String(value ?? '').trim().toLowerCase();
94
+ if (normalized === 'f') {
95
+ config[key] = 'false';
96
+ return;
97
+ }
98
+
99
+ if (normalized === 'true' || normalized === 'false') {
100
+ config[key] = normalized;
101
+ return;
102
+ }
103
+
104
+ if (!isBlank(value)) {
105
+ errors.push(`mbkautheVar.${key} must be either 'true' or 'false' or 'f'`);
106
+ }
107
+ }
108
+
109
+ function normalizePositiveNumber(config, key, errors) {
110
+ const numericValue = Number(config[key]);
111
+ if (!Number.isFinite(numericValue) || numericValue <= 0) {
112
+ errors.push(`mbkautheVar.${key} must be a valid positive number`);
113
+ return;
114
+ }
115
+ config[key] = numericValue;
116
+ }
121
117
 
122
- // Validate IS_DEPLOYED value
123
- if (mbkautheVar.IS_DEPLOYED && !['true', 'false', 'f'].includes((mbkautheVar.IS_DEPLOYED + '').toLowerCase())) {
124
- errors.push("mbkautheVar.IS_DEPLOYED must be either 'true' or 'false' or 'f'");
118
+ function normalizePositiveInteger(config, key, errors) {
119
+ const numericValue = Number(config[key]);
120
+ if (!Number.isInteger(numericValue) || numericValue <= 0) {
121
+ errors.push(`mbkautheVar.${key} must be a valid positive integer`);
122
+ return;
125
123
  }
124
+ config[key] = numericValue;
125
+ }
126
126
 
127
- // Validate MBKAUTH_TWO_FA_ENABLE value
128
- if (mbkautheVar.MBKAUTH_TWO_FA_ENABLE && !['true', 'false', 'f'].includes(mbkautheVar.MBKAUTH_TWO_FA_ENABLE.toLowerCase())) {
129
- errors.push("mbkautheVar.MBKAUTH_TWO_FA_ENABLE must be either 'true' or 'false' or 'f'");
127
+ function normalizeString(config, key) {
128
+ if (hasValue(config[key])) {
129
+ config[key] = String(config[key]).trim();
130
130
  }
131
+ }
131
132
 
132
- // Validate GITHUB_LOGIN_ENABLED value
133
- if (mbkautheVar.GITHUB_LOGIN_ENABLED && !['true', 'false', 'f'].includes(mbkautheVar.GITHUB_LOGIN_ENABLED.toLowerCase())) {
134
- errors.push("mbkautheVar.GITHUB_LOGIN_ENABLED must be either 'true' or 'false' or 'f'");
133
+ function normalizeAndValidateConfig(config, errors) {
134
+ STRING_KEYS.forEach((key) => normalizeString(config, key));
135
+
136
+ if (hasValue(config.APP_NAME)) {
137
+ config.APP_NAME = config.APP_NAME.toLowerCase();
135
138
  }
136
139
 
137
- // Validate GOOGLE_LOGIN_ENABLED value
138
- if (mbkautheVar.GOOGLE_LOGIN_ENABLED && !['true', 'false', 'f'].includes(mbkautheVar.GOOGLE_LOGIN_ENABLED.toLowerCase())) {
139
- errors.push("mbkautheVar.GOOGLE_LOGIN_ENABLED must be either 'true' or 'false' or 'f'");
140
+ if (hasValue(config.DOMAIN)) {
141
+ const domain = config.DOMAIN.toLowerCase().replace(/^\.+/, '');
142
+ config.DOMAIN = domain;
143
+ if (domain.includes('://') || domain.includes('/') || domain.includes(':')) {
144
+ errors.push("mbkautheVar.DOMAIN must be a hostname only, without protocol, path, or port");
145
+ }
140
146
  }
141
147
 
148
+ if (hasValue(config.loginRedirectURL)) {
149
+ const redirectUrl = String(config.loginRedirectURL).trim();
150
+ config.loginRedirectURL = redirectUrl;
151
+ if (!redirectUrl.startsWith('/') || redirectUrl.startsWith('//')) {
152
+ errors.push("mbkautheVar.loginRedirectURL must be a relative path starting with '/'");
153
+ }
154
+ }
155
+
156
+ BOOLEAN_KEYS.forEach((key) => normalizeBooleanFlag(config, key, errors));
157
+ normalizePositiveNumber(config, "COOKIE_EXPIRE_TIME", errors);
158
+ normalizePositiveNumber(config, "DEVICE_TRUST_DURATION_DAYS", errors);
159
+ normalizePositiveInteger(config, "MAX_SESSIONS_PER_USER", errors);
160
+ }
161
+
162
+ // Comprehensive validation function
163
+ function validateConfiguration() {
164
+ const errors = [];
165
+ const usedFromShared = new Set();
166
+ const usedDefaults = new Set();
167
+ const mbkautheVar = parseJsonEnv("mbkautheVar", { required: true });
168
+ const mbkauthShared = parseJsonEnv("mbkauthShared");
169
+
170
+ applySharedFallbacks(mbkautheVar, mbkauthShared, usedFromShared);
171
+ applyDefaults(mbkautheVar, usedDefaults);
172
+ normalizeAndValidateConfig(mbkautheVar, errors);
173
+
174
+ // Validate required keys
175
+ // COOKIE_EXPIRE_TIME is not required but if provided must be valid, COOKIE_EXPIRE_TIME by default is 2 days
176
+ // loginRedirectURL is not required but if provided must be valid, loginRedirectURL by default is /dashboard
177
+ REQUIRED_KEYS.forEach(key => {
178
+ if (isBlank(mbkautheVar[key])) {
179
+ errors.push(`mbkautheVar.${key} is required and cannot be empty`);
180
+ }
181
+ });
182
+
142
183
  // Validate GitHub login configuration
143
184
  if (mbkautheVar.GITHUB_LOGIN_ENABLED === "true") {
144
185
  const hasGithubClientId = !!(mbkautheVar.GITHUB_APP_CLIENT_ID || mbkautheVar.GITHUB_CLIENT_ID);
@@ -154,50 +195,14 @@ function validateConfiguration() {
154
195
 
155
196
  // Validate Google login configuration
156
197
  if (mbkautheVar.GOOGLE_LOGIN_ENABLED === "true") {
157
- if (!mbkautheVar.GOOGLE_CLIENT_ID || mbkautheVar.GOOGLE_CLIENT_ID.trim() === '') {
198
+ if (isBlank(mbkautheVar.GOOGLE_CLIENT_ID)) {
158
199
  errors.push("mbkautheVar.GOOGLE_CLIENT_ID is required when GOOGLE_LOGIN_ENABLED is 'true'");
159
200
  }
160
- if (!mbkautheVar.GOOGLE_CLIENT_SECRET || mbkautheVar.GOOGLE_CLIENT_SECRET.trim() === '') {
201
+ if (isBlank(mbkautheVar.GOOGLE_CLIENT_SECRET)) {
161
202
  errors.push("mbkautheVar.GOOGLE_CLIENT_SECRET is required when GOOGLE_LOGIN_ENABLED is 'true'");
162
203
  }
163
204
  }
164
205
 
165
- // Validate COOKIE_EXPIRE_TIME if provided
166
- if (mbkautheVar.COOKIE_EXPIRE_TIME !== undefined) {
167
- const expireTime = parseFloat(mbkautheVar.COOKIE_EXPIRE_TIME);
168
- if (isNaN(expireTime) || expireTime <= 0) {
169
- errors.push("mbkautheVar.COOKIE_EXPIRE_TIME must be a valid positive number");
170
- }
171
- } else {
172
- // Set default value
173
- mbkautheVar.COOKIE_EXPIRE_TIME = 2;
174
- }
175
-
176
- // Validate DEVICE_TRUST_DURATION_DAYS if provided
177
- if (mbkautheVar.DEVICE_TRUST_DURATION_DAYS !== undefined) {
178
- const trustDuration = parseFloat(mbkautheVar.DEVICE_TRUST_DURATION_DAYS);
179
- if (isNaN(trustDuration) || trustDuration <= 0) {
180
- errors.push("mbkautheVar.DEVICE_TRUST_DURATION_DAYS must be a valid positive number");
181
- }
182
- } else {
183
- // Set default value
184
- mbkautheVar.DEVICE_TRUST_DURATION_DAYS = 7;
185
- }
186
-
187
- // Validate MAX_SESSIONS_PER_USER if provided (must be positive integer)
188
- if (mbkautheVar.MAX_SESSIONS_PER_USER !== undefined) {
189
- const maxSessions = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
190
- if (isNaN(maxSessions) || maxSessions <= 0) {
191
- errors.push("mbkautheVar.MAX_SESSIONS_PER_USER must be a valid positive integer");
192
- } else {
193
- // Normalize to integer
194
- mbkautheVar.MAX_SESSIONS_PER_USER = maxSessions;
195
- }
196
- } else {
197
- // Ensure default value is set
198
- mbkautheVar.MAX_SESSIONS_PER_USER = 5;
199
- }
200
-
201
206
  // Validate LOGIN_DB connection string format
202
207
  if (mbkautheVar.LOGIN_DB && !mbkautheVar.LOGIN_DB.startsWith('postgresql://') && !mbkautheVar.LOGIN_DB.startsWith('postgres://')) {
203
208
  errors.push("mbkautheVar.LOGIN_DB must be a valid PostgreSQL connection string");
@@ -211,14 +216,14 @@ function validateConfiguration() {
211
216
  // Print consolidated configuration summary
212
217
  const configParts = [];
213
218
  if (mbkauthShared) {
214
- configParts.push(`mbkauthShared: ${usedFromShared.length} keys`);
219
+ configParts.push(`mbkauthShared: ${usedFromShared.size} keys`);
215
220
  }
216
- if (usedDefaults.length > 0) {
217
- configParts.push(`defaults: ${usedDefaults.length} keys`);
221
+ if (usedDefaults.size > 0) {
222
+ configParts.push(`defaults: ${usedDefaults.size} keys`);
218
223
  }
219
224
  const configSummary = configParts.length > 0 ? ` (${configParts.join(', ')})` : '';
220
225
  logConfig(`Configuration loaded${configSummary}`);
221
- return mbkautheVar;
226
+ return Object.freeze(mbkautheVar);
222
227
  }
223
228
 
224
229
  // Parse and validate mbkautheVar once
@@ -13,10 +13,37 @@ const IS_DEV = process.env.env === 'dev' || process.env.test === 'dev' || proces
13
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;
14
14
  const isUuid = (val) => typeof val === 'string' && UUID_RE.test(val);
15
15
  const MAX_API_TOKEN_LENGTH = 4096;
16
+ const API_TOKEN_LAST_USED_INTERVAL_MS = 15 * 60 * 1000;
16
17
  const API_TOKEN_SESSION_RESTORE = Symbol('mbkauthe.apiTokenSessionRestore');
18
+ const apiTokenLastUsedCache = new Map();
17
19
  const authRepo = new AuthRepository({ db: dblogin });
18
20
  const logAuth = createLogger("auth");
19
21
 
22
+ function pruneApiTokenLastUsedCache(now) {
23
+ if (apiTokenLastUsedCache.size < 10000) return;
24
+ const staleBefore = now - (API_TOKEN_LAST_USED_INTERVAL_MS * 2);
25
+ for (const [tokenId, lastTouchedAt] of apiTokenLastUsedCache) {
26
+ if (lastTouchedAt < staleBefore) {
27
+ apiTokenLastUsedCache.delete(tokenId);
28
+ }
29
+ }
30
+ }
31
+
32
+ function updateApiTokenLastUsedThrottled(tokenId) {
33
+ if (!tokenId) return;
34
+
35
+ const now = Date.now();
36
+ const lastTouchedAt = apiTokenLastUsedCache.get(tokenId) || 0;
37
+ if (now - lastTouchedAt < API_TOKEN_LAST_USED_INTERVAL_MS) return;
38
+
39
+ pruneApiTokenLastUsedCache(now);
40
+ apiTokenLastUsedCache.set(tokenId, now);
41
+ authRepo.updateApiTokenLastUsed(tokenId).catch(e => {
42
+ apiTokenLastUsedCache.delete(tokenId);
43
+ console.error(`[mbkauthe] Failed to update token usage:`, e);
44
+ });
45
+ }
46
+
20
47
  /**
21
48
  * Decide if the incoming request should return JSON errors instead of HTML.
22
49
  * Non-browser clients (API calls / AJAX) should get JSON.
@@ -80,8 +107,7 @@ async function validateTokenAuthentication(req) {
80
107
  allowedApps = tokenAllowedApps;
81
108
  }
82
109
 
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));
110
+ updateApiTokenLastUsedThrottled(row.id);
85
111
 
86
112
  return {
87
113
  id: row.uid,
@@ -159,7 +185,7 @@ function hasAppAccess(role, allowedApps) {
159
185
  if (role === "SuperAdmin") return true;
160
186
  return Array.isArray(allowedApps)
161
187
  && allowedApps.length > 0
162
- && allowedApps.some((app) => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
188
+ && allowedApps.some((app) => app && app.toLowerCase() === mbkautheVar.APP_NAME);
163
189
  }
164
190
 
165
191
  function destroySessionCookies(req, res) {
@@ -308,11 +334,11 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
308
334
  }
309
335
 
310
336
  const hasWildcard = allowedApps.includes('*');
311
- const hasSpecificApp = allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
337
+ const hasSpecificApp = allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME);
312
338
 
313
339
  if (hasWildcard) {
314
340
  const userHasApp = Array.isArray(userAllowedApps)
315
- && userAllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
341
+ && userAllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME);
316
342
  if (!userHasApp) {
317
343
  return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
318
344
  }
@@ -410,7 +436,7 @@ async function reloadSessionUser(req, res) {
410
436
  if (row.Role !== 'SuperAdmin') {
411
437
  const allowedApps = row.AllowedApps;
412
438
  const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
413
- if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
439
+ if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME)) {
414
440
  req.session.destroy(() => { });
415
441
  clearSessionCookies(res);
416
442
  return false;
@@ -122,11 +122,7 @@ export function sessionCookieSyncMiddleware(req, res, next) {
122
122
  }
123
123
 
124
124
  if (req.session && req.session.user) {
125
- // Decrypt existing cookie to compare with session
126
- const currentDecryptedId = decryptSessionId(req.cookies.sessionId);
127
-
128
- // Only set cookies if they're missing or different
129
- if (currentDecryptedId !== req.session.user.sessionId) {
125
+ if (!req.cookies.sessionId) {
130
126
  res.cookie("fullName", req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
131
127
  const encryptedSessionId = encryptSessionId(req.session.user.sessionId);
132
128
  if (encryptedSessionId) {
@@ -78,7 +78,7 @@ async function fetchActiveSession(sessionId) {
78
78
  if (row.Role !== 'SuperAdmin') {
79
79
  const allowedApps = row.AllowedApps;
80
80
  const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
81
- if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
81
+ if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME)) {
82
82
  return null;
83
83
  }
84
84
  }
@@ -121,7 +121,7 @@ export async function checkTrustedDevice(req, username) {
121
121
 
122
122
  if (deviceUser.Role !== "SuperAdmin") {
123
123
  const allowedApps = deviceUser.AllowedApps;
124
- if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
124
+ if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME)) {
125
125
  console.warn(`[mbkauthe] Trusted device check: User "${username}" is not authorized to use the application "${mbkautheVar.APP_NAME}"`);
126
126
  return null;
127
127
  }
@@ -398,7 +398,7 @@ router.post("/api/login", LoginLimit, async (req, res) => {
398
398
 
399
399
  if (user.Role !== "SuperAdmin") {
400
400
  const allowedApps = user.AllowedApps;
401
- if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
401
+ if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME)) {
402
402
  logError('Login attempt', ErrorCodes.APP_NOT_AUTHORIZED, {
403
403
  username: user.UserName,
404
404
  app: mbkautheVar.APP_NAME
@@ -476,7 +476,7 @@ router.get("/2fa", csrfProtection, (req, res) => {
476
476
  layout: false,
477
477
  customURL: redirectToUse,
478
478
  csrfToken: req.csrfToken(),
479
- appName: mbkautheVar.APP_NAME.toLowerCase(),
479
+ appName: mbkautheVar.APP_NAME,
480
480
  version: packageJson.version,
481
481
  DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
482
482
  });
@@ -636,7 +636,7 @@ router.get("/api/account-sessions", LoginLimit, async (req, res) => {
636
636
  const expired = row?.expires_at && new Date(row.expires_at) <= new Date();
637
637
  const authorized = row && row.Active && (
638
638
  row.Role === "SuperAdmin" ||
639
- (Array.isArray(row.AllowedApps) && row.AllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase()))
639
+ (Array.isArray(row.AllowedApps) && row.AllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME))
640
640
  );
641
641
 
642
642
  if (!row || expired || !authorized) {
@@ -774,7 +774,7 @@ router.get("/login", LoginLimit, csrfProtection, (req, res) => {
774
774
  userLoggedIn: !!req.session?.user,
775
775
  username: req.session?.user?.username || '',
776
776
  version: packageJson.version,
777
- appName: mbkautheVar.APP_NAME.toLowerCase(),
777
+ appName: mbkautheVar.APP_NAME,
778
778
  csrfToken: req.csrfToken(),
779
779
  // Last-login method flags for immediate server-side badge rendering
780
780
  lastLoginMethod: lastLogin,
@@ -797,7 +797,7 @@ router.get("/accounts", LoginLimit, csrfProtection, (req, res) => {
797
797
  layout: false,
798
798
  customURL: safeRedirect,
799
799
  version: packageJson.version,
800
- appName: mbkautheVar.APP_NAME.toLowerCase(),
800
+ appName: mbkautheVar.APP_NAME,
801
801
  csrfToken: req.csrfToken(),
802
802
  userLoggedIn: !!req.session?.user,
803
803
  username: req.session?.user?.username,
@@ -22,6 +22,23 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
22
  const router = express.Router();
23
23
  const authRepo = new AuthRepository({ db: dblogin });
24
24
  const logMisc = createLogger("misc");
25
+ const PROFILE_IMAGE_CACHE_SECONDS = 300;
26
+ const PROFILE_IMAGE_CACHE_CONTROL = `private, max-age=${PROFILE_IMAGE_CACHE_SECONDS}, stale-while-revalidate=${PROFILE_IMAGE_CACHE_SECONDS}`;
27
+ const LATEST_VERSION_CACHE_TTL_MS = 10 * 60 * 1000;
28
+ const LATEST_VERSION_FAILURE_CACHE_TTL_MS = 60 * 1000;
29
+ const latestVersionCache = {
30
+ value: null,
31
+ expiresAt: 0,
32
+ pending: null
33
+ };
34
+
35
+ function setProfileImageCacheHeaders(res, eTag = null) {
36
+ res.setHeader('Cache-Control', PROFILE_IMAGE_CACHE_CONTROL);
37
+ if (eTag) {
38
+ res.setHeader('ETag', eTag);
39
+ }
40
+ }
41
+
25
42
  // Rate limiter for info/test routes
26
43
  const LoginLimit = rateLimit({
27
44
  windowMs: 1 * 60 * 1000,
@@ -76,9 +93,8 @@ router.get('/user/profilepic', async (req, res) => {
76
93
  const serveDefaultIcon = () => {
77
94
  const iconPath = path.join(__dirname, "..", "..", "public", "M.png");
78
95
  res.setHeader('Content-Type', 'image/png');
79
- // Ensure we don't override the Cache-Control we set earlier, or set a default if not set
80
96
  if (!res.getHeader('Cache-Control')) {
81
- res.setHeader('Cache-Control', 'private, no-cache');
97
+ setProfileImageCacheHeaders(res);
82
98
  }
83
99
  const stream = fs.createReadStream(iconPath);
84
100
  stream.on('error', (err) => {
@@ -118,9 +134,7 @@ router.get('/user/profilepic', async (req, res) => {
118
134
  // Generate ETag based on username and image URL
119
135
  const eTag = `"${Buffer.from(username + ':' + imageUrl).toString('base64')}"`;
120
136
 
121
- // Set caching headers
122
- res.setHeader('Cache-Control', 'private, no-cache');
123
- res.setHeader('ETag', eTag);
137
+ setProfileImageCacheHeaders(res, eTag);
124
138
 
125
139
  // Check for conditional request
126
140
  if (req.headers['if-none-match'] === eTag) {
@@ -450,20 +464,44 @@ router.get("/ErrorCode", (req, res) => {
450
464
  }
451
465
  });
452
466
 
453
- // Fetch latest version from GitHub\
454
- export async function getLatestVersion() {
455
- try {
456
- const response = await fetch('https://raw.githubusercontent.com/MIbnEKhalid/mbkauthe/main/package.json');
457
- if (!response.ok) {
458
- console.error(`[mbkauthe] GitHub API responded with status ${response.status}`);
467
+ // Fetch latest version from GitHub with a short in-memory cache.
468
+ export async function getLatestVersion({ forceRefresh = false } = {}) {
469
+ const now = Date.now();
470
+
471
+ if (!forceRefresh && latestVersionCache.expiresAt > now) {
472
+ return latestVersionCache.value;
473
+ }
474
+
475
+ if (!forceRefresh && latestVersionCache.pending) {
476
+ return latestVersionCache.pending;
477
+ }
478
+
479
+ latestVersionCache.pending = (async () => {
480
+ try {
481
+ const response = await fetch('https://raw.githubusercontent.com/MIbnEKhalid/mbkauthe/main/package.json');
482
+ if (!response.ok) {
483
+ console.error(`[mbkauthe] GitHub API responded with status ${response.status}`);
484
+ latestVersionCache.value = null;
485
+ latestVersionCache.expiresAt = Date.now() + LATEST_VERSION_FAILURE_CACHE_TTL_MS;
486
+ return null;
487
+ }
488
+
489
+ const latestPackageJson = await response.json();
490
+ const latestVersion = typeof latestPackageJson.version === 'string' ? latestPackageJson.version : null;
491
+ latestVersionCache.value = latestVersion;
492
+ latestVersionCache.expiresAt = Date.now() + LATEST_VERSION_CACHE_TTL_MS;
493
+ return latestVersion;
494
+ } catch (error) {
495
+ console.error(`[mbkauthe] Error fetching latest version from GitHub`, error);
496
+ latestVersionCache.value = null;
497
+ latestVersionCache.expiresAt = Date.now() + LATEST_VERSION_FAILURE_CACHE_TTL_MS;
459
498
  return null;
499
+ } finally {
500
+ latestVersionCache.pending = null;
460
501
  }
461
- const latestPackageJson = await response.json();
462
- return typeof latestPackageJson.version === 'string' ? latestPackageJson.version : null;
463
- } catch (error) {
464
- console.error(`[mbkauthe] Error fetching latest version from GitHub`, error);
465
- return null;
466
- }
502
+ })();
503
+
504
+ return latestVersionCache.pending;
467
505
  }
468
506
 
469
507
  // Version check with error handling
@@ -66,7 +66,7 @@ const createOAuthStrategy = async (provider, profile, done) => {
66
66
  // Check if user is authorized for this app
67
67
  if (user.Role !== "SuperAdmin") {
68
68
  const allowedApps = user.AllowedApps;
69
- if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
69
+ if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME)) {
70
70
  const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
71
71
  error.code = 'NOT_AUTHORIZED';
72
72
  return done(error);