mbkauthe 1.4.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/api.md +69 -3
- package/env.md +1 -1
- package/lib/main.js +285 -105
- package/lib/pool.js +2 -2
- package/lib/validateSessionAndRole.js +22 -47
- package/package.json +6 -3
- package/public/bg.avif +0 -0
- package/public/main.js +5 -7
- package/views/2fa.handlebars +6 -6
- package/views/Error/dError.handlebars +3 -3
- package/views/info.handlebars +13 -10
- package/views/loginmbkauthe.handlebars +52 -14
- package/views/sharedStyles.handlebars +1 -1
- package/views/showmessage.handlebars +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml)
|
|
7
7
|
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/codeql.yml)
|
|
8
8
|
|
|
9
|
-
**MBKAuth** is a reusable, production-ready authentication system for Node.js applications built by
|
|
9
|
+
**MBKAuth** is a reusable, production-ready authentication system for Node.js applications built by MBKTech.org. It provides secure session management, two-factor authentication (2FA), role-based access control, and multi-application support out of the box.
|
|
10
10
|
|
|
11
11
|
## ✨ Features
|
|
12
12
|
|
|
@@ -313,8 +313,8 @@ Found a bug or need help? Please [open an issue](https://github.com/MIbnEKhalid/
|
|
|
313
313
|
|
|
314
314
|
- [npm Package](https://www.npmjs.com/package/mbkauthe)
|
|
315
315
|
- [GitHub Repository](https://github.com/MIbnEKhalid/mbkauthe)
|
|
316
|
-
- [
|
|
316
|
+
- [MBKTech.org](https://mbktech.org)
|
|
317
317
|
|
|
318
318
|
---
|
|
319
319
|
|
|
320
|
-
Made with ❤️ by [
|
|
320
|
+
Made with ❤️ by [MBKTech.org](https://mbktech.org)
|
package/docs/api.md
CHANGED
|
@@ -328,10 +328,77 @@ Displays MBKAuthe version information and configuration.
|
|
|
328
328
|
|
|
329
329
|
#### `GET /mbkauthe/main.js`
|
|
330
330
|
|
|
331
|
-
Serves the client-side JavaScript file.
|
|
331
|
+
Serves the client-side JavaScript file containing helper functions for authentication operations.
|
|
332
|
+
|
|
333
|
+
**Purpose:** Provides frontend JavaScript utilities including:
|
|
334
|
+
- `logout()` - Logout function with confirmation dialog and cache clearing
|
|
335
|
+
- `logoutuser()` - Alias for logout function
|
|
336
|
+
- `nuclearCacheClear()` - Comprehensive cache and storage clearing (preserves rememberedUsername)
|
|
337
|
+
- `getCookieValue(cookieName)` - Cookie retrieval helper
|
|
338
|
+
- `loadpage(url)` - Page navigation helper
|
|
339
|
+
- `formatDate(date)` - Date formatting utility
|
|
340
|
+
- `reloadPage()` - Page reload helper
|
|
341
|
+
- `checkSession()` - Session validity checker
|
|
332
342
|
|
|
333
343
|
**Response:** JavaScript file (Content-Type: application/javascript)
|
|
334
344
|
|
|
345
|
+
**Usage:**
|
|
346
|
+
```html
|
|
347
|
+
<script src="/mbkauthe/main.js"></script>
|
|
348
|
+
<button onclick="logout()">Logout</button>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Main Functions:**
|
|
352
|
+
|
|
353
|
+
**`logout()`**
|
|
354
|
+
- Shows confirmation dialog before logout
|
|
355
|
+
- Clears all caches except rememberedUsername
|
|
356
|
+
- Calls `/mbkauthe/api/logout` endpoint
|
|
357
|
+
- Redirects to home page on success
|
|
358
|
+
|
|
359
|
+
**`nuclearCacheClear()`**
|
|
360
|
+
- Clears service workers and cache storage
|
|
361
|
+
- Clears localStorage and sessionStorage (preserves rememberedUsername)
|
|
362
|
+
- Clears IndexedDB
|
|
363
|
+
- Clears cookies
|
|
364
|
+
- Forces page reload
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
#### `GET /mbkauthe/test`
|
|
370
|
+
|
|
371
|
+
Test endpoint to verify authentication and display user session information.
|
|
372
|
+
|
|
373
|
+
**Authentication:** Session required
|
|
374
|
+
|
|
375
|
+
**Rate Limit:** 8 requests per minute
|
|
376
|
+
|
|
377
|
+
**Response:** HTML page displaying:
|
|
378
|
+
- Current username
|
|
379
|
+
- User role
|
|
380
|
+
- Logout button
|
|
381
|
+
- Quick links to info and login pages
|
|
382
|
+
|
|
383
|
+
**Example Response:**
|
|
384
|
+
```html
|
|
385
|
+
<head>
|
|
386
|
+
<script src="/mbkauthe/main.js"></script>
|
|
387
|
+
</head>
|
|
388
|
+
<p>if you are seeing this page than User is logged in.</p>
|
|
389
|
+
<p>id: '${req.session.user.id}', UserName: '${req.session.user.username}', Role: '${req.session.user.role}', SessionId: '${req.session.user.sessionId}'</p>
|
|
390
|
+
<button onclick="logout()">Logout</button><br>
|
|
391
|
+
<a href="/mbkauthe/info">Info Page</a><br>
|
|
392
|
+
<a href="/mbkauthe/login">Login Page</a><br>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Usage:**
|
|
396
|
+
```
|
|
397
|
+
GET /mbkauthe/test
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Note:** This endpoint is primarily for testing and debugging authentication. It should not be used in production environments.
|
|
401
|
+
|
|
335
402
|
---
|
|
336
403
|
|
|
337
404
|
## Middleware Reference
|
|
@@ -347,7 +414,7 @@ import { validateSession } from 'mbkauthe';
|
|
|
347
414
|
app.get('/protected', validateSession, (req, res) => {
|
|
348
415
|
// User is authenticated
|
|
349
416
|
const user = req.session.user;
|
|
350
|
-
// user contains: { id, username, UserName, role, Role, sessionId
|
|
417
|
+
// user contains: { id, username, UserName, role, Role, sessionId }
|
|
351
418
|
res.send(`Welcome ${user.username}!`);
|
|
352
419
|
});
|
|
353
420
|
```
|
|
@@ -369,7 +436,6 @@ req.session.user = {
|
|
|
369
436
|
role: "NormalUser", // User role
|
|
370
437
|
Role: "NormalUser", // User role (alias)
|
|
371
438
|
sessionId: "abc123...", // 64-char hex session ID
|
|
372
|
-
allowedApps: ["app1"] // Array of allowed applications
|
|
373
439
|
}
|
|
374
440
|
```
|
|
375
441
|
|
package/env.md
CHANGED
|
@@ -54,7 +54,7 @@ DOMAIN=localhost
|
|
|
54
54
|
**Description:** Your application's domain name.
|
|
55
55
|
|
|
56
56
|
**Configuration:**
|
|
57
|
-
- **Production:** Set to your actual domain (e.g., `
|
|
57
|
+
- **Production:** Set to your actual domain (e.g., `mbktech.com`)
|
|
58
58
|
- **Development:** Use `localhost` or set `IS_DEPLOYED=false`
|
|
59
59
|
- **Subdomains:** When `IS_DEPLOYED=true`, sessions are shared across all subdomains
|
|
60
60
|
|
package/lib/main.js
CHANGED
|
@@ -5,14 +5,14 @@ import session from "express-session";
|
|
|
5
5
|
import pgSession from "connect-pg-simple";
|
|
6
6
|
const PgSession = pgSession(session);
|
|
7
7
|
import { dblogin } from "./pool.js";
|
|
8
|
-
import { authenticate } from "./validateSessionAndRole.js";
|
|
8
|
+
import { authenticate, validateSession, validateSessionAndRole } from "./validateSessionAndRole.js";
|
|
9
9
|
import fetch from 'node-fetch';
|
|
10
10
|
import cookieParser from "cookie-parser";
|
|
11
11
|
import bcrypt from 'bcrypt';
|
|
12
12
|
import rateLimit from 'express-rate-limit';
|
|
13
13
|
import speakeasy from "speakeasy";
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import passport from 'passport';
|
|
15
|
+
import GitHubStrategy from 'passport-github2';
|
|
16
16
|
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
import fs from "fs";
|
|
@@ -36,16 +36,26 @@ router.get('/mbkauthe/main.js', (req, res) => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
// CSRF protection middleware
|
|
39
|
-
const csrfProtection = csurf({ cookie:
|
|
39
|
+
const csrfProtection = csurf({ cookie: true });
|
|
40
40
|
|
|
41
41
|
// CORS and security headers
|
|
42
42
|
router.use((req, res, next) => {
|
|
43
43
|
const origin = req.headers.origin;
|
|
44
|
-
if (origin
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if (origin) {
|
|
45
|
+
try {
|
|
46
|
+
const originUrl = new URL(origin);
|
|
47
|
+
const allowedDomain = `.${mbkautheVar.DOMAIN}`;
|
|
48
|
+
// Exact match or subdomain match (must end with .domain.com, not just domain.com)
|
|
49
|
+
if (originUrl.hostname === mbkautheVar.DOMAIN ||
|
|
50
|
+
(originUrl.hostname.endsWith(allowedDomain) && originUrl.hostname.charAt(originUrl.hostname.length - allowedDomain.length - 1) !== '.')) {
|
|
51
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
52
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
53
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
54
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Invalid origin URL, skip CORS headers
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
next();
|
|
51
61
|
});
|
|
@@ -71,6 +81,12 @@ const TwoFALimit = rateLimit({
|
|
|
71
81
|
message: { success: false, message: "Too many 2FA attempts, please try again later" }
|
|
72
82
|
});
|
|
73
83
|
|
|
84
|
+
const GitHubOAuthLimit = rateLimit({
|
|
85
|
+
windowMs: 5 * 60 * 1000,
|
|
86
|
+
max: 10,
|
|
87
|
+
message: "Too many GitHub login attempts, please try again later"
|
|
88
|
+
});
|
|
89
|
+
|
|
74
90
|
const sessionConfig = {
|
|
75
91
|
store: new PgSession({
|
|
76
92
|
pool: dblogin,
|
|
@@ -85,7 +101,7 @@ const sessionConfig = {
|
|
|
85
101
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
86
102
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
87
103
|
httpOnly: true,
|
|
88
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
104
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
89
105
|
sameSite: 'lax',
|
|
90
106
|
path: '/'
|
|
91
107
|
},
|
|
@@ -100,14 +116,16 @@ router.use(async (req, res, next) => {
|
|
|
100
116
|
try {
|
|
101
117
|
const sessionId = req.cookies.sessionId;
|
|
102
118
|
|
|
103
|
-
// Validate sessionId format (should be 64 hex characters)
|
|
119
|
+
// Validate sessionId format (should be 64 hex characters) and normalize to lowercase
|
|
104
120
|
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
105
121
|
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
106
122
|
return next();
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
const
|
|
110
|
-
|
|
125
|
+
const normalizedSessionId = sessionId.toLowerCase();
|
|
126
|
+
|
|
127
|
+
const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE LOWER("SessionId") = $1 AND "Active" = true`;
|
|
128
|
+
const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [normalizedSessionId] });
|
|
111
129
|
|
|
112
130
|
if (result.rows.length > 0) {
|
|
113
131
|
const user = result.rows[0];
|
|
@@ -117,8 +135,7 @@ router.use(async (req, res, next) => {
|
|
|
117
135
|
UserName: user.UserName,
|
|
118
136
|
role: user.Role,
|
|
119
137
|
Role: user.Role,
|
|
120
|
-
sessionId,
|
|
121
|
-
allowedApps: user.AllowedApps,
|
|
138
|
+
sessionId: normalizedSessionId,
|
|
122
139
|
};
|
|
123
140
|
}
|
|
124
141
|
} catch (err) {
|
|
@@ -131,7 +148,7 @@ router.use(async (req, res, next) => {
|
|
|
131
148
|
const getCookieOptions = () => ({
|
|
132
149
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
133
150
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
134
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
151
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
135
152
|
sameSite: 'lax',
|
|
136
153
|
path: '/',
|
|
137
154
|
httpOnly: true
|
|
@@ -139,32 +156,64 @@ const getCookieOptions = () => ({
|
|
|
139
156
|
|
|
140
157
|
const getClearCookieOptions = () => ({
|
|
141
158
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
142
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
159
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
143
160
|
sameSite: 'lax',
|
|
144
161
|
path: '/',
|
|
145
162
|
httpOnly: true
|
|
146
163
|
});
|
|
147
164
|
|
|
165
|
+
router.get('/mbkauthe/test', validateSession, LoginLimit, async(req, res) => {
|
|
166
|
+
if(req.session?.user) {
|
|
167
|
+
return res.send(`
|
|
168
|
+
<head> <script src="/mbkauthe/main.js"></script> </head>
|
|
169
|
+
<p>if you are seeing this page than User is logged in.</p>
|
|
170
|
+
<p>id: '${req.session.user.id}', UserName: '${req.session.user.username}', Role: '${req.session.user.role}', SessionId: '${req.session.user.sessionId}'</p>
|
|
171
|
+
<button onclick="logout()">Logout</button><br>
|
|
172
|
+
<a href="/mbkauthe/info">Info Page</a><br>
|
|
173
|
+
<a href="/mbkauthe/login">Login Page</a><br>
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
148
178
|
async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
149
179
|
try {
|
|
180
|
+
// Ensure both username formats are available for compatibility
|
|
181
|
+
const username = user.username || user.UserName;
|
|
182
|
+
if (!username) {
|
|
183
|
+
throw new Error('Username is required in user object');
|
|
184
|
+
}
|
|
185
|
+
|
|
150
186
|
// smaller session id is sufficient and faster to generate/serialize
|
|
151
187
|
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
152
|
-
console.log(`[mbkauthe] Generated session ID for username: ${
|
|
188
|
+
console.log(`[mbkauthe] Generated session ID for username: ${username}`);
|
|
189
|
+
|
|
190
|
+
// Regenerate session to prevent session fixation attacks
|
|
191
|
+
const oldSessionId = req.sessionID;
|
|
192
|
+
await new Promise((resolve, reject) => {
|
|
193
|
+
req.session.regenerate((err) => {
|
|
194
|
+
if (err) reject(err);
|
|
195
|
+
else resolve();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
153
198
|
|
|
154
|
-
// Delete old
|
|
155
|
-
await dblogin.query(
|
|
199
|
+
// Delete all old sessions for this user from session table
|
|
200
|
+
await dblogin.query({
|
|
201
|
+
name: 'login-delete-old-user-sessions',
|
|
202
|
+
text: 'DELETE FROM "session" WHERE sess::text LIKE $1',
|
|
203
|
+
values: [`%"username":"${username}"%`]
|
|
204
|
+
});
|
|
156
205
|
|
|
157
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [
|
|
206
|
+
await dblogin.query({ name: 'login-update-session-id', text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, values: [
|
|
158
207
|
sessionId,
|
|
159
208
|
user.id,
|
|
160
|
-
]);
|
|
209
|
+
] });
|
|
161
210
|
|
|
162
211
|
req.session.user = {
|
|
163
212
|
id: user.id,
|
|
164
|
-
username:
|
|
165
|
-
UserName:
|
|
166
|
-
role: user.role,
|
|
167
|
-
Role: user.role,
|
|
213
|
+
username: username,
|
|
214
|
+
UserName: username,
|
|
215
|
+
role: user.role || user.Role,
|
|
216
|
+
Role: user.role || user.Role,
|
|
168
217
|
sessionId,
|
|
169
218
|
};
|
|
170
219
|
|
|
@@ -182,7 +231,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
182
231
|
|
|
183
232
|
const cookieOptions = getCookieOptions();
|
|
184
233
|
res.cookie("sessionId", sessionId, cookieOptions);
|
|
185
|
-
console.log(`[mbkauthe] User "${
|
|
234
|
+
console.log(`[mbkauthe] User "${username}" logged in successfully`);
|
|
186
235
|
|
|
187
236
|
const responsePayload = {
|
|
188
237
|
success: true,
|
|
@@ -216,8 +265,8 @@ router.use(async (req, res, next) => {
|
|
|
216
265
|
|
|
217
266
|
router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
218
267
|
try {
|
|
219
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL`);
|
|
220
|
-
await dblogin.query('DELETE FROM "session"');
|
|
268
|
+
await dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` });
|
|
269
|
+
await dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' });
|
|
221
270
|
|
|
222
271
|
req.session.destroy((err) => {
|
|
223
272
|
if (err) {
|
|
@@ -280,11 +329,11 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
280
329
|
|
|
281
330
|
try {
|
|
282
331
|
const userQuery = `SELECT id, "UserName", "Password", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
|
|
283
|
-
const userResult = await dblogin.query({ name: 'get-user
|
|
332
|
+
const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
|
|
284
333
|
|
|
285
334
|
if (userResult.rows.length === 0) {
|
|
286
|
-
console.log(`[mbkauthe]
|
|
287
|
-
return res.status(
|
|
335
|
+
console.log(`[mbkauthe] Login failed: invalid credentials`);
|
|
336
|
+
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
288
337
|
}
|
|
289
338
|
|
|
290
339
|
const user = userResult.rows[0];
|
|
@@ -299,18 +348,18 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
299
348
|
try {
|
|
300
349
|
const result = await bcrypt.compare(password, user.Password);
|
|
301
350
|
if (!result) {
|
|
302
|
-
console.log("[mbkauthe]
|
|
303
|
-
return res.status(401).json({ success: false, errorCode: 603, message: "
|
|
351
|
+
console.log("[mbkauthe] Login failed: invalid credentials");
|
|
352
|
+
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
304
353
|
}
|
|
305
|
-
console.log("[mbkauthe] Password
|
|
354
|
+
console.log("[mbkauthe] Password validated successfully");
|
|
306
355
|
} catch (err) {
|
|
307
356
|
console.error("[mbkauthe] Error comparing password:", err);
|
|
308
357
|
return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` });
|
|
309
358
|
}
|
|
310
359
|
} else {
|
|
311
360
|
if (user.Password !== password) {
|
|
312
|
-
console.log(`[mbkauthe]
|
|
313
|
-
return res.status(401).json({ success: false, errorCode: 603, message: "
|
|
361
|
+
console.log(`[mbkauthe] Login failed: invalid credentials`);
|
|
362
|
+
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
314
363
|
}
|
|
315
364
|
}
|
|
316
365
|
|
|
@@ -329,7 +378,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
329
378
|
|
|
330
379
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
|
|
331
380
|
const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
332
|
-
const twoFAResult = await dblogin.query({ name: '
|
|
381
|
+
const twoFAResult = await dblogin.query({ name: 'login-check-2fa-status', text: query, values: [trimmedUsername] });
|
|
333
382
|
|
|
334
383
|
if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
|
|
335
384
|
// 2FA is enabled, prompt for token on a separate page
|
|
@@ -367,6 +416,7 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
367
416
|
layout: false,
|
|
368
417
|
customURL: mbkautheVar.loginRedirectURL || '/home',
|
|
369
418
|
csrfToken: req.csrfToken(),
|
|
419
|
+
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
370
420
|
});
|
|
371
421
|
});
|
|
372
422
|
|
|
@@ -391,7 +441,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
391
441
|
|
|
392
442
|
try {
|
|
393
443
|
const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
394
|
-
const twoFAResult = await dblogin.query(query, [username]);
|
|
444
|
+
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
395
445
|
|
|
396
446
|
if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
|
|
397
447
|
return res.status(500).json({ success: false, message: "2FA is not configured correctly." });
|
|
@@ -421,15 +471,15 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
421
471
|
}
|
|
422
472
|
});
|
|
423
473
|
|
|
424
|
-
router.post("/mbkauthe/api/logout", LogoutLimit,
|
|
474
|
+
router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
425
475
|
if (req.session.user) {
|
|
426
476
|
try {
|
|
427
477
|
const { id, username } = req.session.user;
|
|
428
478
|
|
|
429
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, [id]);
|
|
479
|
+
await dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] });
|
|
430
480
|
|
|
431
481
|
if (req.sessionID) {
|
|
432
|
-
await dblogin.query('DELETE FROM "session" WHERE sid = $1', [req.sessionID]);
|
|
482
|
+
await dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] });
|
|
433
483
|
}
|
|
434
484
|
|
|
435
485
|
req.session.destroy((err) => {
|
|
@@ -463,7 +513,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
|
|
|
463
513
|
userLoggedIn: !!req.session?.user,
|
|
464
514
|
username: req.session?.user?.username || '',
|
|
465
515
|
version: packageJson.version,
|
|
466
|
-
appName: mbkautheVar.APP_NAME.
|
|
516
|
+
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
467
517
|
csrfToken: req.csrfToken(),
|
|
468
518
|
});
|
|
469
519
|
});
|
|
@@ -483,9 +533,19 @@ async function getLatestVersion() {
|
|
|
483
533
|
}
|
|
484
534
|
}
|
|
485
535
|
|
|
486
|
-
router.get(
|
|
487
|
-
|
|
536
|
+
router.get("/mbkauthe/bg.avif", (req, res) => {
|
|
537
|
+
res.sendFile("bg.avif", { root: path.join(process.cwd(), "public") });
|
|
538
|
+
});
|
|
488
539
|
|
|
540
|
+
router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
|
|
541
|
+
let latestVersion;
|
|
542
|
+
const parameters = req.query;
|
|
543
|
+
let authorized = false;
|
|
544
|
+
|
|
545
|
+
if (parameters.password && mbkautheVar.Main_SECRET_TOKEN) {
|
|
546
|
+
authorized = String(parameters.password) === String(mbkautheVar.Main_SECRET_TOKEN);
|
|
547
|
+
}
|
|
548
|
+
|
|
489
549
|
try {
|
|
490
550
|
latestVersion = await getLatestVersion();
|
|
491
551
|
//latestVersion = "Under Development"; // Placeholder for the latest version
|
|
@@ -499,6 +559,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
|
|
|
499
559
|
mbkautheVar: mbkautheVar,
|
|
500
560
|
version: packageJson.version,
|
|
501
561
|
latestVersion,
|
|
562
|
+
authorized: authorized,
|
|
502
563
|
});
|
|
503
564
|
} catch (err) {
|
|
504
565
|
console.error("[mbkauthe] Error fetching version information:", err);
|
|
@@ -515,7 +576,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
|
|
|
515
576
|
`);
|
|
516
577
|
}
|
|
517
578
|
});
|
|
518
|
-
|
|
579
|
+
|
|
519
580
|
// Configure GitHub Strategy for login
|
|
520
581
|
passport.use('github-login', new GitHubStrategy({
|
|
521
582
|
clientID: process.env.GITHUB_CLIENT_ID,
|
|
@@ -526,28 +587,35 @@ passport.use('github-login', new GitHubStrategy({
|
|
|
526
587
|
async (accessToken, refreshToken, profile, done) => {
|
|
527
588
|
try {
|
|
528
589
|
// Check if this GitHub account is linked to any user
|
|
529
|
-
const githubUser = await dblogin.query(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
590
|
+
const githubUser = await dblogin.query({
|
|
591
|
+
name: 'github-login-get-user',
|
|
592
|
+
text: 'SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id" FROM user_github ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.github_id = $1',
|
|
593
|
+
values: [profile.id]
|
|
594
|
+
});
|
|
533
595
|
|
|
534
596
|
if (githubUser.rows.length === 0) {
|
|
535
597
|
// GitHub account is not linked to any user
|
|
536
|
-
|
|
598
|
+
const error = new Error('GitHub account not linked to any user');
|
|
599
|
+
error.code = 'GITHUB_NOT_LINKED';
|
|
600
|
+
return done(error);
|
|
537
601
|
}
|
|
538
602
|
|
|
539
603
|
const user = githubUser.rows[0];
|
|
540
604
|
|
|
541
605
|
// Check if the user account is active
|
|
542
606
|
if (!user.Active) {
|
|
543
|
-
|
|
607
|
+
const error = new Error('Account is inactive');
|
|
608
|
+
error.code = 'ACCOUNT_INACTIVE';
|
|
609
|
+
return done(error);
|
|
544
610
|
}
|
|
545
611
|
|
|
546
612
|
// Check if user is authorized for this app (same logic as regular login)
|
|
547
613
|
if (user.Role !== "SuperAdmin") {
|
|
548
614
|
const allowedApps = user.AllowedApps;
|
|
549
615
|
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
550
|
-
|
|
616
|
+
const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
|
|
617
|
+
error.code = 'NOT_AUTHORIZED';
|
|
618
|
+
return done(error);
|
|
551
619
|
}
|
|
552
620
|
}
|
|
553
621
|
|
|
@@ -561,6 +629,7 @@ passport.use('github-login', new GitHubStrategy({
|
|
|
561
629
|
});
|
|
562
630
|
} catch (err) {
|
|
563
631
|
console.error('[mbkauthe] GitHub login error:', err);
|
|
632
|
+
err.code = err.code || 'GITHUB_AUTH_ERROR';
|
|
564
633
|
return done(err);
|
|
565
634
|
}
|
|
566
635
|
}
|
|
@@ -580,25 +649,128 @@ router.use(passport.initialize());
|
|
|
580
649
|
router.use(passport.session());
|
|
581
650
|
|
|
582
651
|
// GitHub login initiation
|
|
583
|
-
router.get('/mbkauthe/api/github/login',
|
|
652
|
+
router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
|
|
653
|
+
// Store redirect parameter in session before OAuth flow (validate to prevent open redirect)
|
|
654
|
+
const redirect = req.query.redirect;
|
|
655
|
+
if (redirect && typeof redirect === 'string') {
|
|
656
|
+
// Only allow relative URLs or same-origin URLs to prevent open redirect attacks
|
|
657
|
+
if (redirect.startsWith('/') && !redirect.startsWith('//')) {
|
|
658
|
+
req.session.oauthRedirect = redirect;
|
|
659
|
+
} else {
|
|
660
|
+
console.warn(`[mbkauthe] Invalid redirect parameter rejected: ${redirect}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
passport.authenticate('github-login')(req, res, next);
|
|
664
|
+
});
|
|
584
665
|
|
|
585
666
|
// GitHub login callback
|
|
586
667
|
router.get('/mbkauthe/api/github/login/callback',
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
668
|
+
GitHubOAuthLimit,
|
|
669
|
+
(req, res, next) => {
|
|
670
|
+
passport.authenticate('github-login', {
|
|
671
|
+
session: false // We'll handle session manually
|
|
672
|
+
}, (err, user, info) => {
|
|
673
|
+
// Custom error handling for passport authentication
|
|
674
|
+
if (err) {
|
|
675
|
+
console.error('[mbkauthe] GitHub authentication error:', err);
|
|
676
|
+
|
|
677
|
+
// Map error codes to user-friendly messages
|
|
678
|
+
switch(err.code) {
|
|
679
|
+
case 'GITHUB_NOT_LINKED':
|
|
680
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
681
|
+
layout: false,
|
|
682
|
+
code: '403',
|
|
683
|
+
error: 'GitHub Account Not Linked',
|
|
684
|
+
message: 'Your GitHub account is not linked to any user in our system. To link your GitHub account, a User must connect their GitHub account to mbktech account through the user settings.',
|
|
685
|
+
page: '/mbkauthe/login',
|
|
686
|
+
pagename: 'Login',
|
|
687
|
+
version: packageJson.version,
|
|
688
|
+
app: mbkautheVar.APP_NAME
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
case 'ACCOUNT_INACTIVE':
|
|
692
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
693
|
+
layout: false,
|
|
694
|
+
code: '403',
|
|
695
|
+
error: 'Account Inactive',
|
|
696
|
+
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
697
|
+
page: '/mbkauthe/login',
|
|
698
|
+
pagename: 'Login',
|
|
699
|
+
version: packageJson.version,
|
|
700
|
+
app: mbkautheVar.APP_NAME
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
case 'NOT_AUTHORIZED':
|
|
704
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
705
|
+
layout: false,
|
|
706
|
+
code: '403',
|
|
707
|
+
error: 'Not Authorized',
|
|
708
|
+
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
709
|
+
page: '/mbkauthe/login',
|
|
710
|
+
pagename: 'Login',
|
|
711
|
+
version: packageJson.version,
|
|
712
|
+
app: mbkautheVar.APP_NAME
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
default:
|
|
716
|
+
return res.status(500).render('Error/dError.handlebars', {
|
|
717
|
+
layout: false,
|
|
718
|
+
code: '500',
|
|
719
|
+
error: 'Authentication Error',
|
|
720
|
+
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
721
|
+
page: '/mbkauthe/login',
|
|
722
|
+
pagename: 'Login',
|
|
723
|
+
version: packageJson.version,
|
|
724
|
+
app: mbkautheVar.APP_NAME,
|
|
725
|
+
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!user) {
|
|
731
|
+
console.error('[mbkauthe] GitHub callback: No user data received');
|
|
732
|
+
return res.status(401).render('Error/dError.handlebars', {
|
|
733
|
+
layout: false,
|
|
734
|
+
code: '401',
|
|
735
|
+
error: 'Authentication Failed',
|
|
736
|
+
message: 'GitHub authentication failed. Please try again.',
|
|
737
|
+
page: '/mbkauthe/login',
|
|
738
|
+
pagename: 'Login',
|
|
739
|
+
version: packageJson.version,
|
|
740
|
+
app: mbkautheVar.APP_NAME
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Authentication successful, attach user to request
|
|
745
|
+
req.user = user;
|
|
746
|
+
next();
|
|
747
|
+
})(req, res, next);
|
|
748
|
+
},
|
|
591
749
|
async (req, res) => {
|
|
592
750
|
try {
|
|
593
751
|
const githubUser = req.user;
|
|
594
752
|
|
|
595
|
-
// Find the actual user record
|
|
596
|
-
const userQuery = `SELECT
|
|
597
|
-
const userResult = await dblogin.query(
|
|
753
|
+
// Find the actual user record with named query
|
|
754
|
+
const userQuery = `SELECT id, "UserName", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
|
|
755
|
+
const userResult = await dblogin.query({
|
|
756
|
+
name: 'github-callback-get-user',
|
|
757
|
+
text: userQuery,
|
|
758
|
+
values: [githubUser.username]
|
|
759
|
+
});
|
|
598
760
|
|
|
599
761
|
if (userResult.rows.length === 0) {
|
|
600
|
-
console.
|
|
601
|
-
return res.
|
|
762
|
+
console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
|
|
763
|
+
return res.status(404).render('Error/dError.handlebars', {
|
|
764
|
+
layout: false,
|
|
765
|
+
code: '404',
|
|
766
|
+
error: 'User Not Found',
|
|
767
|
+
message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
|
|
768
|
+
page: '/mbkauthe/login',
|
|
769
|
+
pagename: 'Login',
|
|
770
|
+
version: packageJson.version,
|
|
771
|
+
app: mbkautheVar.APP_NAME,
|
|
772
|
+
details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
|
|
773
|
+
});
|
|
602
774
|
}
|
|
603
775
|
|
|
604
776
|
const user = userResult.rows[0];
|
|
@@ -606,14 +778,20 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
606
778
|
// Check 2FA if enabled
|
|
607
779
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
|
|
608
780
|
const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
609
|
-
const twoFAResult = await dblogin.query(
|
|
781
|
+
const twoFAResult = await dblogin.query({
|
|
782
|
+
name: 'github-check-2fa-status',
|
|
783
|
+
text: twoFAQuery,
|
|
784
|
+
values: [githubUser.username]
|
|
785
|
+
});
|
|
610
786
|
|
|
611
787
|
if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
|
|
612
788
|
// 2FA is enabled, store pre-auth user and redirect to 2FA
|
|
613
789
|
req.session.preAuthUser = {
|
|
614
790
|
id: user.id,
|
|
615
791
|
username: user.UserName,
|
|
792
|
+
UserName: user.UserName,
|
|
616
793
|
role: user.Role,
|
|
794
|
+
Role: user.Role,
|
|
617
795
|
loginMethod: 'github'
|
|
618
796
|
};
|
|
619
797
|
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
@@ -621,62 +799,64 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
621
799
|
}
|
|
622
800
|
}
|
|
623
801
|
|
|
624
|
-
// Complete login process
|
|
802
|
+
// Complete login process using the shared function
|
|
625
803
|
const userForSession = {
|
|
626
804
|
id: user.id,
|
|
627
805
|
username: user.UserName,
|
|
806
|
+
UserName: user.UserName,
|
|
628
807
|
role: user.Role,
|
|
808
|
+
Role: user.Role
|
|
629
809
|
};
|
|
630
810
|
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
// Delete old session record for this user
|
|
636
|
-
await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.UserName]);
|
|
811
|
+
// For OAuth redirect flow, we need to handle redirect differently
|
|
812
|
+
// Store the redirect URL before calling completeLoginProcess
|
|
813
|
+
const oauthRedirect = req.session.oauthRedirect;
|
|
814
|
+
delete req.session.oauthRedirect;
|
|
637
815
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
role: user.Role,
|
|
647
|
-
sessionId,
|
|
816
|
+
// Custom response handler for OAuth flow - wrap the response object
|
|
817
|
+
const originalJson = res.json.bind(res);
|
|
818
|
+
const originalStatus = res.status.bind(res);
|
|
819
|
+
let statusCode = 200;
|
|
820
|
+
|
|
821
|
+
res.status = function(code) {
|
|
822
|
+
statusCode = code;
|
|
823
|
+
return originalStatus(code);
|
|
648
824
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
[user.UserName, req.sessionID]
|
|
660
|
-
);
|
|
661
|
-
} catch (e) {
|
|
662
|
-
console.log("[mbkauthe] GitHub login: Failed to update username in session table:", e);
|
|
825
|
+
|
|
826
|
+
res.json = function(data) {
|
|
827
|
+
if (data.success && statusCode === 200) {
|
|
828
|
+
// If login successful, redirect instead of sending JSON
|
|
829
|
+
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/home';
|
|
830
|
+
console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
|
|
831
|
+
// Restore original methods before redirect
|
|
832
|
+
res.json = originalJson;
|
|
833
|
+
res.status = originalStatus;
|
|
834
|
+
return res.redirect(redirectUrl);
|
|
663
835
|
}
|
|
836
|
+
// Restore original methods for error responses
|
|
837
|
+
res.json = originalJson;
|
|
838
|
+
res.status = originalStatus;
|
|
839
|
+
return originalJson(data);
|
|
840
|
+
};
|
|
664
841
|
|
|
665
|
-
|
|
666
|
-
res.cookie("sessionId", sessionId, cookieOptions);
|
|
667
|
-
console.log(`[mbkauthe] GitHub login: User "${user.UserName}" logged in successfully`);
|
|
668
|
-
|
|
669
|
-
// Redirect to the configured URL or home
|
|
670
|
-
const redirectUrl = mbkautheVar.loginRedirectURL || '/home';
|
|
671
|
-
res.redirect(redirectUrl);
|
|
672
|
-
});
|
|
842
|
+
await completeLoginProcess(req, res, userForSession);
|
|
673
843
|
|
|
674
844
|
} catch (err) {
|
|
675
845
|
console.error('[mbkauthe] GitHub login callback error:', err);
|
|
676
|
-
res.
|
|
846
|
+
return res.status(500).render('Error/dError.handlebars', {
|
|
847
|
+
layout: false,
|
|
848
|
+
code: '500',
|
|
849
|
+
error: 'Internal Server Error',
|
|
850
|
+
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
851
|
+
page: '/mbkauthe/login',
|
|
852
|
+
pagename: 'Login',
|
|
853
|
+
version: packageJson.version,
|
|
854
|
+
app: mbkautheVar.APP_NAME,
|
|
855
|
+
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
856
|
+
});
|
|
677
857
|
}
|
|
678
858
|
}
|
|
679
859
|
);
|
|
680
|
-
|
|
860
|
+
|
|
681
861
|
export { getLatestVersion };
|
|
682
862
|
export default router;
|
package/lib/pool.js
CHANGED
|
@@ -36,8 +36,8 @@ const poolConfig = {
|
|
|
36
36
|
// - keep max small to avoid exhausting DB connections
|
|
37
37
|
// - reduce idle time so connections are returned sooner
|
|
38
38
|
// - set a short connection timeout to fail fast
|
|
39
|
-
max:
|
|
40
|
-
idleTimeoutMillis:
|
|
39
|
+
max: 20,
|
|
40
|
+
idleTimeoutMillis: 10000,
|
|
41
41
|
connectionTimeoutMillis: 5000,
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { dblogin } from "./pool.js";
|
|
2
2
|
const mbkautheVar = JSON.parse(process.env.mbkautheVar);
|
|
3
|
-
let pool = dblogin;
|
|
4
3
|
|
|
5
4
|
const getCookieOptions = () => ({
|
|
6
5
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
7
6
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
8
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
7
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
9
8
|
sameSite: 'lax',
|
|
10
9
|
path: '/',
|
|
11
10
|
httpOnly: true
|
|
@@ -13,44 +12,13 @@ const getCookieOptions = () => ({
|
|
|
13
12
|
|
|
14
13
|
const getClearCookieOptions = () => ({
|
|
15
14
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
16
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
15
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
17
16
|
sameSite: 'lax',
|
|
18
17
|
path: '/',
|
|
19
18
|
httpOnly: true
|
|
20
19
|
});
|
|
21
20
|
|
|
22
21
|
async function validateSession(req, res, next) {
|
|
23
|
-
if (!req.session.user && req.cookies.sessionId) {
|
|
24
|
-
try {
|
|
25
|
-
const sessionId = req.cookies.sessionId;
|
|
26
|
-
|
|
27
|
-
// Validate sessionId format (should be 64 hex characters)
|
|
28
|
-
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
29
|
-
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
30
|
-
return next();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE "SessionId" = $1 AND "Active" = true`;
|
|
34
|
-
const result = await dblogin.query({ name: 'get-user-by-sessionid', text: query, values: [sessionId] });
|
|
35
|
-
|
|
36
|
-
if (result.rows.length > 0) {
|
|
37
|
-
const user = result.rows[0];
|
|
38
|
-
req.session.user = {
|
|
39
|
-
id: user.id,
|
|
40
|
-
username: user.UserName,
|
|
41
|
-
UserName: user.UserName,
|
|
42
|
-
role: user.Role,
|
|
43
|
-
Role: user.Role,
|
|
44
|
-
sessionId,
|
|
45
|
-
allowedApps: user.AllowedApps,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.error("[mbkauthe] Session validation error:", err);
|
|
50
|
-
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
22
|
if (!req.session.user) {
|
|
55
23
|
console.log("[mbkauthe] User not authenticated");
|
|
56
24
|
console.log("[mbkauthe]: ", req.session.user);
|
|
@@ -67,11 +35,14 @@ async function validateSession(req, res, next) {
|
|
|
67
35
|
try {
|
|
68
36
|
const { id, sessionId, role, allowedApps } = req.session.user;
|
|
69
37
|
|
|
70
|
-
//
|
|
38
|
+
// Normalize sessionId to lowercase for consistent comparison
|
|
39
|
+
const normalizedSessionId = sessionId.toLowerCase();
|
|
40
|
+
|
|
41
|
+
// Single optimized query to validate session
|
|
71
42
|
const query = `SELECT "SessionId", "Active" FROM "Users" WHERE "id" = $1`;
|
|
72
|
-
const result = await dblogin.query({ name: '
|
|
43
|
+
const result = await dblogin.query({ name: 'validate-user-session', text: query, values: [id] });
|
|
73
44
|
|
|
74
|
-
if (result.rows.length === 0 || result.rows[0].SessionId !==
|
|
45
|
+
if (result.rows.length === 0 || result.rows[0].SessionId.toLowerCase() !== normalizedSessionId) {
|
|
75
46
|
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
76
47
|
req.session.destroy();
|
|
77
48
|
const cookieOptions = getClearCookieOptions();
|
|
@@ -101,7 +72,7 @@ async function validateSession(req, res, next) {
|
|
|
101
72
|
error: "Account Inactive",
|
|
102
73
|
message: "Your Account Is Inactive. Please Contact Support.",
|
|
103
74
|
pagename: "Support",
|
|
104
|
-
page: "https://
|
|
75
|
+
page: "https://mbktech.org/Support",
|
|
105
76
|
});
|
|
106
77
|
}
|
|
107
78
|
|
|
@@ -131,12 +102,11 @@ async function validateSession(req, res, next) {
|
|
|
131
102
|
}
|
|
132
103
|
}
|
|
133
104
|
|
|
134
|
-
const checkRolePermission = (
|
|
105
|
+
const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
135
106
|
return async (req, res, next) => {
|
|
136
107
|
try {
|
|
137
108
|
if (!req.session || !req.session.user || !req.session.user.id) {
|
|
138
109
|
console.log("[mbkauthe] User not authenticated");
|
|
139
|
-
console.log("[mbkauthe]: ", req.session);
|
|
140
110
|
return res.render("Error/dError.handlebars", {
|
|
141
111
|
layout: false,
|
|
142
112
|
code: 401,
|
|
@@ -150,10 +120,10 @@ const checkRolePermission = (requiredRole, notAllowed) => {
|
|
|
150
120
|
const userId = req.session.user.id;
|
|
151
121
|
|
|
152
122
|
const query = `SELECT "Role" FROM "Users" WHERE "id" = $1`;
|
|
153
|
-
const result = await dblogin.query({ name: '
|
|
123
|
+
const result = await dblogin.query({ name: 'check-role-permission', text: query, values: [userId] });
|
|
154
124
|
|
|
155
125
|
if (result.rows.length === 0) {
|
|
156
|
-
return res.status(401).json({ success: false, message: "
|
|
126
|
+
return res.status(401).json({ success: false, message: "Authentication failed" });
|
|
157
127
|
}
|
|
158
128
|
|
|
159
129
|
const userRole = result.rows[0].Role;
|
|
@@ -164,22 +134,27 @@ const checkRolePermission = (requiredRole, notAllowed) => {
|
|
|
164
134
|
layout: false,
|
|
165
135
|
code: 403,
|
|
166
136
|
error: "Access Denied",
|
|
167
|
-
message:
|
|
137
|
+
message: "You are not allowed to access this resource",
|
|
168
138
|
pagename: "Home",
|
|
169
139
|
page: `/${mbkautheVar.loginRedirectURL}`
|
|
170
140
|
});
|
|
171
141
|
}
|
|
172
142
|
|
|
173
|
-
|
|
143
|
+
// Convert to array if single role provided
|
|
144
|
+
const rolesArray = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
|
|
145
|
+
|
|
146
|
+
// Check for "Any" or "any" role
|
|
147
|
+
if (rolesArray.includes("Any") || rolesArray.includes("any")) {
|
|
174
148
|
return next();
|
|
175
149
|
}
|
|
176
150
|
|
|
177
|
-
if
|
|
151
|
+
// Check if user role is in allowed roles
|
|
152
|
+
if (!rolesArray.includes(userRole)) {
|
|
178
153
|
return res.render("Error/dError.handlebars", {
|
|
179
154
|
layout: false,
|
|
180
155
|
code: 403,
|
|
181
156
|
error: "Access Denied",
|
|
182
|
-
message:
|
|
157
|
+
message: "You do not have permission to access this resource",
|
|
183
158
|
pagename: "Home",
|
|
184
159
|
page: `/${mbkautheVar.loginRedirectURL}`
|
|
185
160
|
});
|
|
@@ -253,7 +228,7 @@ const authapi = (requiredRole = []) => {
|
|
|
253
228
|
LIMIT 1
|
|
254
229
|
`;
|
|
255
230
|
|
|
256
|
-
const result = await
|
|
231
|
+
const result = await dblogin.query({ name: 'validate-api-key', text: jointQuery, values: [token] });
|
|
257
232
|
|
|
258
233
|
if (result.rows.length === 0) {
|
|
259
234
|
console.warn("[mbkauthe] [authapi] Invalid token or associated user inactive");
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkauthe",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MBKTech's reusable authentication system for Node.js applications.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "
|
|
8
|
+
"dev": "cross-env test=dev nodemon index.js"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -41,12 +41,15 @@
|
|
|
41
41
|
"express-session": "^1.18.1",
|
|
42
42
|
"marked": "^15.0.11",
|
|
43
43
|
"node-fetch": "^3.3.2",
|
|
44
|
+
"passport": "^0.7.0",
|
|
45
|
+
"passport-github2": "^0.1.12",
|
|
44
46
|
"path": "^0.12.7",
|
|
45
47
|
"pg": "^8.14.1",
|
|
46
48
|
"speakeasy": "^2.0.0",
|
|
47
49
|
"url": "^0.11.4"
|
|
48
50
|
},
|
|
49
51
|
"devDependencies": {
|
|
52
|
+
"cross-env": "^7.0.3",
|
|
50
53
|
"nodemon": "^3.1.11"
|
|
51
54
|
}
|
|
52
55
|
}
|
package/public/bg.avif
ADDED
|
Binary file
|
package/public/main.js
CHANGED
|
@@ -5,9 +5,7 @@ async function logout() {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
try {
|
|
8
|
-
//
|
|
9
|
-
await nuclearCacheClear();
|
|
10
|
-
|
|
8
|
+
// First, logout from server
|
|
11
9
|
const response = await fetch("/mbkauthe/api/logout", {
|
|
12
10
|
method: "POST",
|
|
13
11
|
headers: {
|
|
@@ -20,15 +18,15 @@ async function logout() {
|
|
|
20
18
|
const result = await response.json();
|
|
21
19
|
|
|
22
20
|
if (response.ok) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
// Then clear all caches after successful logout (except rememberedUsername)
|
|
22
|
+
await nuclearCacheClear();
|
|
23
|
+
// nuclearCacheClear already redirects, so no need for additional redirect
|
|
26
24
|
} else {
|
|
27
25
|
alert(result.message);
|
|
28
26
|
}
|
|
29
27
|
} catch (error) {
|
|
30
28
|
console.error("Error during logout:", error);
|
|
31
|
-
alert(
|
|
29
|
+
alert(`Logout failed: ${error.message}`);
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
|
package/views/2fa.handlebars
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<meta name="description"
|
|
8
|
-
content="Log in to portal.
|
|
8
|
+
content="Log in to portal.mbktech.org to access your resources and manage projects securely.">
|
|
9
9
|
<meta name="keywords" content="MBK Tech Studio, Web-Portal, Web, Portal, Admin-Panel, Admin, login">
|
|
10
10
|
<meta property="og:title" content="Login | MBK Tech Studio" />
|
|
11
|
-
<meta property="og:image" content="https://
|
|
11
|
+
<meta property="og:image" content="https://mbktech.org/Assets/Images/Icon/logo.png" />
|
|
12
12
|
<meta property="og:url" content="/mbkauthe/2fa">
|
|
13
13
|
<title>2FA | MBK Tech Studio Portal</title>
|
|
14
|
-
<link rel="icon" type="image/x-icon" href="https://
|
|
14
|
+
<link rel="icon" type="image/x-icon" href="https://mbktech.org/Assets/Images/Icon/dgicon.svg">
|
|
15
15
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
16
16
|
{{> sharedStyles}}
|
|
17
17
|
</head>
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
</path>
|
|
29
29
|
</g>
|
|
30
30
|
</svg>
|
|
31
|
-
<span class="logo-text">{{appName}} <span class="logo-comp">
|
|
31
|
+
<span class="logo-text">{{appName}} <span class="logo-comp">mbktech</span></span>
|
|
32
32
|
</a>
|
|
33
33
|
</div>
|
|
34
34
|
</header>
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
|
|
64
64
|
<p class="terms-info">
|
|
65
65
|
By logging in, you agree to our
|
|
66
|
-
<a href="https://portal.
|
|
66
|
+
<a href="https://portal.mbktech.org/info/Terms&Conditions" target="_blank"
|
|
67
67
|
class="terms-link">Terms & Conditions</a>
|
|
68
68
|
and
|
|
69
|
-
<a href="https://portal.
|
|
69
|
+
<a href="https://portal.mbktech.org/info/PrivacyPolicy" target="_blank"
|
|
70
70
|
class="terms-link">Privacy Policy</a>.
|
|
71
71
|
</p>
|
|
72
72
|
</form>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>{{code}} - {{error}}</title>
|
|
8
|
-
<link rel="icon" type="image/x-icon" href="https://
|
|
8
|
+
<link rel="icon" type="image/x-icon" href="https://mbktech.org/Assets/Images/Icon/dgicon.svg">
|
|
9
9
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
10
10
|
{{> sharedStyles}}
|
|
11
11
|
<style>
|
|
@@ -186,8 +186,8 @@
|
|
|
186
186
|
</path>
|
|
187
187
|
</g>
|
|
188
188
|
</svg>
|
|
189
|
-
<span class="logo-text">{{#if app}}{{app}}{{else}}
|
|
190
|
-
class="logo-comp">
|
|
189
|
+
<span class="logo-text">{{#if app}}{{app}}{{else}}mbkauthe{{/if}} <span
|
|
190
|
+
class="logo-comp">mbktech</span></span>
|
|
191
191
|
</a>
|
|
192
192
|
</div>
|
|
193
193
|
</header>
|
package/views/info.handlebars
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>Version and Configuration Information</title>
|
|
8
|
-
<link rel="icon" type="image/x-icon" href="https://
|
|
8
|
+
<link rel="icon" type="image/x-icon" href="https://mbktech.org/Assets/Images/Icon/dgicon.svg">
|
|
9
9
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
10
10
|
{{> sharedStyles}}
|
|
11
11
|
<style>
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
</path>
|
|
185
185
|
</g>
|
|
186
186
|
</svg>
|
|
187
|
-
<span class="logo-text">{{mbkautheVar.APP_NAME}} <span class="logo-comp">
|
|
187
|
+
<span class="logo-text">{{mbkautheVar.APP_NAME}} <span class="logo-comp">mbktech</span></span>
|
|
188
188
|
</a>
|
|
189
189
|
</div>
|
|
190
190
|
</header>
|
|
@@ -224,6 +224,16 @@
|
|
|
224
224
|
<div class="info-value">{{mbkautheVar.APP_NAME}}</div>
|
|
225
225
|
</div>
|
|
226
226
|
<div class="info-row">
|
|
227
|
+
<div class="info-label">Domain:</div>
|
|
228
|
+
<div class="info-value">{{mbkautheVar.DOMAIN}}</div>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="info-row">
|
|
231
|
+
<div class="info-label">Login Redirect URL:</div>
|
|
232
|
+
<div class="info-value">{{mbkautheVar.loginRedirectURL}}</div>
|
|
233
|
+
</div>
|
|
234
|
+
{{#if authorized}}
|
|
235
|
+
|
|
236
|
+
<div class="info-row">
|
|
227
237
|
<div class="info-label">Two Factor Authentication:</div>
|
|
228
238
|
<div class="info-value">{{mbkautheVar.MBKAUTH_TWO_FA_ENABLE}}</div>
|
|
229
239
|
</div>
|
|
@@ -235,14 +245,7 @@
|
|
|
235
245
|
<div class="info-label">Deployment Status:</div>
|
|
236
246
|
<div class="info-value">{{mbkautheVar.IS_DEPLOYED}}</div>
|
|
237
247
|
</div>
|
|
238
|
-
|
|
239
|
-
<div class="info-label">Domain:</div>
|
|
240
|
-
<div class="info-value">{{mbkautheVar.DOMAIN}}</div>
|
|
241
|
-
</div>
|
|
242
|
-
<div class="info-row">
|
|
243
|
-
<div class="info-label">Login Redirect URL:</div>
|
|
244
|
-
<div class="info-value">{{mbkautheVar.loginRedirectURL}}</div>
|
|
245
|
-
</div>
|
|
248
|
+
{{/if}}
|
|
246
249
|
</div>
|
|
247
250
|
</div>
|
|
248
251
|
</section>
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<meta name="description"
|
|
8
|
-
content="Log in to portal.
|
|
8
|
+
content="Log in to portal.mbktech.org to access your resources and manage projects securely.">
|
|
9
9
|
<meta name="keywords" content="MBK Tech Studio, Web-Portal, Web, Portal, Admin-Panel, Admin, login">
|
|
10
10
|
<meta property="og:title" content="Login | MBK Tech Studio" />
|
|
11
|
-
<meta property="og:image" content="https://
|
|
11
|
+
<meta property="og:image" content="https://mbktech.org/Assets/Images/Icon/logo.png" />
|
|
12
12
|
<meta property="og:url" content="/mbkauthe/login">
|
|
13
13
|
<title>Login | MBK Tech Studio Portal</title>
|
|
14
|
-
<link rel="icon" type="image/x-icon" href="https://
|
|
14
|
+
<link rel="icon" type="image/x-icon" href="https://mbktech.org/Assets/Images/Icon/dgicon.svg">
|
|
15
15
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
16
16
|
{{> sharedStyles}}
|
|
17
17
|
</head>
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
</path>
|
|
29
29
|
</g>
|
|
30
30
|
</svg>
|
|
31
|
-
<span class="logo-text">{{appName}} <span class="logo-comp">
|
|
31
|
+
<span class="logo-text">{{appName}} <span class="logo-comp">mbktech</span></span>
|
|
32
32
|
</a>
|
|
33
33
|
</div>
|
|
34
34
|
</header>
|
|
@@ -68,17 +68,16 @@
|
|
|
68
68
|
<button type="submit" class="btn-login" id="loginButton">
|
|
69
69
|
<span id="loginButtonText">Login</span>
|
|
70
70
|
</button>
|
|
71
|
-
{{#if githubLoginEnabled }}
|
|
72
71
|
<div class="social-login">
|
|
73
72
|
<div class="divider">
|
|
74
73
|
<span>or</span>
|
|
75
74
|
</div>
|
|
76
|
-
|
|
75
|
+
<!-- Use JS to initiate GitHub login and pass redirect param to backend -->
|
|
76
|
+
<a type="button" id="githubLoginBtn" class="btn-github">
|
|
77
77
|
<i class="fab fa-github"></i>
|
|
78
78
|
<span>Continue with GitHub</span>
|
|
79
79
|
</a>
|
|
80
80
|
</div>
|
|
81
|
-
{{/if }}
|
|
82
81
|
|
|
83
82
|
<div class="remember-me">
|
|
84
83
|
<input type="checkbox" id="rememberMe" name="rememberMe">
|
|
@@ -94,16 +93,16 @@
|
|
|
94
93
|
{{/if }}
|
|
95
94
|
|
|
96
95
|
<div class="login-links">
|
|
97
|
-
<a href="https://portal.
|
|
98
|
-
<a href="https://
|
|
96
|
+
<a href="https://portal.mbktech.org/forgot-password" class="login-link">Forgot Password?</a>
|
|
97
|
+
<a href="https://mbktech.org/Support" target="_blank" class="login-link">Need Help?</a>
|
|
99
98
|
</div>
|
|
100
99
|
|
|
101
100
|
<p class="terms-info">
|
|
102
101
|
By logging in, you agree to our
|
|
103
|
-
<a href="https://portal.
|
|
102
|
+
<a href="https://portal.mbktech.org/info/Terms&Conditions" target="_blank"
|
|
104
103
|
class="terms-link">Terms & Conditions</a>
|
|
105
104
|
and
|
|
106
|
-
<a href="https://portal.
|
|
105
|
+
<a href="https://portal.mbktech.org/info/PrivacyPolicy" target="_blank"
|
|
107
106
|
class="terms-link">Privacy Policy</a>.
|
|
108
107
|
</p>
|
|
109
108
|
</form>
|
|
@@ -128,13 +127,13 @@
|
|
|
128
127
|
});
|
|
129
128
|
|
|
130
129
|
function fpass() {
|
|
131
|
-
showMessage(`If you have forgotten your password, please contact support at <a href="https://
|
|
130
|
+
showMessage(`If you have forgotten your password, please contact support at <a href="https://mbktech.org/Support" target="_blank">https://mbktech.org/Support</a> to reset it.`, `Forgot Password`);
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
// Info dialogs
|
|
135
134
|
function usernameinfo() {
|
|
136
|
-
showMessage(`Your username is the part of your MBK Tech Studio email before the @ (e.g., abc.xyz@
|
|
137
|
-
→ abc.xyz). For guests or if you’ve forgotten your credentials, contact <a href="https://
|
|
135
|
+
showMessage(`Your username is the part of your MBK Tech Studio email before the @ (e.g., abc.xyz@mbktech.org
|
|
136
|
+
→ abc.xyz). For guests or if you’ve forgotten your credentials, contact <a href="https://mbktech.org/Support">Support</a>.`, `What is my username?`);
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
function tokeninfo() {
|
|
@@ -272,6 +271,45 @@
|
|
|
272
271
|
document.getElementById('loginPassword').focus();
|
|
273
272
|
}
|
|
274
273
|
});
|
|
274
|
+
|
|
275
|
+
// GitHub login: Attempt to POST redirect to backend, fallback to direct navigation
|
|
276
|
+
async function startGithubLogin() {
|
|
277
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
278
|
+
const redirect = urlParams.get('redirect') || '{{customURL}}';
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Try POSTing to the backend so it can establish any session state
|
|
282
|
+
const resp = await fetch('/mbkauthe/api/github/login', {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
headers: { 'Content-Type': 'application/json' },
|
|
285
|
+
credentials: 'include',
|
|
286
|
+
body: JSON.stringify({ redirect })
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// If backend responds with a JSON containing redirectUrl, navigate there
|
|
290
|
+
if (resp.ok) {
|
|
291
|
+
// If server redirected directly (resp.redirected), follow the final URL
|
|
292
|
+
if (resp.redirected) {
|
|
293
|
+
window.location.href = resp.url;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const data = await resp.json().catch(() => null);
|
|
297
|
+
if (data && data.redirectUrl) {
|
|
298
|
+
window.location.href = data.redirectUrl;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
// swallow and fallback to direct navigation
|
|
304
|
+
console.warn('[mbkauthe] GitHub login POST failed, falling back to direct redirect', error);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Fallback: navigate to the backend GET endpoint with redirect query
|
|
308
|
+
window.location.href = `/mbkauthe/api/github/login?redirect=${encodeURIComponent(redirect)}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const githubBtn = document.getElementById('githubLoginBtn');
|
|
312
|
+
if (githubBtn) githubBtn.addEventListener('click', startGithubLogin);
|
|
275
313
|
</script>
|
|
276
314
|
</body>
|
|
277
315
|
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
left: 0;
|
|
99
99
|
width: 100%;
|
|
100
100
|
height: 100%;
|
|
101
|
-
background: url(
|
|
101
|
+
background: url(/mbkauthe/bg.avif) center/cover no-repeat;
|
|
102
102
|
opacity: .1;
|
|
103
103
|
z-index: 0;
|
|
104
104
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
document.querySelector(".showmessageWindow .error-code").style.display = "none";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
document.querySelector(".showmessageWindow .error-code").href = `https://
|
|
23
|
+
document.querySelector(".showmessageWindow .error-code").href = `https://mbktech.org/ErrorCode/#${errorCode}`;
|
|
24
24
|
*/
|
|
25
25
|
document
|
|
26
26
|
.querySelector(".showMessageblurWindow")
|