mbkauthe 3.3.0 → 3.5.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
@@ -1,4 +1,4 @@
1
- # MBKAuthe v3.2 - Authentication System for Node.js
1
+ # MBKAuthe - Authentication System for Node.js
2
2
 
3
3
  [![Version](https://img.shields.io/npm/v/mbkauthe.svg)](https://www.npmjs.com/package/mbkauthe)
4
4
  [![License](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE)
@@ -16,7 +16,7 @@
16
16
  <img height="48px" src="https://handlebarsjs.com/handlebars-icon.svg" alt="Handlebars" />
17
17
  </p>
18
18
 
19
- **MBKAuth v3.2** is a production-ready authentication system for Node.js applications. Built with Express and PostgreSQL, it provides secure authentication, 2FA, role-based access, and OAuth integration (GitHub & Google) out of the box.
19
+ **MBKAuthe** is a production-ready authentication system for Node.js applications. Built with Express and PostgreSQL, it provides secure authentication, 2FA, role-based access, and OAuth integration (GitHub & Google) out of the box.
20
20
 
21
21
  ## ✨ Key Features
22
22
 
@@ -92,6 +92,7 @@ import dotenv from 'dotenv';
92
92
 
93
93
  dotenv.config();
94
94
 
95
+ // App-specific configuration (as JSON string)
95
96
  process.env.mbkautheVar = JSON.stringify({
96
97
  APP_NAME: process.env.APP_NAME,
97
98
  SESSION_SECRET_KEY: process.env.SESSION_SECRET_KEY,
@@ -102,6 +103,16 @@ process.env.mbkautheVar = JSON.stringify({
102
103
  loginRedirectURL: '/dashboard'
103
104
  });
104
105
 
106
+ // Optional shared configuration (useful for shared OAuth credentials across multiple projects)
107
+ process.env.mbkauthShared = JSON.stringify({
108
+ GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
109
+ GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
110
+ GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
111
+ GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET
112
+ });
113
+
114
+ // MBKAuth prioritizes values in mbkautheVar, then mbkauthShared, then built-in defaults.
115
+
105
116
  const app = express();
106
117
 
107
118
  // Mount authentication routes
@@ -335,6 +346,8 @@ const result = await dblogin.query('SELECT * FROM "Users"');
335
346
  - ✅ Use environment variables for all secrets
336
347
 
337
348
  **Vercel:**
349
+
350
+ Tip: On Vercel you can set `mbkauthShared` at the project or team level to share common OAuth credentials across multiple deployments. MBKAuth will use values from `mbkautheVar` first and fall back to `mbkauthShared`.
338
351
  ```json
339
352
  {
340
353
  "version": 2,
package/docs/api.md CHANGED
@@ -194,6 +194,39 @@ fetch('/mbkauthe/api/login', {
194
194
 
195
195
  ---
196
196
 
197
+ #### `GET /mbkauthe/api/checkSession`
198
+
199
+ Checks whether the current session (cookie-based) is valid. Returns a JSON response suitable for AJAX/SPA checks.
200
+
201
+ **Authentication:** Requires a valid session cookie set by `/mbkauthe/api/login`.
202
+
203
+ **Success Response (200 OK):**
204
+ ```json
205
+ {
206
+ "sessionValid": true,
207
+ "expiry": "2025-12-27T12:34:56.000Z"
208
+ }
209
+ ```
210
+
211
+ **Error Responses (examples):**
212
+ - 200 Session invalid ( { "sessionValid": false, "expiry": null } )
213
+ - 500 Internal Server Error (rare)
214
+
215
+ **Example Request:**
216
+ ```javascript
217
+ fetch('/mbkauthe/api/checkSession')
218
+ .then(res => res.json())
219
+ .then(data => {
220
+ if (data.sessionValid) {
221
+ // session active, expiry available in data.expiry
222
+ } else {
223
+ // not authenticated
224
+ }
225
+ });
226
+ ```
227
+
228
+ ---
229
+
197
230
  #### `GET /mbkauthe/2fa`
198
231
 
199
232
  Renders the Two-Factor Authentication verification page.
@@ -759,16 +792,51 @@ app.get('/protected', validateSession, (req, res) => {
759
792
  - Verifies user is authorized for the current application
760
793
  - Redirects to login page if validation fails
761
794
 
795
+ ### reloadSessionUser(req, res)
796
+
797
+ Use this helper when you need to refresh the values stored in `req.session.user` from the authoritative database record (for example, after a profile update that changes FullName, or when session expiration policies are updated).
798
+
799
+ - Behavior:
800
+ - Validates the session against the database (sessionId, active)
801
+ - Updates `req.session.user` fields: `username`, `role`, `allowedApps`, `fullname`
802
+ - Uses cached `fullName` cookie if available; falls back to querying `profiledata`
803
+ - Syncs `username`, `fullName`, and `sessionId` cookies for client display
804
+ - If the session is invalid (sessionId mismatch, inactive account, or unauthorized), it destroys the session and clears cookies
805
+
806
+ - Returns: `Promise<boolean>` — `true` if session was refreshed and still valid, `false` if session was invalidated or reload failed.
807
+
808
+ - Example:
809
+ ```javascript
810
+ import { reloadSessionUser } from 'mbkauthe';
811
+
812
+ // After updating profile data
813
+ app.post('/mbkauthe/api/update-profile', validateSession, async (req, res) => {
814
+ // ... update profiledata.FullName in DB ...
815
+ const refreshed = await reloadSessionUser(req, res);
816
+ if (!refreshed) {
817
+ return res.status(401).json({ success: false, message: 'Session invalidated' });
818
+ }
819
+ res.json({ success: true, fullname: req.session.user.fullname });
820
+ });
821
+ ```
822
+
762
823
  **Session Object:**
763
824
  ```javascript
764
825
  req.session.user = {
765
826
  id: 1, // User ID
766
- username: "john.doe", // Username
827
+ username: "john.doe", // Username (login name)
828
+ fullname: "John Doe", // Optional display name fetched from profiledata
767
829
  role: "NormalUser", // User role
768
830
  sessionId: "abc123...", // 64-char hex session ID
769
831
  }
770
832
  ```
771
833
 
834
+ **Session Cookie Sync:**
835
+ - The middleware sets non-httpOnly cookies for client display:
836
+ - `username` — the login username (exposed for UI)
837
+ - `fullName` — the display name (falls back to username if not available)
838
+
839
+ These cookies allow front-end UI to display a friendly name without making extra requests to the server.
772
840
  ---
773
841
 
774
842
  ### `checkRolePermission(requiredRole, notAllowed)`
package/docs/env.md CHANGED
@@ -1,455 +1,110 @@
1
1
  # Environment Configuration Guide
2
2
 
3
3
  [← Back to README](README.md)
4
+ This document describes the environment variables MBKAuth expects and keeps brief usage notes for each parameter. Validation and defaults are implemented in `lib/config/index.js` (it parses `mbkautheVar`, applies optional `mbkauthShared` fallbacks, normalizes values, and throws on validation failures).
4
5
 
5
- This guide explains how to configure your MBKAuth application using environment variables. Create a `.env` file in your project root and set the following variables according to your deployment needs.
6
+ ## How configuration is provided
7
+ - Primary payload: `mbkautheVar` — a JSON string with app-specific keys.
8
+ - Optional shared defaults: `mbkauthShared` — a JSON string used only for missing or empty keys.
9
+ - Example: `mbkautheVar={"APP_NAME":"mbkauthe", ...}`
6
10
 
7
11
  ---
8
12
 
9
- ## 📱 Application Settings
10
-
11
- ### App Name Configuration
12
- ```env
13
- APP_NAME=mbkauthe
14
- ```
15
-
16
- **Description:** Defines the application identifier used for user access control.
17
-
18
- - **Purpose:** Distinguishes this application from others in your ecosystem
19
- - **Security:** Users are restricted to apps they're authorized for via the `AllowedApp` column in the Users table
20
- - **Required:** Yes
21
-
22
- ---
23
-
24
- ## 🔐 Session Management
25
-
26
- ### Session Configuration
27
- ```env
28
- Main_SECRET_TOKEN=your-secure-token-number
29
- SESSION_SECRET_KEY=your-secure-random-key-here
30
- IS_DEPLOYED=false
31
- DOMAIN=localhost
32
- EncPass=false
33
- ```
34
-
35
- #### Main_SECRET_TOKEN
36
- **Description:** Primary authentication token for secure operations.
37
-
38
- - **Security:** Use a secure numeric or alphanumeric token
39
- - **Purpose:** Used for internal authentication and validation processes
40
- - **Format:** Numeric or string value
41
- - **Required:** Yes
42
-
43
- **Example:** `Main_SECRET_TOKEN=123456789`
44
-
45
- #### SESSION_SECRET_KEY
46
- **Description:** Cryptographic key for session security.
47
-
48
- - **Security:** Use a strong, randomly generated key (minimum 32 characters)
49
- - **Generation:** Generate securely at [Generate Secret](https://generate-secret.vercel.app/32)
50
- - **Example:** `SESSION_SECRET_KEY=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`
51
- - **Required:** Yes
52
-
53
- #### IS_DEPLOYED
54
- **Description:** Deployment environment flag that affects session behavior.
55
-
56
- **Values:**
57
- - `true` - Production/deployed environment
58
- - Sessions work across all subdomains of your specified domain
59
- - **Important:** Login will NOT work on `localhost` when set to `true`
60
- - `false` - Local development environment
61
- - Sessions work on localhost for development
62
-
63
- **Default:** `false`
64
-
65
- #### DOMAIN
66
- **Description:** Your application's domain name.
67
-
68
- **Configuration:**
69
- - **Production:** Set to your actual domain (e.g., `mbktech.com`)
70
- - **Development:** Use `localhost` or set `IS_DEPLOYED=false`
71
- - **Subdomains:** When `IS_DEPLOYED=true`, sessions are shared across all subdomains
72
-
73
- **Examples:**
74
- ```env
75
- # Production
76
- DOMAIN=yourdomain.com
77
- IS_DEPLOYED=true
78
-
79
- # Development
80
- DOMAIN=localhost
81
- IS_DEPLOYED=false
82
- ```
83
-
84
- #### EncPass
85
- **Description:** Controls whether passwords are stored and validated in encrypted format.
86
-
87
- **Values:**
88
- - `true` - Use encrypted password validation
89
- - Passwords are hashed using PBKDF2 with the username as salt
90
- - Compares against `PasswordEnc` column in Users table
91
- - `false` - Use raw password validation (default)
92
- - Passwords are stored and compared in plain text
93
- - Compares against `Password` column in Users table
94
-
95
- **Default:** `false`
96
-
97
- **Security Note:** Setting `EncPass=true` is recommended for production environments as it provides better security by storing hashed passwords instead of plain text.
98
-
99
- **Examples:**
100
- ```env
101
- # Production (recommended)
102
- EncPass=true
103
-
104
- # Development
105
- EncPass=false
106
- ```
107
-
108
- **Database Implications:**
109
- - When `EncPass=true`: The system uses the `PasswordEnc` column
110
- - When `EncPass=false`: The system uses the `Password` column
111
- - Ensure your database schema includes the appropriate column based on your configuration
112
-
113
- ---
114
-
115
- ## 🗄️ Database Configuration
116
-
117
- ### PostgreSQL Connection
118
- ```env
119
- LOGIN_DB=postgresql://username:password@host:port/database_name
120
- ```
121
-
122
- **Description:** PostgreSQL database connection string for user authentication.
123
-
124
- **Format:** `postgresql://[username]:[password]@[host]:[port]/[database]`
125
-
126
- **Examples:**
127
- ```env
128
- # Local database
129
- LOGIN_DB=postgresql://admin:password123@localhost:5432/mbkauth_db
130
-
131
- # Remote database
132
- LOGIN_DB=postgresql://user:pass@db.example.com:5432/production_db
133
-
134
- # With SSL (recommended for production)
135
- LOGIN_DB=postgresql://user:pass@host:5432/db?sslmode=require
136
- ```
137
-
138
- **Required:** Yes
139
-
140
- ---
141
-
142
- ## 🔒 Two-Factor Authentication (2FA)
143
-
144
- ### 2FA Configuration
145
- ```env
146
- MBKAUTH_TWO_FA_ENABLE=false
147
- ```
148
-
149
- **Description:** Enables or disables Two-Factor Authentication for enhanced security.
150
-
151
- **Values:**
152
- - `true` - Enable 2FA (recommended for production)
153
- - `false` - Disable 2FA (default)
154
-
155
- **Note:** When enabled, users will need to configure an authenticator app (Google Authenticator, Authy, etc.) for login.
156
-
157
- ---
158
-
159
- ## 🔄 Redirect Configuration
160
-
161
- ### Login Redirect URL
162
- ```env
163
- loginRedirectURL=/mbkauthe/test
164
- ```
165
-
166
- **Description:** Specifies the URL path where users are redirected after successful authentication.
167
-
168
- - **Purpose:** Controls post-login navigation flow
169
- - **Format:** URL path (relative or absolute)
170
- - **Default:** `/` (root path if not specified)
171
- - **Required:** No (optional configuration)
172
-
173
- **Examples:**
174
- ```env
175
- # Redirect to dashboard
176
- loginRedirectURL=/dashboard
177
-
178
- # Redirect to specific app section
179
- loginRedirectURL=/mbkauthe/test
180
-
181
- # Redirect to home page
182
- loginRedirectURL=/
183
-
184
- # Redirect to external URL (if supported)
185
- loginRedirectURL=https://example.com/app
186
- ```
187
-
188
- ---
189
-
190
- ## 🍪 Cookie Settings
191
-
192
- ### Cookie Expiration
193
- ```env
194
- COOKIE_EXPIRE_TIME=2
195
- ```
196
-
197
- **Description:** Sets how long authentication cookies remain valid.
198
-
199
- - **Unit:** Days
200
- - **Default:** `2` days
201
- - **Range:** 1-30 days (recommended)
202
- - **Security:** Shorter periods are more secure but require more frequent logins
203
-
204
- **Examples:**
205
- ```env
206
- COOKIE_EXPIRE_TIME=1 # 1 day (high security)
207
- COOKIE_EXPIRE_TIME=7 # 7 days (balanced)
208
- COOKIE_EXPIRE_TIME=30 # 30 days (convenience)
209
- ```
210
-
211
- ### Device Trust Duration
212
- ```env
213
- DEVICE_TRUST_DURATION_DAYS=7
214
- ```
215
-
216
- **Description:** Sets how long a device remains trusted after successful authentication.
217
-
218
- - **Unit:** Days
219
- - **Default:** `7` days
220
- - **Purpose:** Controls device recognition and trust persistence
221
- - **Range:** 1-365 days (recommended)
222
- - **Behavior:** Trusted devices may skip certain authentication steps (like 2FA) during this period
223
-
224
- **Examples:**
225
- ```env
226
- DEVICE_TRUST_DURATION_DAYS=1 # 1 day (high security)
227
- DEVICE_TRUST_DURATION_DAYS=7 # 1 week (balanced)
228
- DEVICE_TRUST_DURATION_DAYS=30 # 30 days (convenience)
229
- ```
230
-
231
- ---
232
-
233
- ## 🔐 OAuth Authentication
234
-
235
- ### GitHub OAuth Authentication
236
-
237
- #### GitHub Login Configuration
238
- ```env
239
- GITHUB_LOGIN_ENABLED=false
240
- GITHUB_CLIENT_ID=your-github-client-id
241
- GITHUB_CLIENT_SECRET=your-github-client-secret
242
- ```
243
-
244
- ##### GITHUB_LOGIN_ENABLED
245
- **Description:** Enables or disables GitHub OAuth login functionality.
246
-
247
- **Values:**
248
- - `true` - Enable GitHub login (users can authenticate via GitHub)
249
- - `false` - Disable GitHub login (default)
250
-
251
- **Required:** Yes (if using GitHub authentication)
252
-
253
- ##### GITHUB_CLIENT_ID
254
- **Description:** OAuth application client ID from GitHub.
255
-
256
- - **Purpose:** Identifies your application to GitHub's OAuth service
257
- - **Format:** Alphanumeric string provided by GitHub
258
- - **Setup:** Obtain from [GitHub Developer Settings](https://github.com/settings/developers)
259
- - **Required:** Yes (when `GITHUB_LOGIN_ENABLED=true`)
260
-
261
- **Example:** `GITHUB_CLIENT_ID=Iv1.a1b2c3d4e5f6g7h8`
262
-
263
- ##### GITHUB_CLIENT_SECRET
264
- **Description:** OAuth application client secret from GitHub.
265
-
266
- - **Purpose:** Authenticates your application with GitHub's OAuth service
267
- - **Security:** Keep this secret secure and never commit to version control
268
- - **Format:** Alphanumeric string provided by GitHub
269
- - **Setup:** Generated when creating OAuth app in GitHub Developer Settings
270
- - **Required:** Yes (when `GITHUB_LOGIN_ENABLED=true`)
271
-
272
- **Example:** `GITHUB_CLIENT_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0`
273
-
274
- #### Setting Up GitHub OAuth
275
-
276
- 1. **Create GitHub OAuth App:**
277
- - Go to [GitHub Developer Settings](https://github.com/settings/developers)
278
- - Click "New OAuth App"
279
- - Fill in application details:
280
- - **Application name:** Your app name
281
- - **Homepage URL:** `https://yourdomain.com` (or `http://localhost:3000` for dev)
282
- - **Authorization callback URL:** `https://yourdomain.com/mbkauthe/api/github/login/callback`
283
- - Click "Register application"
284
-
285
- 2. **Copy Credentials:**
286
- - Copy the **Client ID**
287
- - Generate and copy the **Client Secret**
288
-
289
- 3. **Configure Environment:**
290
- ```env
291
- GITHUB_LOGIN_ENABLED=true
292
- GITHUB_CLIENT_ID=your-copied-client-id
293
- GITHUB_CLIENT_SECRET=your-copied-client-secret
294
- ```
13
+ ## Parameters (short descriptions)
14
+
15
+ - APP_NAME
16
+ - Description: Application identifier used for access control.
17
+ - Example: `"APP_NAME":"mbkauthe"`
18
+ - Required: Yes
19
+
20
+ - Main_SECRET_TOKEN
21
+ - Description: Primary token used for internal auth and validations.
22
+ - Example: `"Main_SECRET_TOKEN":"my-secret-token"`
23
+ - Required: Yes
24
+
25
+ - SESSION_SECRET_KEY
26
+ - Description: Cryptographic key for sessions/cookies. Use a long random string.
27
+ - Example: `"SESSION_SECRET_KEY":"<32+ random chars>"`
28
+ - Required: Yes
29
+
30
+ - IS_DEPLOYED
31
+ - Description: Deployment mode flag; affects cookie domain and localhost behavior.
32
+ - Values: `true` / `false` / `f` (normalized to strings)
33
+ - Example: `"IS_DEPLOYED":"false"`
34
+ - Required: Yes
35
+
36
+ - DOMAIN
37
+ - Description: App domain (e.g., `localhost` or `yourdomain.com`). Required when deployed.
38
+ - Example: `"DOMAIN":"localhost"`
39
+ - Required: Yes
40
+
41
+ - LOGIN_DB
42
+ - Description: PostgreSQL connection string for auth (must start with `postgresql://` or `postgres://`).
43
+ - Example: `"LOGIN_DB":"postgresql://user:pass@localhost:5432/mbkauth"`
44
+ - Required: Yes
45
+
46
+ - MBKAUTH_TWO_FA_ENABLE
47
+ - Description: Enable Two-Factor Authentication.
48
+ - Values: `true` / `false` / `f`
49
+ - Example: `"MBKAUTH_TWO_FA_ENABLE":"true"`
50
+ - Required: Yes
51
+
52
+ - EncPass
53
+ - Description: When `true`, use hashed password column (`PasswordEnc`) instead of plain `Password`.
54
+ - Default: `false` (recommended `true` in production)
55
+ - Example: `"EncPass":"true"`
56
+ - Required: No
57
+
58
+ - COOKIE_EXPIRE_TIME
59
+ - Description: Session cookie lifetime (days).
60
+ - Default: `2`
61
+ - Example: `"COOKIE_EXPIRE_TIME":7`
62
+ - Required: No
63
+
64
+ - DEVICE_TRUST_DURATION_DAYS
65
+ - Description: Days a device remains trusted (skips some auth steps).
66
+ - Default: `7`
67
+ - Example: `"DEVICE_TRUST_DURATION_DAYS":30`
68
+ - Required: No
69
+
70
+ - loginRedirectURL
71
+ - Description: Post-login redirect path.
72
+ - Default: `/dashboard`
73
+ - Example: `"loginRedirectURL":"/dashboard"`
74
+ - Required: No
75
+
76
+ - GITHUB_LOGIN_ENABLED / GOOGLE_LOGIN_ENABLED
77
+ - Description: Enable OAuth providers.
78
+ - Default: `false`
79
+ - If `true`, corresponding `*_CLIENT_ID` and `*_CLIENT_SECRET` are required.
80
+
81
+ - GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
82
+ - Description: OAuth credentials (put in `mbkautheVar` preferred, or `mbkauthShared`).
83
+ - Required when provider enabled.
295
84
 
296
85
  ---
297
86
 
298
- ### Google OAuth Authentication
87
+ ## Quick examples
88
+ Development (.env):
299
89
 
300
- #### Google Login Configuration
301
90
  ```env
302
- GOOGLE_LOGIN_ENABLED=false
303
- GOOGLE_CLIENT_ID=your-google-client-id
304
- GOOGLE_CLIENT_SECRET=your-google-client-secret
91
+ mbkautheVar={"APP_NAME":"mbkauthe","Main_SECRET_TOKEN":"dev-token","SESSION_SECRET_KEY":"dev-secret","IS_DEPLOYED":"false","DOMAIN":"localhost","EncPass":"false","LOGIN_DB":"postgresql://user:pass@localhost:5432/mbkauth_dev","MBKAUTH_TWO_FA_ENABLE":"false"}
92
+ mbkauthShared={"GITHUB_LOGIN_ENABLED":"false"}
305
93
  ```
306
94
 
307
- ##### GOOGLE_LOGIN_ENABLED
308
- **Description:** Enables or disables Google OAuth login functionality.
309
-
310
- **Values:**
311
- - `true` - Enable Google login (users can authenticate via Google)
312
- - `false` - Disable Google login (default)
313
-
314
- **Required:** Yes (if using Google authentication)
315
-
316
- ##### GOOGLE_CLIENT_ID
317
- **Description:** OAuth 2.0 client ID from Google Cloud Console.
318
-
319
- - **Purpose:** Identifies your application to Google's OAuth service
320
- - **Format:** String ending in `.apps.googleusercontent.com`
321
- - **Setup:** Obtain from [Google Cloud Console](https://console.cloud.google.com/)
322
- - **Required:** Yes (when `GOOGLE_LOGIN_ENABLED=true`)
323
-
324
- **Example:** `GOOGLE_CLIENT_ID=123456789-abc123def456.apps.googleusercontent.com`
325
-
326
- ##### GOOGLE_CLIENT_SECRET
327
- **Description:** OAuth 2.0 client secret from Google Cloud Console.
328
-
329
- - **Purpose:** Authenticates your application with Google's OAuth service
330
- - **Security:** Keep this secret secure and never commit to version control
331
- - **Format:** Alphanumeric string provided by Google
332
- - **Setup:** Generated when creating OAuth credentials in Google Cloud Console
333
- - **Required:** Yes (when `GOOGLE_LOGIN_ENABLED=true`)
334
-
335
- **Example:** `GOOGLE_CLIENT_SECRET=GOCSPX-abc123def456ghi789jkl012mno`
336
-
337
- #### Setting Up Google OAuth
338
-
339
- 1. **Create Google Cloud Project:**
340
- - Go to [Google Cloud Console](https://console.cloud.google.com/)
341
- - Create a new project or select an existing one
342
- - Enable the Google+ API (or People API)
343
-
344
- 2. **Configure OAuth Consent Screen:**
345
- - Navigate to "OAuth consent screen" in the sidebar
346
- - Choose "External" user type (or "Internal" for Google Workspace)
347
- - Fill in required app information
348
- - Add your domain to authorized domains
349
- - Save and continue
350
-
351
- 3. **Create OAuth Credentials:**
352
- - Navigate to "Credentials" in the sidebar
353
- - Click "Create Credentials" > "OAuth 2.0 Client ID"
354
- - Choose "Web application" as application type
355
- - Add authorized JavaScript origins:
356
- - `https://yourdomain.com`
357
- - `http://localhost:3000` (for development)
358
- - Add authorized redirect URIs:
359
- - `https://yourdomain.com/mbkauthe/api/google/login/callback`
360
- - `http://localhost:3000/mbkauthe/api/google/login/callback` (for development)
361
- - Click "Create"
362
-
363
- 4. **Copy Credentials:**
364
- - Copy the **Client ID**
365
- - Copy the **Client Secret**
95
+ Production (short):
366
96
 
367
- 5. **Configure Environment:**
368
- ```env
369
- GOOGLE_LOGIN_ENABLED=true
370
- GOOGLE_CLIENT_ID=your-copied-client-id
371
- GOOGLE_CLIENT_SECRET=your-copied-client-secret
372
- ```
373
-
374
- ### OAuth Security Notes
375
-
376
- - Use separate OAuth apps for development and production environments
377
- - Rotate client secrets periodically
378
- - Never expose client secrets in client-side code
379
- - Ensure callback URLs match exactly with OAuth provider configuration
380
- - Users must link their OAuth accounts before they can use OAuth login
381
-
382
- ---
383
-
384
- ## 🚀 Quick Setup Examples
385
-
386
- ### Development Environment
387
- ```env
388
- # .env file for local development
389
- APP_NAME=mbkauthe
390
- Main_SECRET_TOKEN=dev-token-123
391
- SESSION_SECRET_KEY=dev-secret-key-change-in-production
392
- IS_DEPLOYED=false
393
- DOMAIN=localhost
394
- EncPass=false
395
- LOGIN_DB=postgresql://admin:password@localhost:5432/mbkauth_dev
396
- MBKAUTH_TWO_FA_ENABLE=false
397
- COOKIE_EXPIRE_TIME=7
398
- DEVICE_TRUST_DURATION_DAYS=7
399
- loginRedirectURL=/dashboard
400
- GITHUB_LOGIN_ENABLED=false
401
- GITHUB_CLIENT_ID=
402
- GITHUB_CLIENT_SECRET=
403
- ```
404
-
405
- ### Production Environment
406
97
  ```env
407
- # .env file for production deployment
408
- APP_NAME=mbkauthe
409
- Main_SECRET_TOKEN=your-secure-production-token
410
- SESSION_SECRET_KEY=your-super-secure-production-key-here
411
- IS_DEPLOYED=true
412
- DOMAIN=yourdomain.com
413
- EncPass=true
414
- LOGIN_DB=postgresql://dbuser:securepass@prod-db.example.com:5432/mbkauth_prod
415
- MBKAUTH_TWO_FA_ENABLE=true
416
- COOKIE_EXPIRE_TIME=2
417
- DEVICE_TRUST_DURATION_DAYS=7
418
- loginRedirectURL=/dashboard
419
- GITHUB_LOGIN_ENABLED=false
420
- GITHUB_CLIENT_ID=
421
- GITHUB_CLIENT_SECRET=
98
+ mbkautheVar={"APP_NAME":"mbkauthe","Main_SECRET_TOKEN":"prod-token","SESSION_SECRET_KEY":"prod-secret","IS_DEPLOYED":"true","DOMAIN":"yourdomain.com","EncPass":"true","LOGIN_DB":"postgresql://dbuser:secure@db:5432/mbkauth_prod","MBKAUTH_TWO_FA_ENABLE":"true"}
422
99
  ```
423
100
 
424
101
  ---
425
102
 
426
- ## ⚠️ Important Security Notes
427
-
428
- 1. **Never commit your `.env` file** to version control
429
- 2. **Use strong, unique secrets** for production environments
430
- 3. **Enable HTTPS** when `IS_DEPLOYED=true`
431
- 4. **Regularly rotate** your `SESSION_SECRET_KEY`
432
- 5. **Use environment-specific databases** (separate dev/prod databases)
433
- 6. **Enable 2FA** for production environments
434
-
435
- ---
436
-
437
- ## 🔧 Troubleshooting
438
-
439
- ### Common Issues
440
-
441
- **Login not working on localhost:**
442
- - Ensure `IS_DEPLOYED=false` for local development
443
- - Check that `DOMAIN=localhost`
444
-
445
- **Session not persisting:**
446
- - Verify `SESSION_SECRET_KEY` is set and consistent
447
- - Check cookie settings in your browser
448
-
449
- **Database connection errors:**
450
- - Verify database credentials and connection string format
451
- - Ensure database server is running and accessible
103
+ ## Rules & best practices
104
+ - Boolean-like fields: use `"true"`, `"false"`, or `"f"` (the parser accepts booleans too and normalizes to strings).
105
+ - Numeric fields: must be positive numbers (e.g., `COOKIE_EXPIRE_TIME`, `DEVICE_TRUST_DURATION_DAYS`).
106
+ - `LOGIN_DB` must start with `postgresql://` or `postgres://`.
107
+ - Never commit `.env` to source control and use HTTPS in production (when `IS_DEPLOYED=true`).
108
+ - Use a >=32-char `SESSION_SECRET_KEY` and rotate secrets regularly.
452
109
 
453
- **2FA issues:**
454
- - Confirm authenticator app time is synchronized
455
- - Verify `MBKAUTH_TWO_FA_ENABLE` setting matches your setup
110
+ For the exact validation messages and default application, consult `lib/config/index.js` (it will throw a comprehensive error if validation fails at startup).
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,6 +21,7 @@ declare global {
20
21
  user?: {
21
22
  id: number;
22
23
  username: string;
24
+ fullname?: string;
23
25
  role: 'SuperAdmin' | 'NormalUser' | 'Guest';
24
26
  sessionId: string;
25
27
  allowedApps?: string[];
@@ -75,6 +77,7 @@ declare module 'mbkauthe' {
75
77
  export interface SessionUser {
76
78
  id: number;
77
79
  username: string;
80
+ fullname?: string;
78
81
  role: UserRole;
79
82
  sessionId: string;
80
83
  allowedApps?: string[];
@@ -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";
@@ -46,6 +46,7 @@ export const clearSessionCookies = (res) => {
46
46
  res.clearCookie("mbkauthe.sid", cachedClearCookieOptions);
47
47
  res.clearCookie("sessionId", cachedClearCookieOptions);
48
48
  res.clearCookie("username", cachedClearCookieOptions);
49
+ res.clearCookie("fullName", cachedClearCookieOptions);
49
50
  res.clearCookie("device_token", cachedClearCookieOptions);
50
51
  };
51
52
 
@@ -28,37 +28,81 @@ function validateConfiguration() {
28
28
  throw new Error(`[mbkauthe] Configuration Error:\n - ${errors.join('\n - ')}`);
29
29
  }
30
30
 
31
- // Parse and validate oAuthVar (optional fallback for OAuth settings)
32
- let oAuthVar = null;
31
+ // Parse and validate mbkauthShared (optional fallback for shared settings)
32
+ let mbkauthShared = null;
33
33
  try {
34
- if (process.env.oAuthVar) {
35
- oAuthVar = JSON.parse(process.env.oAuthVar);
36
- if (oAuthVar && typeof oAuthVar !== 'object') {
37
- console.warn('[mbkauthe] oAuthVar is not a valid object, ignoring it');
38
- oAuthVar = null;
34
+ if (process.env.mbkauthShared) {
35
+ mbkauthShared = JSON.parse(process.env.mbkauthShared);
36
+ if (mbkauthShared && typeof mbkauthShared !== 'object') {
37
+ console.warn('[mbkauthe] mbkauthShared is not a valid object, ignoring it');
38
+ mbkauthShared = null;
39
39
  } else {
40
- console.log('[mbkauthe] oAuthVar detected and parsed successfully');
40
+ console.log('[mbkauthe] mbkauthShared detected and parsed successfully');
41
41
  }
42
42
  }
43
43
  } catch (error) {
44
- console.warn('[mbkauthe] Invalid JSON in process.env.oAuthVar, ignoring it');
45
- oAuthVar = null;
46
- }
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 applyFallback = (source, sourceName) => {
50
+ if (!source) return;
51
+ Object.keys(source).forEach(key => {
52
+ const val = source[key];
53
+ if ((mbkautheVar[key] === undefined || (typeof mbkautheVar[key] === 'string' && mbkautheVar[key].trim() === '')) &&
54
+ val !== undefined && !(typeof val === 'string' && val.trim() === '')) {
55
+ mbkautheVar[key] = val;
56
+ console.log(`[mbkauthe] Using ${key} from ${sourceName}`);
57
+ }
58
+ });
59
+ };
60
+
61
+ applyFallback(mbkauthShared, 'mbkauthShared');
47
62
 
48
- // Merge OAuth settings: use oAuthVar as fallback if values not in mbkautheVar
49
- const oAuthKeys = [
50
- 'GITHUB_LOGIN_ENABLED', 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET',
51
- 'GOOGLE_LOGIN_ENABLED', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'
63
+ // Ensure specific keys are checked in mbkautheVar first, then mbkauthShared, then apply config defaults
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"
52
68
  ];
53
-
54
- if (oAuthVar) {
55
- oAuthKeys.forEach(key => {
56
- if ((!mbkautheVar[key] || (typeof mbkautheVar[key] === 'string' && mbkautheVar[key].trim() === '')) && oAuthVar[key]) {
57
- mbkautheVar[key] = oAuthVar[key];
58
- console.log(`[mbkauthe] Using ${key} from oAuthVar`);
69
+
70
+ const defaults = {
71
+ DEVICE_TRUST_DURATION_DAYS: 7,
72
+ EncPass: 'false',
73
+ IS_DEPLOYED: 'false',
74
+ MBKAUTH_TWO_FA_ENABLE: 'false',
75
+ COOKIE_EXPIRE_TIME: 2,
76
+ loginRedirectURL: '/dashboard',
77
+ GITHUB_LOGIN_ENABLED: 'false',
78
+ GOOGLE_LOGIN_ENABLED: 'false'
79
+ };
80
+
81
+ keysToCheck.forEach(key => {
82
+ const current = mbkautheVar[key];
83
+ const isEmpty = current === undefined || (typeof current === 'string' && current.trim() === '');
84
+ if (isEmpty) {
85
+ if (mbkauthShared && mbkauthShared[key] !== undefined && !(typeof mbkauthShared[key] === 'string' && mbkauthShared[key].trim() === '')) {
86
+ mbkautheVar[key] = mbkauthShared[key];
87
+ console.log(`[mbkauthe] Using ${key} from mbkauthShared`);
88
+ } else if (defaults[key] !== undefined) {
89
+ mbkautheVar[key] = defaults[key];
90
+ console.log(`[mbkauthe] Using default value for ${key}`);
59
91
  }
60
- });
61
- }
92
+ }
93
+ });
94
+
95
+ // Normalize boolean-like values to consistent lowercase 'true'/'false' strings
96
+ ['GITHUB_LOGIN_ENABLED','GOOGLE_LOGIN_ENABLED','MBKAUTH_TWO_FA_ENABLE','IS_DEPLOYED','EncPass'].forEach(k => {
97
+ const val = mbkautheVar[k];
98
+ if (typeof val === 'boolean') {
99
+ mbkautheVar[k] = val ? 'true' : 'false';
100
+ } else if (typeof val === 'string') {
101
+ const norm = val.trim().toLowerCase();
102
+ // Accept 'f' as shorthand for false but normalize it to 'false'
103
+ mbkautheVar[k] = (norm === 'f') ? 'false' : norm;
104
+ }
105
+ });
62
106
 
63
107
  // Validate required keys
64
108
  // COOKIE_EXPIRE_TIME is not required but if provided must be valid, COOKIE_EXPIRE_TIME by default is 2 days
@@ -73,7 +117,7 @@ function validateConfiguration() {
73
117
  });
74
118
 
75
119
  // Validate IS_DEPLOYED value
76
- if (mbkautheVar.IS_DEPLOYED && !['true', 'false', 'f'].includes(mbkautheVar.IS_DEPLOYED)) {
120
+ if (mbkautheVar.IS_DEPLOYED && !['true', 'false', 'f'].includes((mbkautheVar.IS_DEPLOYED + '').toLowerCase())) {
77
121
  errors.push("mbkautheVar.IS_DEPLOYED must be either 'true' or 'false' or 'f'");
78
122
  }
79
123
 
@@ -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) {
@@ -97,6 +97,160 @@ async function validateSession(req, res, next) {
97
97
  }
98
98
  }
99
99
 
100
+ /**
101
+ * API-friendly session validation middleware
102
+ * Returns JSON error responses instead of rendering pages
103
+ */
104
+ async function validateApiSession(req, res, next) {
105
+ if (!req.session.user) {
106
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
107
+ }
108
+
109
+ try {
110
+ const { id, sessionId, role, allowedApps } = req.session.user;
111
+
112
+ // Defensive checks for sessionId and allowedApps
113
+ if (!sessionId) {
114
+ console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
115
+ req.session.destroy();
116
+ clearSessionCookies(res);
117
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
118
+ }
119
+
120
+ // Normalize sessionId to lowercase for consistent comparison
121
+ const normalizedSessionId = sessionId.toLowerCase();
122
+
123
+ // Single optimized query to validate session and get role
124
+ const query = `SELECT "SessionId", "Active", "Role" FROM "Users" WHERE "id" = $1`;
125
+ const result = await dblogin.query({ name: 'validate-user-session-for-api', text: query, values: [id] });
126
+
127
+ const dbSessionId = result.rows.length > 0 && result.rows[0].SessionId ? String(result.rows[0].SessionId).toLowerCase() : null;
128
+ if (!dbSessionId || dbSessionId !== normalizedSessionId) {
129
+ if (result.rows.length > 0 && !result.rows[0].SessionId) {
130
+ console.warn(`[mbkauthe] DB sessionId is null for user "${req.session.user.username}"`);
131
+ }
132
+ console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
133
+ req.session.destroy();
134
+ clearSessionCookies(res);
135
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
136
+ }
137
+
138
+ if (!result.rows[0].Active) {
139
+ console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
140
+ req.session.destroy();
141
+ clearSessionCookies(res);
142
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
143
+ }
144
+
145
+ if (role !== "SuperAdmin") {
146
+ // If allowedApps is not provided or not an array, treat as no access
147
+ const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
148
+ if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
149
+ console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
150
+ req.session.destroy();
151
+ clearSessionCookies(res);
152
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
153
+ }
154
+ }
155
+
156
+ // Store user role in request for checkRolePermission to use
157
+ req.userRole = result.rows[0].Role;
158
+
159
+ next();
160
+ } catch (err) {
161
+ console.error("[mbkauthe] API session validation error:", err);
162
+ return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Reload session user values from the database and refresh cookies.
168
+ * - Validates sessionId and active status
169
+ * - Updates `req.session.user` fields (username, role, allowedApps, fullname)
170
+ * - Uses cached `fullName` cookie when available, otherwise queries `profiledata`
171
+ * - Syncs `username`, `fullName` and `sessionId` cookies
172
+ * Returns: true if session refreshed and valid, false if session invalidated
173
+ */
174
+ export async function reloadSessionUser(req, res) {
175
+ if (!req.session || !req.session.user || !req.session.user.id) return false;
176
+ try {
177
+ const { id, sessionId: currentSessionId } = req.session.user;
178
+
179
+ // Fetch fresh user record
180
+ const query = `SELECT id, "UserName", "Active", "Role", "AllowedApps", "SessionId" FROM "Users" WHERE id = $1`;
181
+ const result = await dblogin.query({ name: 'reload-session-user', text: query, values: [id] });
182
+
183
+ if (result.rows.length === 0) {
184
+ // User not found — invalidate session
185
+ req.session.destroy(() => {});
186
+ clearSessionCookies(res);
187
+ return false;
188
+ }
189
+
190
+ const row = result.rows[0];
191
+ const dbSessionId = row.SessionId ? String(row.SessionId).toLowerCase() : null;
192
+ if (!dbSessionId || dbSessionId !== String(currentSessionId).toLowerCase()) {
193
+ // Session invalidated in DB
194
+ req.session.destroy(() => {});
195
+ clearSessionCookies(res);
196
+ return false;
197
+ }
198
+
199
+ if (!row.Active) {
200
+ // Account is inactive
201
+ req.session.destroy(() => {});
202
+ clearSessionCookies(res);
203
+ return false;
204
+ }
205
+
206
+ // Authorization: ensure allowed for current app unless SuperAdmin
207
+ if (row.Role !== 'SuperAdmin') {
208
+ const allowedApps = row.AllowedApps;
209
+ const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
210
+ if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
211
+ req.session.destroy(() => {});
212
+ clearSessionCookies(res);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ // Update session fields
218
+ req.session.user.username = row.UserName;
219
+ req.session.user.role = row.Role;
220
+ req.session.user.allowedApps = row.AllowedApps;
221
+
222
+ // Obtain fullname from client cookie cache when present else DB
223
+ if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
224
+ req.session.user.fullname = req.cookies.fullName;
225
+ } else {
226
+ try {
227
+ const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
228
+ if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
229
+ } catch (profileErr) {
230
+ console.error('[mbkauthe] Error fetching fullname during reload:', profileErr);
231
+ }
232
+ }
233
+
234
+ // Persist session changes
235
+ await new Promise((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
236
+
237
+ // Sync cookies for client UI (non-httpOnly)
238
+ try {
239
+ res.cookie('username', req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
240
+ res.cookie('fullName', req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
241
+ res.cookie('sessionId', req.session.user.sessionId, cachedCookieOptions);
242
+ } catch (cookieErr) {
243
+ // Ignore cookie setting errors, session is still refreshed
244
+ console.error('[mbkauthe] Error syncing cookies during reload:', cookieErr);
245
+ }
246
+
247
+ return true;
248
+ } catch (err) {
249
+ console.error('[mbkauthe] reloadSessionUser error:', err);
250
+ return false;
251
+ }
252
+ }
253
+
100
254
  const checkRolePermission = (requiredRoles, notAllowed) => {
101
255
  return async (req, res, next) => {
102
256
  try {
@@ -173,5 +327,4 @@ const authenticate = (authentication) => {
173
327
  };
174
328
  };
175
329
 
176
-
177
- export { validateSession, checkRolePermission, validateSessionAndRole, authenticate };
330
+ export { validateSession, validateApiSession, checkRolePermission, validateSessionAndRole, authenticate };
@@ -85,6 +85,25 @@ export async function sessionRestorationMiddleware(req, res, next) {
85
85
  sessionId: normalizedSessionId,
86
86
  allowedApps: user.AllowedApps,
87
87
  };
88
+
89
+ // Use cached FullName from client cookie when available to avoid extra DB queries
90
+ if (req.cookies.fullName && typeof req.cookies.fullName === 'string') {
91
+ req.session.user.fullname = req.cookies.fullName;
92
+ } else {
93
+ // Fallback: attempt to fetch FullName from profiledata to populate session
94
+ try {
95
+ const profileRes = await dblogin.query({
96
+ name: 'restore-get-fullname',
97
+ text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1',
98
+ values: [user.UserName]
99
+ });
100
+ if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {
101
+ req.session.user.fullname = profileRes.rows[0].FullName;
102
+ }
103
+ } catch (profileErr) {
104
+ console.error("[mbkauthe] Error fetching FullName during session restore:", profileErr);
105
+ }
106
+ }
88
107
  }
89
108
  } catch (err) {
90
109
  console.error("[mbkauthe] Session restoration error:", err);
@@ -99,8 +118,10 @@ export function sessionCookieSyncMiddleware(req, res, next) {
99
118
  // Only set cookies if they're missing or different
100
119
  if (req.cookies.sessionId !== req.session.user.sessionId) {
101
120
  res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
121
+ // Also expose FullName (fallback to username) for display in client-side UI
122
+ res.cookie("fullName", req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
102
123
  res.cookie("sessionId", req.session.user.sessionId, cachedCookieOptions);
103
124
  }
104
125
  }
105
126
  next();
106
- }
127
+ }
@@ -170,6 +170,20 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
170
170
  allowedApps: user.allowedApps || user.AllowedApps,
171
171
  };
172
172
 
173
+ // Attempt to fetch FullName from profiledata and store it in session for display purposes
174
+ try {
175
+ const profileResult = await dblogin.query({
176
+ name: 'login-get-fullname',
177
+ text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1',
178
+ values: [username]
179
+ });
180
+ if (profileResult.rows.length > 0 && profileResult.rows[0].FullName) {
181
+ req.session.user.fullname = profileResult.rows[0].FullName;
182
+ }
183
+ } catch (profileErr) {
184
+ console.error("[mbkauthe] Error fetching FullName for user:", profileErr);
185
+ }
186
+
173
187
  if (req.session.preAuthUser) {
174
188
  delete req.session.preAuthUser;
175
189
  }
@@ -180,7 +194,9 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
180
194
  return res.status(500).json({ success: false, message: "Internal Server Error" });
181
195
  }
182
196
 
197
+ // Expose sessionId and display name to client for UI (fullName falls back to username)
183
198
  res.cookie("sessionId", sessionId, cachedCookieOptions);
199
+ res.cookie("fullName", req.session.user.fullname || username, { ...cachedCookieOptions, httpOnly: false });
184
200
 
185
201
  // Handle trusted device if requested
186
202
  if (trustDevice) {
@@ -3,7 +3,7 @@ import fetch from 'node-fetch';
3
3
  import rateLimit from 'express-rate-limit';
4
4
  import { mbkautheVar, packageJson, appVersion } from "../config/index.js";
5
5
  import { renderError } from "../utils/response.js";
6
- import { authenticate, validateSession } from "../middleware/auth.js";
6
+ import { authenticate, validateSession, validateApiSession } from "../middleware/auth.js";
7
7
  import { ErrorCodes, ErrorMessages } from "../utils/errors.js";
8
8
  import { dblogin } from "../database/pool.js";
9
9
  import { clearSessionCookies } from "../config/cookies.js";
@@ -90,6 +90,41 @@ router.get('/test', validateSession, LoginLimit, async (req, res) => {
90
90
  }
91
91
  });
92
92
 
93
+ // API: check current session validity (JSON) — minimal response
94
+ router.get('/api/checkSession', LoginLimit, async (req, res) => {
95
+ try {
96
+ if (!req.session?.user) {
97
+ return res.status(200).json({ sessionValid: false, expiry: null });
98
+ }
99
+
100
+ const { id, sessionId } = req.session.user;
101
+ if (!sessionId) {
102
+ req.session.destroy(() => { });
103
+ clearSessionCookies(res);
104
+ return res.status(200).json({ sessionValid: false, expiry: null });
105
+ }
106
+
107
+ const normalizedSessionId = String(sessionId).toLowerCase();
108
+ const result = await dblogin.query({ name: 'check-session-validity', text: 'SELECT "SessionId", "Active" FROM "Users" WHERE id = $1', values: [id] });
109
+
110
+ const dbSessionId = result.rows.length > 0 && result.rows[0].SessionId ? String(result.rows[0].SessionId).toLowerCase() : null;
111
+ if (!dbSessionId || dbSessionId !== normalizedSessionId || !result.rows[0].Active) {
112
+ req.session.destroy(() => { });
113
+ clearSessionCookies(res);
114
+ return res.status(200).json({ sessionValid: false, expiry: null });
115
+ }
116
+
117
+ // Fetch expiry timestamp from session table (connect-pg-simple)
118
+ const sessResult = await dblogin.query({ name: 'get-session-expiry', text: 'SELECT expire FROM "session" WHERE sid = $1', values: [req.sessionID] });
119
+ const expiry = sessResult.rows.length > 0 && sessResult.rows[0].expire ? new Date(sessResult.rows[0].expire).toISOString() : null;
120
+
121
+ return res.status(200).json({ sessionValid: true, expiry });
122
+ } catch (err) {
123
+ console.error('[mbkauthe] checkSession error:', err);
124
+ return res.status(200).json({ sessionValid: false, expiry: null });
125
+ }
126
+ });
127
+
93
128
  // Error codes page
94
129
  router.get("/ErrorCode", (req, res) => {
95
130
  try {
@@ -251,12 +286,12 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
251
286
  try {
252
287
  // Run both operations in parallel for better performance
253
288
  await Promise.all([
254
- dblogin.query({
255
- name: 'terminate-all-user-sessions',
289
+ dblogin.query({
290
+ name: 'terminate-all-user-sessions',
256
291
  text: 'UPDATE "Users" SET "SessionId" = NULL WHERE "SessionId" IS NOT NULL'
257
292
  }),
258
- dblogin.query({
259
- name: 'terminate-all-db-sessions',
293
+ dblogin.query({
294
+ name: 'terminate-all-db-sessions',
260
295
  text: 'DELETE FROM "session" WHERE expire > NOW()'
261
296
  })
262
297
  ]);
@@ -108,25 +108,43 @@ const createOAuthStrategy = async (provider, profile, done) => {
108
108
  }
109
109
  };
110
110
 
111
- // Configure GitHub Strategy for login
112
- passport.use('github-login', new GitHubStrategy({
113
- clientID: mbkautheVar.GITHUB_CLIENT_ID,
114
- clientSecret: mbkautheVar.GITHUB_CLIENT_SECRET,
115
- callbackURL: '/mbkauthe/api/github/login/callback',
116
- scope: ['user:email']
117
- }, (accessToken, refreshToken, profile, done) =>
118
- createOAuthStrategy('GitHub', profile, done)
119
- ));
120
-
121
- // Configure Google Strategy for login
122
- passport.use('google-login', new GoogleStrategy({
123
- clientID: mbkautheVar.GOOGLE_CLIENT_ID,
124
- clientSecret: mbkautheVar.GOOGLE_CLIENT_SECRET,
125
- callbackURL: '/mbkauthe/api/google/login/callback',
126
- scope: ['profile', 'email']
127
- }, (accessToken, refreshToken, profile, done) =>
128
- createOAuthStrategy('Google', profile, done)
129
- ));
111
+ // Configure GitHub Strategy for login (only if enabled and configured)
112
+ if ((mbkautheVar.GITHUB_LOGIN_ENABLED || "").toLowerCase() === "true") {
113
+ if (mbkautheVar.GITHUB_CLIENT_ID && mbkautheVar.GITHUB_CLIENT_SECRET) {
114
+ passport.use('github-login', new GitHubStrategy({
115
+ clientID: mbkautheVar.GITHUB_CLIENT_ID,
116
+ clientSecret: mbkautheVar.GITHUB_CLIENT_SECRET,
117
+ callbackURL: '/mbkauthe/api/github/login/callback',
118
+ scope: ['user:email']
119
+ }, (accessToken, refreshToken, profile, done) =>
120
+ createOAuthStrategy('GitHub', profile, done)
121
+ ));
122
+ console.log('[mbkauthe] GitHub OAuth strategy registered');
123
+ } else {
124
+ console.warn('[mbkauthe] GITHUB_LOGIN_ENABLED is true but GITHUB_CLIENT_ID/SECRET missing; skipping GitHub strategy registration');
125
+ }
126
+ } else {
127
+ console.log('[mbkauthe] GitHub OAuth not enabled; skipping GitHub strategy registration');
128
+ }
129
+
130
+ // Configure Google Strategy for login (only if enabled and configured)
131
+ if ((mbkautheVar.GOOGLE_LOGIN_ENABLED || "").toLowerCase() === "true") {
132
+ if (mbkautheVar.GOOGLE_CLIENT_ID && mbkautheVar.GOOGLE_CLIENT_SECRET) {
133
+ passport.use('google-login', new GoogleStrategy({
134
+ clientID: mbkautheVar.GOOGLE_CLIENT_ID,
135
+ clientSecret: mbkautheVar.GOOGLE_CLIENT_SECRET,
136
+ callbackURL: '/mbkauthe/api/google/login/callback',
137
+ scope: ['profile', 'email']
138
+ }, (accessToken, refreshToken, profile, done) =>
139
+ createOAuthStrategy('Google', profile, done)
140
+ ));
141
+ console.log('[mbkauthe] Google OAuth strategy registered');
142
+ } else {
143
+ console.warn('[mbkauthe] GOOGLE_LOGIN_ENABLED is true but GOOGLE_CLIENT_ID/SECRET missing; skipping Google strategy registration');
144
+ }
145
+ } else {
146
+ console.log('[mbkauthe] Google OAuth not enabled; skipping Google strategy registration');
147
+ }
130
148
 
131
149
  // Serialize/Deserialize user for OAuth login
132
150
  passport.serializeUser((user, done) => {
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "types": "index.d.ts",
8
8
  "scripts": {
9
- "dev": "node scripts/setup-hooks.js && cross-env test=dev nodemon index.js",
10
- "test": "node scripts/setup-hooks.js && node --experimental-vm-modules node_modules/jest/bin/jest.js",
11
- "test:watch": "node scripts/setup-hooks.js && node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
9
+ "dev": "cross-env test=dev nodemon index.js",
10
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
11
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
package/test.spec.js CHANGED
@@ -192,5 +192,20 @@ describe('mbkauthe Routes', () => {
192
192
  expect(response.headers['content-type']).toContain('application/json');
193
193
  }
194
194
  });
195
+
196
+ test('GET /mbkauthe/api/checkSession handles session check', async () => {
197
+ const response = await request(BASE_URL).get('/mbkauthe/api/checkSession');
198
+ expect(response.status).toBe(200);
199
+ expect(response.headers['content-type']).toContain('application/json');
200
+ expect(response.body).toHaveProperty('sessionValid');
201
+ });
202
+
203
+ test('POST /mbkauthe/api/logout handles logout', async () => {
204
+ const response = await request(BASE_URL).post('/mbkauthe/api/logout').send();
205
+ expect([200, 400, 401, 403, 429]).toContain(response.status);
206
+ if (response.status === 200) {
207
+ expect(response.headers['content-type']).toContain('application/json');
208
+ }
209
+ });
195
210
  });
196
211
  });