mbkauthe 3.4.0 → 4.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.
package/docs/db.sql ADDED
@@ -0,0 +1,116 @@
1
+
2
+ -- GitHub users table
3
+ CREATE TABLE user_github (
4
+ id SERIAL PRIMARY KEY,
5
+ user_name VARCHAR(50) REFERENCES "Users"("UserName"),
6
+ github_id VARCHAR(255) UNIQUE,
7
+ github_username VARCHAR(255),
8
+ access_token TEXT,
9
+ created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
10
+ updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
11
+ );
12
+
13
+ -- Add indexes for performance optimization
14
+ CREATE INDEX IF NOT EXISTS idx_user_github_github_id ON user_github (github_id);
15
+ CREATE INDEX IF NOT EXISTS idx_user_github_user_name ON user_github (user_name);
16
+
17
+ -- Google users table
18
+ CREATE TABLE user_google (
19
+ id SERIAL PRIMARY KEY,
20
+ user_name VARCHAR(50) REFERENCES "Users"("UserName"),
21
+ google_id VARCHAR(255) UNIQUE,
22
+ google_email VARCHAR(255),
23
+ access_token TEXT,
24
+ created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
25
+ updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
26
+ );
27
+
28
+ -- Add indexes for performance optimization
29
+ CREATE INDEX IF NOT EXISTS idx_user_google_google_id ON user_google (google_id);
30
+ CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
31
+
32
+
33
+
34
+
35
+ CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
36
+
37
+ CREATE TABLE "Users" (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ "UserName" VARCHAR(50) NOT NULL UNIQUE,
40
+ "Password" VARCHAR(61), -- For raw passwords (when EncPass=false)
41
+ "PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
42
+ "Role" role DEFAULT 'NormalUser' NOT NULL,
43
+ "Active" BOOLEAN DEFAULT FALSE,
44
+ "HaveMailAccount" BOOLEAN DEFAULT FALSE,
45
+ "AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
46
+ "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
47
+ "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
48
+ "last_login" TIMESTAMP WITH TIME ZONE
49
+ );
50
+
51
+ -- Add indexes for performance optimization
52
+ CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" ("UserName");
53
+ CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" ("Active");
54
+ CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" ("Role");
55
+ CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" (last_login);
56
+
57
+ -- Application Sessions table (stores multiple concurrent sessions per user)
58
+ -- Note: this is separate from the express-session store table named "session"
59
+ CREATE TABLE "Sessions" (
60
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- requires pgcrypto or uuid-ossp
61
+ "UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
62
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
63
+ expires_at TIMESTAMP WITH TIME ZONE,
64
+ meta JSONB
65
+ );
66
+
67
+ -- Indexes optimized by username instead of numeric user id
68
+ CREATE INDEX IF NOT EXISTS idx_sessions_username ON "Sessions" ("UserName");
69
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_created ON "Sessions" ("UserName", created_at);
70
+
71
+
72
+ CREATE TABLE "session" (
73
+ sid VARCHAR(33) PRIMARY KEY NOT NULL,
74
+ sess JSONB NOT NULL,
75
+ expire TimeStamp WITH TIME ZONE Not Null,
76
+ );
77
+
78
+ -- Add indexes for performance optimization
79
+ CREATE INDEX IF NOT EXISTS idx_session_expire ON "session" ("expire");
80
+ CREATE INDEX IF NOT EXISTS idx_session_user_id ON "session" ((sess->'user'->>'id'));
81
+
82
+
83
+
84
+ CREATE TABLE "TwoFA" (
85
+ "UserName" VARCHAR(50) primary key REFERENCES "Users"("UserName"),
86
+ "TwoFAStatus" boolean NOT NULL,
87
+ "TwoFASecret" TEXT
88
+ );
89
+
90
+ -- Add indexes for performance optimization
91
+ CREATE INDEX IF NOT EXISTS idx_twofa_username ON "TwoFA" ("UserName");
92
+ CREATE INDEX IF NOT EXISTS idx_twofa_username_status ON "TwoFA" ("UserName", "TwoFAStatus");
93
+
94
+
95
+
96
+ CREATE TABLE "TrustedDevices" (
97
+ "id" SERIAL PRIMARY KEY,
98
+ "UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
99
+ "DeviceToken" VARCHAR(64) UNIQUE NOT NULL,
100
+ "DeviceName" VARCHAR(255),
101
+ "UserAgent" TEXT,
102
+ "IpAddress" VARCHAR(45),
103
+ "CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
104
+ "ExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL,
105
+ "LastUsed" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
106
+ );
107
+
108
+ -- Add indexes for performance optimization
109
+ CREATE INDEX IF NOT EXISTS idx_trusted_devices_token ON "TrustedDevices"("DeviceToken");
110
+ CREATE INDEX IF NOT EXISTS idx_trusted_devices_username ON "TrustedDevices"("UserName");
111
+ CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices"("ExpiresAt");
112
+
113
+
114
+ -- No Encrypted password for 'support' user
115
+ INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "GuestRole")
116
+ VALUES ('support', '12345678', 'SuperAdmin', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
package/docs/env.md CHANGED
@@ -42,6 +42,7 @@ This document describes the environment variables MBKAuth expects and keeps brie
42
42
  - Description: PostgreSQL connection string for auth (must start with `postgresql://` or `postgres://`).
43
43
  - Example: `"LOGIN_DB":"postgresql://user:pass@localhost:5432/mbkauth"`
44
44
  - Required: Yes
45
+ - Create free postgres db: https://neon.com/
45
46
 
46
47
  - MBKAUTH_TWO_FA_ENABLE
47
48
  - Description: Enable Two-Factor Authentication.
@@ -67,6 +68,13 @@ This document describes the environment variables MBKAuth expects and keeps brie
67
68
  - Example: `"DEVICE_TRUST_DURATION_DAYS":30`
68
69
  - Required: No
69
70
 
71
+ - MAX_SESSIONS_PER_USER
72
+ - Description: Maximum number of concurrent application sessions allowed per user. When creating a new session that would exceed this number, the oldest session(s) for that user are pruned to make room for the new session.
73
+ - Default: `5`
74
+ - Example: `"MAX_SESSIONS_PER_USER": 10`
75
+ - Notes: Must be a positive integer. Validation is performed at startup by `lib/config/index.js`.
76
+ - Required: No
77
+
70
78
  - loginRedirectURL
71
79
  - Description: Post-login redirect path.
72
80
  - Default: `/dashboard`
@@ -81,6 +89,8 @@ This document describes the environment variables MBKAuth expects and keeps brie
81
89
  - GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
82
90
  - Description: OAuth credentials (put in `mbkautheVar` preferred, or `mbkauthShared`).
83
91
  - Required when provider enabled.
92
+ - Create Github OAuth App: https://github.com/settings/developers
93
+ - Create Google OAuth: https://console.cloud.google.com/
84
94
 
85
95
  ---
86
96
 
package/index.d.ts CHANGED
@@ -12,6 +12,7 @@ declare global {
12
12
  user?: {
13
13
  username: string;
14
14
  role: 'SuperAdmin' | 'NormalUser' | 'Guest';
15
+ fullname?: string;
15
16
  };
16
17
  userRole?: 'SuperAdmin' | 'NormalUser' | 'Guest';
17
18
  }
@@ -20,8 +21,9 @@ declare global {
20
21
  user?: {
21
22
  id: number;
22
23
  username: string;
24
+ fullname?: string;
23
25
  role: 'SuperAdmin' | 'NormalUser' | 'Guest';
24
- sessionId: string;
26
+ sessionId?: string;
25
27
  allowedApps?: string[];
26
28
  };
27
29
  preAuthUser?: {
@@ -75,8 +77,9 @@ declare module 'mbkauthe' {
75
77
  export interface SessionUser {
76
78
  id: number;
77
79
  username: string;
80
+ fullname?: string;
78
81
  role: UserRole;
79
- sessionId: string;
82
+ sessionId?: string;
80
83
  allowedApps?: string[];
81
84
  }
82
85
 
@@ -98,7 +101,7 @@ declare module 'mbkauthe' {
98
101
  Role: UserRole;
99
102
  Active: boolean;
100
103
  AllowedApps: string[];
101
- SessionId?: string;
104
+
102
105
  created_at?: Date;
103
106
  updated_at?: Date;
104
107
  last_login?: Date;
@@ -209,6 +212,10 @@ declare module 'mbkauthe' {
209
212
 
210
213
  export function authenticate(token: string): AuthMiddleware;
211
214
 
215
+ // Reload session user values from DB and refresh cookies.
216
+ // Returns true when session is refreshed and valid, false if session invalidated.
217
+ export function reloadSessionUser(req: Request, res: Response): Promise<boolean>;
218
+
212
219
  // Utility Functions
213
220
  export function renderError(
214
221
  res: Response,
package/index.js CHANGED
@@ -89,7 +89,10 @@ if (process.env.test !== "dev") {
89
89
  await checkVersion();
90
90
  }
91
91
 
92
- export { validateSession, checkRolePermission, validateSessionAndRole, authenticate } from "./lib/middleware/auth.js";
92
+ export {
93
+ validateSession, validateApiSession, checkRolePermission,
94
+ validateSessionAndRole, authenticate, reloadSessionUser
95
+ } from "./lib/middleware/auth.js";
93
96
  export { renderError } from "./lib/utils/response.js";
94
97
  export { dblogin } from "./lib/database/pool.js";
95
98
  export { ErrorCodes, ErrorMessages, getErrorByCode, createErrorResponse, logError } from "./lib/utils/errors.js";
@@ -32,6 +32,12 @@ export const generateDeviceToken = () => {
32
32
  return crypto.randomBytes(32).toString('hex');
33
33
  };
34
34
 
35
+ // Hash a device token for safe storage in the database
36
+ export const hashDeviceToken = (token) => {
37
+ if (!token || typeof token !== 'string') return null;
38
+ return crypto.createHmac('sha256').update(token).digest('hex');
39
+ };
40
+
35
41
  export const getDeviceTokenCookieOptions = () => ({
36
42
  maxAge: DEVICE_TRUST_DURATION_MS,
37
43
  domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
@@ -46,6 +52,7 @@ export const clearSessionCookies = (res) => {
46
52
  res.clearCookie("mbkauthe.sid", cachedClearCookieOptions);
47
53
  res.clearCookie("sessionId", cachedClearCookieOptions);
48
54
  res.clearCookie("username", cachedClearCookieOptions);
55
+ res.clearCookie("fullName", cachedClearCookieOptions);
49
56
  res.clearCookie("device_token", cachedClearCookieOptions);
50
57
  };
51
58
 
@@ -62,9 +62,10 @@ function validateConfiguration() {
62
62
 
63
63
  // Ensure specific keys are checked in mbkautheVar first, then mbkauthShared, then apply config defaults
64
64
  const keysToCheck = [
65
- "APP_NAME","DEVICE_TRUST_DURATION_DAYS","EncPass","Main_SECRET_TOKEN","SESSION_SECRET_KEY","IS_DEPLOYED",
66
- "LOGIN_DB","MBKAUTH_TWO_FA_ENABLE","COOKIE_EXPIRE_TIME","DOMAIN","loginRedirectURL",
67
- "GITHUB_LOGIN_ENABLED","GITHUB_CLIENT_ID","GITHUB_CLIENT_SECRET","GOOGLE_LOGIN_ENABLED","GOOGLE_CLIENT_ID","GOOGLE_CLIENT_SECRET"
65
+ "APP_NAME","DEVICE_TRUST_DURATION_DAYS","EncPass","Main_SECRET_TOKEN","SESSION_SECRET_KEY",
66
+ "IS_DEPLOYED","LOGIN_DB","MBKAUTH_TWO_FA_ENABLE","COOKIE_EXPIRE_TIME","DOMAIN","loginRedirectURL",
67
+ "GITHUB_LOGIN_ENABLED","GITHUB_CLIENT_ID","GITHUB_CLIENT_SECRET","GOOGLE_LOGIN_ENABLED","GOOGLE_CLIENT_ID",
68
+ "GOOGLE_CLIENT_SECRET","MAX_SESSIONS_PER_USER"
68
69
  ];
69
70
 
70
71
  const defaults = {
@@ -75,7 +76,8 @@ function validateConfiguration() {
75
76
  COOKIE_EXPIRE_TIME: 2,
76
77
  loginRedirectURL: '/dashboard',
77
78
  GITHUB_LOGIN_ENABLED: 'false',
78
- GOOGLE_LOGIN_ENABLED: 'false'
79
+ GOOGLE_LOGIN_ENABLED: 'false',
80
+ MAX_SESSIONS_PER_USER: 5
79
81
  };
80
82
 
81
83
  keysToCheck.forEach(key => {
@@ -183,6 +185,20 @@ function validateConfiguration() {
183
185
  mbkautheVar.DEVICE_TRUST_DURATION_DAYS = 7;
184
186
  }
185
187
 
188
+ // Validate MAX_SESSIONS_PER_USER if provided (must be positive integer)
189
+ if (mbkautheVar.MAX_SESSIONS_PER_USER !== undefined) {
190
+ const maxSessions = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
191
+ if (isNaN(maxSessions) || maxSessions <= 0) {
192
+ errors.push("mbkautheVar.MAX_SESSIONS_PER_USER must be a valid positive integer");
193
+ } else {
194
+ // Normalize to integer
195
+ mbkautheVar.MAX_SESSIONS_PER_USER = maxSessions;
196
+ }
197
+ } else {
198
+ // Ensure default value is set
199
+ mbkautheVar.MAX_SESSIONS_PER_USER = 5;
200
+ }
201
+
186
202
  // Validate LOGIN_DB connection string format
187
203
  if (mbkautheVar.LOGIN_DB && !mbkautheVar.LOGIN_DB.startsWith('postgresql://') && !mbkautheVar.LOGIN_DB.startsWith('postgres://')) {
188
204
  errors.push("mbkautheVar.LOGIN_DB must be a valid PostgreSQL connection string");
@@ -5,4 +5,4 @@ export const hashPassword = (password, username) => {
5
5
  const salt = username;
6
6
  // 128 characters returned
7
7
  return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
8
- };
8
+ };
@@ -1,7 +1,7 @@
1
1
  import { dblogin } from "../database/pool.js";
2
2
  import { mbkautheVar } from "../config/index.js";
3
3
  import { renderError } from "../utils/response.js";
4
- import { clearSessionCookies } from "../config/cookies.js";
4
+ import { clearSessionCookies, cachedCookieOptions } from "../config/cookies.js";
5
5
 
6
6
  async function validateSession(req, res, next) {
7
7
  if (!req.session.user) {
@@ -33,19 +33,18 @@ async function validateSession(req, res, next) {
33
33
  });
34
34
  }
35
35
 
36
- // Normalize sessionId to lowercase for consistent comparison
37
- const normalizedSessionId = sessionId.toLowerCase();
36
+ // Normalize sessionId (DB id) for consistent comparison
37
+ const normalizedSessionId = sessionId;
38
38
 
39
- // Single optimized query to validate session and get role
40
- const query = `SELECT "SessionId", "Active", "Role" FROM "Users" WHERE "id" = $1`;
41
- const result = await dblogin.query({ name: 'validate-user-session', text: query, values: [id] });
39
+ // Validate session by DB primary key id and join to user
40
+ const query = `SELECT s.id as sid, s.expires_at, u."Active", u."Role"
41
+ FROM "Sessions" s
42
+ JOIN "Users" u ON s."UserName" = u."UserName"
43
+ WHERE s.id = $1 LIMIT 1`;
44
+ const result = await dblogin.query({ name: 'validate-app-session', text: query, values: [normalizedSessionId] });
42
45
 
43
- const dbSessionId = result.rows.length > 0 && result.rows[0].SessionId ? String(result.rows[0].SessionId).toLowerCase() : null;
44
- if (!dbSessionId || dbSessionId !== normalizedSessionId) {
45
- if (result.rows.length > 0 && !result.rows[0].SessionId) {
46
- console.warn(`[mbkauthe] DB sessionId is null for user "${req.session.user.username}"`);
47
- }
48
- console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
46
+ if (result.rows.length === 0) {
47
+ console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
49
48
  req.session.destroy();
50
49
  clearSessionCookies(res);
51
50
  return renderError(res, {
@@ -57,7 +56,25 @@ async function validateSession(req, res, next) {
57
56
  });
58
57
  }
59
58
 
60
- if (!result.rows[0].Active) {
59
+ const sessionRow = result.rows[0];
60
+
61
+ // Check expired
62
+ if (sessionRow.expires_at && new Date(sessionRow.expires_at) <= new Date()) {
63
+ console.log(`[mbkauthe] Session invalidated (expired) for user "${req.session.user.username}"`);
64
+ // destroy and clear cookies
65
+ req.session.destroy();
66
+ clearSessionCookies(res);
67
+ return renderError(res, {
68
+ code: 401,
69
+ error: "Session Expired",
70
+ message: "Your Session Has Expired. Please Log In Again.",
71
+ pagename: "Login",
72
+ page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
73
+ });
74
+ }
75
+
76
+
77
+ if (!sessionRow.Active) {
61
78
  console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
62
79
  req.session.destroy();
63
80
  clearSessionCookies(res);
@@ -97,6 +114,177 @@ async function validateSession(req, res, next) {
97
114
  }
98
115
  }
99
116
 
117
+ /**
118
+ * API-friendly session validation middleware
119
+ * Returns JSON error responses instead of rendering pages
120
+ */
121
+ async function validateApiSession(req, res, next) {
122
+ if (!req.session.user) {
123
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
124
+ }
125
+
126
+ try {
127
+ const { id, sessionId, role, allowedApps } = req.session.user;
128
+
129
+ // Defensive checks for sessionId and allowedApps
130
+ if (!sessionId) {
131
+ console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
132
+ req.session.destroy();
133
+ clearSessionCookies(res);
134
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
135
+ }
136
+
137
+ // Normalize sessionId (DB id) for consistent comparison
138
+ const normalizedSessionId = sessionId;
139
+
140
+ // Validate session by DB primary key id and join to user
141
+ const query = `SELECT s.id as sid, s.expires_at, u."Active", u."Role"
142
+ FROM "Sessions" s
143
+ JOIN "Users" u ON s."UserName" = u."UserName"
144
+ WHERE s.id = $1 LIMIT 1`;
145
+ const result = await dblogin.query({ name: 'validate-app-session-for-api', text: query, values: [normalizedSessionId] });
146
+
147
+ if (result.rows.length === 0) {
148
+ req.session.destroy();
149
+ clearSessionCookies(res);
150
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
151
+ }
152
+
153
+ const sessionRow = result.rows[0];
154
+
155
+ // Check expired
156
+ if (sessionRow.expires_at && new Date(sessionRow.expires_at) <= new Date()) {
157
+ req.session.destroy();
158
+ clearSessionCookies(res);
159
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
160
+ }
161
+
162
+
163
+ if (!result.rows[0].Active) {
164
+ console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
165
+ req.session.destroy();
166
+ clearSessionCookies(res);
167
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
168
+ }
169
+
170
+ if (role !== "SuperAdmin") {
171
+ // If allowedApps is not provided or not an array, treat as no access
172
+ const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
173
+ if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
174
+ console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
175
+ req.session.destroy();
176
+ clearSessionCookies(res);
177
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
178
+ }
179
+ }
180
+
181
+ // Store user role in request for checkRolePermission to use
182
+ req.userRole = result.rows[0].Role;
183
+
184
+ next();
185
+ } catch (err) {
186
+ console.error("[mbkauthe] API session validation error:", err);
187
+ return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Reload session user values from the database and refresh cookies.
193
+ * - Validates sessionId and active status
194
+ * - Updates `req.session.user` fields (username, role, allowedApps, fullname)
195
+ * - Uses cached `fullName` cookie when available, otherwise queries `profiledata`
196
+ * - Syncs `username`, `fullName` and `sessionId` cookies
197
+ * Returns: true if session refreshed and valid, false if session invalidated
198
+ */
199
+ export async function reloadSessionUser(req, res) {
200
+ if (!req.session || !req.session.user || !req.session.user.id) return false;
201
+ try {
202
+ const { id, sessionId: currentSessionId } = req.session.user;
203
+
204
+ if (!currentSessionId) {
205
+ req.session.destroy(() => {});
206
+ clearSessionCookies(res);
207
+ return false;
208
+ }
209
+
210
+ const normalizedSessionId = String(currentSessionId);
211
+ const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
212
+ FROM "Sessions" s
213
+ JOIN "Users" u ON s."UserName" = u."UserName"
214
+ WHERE s.id = $1 LIMIT 1`;
215
+ const result = await dblogin.query({ name: 'reload-session-user', text: query, values: [normalizedSessionId] });
216
+
217
+ if (result.rows.length === 0) {
218
+ // Session not found — invalidate session
219
+ req.session.destroy(() => {});
220
+ clearSessionCookies(res);
221
+ return false;
222
+ }
223
+
224
+ const row = result.rows[0];
225
+
226
+ // Check expired
227
+ if (row.expires_at && new Date(row.expires_at) <= new Date()) {
228
+ req.session.destroy(() => {});
229
+ clearSessionCookies(res);
230
+ return false;
231
+ }
232
+
233
+ if (!row.Active) {
234
+ // Account is inactive
235
+ req.session.destroy(() => {});
236
+ clearSessionCookies(res);
237
+ return false;
238
+ }
239
+
240
+ // Authorization: ensure allowed for current app unless SuperAdmin
241
+ if (row.Role !== 'SuperAdmin') {
242
+ const allowedApps = row.AllowedApps;
243
+ const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
244
+ if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
245
+ req.session.destroy(() => {});
246
+ clearSessionCookies(res);
247
+ return false;
248
+ }
249
+ }
250
+
251
+ // Update session fields
252
+ req.session.user.username = row.UserName;
253
+ req.session.user.role = row.Role;
254
+ req.session.user.allowedApps = row.AllowedApps;
255
+
256
+ // Obtain fullname from client cookie cache when present else DB
257
+ if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
258
+ req.session.user.fullname = req.cookies.fullName;
259
+ } else {
260
+ try {
261
+ const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
262
+ if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
263
+ } catch (profileErr) {
264
+ console.error('[mbkauthe] Error fetching fullname during reload:', profileErr);
265
+ }
266
+ }
267
+
268
+ // Persist session changes
269
+ await new Promise((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
270
+
271
+ // Sync cookies for client UI (non-httpOnly)
272
+ try {
273
+ res.cookie('username', req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
274
+ res.cookie('fullName', req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
275
+ res.cookie('sessionId', req.session.user.sessionId, cachedCookieOptions);
276
+ } catch (cookieErr) {
277
+ // Ignore cookie setting errors, session is still refreshed
278
+ console.error('[mbkauthe] Error syncing cookies during reload:', cookieErr);
279
+ }
280
+
281
+ return true;
282
+ } catch (err) {
283
+ console.error('[mbkauthe] reloadSessionUser error:', err);
284
+ return false;
285
+ }
286
+ }
287
+
100
288
  const checkRolePermission = (requiredRoles, notAllowed) => {
101
289
  return async (req, res, next) => {
102
290
  try {
@@ -173,5 +361,4 @@ const authenticate = (authentication) => {
173
361
  };
174
362
  };
175
363
 
176
-
177
- export { validateSession, checkRolePermission, validateSessionAndRole, authenticate };
364
+ export { validateSession, validateApiSession, checkRolePermission, validateSessionAndRole, authenticate };
@@ -57,8 +57,8 @@ export async function sessionRestorationMiddleware(req, res, next) {
57
57
  if (!req.session.user && req.cookies.sessionId) {
58
58
  const sessionId = req.cookies.sessionId;
59
59
 
60
- // Early validation to avoid unnecessary processing
61
- if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
60
+ // Early validation to avoid unnecessary processing (expect DB UUID id)
61
+ if (typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
62
62
  // Clear invalid cookie to prevent repeated attempts
63
63
  res.clearCookie('sessionId', {
64
64
  domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
@@ -71,20 +71,48 @@ export async function sessionRestorationMiddleware(req, res, next) {
71
71
  }
72
72
 
73
73
  try {
74
- const normalizedSessionId = sessionId.toLowerCase();
75
-
76
- const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE LOWER("SessionId") = $1 AND "Active" = true`;
77
- const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [normalizedSessionId] });
74
+ // Validate session by DB primary key id and join to user
75
+ const query = `SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps", s.expires_at
76
+ FROM "Sessions" s
77
+ JOIN "Users" u ON s."UserName" = u."UserName"
78
+ WHERE s.id = $1 LIMIT 1`;
79
+ const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [sessionId] });
78
80
 
79
81
  if (result.rows.length > 0) {
80
- const user = result.rows[0];
81
- req.session.user = {
82
- id: user.id,
83
- username: user.UserName,
84
- role: user.Role,
85
- sessionId: normalizedSessionId,
86
- allowedApps: user.AllowedApps,
87
- };
82
+ const row = result.rows[0];
83
+
84
+ // Reject expired sessions or inactive users
85
+ if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
86
+ // leave cookies cleared and don't restore session
87
+ } else {
88
+ const normalizedSessionId = String(sessionId);
89
+ req.session.user = {
90
+ id: row.id,
91
+ username: row.UserName,
92
+ role: row.Role,
93
+ sessionId: normalizedSessionId,
94
+ allowedApps: row.AllowedApps,
95
+ };
96
+
97
+ // Use cached FullName from client cookie when available to avoid extra DB queries
98
+ if (req.cookies.fullName && typeof req.cookies.fullName === 'string') {
99
+ req.session.user.fullname = req.cookies.fullName;
100
+ } else {
101
+ // Fallback: attempt to fetch FullName from profiledata to populate session
102
+ try {
103
+ const profileRes = await dblogin.query({
104
+ name: 'restore-get-fullname',
105
+ text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1',
106
+ values: [row.UserName]
107
+ });
108
+ if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {
109
+ req.session.user.fullname = profileRes.rows[0].FullName;
110
+ }
111
+ } catch (profileErr) {
112
+ console.error("[mbkauthe] Error fetching FullName during session restore:", profileErr);
113
+ }
114
+ }
115
+ }
88
116
  }
89
117
  } catch (err) {
90
118
  console.error("[mbkauthe] Session restoration error:", err);
@@ -99,8 +127,10 @@ export function sessionCookieSyncMiddleware(req, res, next) {
99
127
  // Only set cookies if they're missing or different
100
128
  if (req.cookies.sessionId !== req.session.user.sessionId) {
101
129
  res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
130
+ // Also expose FullName (fallback to username) for display in client-side UI
131
+ res.cookie("fullName", req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
102
132
  res.cookie("sessionId", req.session.user.sessionId, cachedCookieOptions);
103
133
  }
104
134
  }
105
135
  next();
106
- }
136
+ }