mbkauthe 3.4.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/docs/api.md +69 -1
- package/index.d.ts +7 -0
- package/index.js +4 -1
- package/lib/config/cookies.js +1 -0
- package/lib/middleware/auth.js +156 -3
- package/lib/middleware/index.js +22 -1
- package/lib/routes/auth.js +16 -0
- package/lib/routes/misc.js +40 -5
- package/package.json +1 -1
- package/test.spec.js +15 -0
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/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 {
|
|
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";
|
package/lib/config/cookies.js
CHANGED
|
@@ -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
|
|
package/lib/middleware/auth.js
CHANGED
|
@@ -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 };
|
package/lib/middleware/index.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/routes/auth.js
CHANGED
|
@@ -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) {
|
package/lib/routes/misc.js
CHANGED
|
@@ -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
|
]);
|
package/package.json
CHANGED
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
|
});
|