mbkauthe 4.0.0 → 4.1.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/README.md CHANGED
@@ -23,6 +23,8 @@
23
23
  - Easy Express.js integration
24
24
  - Customizable Handlebars templates
25
25
  - Session fixation prevention
26
+ - Dynamic profile picture routing with session caching
27
+ - Modern responsive UI with desktop two-column layout
26
28
 
27
29
  ## 📦 Installation
28
30
 
package/docs/api.md CHANGED
@@ -356,6 +356,129 @@ fetch('/mbkauthe/api/logout', {
356
356
 
357
357
  ---
358
358
 
359
+ ### Multi-Account Endpoints
360
+
361
+ #### `GET /mbkauthe/accounts`
362
+
363
+ Renders the account switching page, allowing users to switch between remembered accounts on the device.
364
+
365
+ **Rate Limit:** 8 requests per minute
366
+
367
+ **CSRF Protection:** Required
368
+
369
+ **Response:** HTML page with account list
370
+
371
+ **Template Variables:**
372
+ - `customURL` - Redirect URL after switch
373
+ - `userLoggedIn` - Whether a user is currently logged in
374
+ - `username` - Current username
375
+ - `fullname` - Current user's full name
376
+ - `role` - Current user's role
377
+
378
+ **Usage:**
379
+ ```
380
+ GET /mbkauthe/accounts
381
+ ```
382
+
383
+ ---
384
+
385
+ #### `GET /mbkauthe/api/account-sessions`
386
+
387
+ Retrieves the list of remembered accounts for the current device.
388
+
389
+ **Rate Limit:** 8 requests per minute
390
+
391
+ **Response (200 OK):**
392
+ ```json
393
+ {
394
+ "accounts": [
395
+ {
396
+ "sessionId": "64-char-session-id",
397
+ "username": "john.doe",
398
+ "fullName": "John Doe",
399
+ "isCurrent": true
400
+ },
401
+ {
402
+ "sessionId": "another-session-id",
403
+ "username": "jane.smith",
404
+ "fullName": "Jane Smith",
405
+ "isCurrent": false
406
+ }
407
+ ],
408
+ "currentSessionId": "64-char-session-id"
409
+ }
410
+ ```
411
+
412
+ **Behavior:**
413
+ - Validates each stored session against the database
414
+ - Automatically removes invalid/expired sessions from the cookie
415
+ - Returns only valid, active sessions
416
+
417
+ ---
418
+
419
+ #### `POST /mbkauthe/api/switch-session`
420
+
421
+ Switches the active session to another remembered account.
422
+
423
+ **Rate Limit:** 8 requests per minute
424
+
425
+ **Request Body:**
426
+ ```json
427
+ {
428
+ "sessionId": "target-session-id (required)",
429
+ "redirect": "/dashboard (optional)"
430
+ }
431
+ ```
432
+
433
+ **Success Response (200 OK):**
434
+ ```json
435
+ {
436
+ "success": true,
437
+ "username": "jane.smith",
438
+ "fullName": "Jane Smith",
439
+ "redirect": "/dashboard"
440
+ }
441
+ ```
442
+
443
+ **Error Responses:**
444
+
445
+ | Status Code | Message |
446
+ |------------|---------|
447
+ | 400 | Invalid session ID format |
448
+ | 401 | Session expired |
449
+ | 403 | Account not available on this device |
450
+ | 500 | Internal Server Error |
451
+
452
+ **Behavior:**
453
+ - Verifies the target session exists in the device's remembered list
454
+ - Validates the session against the database
455
+ - Regenerates the session ID to prevent fixation
456
+ - Updates session cookies and current user context
457
+
458
+ ---
459
+
460
+ #### `POST /mbkauthe/api/logout-all`
461
+
462
+ Logs out all remembered accounts on the current device.
463
+
464
+ **Rate Limit:** 8 requests per minute
465
+
466
+ **Response (200 OK):**
467
+ ```json
468
+ {
469
+ "success": true,
470
+ "message": "All accounts logged out"
471
+ }
472
+ ```
473
+
474
+ **Behavior:**
475
+ - Deletes all session records associated with the device's remembered accounts from the database
476
+ - Clears the account list cookie
477
+ - Destroys the current session
478
+ - Clears all session cookies
479
+
480
+ ---
481
+
359
482
  #### `POST /mbkauthe/api/terminateAllSessions`
360
483
 
361
484
  Terminates all active sessions across all users (admin only).
@@ -523,34 +646,50 @@ Serves the application's SVG icon file from the root level.
523
646
 
524
647
  ---
525
648
 
526
- #### `GET /favicon.ico`
527
-
528
- Serves the application's favicon.
649
+ #### `GET /mbkauthe/bg.webp`
529
650
 
530
- **Aliases:** `/icon.ico`
651
+ Serves the background image for authentication pages.
531
652
 
532
- **Response:** ICO image file (Content-Type: image/x-icon)
653
+ **Response:** WEBP image file (Content-Type: image/webp)
533
654
 
534
655
  **Cache:** Cached for 1 year (max-age=31536000)
535
656
 
536
657
  **Usage:**
537
- ```html
538
- <link rel="icon" type="image/x-icon" href="/favicon.ico">
658
+ ```css
659
+ background-image: url('/mbkauthe/bg.webp');
539
660
  ```
540
661
 
541
662
  ---
542
663
 
543
- #### `GET /mbkauthe/bg.webp`
664
+ #### `GET /mbkauthe/user/profilepic`
544
665
 
545
- Serves the background image for authentication pages.
666
+ Serves the current user's profile picture or a default icon.
546
667
 
547
- **Response:** WEBP image file (Content-Type: image/webp)
668
+ **Authentication:** Optional (returns default icon if not logged in)
548
669
 
549
- **Cache:** Cached for 1 year (max-age=31536000)
670
+ **Response:**
671
+ - If logged in with valid profile picture URL: 302 redirect to the user's profile picture URL (from `Users.Image` column)
672
+ - If not logged in or no profile picture: SVG image file (Content-Type: image/svg+xml) streaming `/icon.svg`
673
+
674
+ **Cache:**
675
+ - Profile picture URL is cached in session for performance
676
+ - Cache is automatically cleared on login, logout, or account switch
677
+
678
+ **Behavior:**
679
+ 1. First request: Queries `Users` table for `Image` column value
680
+ 2. Subsequent requests: Returns cached value from session
681
+ 3. On login/logout/switch: Cache is invalidated and fresh data is fetched
550
682
 
551
683
  **Usage:**
552
- ```css
553
- background-image: url('/mbkauthe/bg.webp');
684
+ ```html
685
+ <img src="/mbkauthe/user/profilepic" alt="Profile Picture">
686
+ ```
687
+
688
+ **Example Response Flow:**
689
+ ```
690
+ User logged in → Query DB → Cache URL → Redirect to URL
691
+ User not logged in → Stream /icon.svg
692
+ Empty Image value → Stream /icon.svg
554
693
  ```
555
694
 
556
695
  ---
package/docs/db.md CHANGED
@@ -196,30 +196,48 @@ The GitHub login feature is now fully integrated into your mbkauthe system and r
196
196
  - (SessionId removed) The application now stores multiple concurrent sessions in the `Sessions` table.
197
197
  - `GuestRole` (JSONB): Stores additional guest-specific role information in binary JSON format.
198
198
  - `AllowedApps`(JSONB): Array of applications the user is authorized to access.
199
+ - `Image` (TEXT): URL to the user's profile picture. Used by the `/mbkauthe/user/profilepic` route to serve profile images. If empty, the route returns the default icon.svg. The URL is cached in the session for performance and automatically refreshed on login/logout/account switch.
200
+ - `FullName` (VARCHAR): The full name/display name of the user.
201
+ - `email` (TEXT): The user's email address.
199
202
 
200
203
  - **Schema:**
201
204
  ```sql
202
205
  CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
203
206
 
204
207
  CREATE TABLE "Users" (
205
- id INTEGER PRIMARY KEY AUTOINCREMENT,
208
+ id INTEGER PRIMARY KEY AUTOINCREMENT AS IDENTITY,
206
209
  "UserName" VARCHAR(50) NOT NULL UNIQUE,
207
- "Password" VARCHAR(61), -- For raw passwords (when EncPass=false)
208
- "PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
209
- "Role" role DEFAULT 'NormalUser' NOT NULL,
210
+ "Password" VARCHAR(255) NOT NULL,
210
211
  "Active" BOOLEAN DEFAULT FALSE,
212
+ "Role" role DEFAULT 'NormalUser' NOT NULL,
211
213
  "HaveMailAccount" BOOLEAN DEFAULT FALSE,
212
214
  "AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
213
215
  "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
214
216
  "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
215
- "last_login" TIMESTAMP WITH TIME ZONE
217
+ "last_login" TIMESTAMP WITH TIME ZONE,
218
+ "PasswordEnc" VARCHAR(128),
219
+
220
+ "FullName" VARCHAR(255),
221
+ "email" TEXT DEFAULT 'support@mbktech.org',
222
+ "Image" TEXT DEFAULT 'https://portal.mbktech.org/icon.svg', -- Profile picture URL (used by /mbkauthe/user/profilepic route)
223
+ "Bio" TEXT DEFAULT 'I am ....',
224
+ "SocialAccounts" TEXT DEFAULT '{}',
225
+ "Positions" jsonb DEFAULT '{"Not_Permanent":"Member Is Not Permanent"}',
226
+ "resetToken" TEXT,
227
+ "resetTokenExpires" TimeStamp,
228
+ "resetAttempts" INTEGER DEFAULT '0',
229
+ "lastResetAttempt" TimeStamp WITH TIME ZONE
216
230
  );
217
231
 
218
- -- Add indexes for performance optimization
219
- CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" ("UserName");
220
- CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" ("Active");
221
- CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" ("Role");
222
- CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" (last_login);
232
+
233
+ CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" USING BTREE ("UserName");
234
+ CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" USING BTREE ("Role");
235
+ CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" USING BTREE ("Active");
236
+ CREATE INDEX IF NOT EXISTS idx_users_email ON "Users" USING BTREE ("email");
237
+ CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" USING BTREE (last_login);
238
+ -- JSONB GIN indexes for common filters/queries on JSON fields
239
+ CREATE INDEX IF NOT EXISTS idx_users_allowedapps_gin ON "Users" USING GIN ("AllowedApps");
240
+ CREATE INDEX IF NOT EXISTS idx_users_positions_gin ON "Users" USING GIN ("Positions");
223
241
 
224
242
  -- Application Sessions table (stores multiple concurrent sessions per user)
225
243
  -- Note: this is separate from the express-session store table named "session"
package/docs/db.sql CHANGED
@@ -34,25 +34,41 @@ CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
34
34
 
35
35
  CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
36
36
 
37
+
37
38
  CREATE TABLE "Users" (
38
- id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT AS IDENTITY,
39
40
  "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,
41
+ "Password" VARCHAR(255) NOT NULL,
43
42
  "Active" BOOLEAN DEFAULT FALSE,
43
+ "Role" role DEFAULT 'NormalUser' NOT NULL,
44
44
  "HaveMailAccount" BOOLEAN DEFAULT FALSE,
45
45
  "AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
46
46
  "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
47
47
  "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
48
- "last_login" TIMESTAMP WITH TIME ZONE
48
+ "last_login" TIMESTAMP WITH TIME ZONE,
49
+ "PasswordEnc" VARCHAR(128),
50
+
51
+ "FullName" VARCHAR(255),
52
+ "email" TEXT DEFAULT 'support@mbktech.org',
53
+ "Image" TEXT DEFAULT 'https://portal.mbktech.org/icon.svg',
54
+ "Bio" TEXT DEFAULT 'I am ....',
55
+ "SocialAccounts" TEXT DEFAULT '{}',
56
+ "Positions" jsonb DEFAULT '{"Not_Permanent":"Member Is Not Permanent"}',
57
+ "resetToken" TEXT,
58
+ "resetTokenExpires" TimeStamp,
59
+ "resetAttempts" INTEGER DEFAULT '0',
60
+ "lastResetAttempt" TimeStamp WITH TIME ZONE
49
61
  );
50
62
 
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);
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" USING BTREE ("UserName");
65
+ CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" USING BTREE ("Role");
66
+ CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" USING BTREE ("Active");
67
+ CREATE INDEX IF NOT EXISTS idx_users_email ON "Users" USING BTREE ("email");
68
+ CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" USING BTREE (last_login);
69
+ -- JSONB GIN indexes for common filters/queries on JSON fields
70
+ CREATE INDEX IF NOT EXISTS idx_users_allowedapps_gin ON "Users" USING GIN ("AllowedApps");
71
+ CREATE INDEX IF NOT EXISTS idx_users_positions_gin ON "Users" USING GIN ("Positions");
56
72
 
57
73
  -- Application Sessions table (stores multiple concurrent sessions per user)
58
74
  -- Note: this is separate from the express-session store table named "session"
package/index.d.ts CHANGED
@@ -36,6 +36,7 @@ declare global {
36
36
  };
37
37
  oauthRedirect?: string;
38
38
  oauthCsrfToken?: string;
39
+ [key: string]: any;
39
40
  }
40
41
  }
41
42
  }
@@ -254,6 +255,8 @@ declare module 'mbkauthe' {
254
255
 
255
256
  export function clearSessionCookies(res: Response): void;
256
257
 
258
+ export function getLatestVersion(): Promise<string>;
259
+
257
260
  // Exports
258
261
  export const dblogin: Pool;
259
262
  export const mbkautheVar: MBKAuthConfig;
package/index.js CHANGED
@@ -71,7 +71,7 @@ if (process.env.test === "dev") {
71
71
  app.use(router);
72
72
  app.use((req, res) => {
73
73
  console.log(`[mbkauthe] Path not found: ${req.method} ${req.url}`);
74
- return renderError(res, {
74
+ return renderError(res, req, {
75
75
  layout: false,
76
76
  code: 404,
77
77
  error: "Not Found",
@@ -95,5 +95,8 @@ export {
95
95
  } from "./lib/middleware/auth.js";
96
96
  export { renderError } from "./lib/utils/response.js";
97
97
  export { dblogin } from "./lib/database/pool.js";
98
- export { ErrorCodes, ErrorMessages, getErrorByCode, createErrorResponse, logError } from "./lib/utils/errors.js";
98
+ export {
99
+ ErrorCodes, ErrorMessages, getErrorByCode,
100
+ createErrorResponse, logError
101
+ } from "./lib/utils/errors.js";
99
102
  export default router;
@@ -1,6 +1,83 @@
1
1
  import crypto from "crypto";
2
2
  import { mbkautheVar } from "./index.js";
3
3
 
4
+ // Maximum number of remembered accounts per device
5
+ const MAX_REMEMBERED_ACCOUNTS = 5;
6
+ const ACCOUNT_LIST_COOKIE = 'mbkauthe_accounts';
7
+
8
+ // Cookie security: encryption and signing
9
+ const COOKIE_ENCRYPTION_KEY = mbkautheVar.SESSION_SECRET || 'fallback-secret-key-change-this';
10
+ const ENCRYPTION_ALGORITHM = 'aes-256-gcm';
11
+
12
+ // Derive encryption key from session secret
13
+ const getEncryptionKey = () => {
14
+ return crypto.createHash('sha256').update(COOKIE_ENCRYPTION_KEY).digest();
15
+ };
16
+
17
+ // Encrypt and sign cookie payload
18
+ const encryptCookiePayload = (data) => {
19
+ try {
20
+ const iv = crypto.randomBytes(16);
21
+ const key = getEncryptionKey();
22
+ const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
23
+
24
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
25
+ encrypted += cipher.final('hex');
26
+
27
+ const authTag = cipher.getAuthTag();
28
+
29
+ // Combine iv + authTag + encrypted data
30
+ return {
31
+ iv: iv.toString('hex'),
32
+ authTag: authTag.toString('hex'),
33
+ data: encrypted
34
+ };
35
+ } catch (error) {
36
+ console.error('[mbkauthe] Cookie encryption error:', error);
37
+ return null;
38
+ }
39
+ };
40
+
41
+ // Decrypt and verify cookie payload
42
+ const decryptCookiePayload = (payload) => {
43
+ try {
44
+ if (!payload || !payload.iv || !payload.authTag || !payload.data) {
45
+ return null;
46
+ }
47
+
48
+ const key = getEncryptionKey();
49
+ const decipher = crypto.createDecipheriv(
50
+ ENCRYPTION_ALGORITHM,
51
+ key,
52
+ Buffer.from(payload.iv, 'hex')
53
+ );
54
+
55
+ decipher.setAuthTag(Buffer.from(payload.authTag, 'hex'));
56
+
57
+ let decrypted = decipher.update(payload.data, 'hex', 'utf8');
58
+ decrypted += decipher.final('utf8');
59
+
60
+ return JSON.parse(decrypted);
61
+ } catch (error) {
62
+ console.error('[mbkauthe] Cookie decryption error:', error);
63
+ return null;
64
+ }
65
+ };
66
+
67
+ // Generate fingerprint from user-agent only (salted)
68
+ const generateFingerprint = (req) => {
69
+ const userAgent = req.headers['user-agent'] || '';
70
+ // Use SESSION_SECRET_KEY as salt if available, otherwise fallback to encryption key
71
+ const salt = mbkautheVar.SESSION_SECRET_KEY || COOKIE_ENCRYPTION_KEY;
72
+
73
+ // Hash user-agent with salt to prevent rainbow table attacks on UAs
74
+ return crypto
75
+ .createHash('sha256')
76
+ .update(`${userAgent}:${salt}`)
77
+ .digest('hex')
78
+ .substring(0, 32);
79
+ };
80
+
4
81
  // Shared cookie options functions
5
82
  const getCookieOptions = () => ({
6
83
  maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
@@ -57,3 +134,83 @@ export const clearSessionCookies = (res) => {
57
134
  };
58
135
 
59
136
  export { getCookieOptions, getClearCookieOptions };
137
+
138
+ // ---- Multi-account helpers ----
139
+ const parseAccountList = (raw, req) => {
140
+ if (!raw) return [];
141
+ try {
142
+ // First, decrypt the cookie payload
143
+ const parsed = JSON.parse(raw);
144
+ const decrypted = decryptCookiePayload(parsed);
145
+
146
+ if (!decrypted || !decrypted.accounts || !decrypted.fingerprint) {
147
+ return [];
148
+ }
149
+
150
+ // Verify fingerprint matches current request
151
+ const currentFingerprint = generateFingerprint(req);
152
+ if (decrypted.fingerprint !== currentFingerprint) {
153
+ console.warn('[mbkauthe] Cookie fingerprint mismatch - possible cookie theft attempt');
154
+ return [];
155
+ }
156
+
157
+ const accounts = decrypted.accounts;
158
+ if (!Array.isArray(accounts)) return [];
159
+
160
+ // Accept only minimal safe fields
161
+ return accounts
162
+ .filter(item => item && typeof item === 'object')
163
+ .map(item => ({
164
+ sessionId: typeof item.sessionId === 'string' ? item.sessionId : null,
165
+ username: typeof item.username === 'string' ? item.username : null,
166
+ fullName: typeof item.fullName === 'string' ? item.fullName : null
167
+ }))
168
+ .filter(item => item.sessionId && item.username)
169
+ .slice(0, MAX_REMEMBERED_ACCOUNTS);
170
+ } catch (error) {
171
+ console.error('[mbkauthe] Error parsing account list:', error);
172
+ return [];
173
+ }
174
+ };
175
+
176
+ const writeAccountList = (res, list, req) => {
177
+ const sanitized = Array.isArray(list) ? list.slice(0, MAX_REMEMBERED_ACCOUNTS) : [];
178
+
179
+ // Create payload with fingerprint
180
+ const payload = {
181
+ accounts: sanitized,
182
+ fingerprint: generateFingerprint(req)
183
+ };
184
+
185
+ // Encrypt the payload
186
+ const encrypted = encryptCookiePayload(payload);
187
+ if (!encrypted) {
188
+ console.error('[mbkauthe] Failed to encrypt account list cookie');
189
+ return;
190
+ }
191
+
192
+ res.cookie(ACCOUNT_LIST_COOKIE, JSON.stringify(encrypted), cachedCookieOptions);
193
+ };
194
+
195
+ export const readAccountListFromCookie = (req) => {
196
+ const raw = req?.cookies ? req.cookies[ACCOUNT_LIST_COOKIE] : null;
197
+ return parseAccountList(raw, req);
198
+ };
199
+
200
+ export const upsertAccountListCookie = (req, res, entry) => {
201
+ if (!entry || !entry.sessionId || !entry.username) return;
202
+ const current = readAccountListFromCookie(req);
203
+ const filtered = current.filter(item => item.sessionId !== entry.sessionId && item.username !== entry.username);
204
+ const next = [{ sessionId: entry.sessionId, username: entry.username, fullName: entry.fullName || entry.username }, ...filtered];
205
+ writeAccountList(res, next, req);
206
+ };
207
+
208
+ export const removeAccountFromCookie = (req, res, sessionId) => {
209
+ const current = readAccountListFromCookie(req);
210
+ const next = current.filter(item => item.sessionId !== sessionId);
211
+ writeAccountList(res, next, req);
212
+ };
213
+
214
+ export const clearAccountListCookie = (res) => {
215
+ res.clearCookie(ACCOUNT_LIST_COOKIE, cachedClearCookieOptions);
216
+ };
package/lib/main.js CHANGED
@@ -69,10 +69,5 @@ router.get('/icon.svg', (req, res) => {
69
69
  res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
70
70
  });
71
71
 
72
- router.get(['/favicon.ico', '/icon.ico'], (req, res) => {
73
- res.setHeader('Cache-Control', 'public, max-age=31536000');
74
- res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
75
- });
76
-
77
72
  export { getLatestVersion } from "./routes/misc.js";
78
73
  export default router;
@@ -1,18 +1,23 @@
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, cachedCookieOptions } from "../config/cookies.js";
4
+ import { clearSessionCookies, cachedCookieOptions, readAccountListFromCookie } from "../config/cookies.js";
5
5
 
6
6
  async function validateSession(req, res, next) {
7
7
  if (!req.session.user) {
8
8
  console.log("[mbkauthe] User not authenticated");
9
- console.log("[mbkauthe]: ", req.session.user);
10
- return renderError(res, {
9
+ const remembered = readAccountListFromCookie(req) || [];
10
+ const hasRemembered = remembered.some(acct => acct && typeof acct.sessionId === 'string' && acct.sessionId.length > 0);
11
+ const pageTarget = hasRemembered ? '/mbkauthe/accounts' : `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`;
12
+ const message = hasRemembered
13
+ ? "Another saved account is available. Open the switch page to continue."
14
+ : "You Are Not Logged In. Please Log In To Continue.";
15
+ return renderError(res, req, {
11
16
  code: 401,
12
17
  error: "Not Logged In",
13
- message: "You Are Not Logged In. Please Log In To Continue.",
14
- pagename: "Login",
15
- page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
18
+ message,
19
+ pagename: hasRemembered ? "Switch Account" : "Login",
20
+ page: pageTarget,
16
21
  });
17
22
  }
18
23
 
@@ -24,7 +29,7 @@ async function validateSession(req, res, next) {
24
29
  console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
25
30
  req.session.destroy();
26
31
  clearSessionCookies(res);
27
- return renderError(res, {
32
+ return renderError(res, req, {
28
33
  code: 401,
29
34
  error: "Session Expired",
30
35
  message: "Your Session Has Expired. Please Log In Again.",
@@ -47,7 +52,7 @@ async function validateSession(req, res, next) {
47
52
  console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
48
53
  req.session.destroy();
49
54
  clearSessionCookies(res);
50
- return renderError(res, {
55
+ return renderError(res, req, {
51
56
  code: 401,
52
57
  error: "Session Expired",
53
58
  message: "Your Session Has Expired. Please Log In Again.",
@@ -64,7 +69,7 @@ async function validateSession(req, res, next) {
64
69
  // destroy and clear cookies
65
70
  req.session.destroy();
66
71
  clearSessionCookies(res);
67
- return renderError(res, {
72
+ return renderError(res, req, {
68
73
  code: 401,
69
74
  error: "Session Expired",
70
75
  message: "Your Session Has Expired. Please Log In Again.",
@@ -78,7 +83,7 @@ async function validateSession(req, res, next) {
78
83
  console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
79
84
  req.session.destroy();
80
85
  clearSessionCookies(res);
81
- return renderError(res, {
86
+ return renderError(res, req, {
82
87
  code: 401,
83
88
  error: "Account Inactive",
84
89
  message: "Your Account Is Inactive. Please Contact Support.",
@@ -94,7 +99,7 @@ async function validateSession(req, res, next) {
94
99
  console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
95
100
  req.session.destroy();
96
101
  clearSessionCookies(res);
97
- return renderError(res, {
102
+ return renderError(res, req, {
98
103
  code: 401,
99
104
  error: "Unauthorized",
100
105
  message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"`,
@@ -192,7 +197,7 @@ async function validateApiSession(req, res, next) {
192
197
  * Reload session user values from the database and refresh cookies.
193
198
  * - Validates sessionId and active status
194
199
  * - Updates `req.session.user` fields (username, role, allowedApps, fullname)
195
- * - Uses cached `fullName` cookie when available, otherwise queries `profiledata`
200
+ * - Uses cached `fullName` cookie when available, otherwise queries `Users`
196
201
  * - Syncs `username`, `fullName` and `sessionId` cookies
197
202
  * Returns: true if session refreshed and valid, false if session invalidated
198
203
  */
@@ -258,7 +263,7 @@ export async function reloadSessionUser(req, res) {
258
263
  req.session.user.fullname = req.cookies.fullName;
259
264
  } else {
260
265
  try {
261
- const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
266
+ const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "USers" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
262
267
  if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
263
268
  } catch (profileErr) {
264
269
  console.error('[mbkauthe] Error fetching fullname during reload:', profileErr);
@@ -290,7 +295,7 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
290
295
  try {
291
296
  if (!req.session || !req.session.user || !req.session.user.id) {
292
297
  console.log("[mbkauthe] User not authenticated");
293
- return renderError(res, {
298
+ return renderError(res, req, {
294
299
  code: 401,
295
300
  error: "Not Logged In",
296
301
  message: "You Are Not Logged In. Please Log In To Continue.",
@@ -304,7 +309,7 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
304
309
 
305
310
  // Check notAllowed role
306
311
  if (notAllowed && userRole === notAllowed) {
307
- return renderError(res, {
312
+ return renderError(res, req, {
308
313
  code: 403,
309
314
  error: "Access Denied",
310
315
  message: "You are not allowed to access this resource",
@@ -323,7 +328,7 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
323
328
 
324
329
  // Check if user role is in allowed roles
325
330
  if (!rolesArray.includes(userRole)) {
326
- return renderError(res, {
331
+ return renderError(res, req, {
327
332
  code: 403,
328
333
  error: "Access Denied",
329
334
  message: "You do not have permission to access this resource",
@@ -98,11 +98,11 @@ export async function sessionRestorationMiddleware(req, res, next) {
98
98
  if (req.cookies.fullName && typeof req.cookies.fullName === 'string') {
99
99
  req.session.user.fullname = req.cookies.fullName;
100
100
  } else {
101
- // Fallback: attempt to fetch FullName from profiledata to populate session
101
+ // Fallback: attempt to fetch FullName from Users to populate session
102
102
  try {
103
103
  const profileRes = await dblogin.query({
104
104
  name: 'restore-get-fullname',
105
- text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1',
105
+ text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
106
106
  values: [row.UserName]
107
107
  });
108
108
  if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {