mbkauthe 4.3.0 → 4.3.2
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 +92 -1
- package/lib/routes/misc.js +104 -3
- package/package.json +1 -1
- package/views/profilemenu.handlebars +134 -49
package/docs/api.md
CHANGED
|
@@ -108,7 +108,7 @@ When a user logs in, MBKAuthe creates a session and sets the following cookies:
|
|
|
108
108
|
| Cookie Name | Description | HttpOnly | Secure | SameSite |
|
|
109
109
|
|------------|-------------|----------|--------|----------|
|
|
110
110
|
| `mbkauthe.sid` | Session identifier | ✓ | Auto* | lax |
|
|
111
|
-
| `sessionId` | Encrypted session token (AES-256-GCM). This cookie is encrypted and treated as an opaque value by clients; do not attempt to parse or rely on the raw cookie contents. Use server endpoints (e.g.,
|
|
111
|
+
| `sessionId` | Encrypted session token (AES-256-GCM). This cookie is encrypted and treated as an opaque value by clients; do not attempt to parse or rely on the raw cookie contents. Use server endpoints (e.g., `GET /mbkauthe/api/checkSession`, `POST /mbkauthe/api/checkSession` (body) or `POST /mbkauthe/api/verifySession`) to validate or query session information. | ✓ | Auto* | lax |
|
|
112
112
|
| `username` | Username | ✗ | Auto* | lax |
|
|
113
113
|
|
|
114
114
|
\* `secure` flag is automatically set to `true` in production when `IS_DEPLOYED=true`
|
|
@@ -301,6 +301,97 @@ fetch('/mbkauthe/api/checkSession')
|
|
|
301
301
|
|
|
302
302
|
---
|
|
303
303
|
|
|
304
|
+
#### `POST /mbkauthe/api/checkSession` (body)
|
|
305
|
+
|
|
306
|
+
Validate a session by providing a session identifier in the request body. Useful for server-to-server checks or when you have an encrypted `sessionId` value from a cookie and need to validate it server-side.
|
|
307
|
+
|
|
308
|
+
**Rate Limit:** 8 requests per minute (same limiter used by public session endpoints)
|
|
309
|
+
|
|
310
|
+
**Request Body (JSON):**
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"sessionId": "string (uuid or encrypted string)",
|
|
314
|
+
"isEncrypt": "boolean | 'true' (optional, indicates sessionId is encrypted)
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Notes:**
|
|
319
|
+
- The endpoint accepts `isEncrypt` or the misspelled `isEncryt` (both `true` or the string `'true'` are accepted).
|
|
320
|
+
- If `isEncrypt` is true, the server will first attempt `decodeURIComponent()` on the value and then decrypt it (AES) to obtain a UUID session id. If decryption fails or the resulting value is not a UUID, the server returns `400 Bad Request` with `SESSION_INVALID`.
|
|
321
|
+
- A missing `sessionId` returns `400 Bad Request` with `MISSING_REQUIRED_FIELD`.
|
|
322
|
+
|
|
323
|
+
**Success Response (200 OK):**
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"sessionValid": true,
|
|
327
|
+
"expiry": "2025-12-27T12:34:56.000Z"
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Invalid/Expired Session:**
|
|
332
|
+
- Returns 200 with `{ "sessionValid": false, "expiry": null }` for unknown/expired/inactive sessions.
|
|
333
|
+
|
|
334
|
+
**Example Request (Fetch):**
|
|
335
|
+
```javascript
|
|
336
|
+
fetch('/mbkauthe/api/checkSession', {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: { 'Content-Type': 'application/json' },
|
|
339
|
+
body: JSON.stringify({ sessionId: '550e8400-e29b-41d4-a716-446655440000' })
|
|
340
|
+
}).then(r => r.json()).then(console.log);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Example Request (Encrypted sessionId):**
|
|
344
|
+
```javascript
|
|
345
|
+
fetch('/mbkauthe/api/checkSession', {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'Content-Type': 'application/json' },
|
|
348
|
+
body: JSON.stringify({ sessionId: 'ENCRYPTED_VALUE', isEncrypt: true })
|
|
349
|
+
}).then(r => r.json()).then(console.log);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
#### `POST /mbkauthe/api/verifySession`
|
|
355
|
+
|
|
356
|
+
Returns session details for a provided `sessionId`. Intended for server-side validation and to retrieve associated user metadata without relying on an active cookie session.
|
|
357
|
+
|
|
358
|
+
**Request Body (JSON):**
|
|
359
|
+
```json
|
|
360
|
+
{
|
|
361
|
+
"sessionId": "string (uuid or encrypted string)",
|
|
362
|
+
"isEncrypt": "boolean | 'true' (optional)"
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Behavior and Notes:**
|
|
367
|
+
- `isEncrypt`/`isEncryt` have the same behavior as in `/api/checkSession`.
|
|
368
|
+
- If the session is valid and active, the response includes `username` and `role`.
|
|
369
|
+
- Missing or invalid `sessionId` results in `400 Bad Request` with an appropriate error code (`MISSING_REQUIRED_FIELD` or `SESSION_INVALID`).
|
|
370
|
+
|
|
371
|
+
**Success Response (200 OK):**
|
|
372
|
+
```json
|
|
373
|
+
{
|
|
374
|
+
"valid": true,
|
|
375
|
+
"expiry": "2025-12-27T12:34:56.000Z",
|
|
376
|
+
"username": "john.doe",
|
|
377
|
+
"role": "NormalUser"
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Invalid/Expired Session:**
|
|
382
|
+
- Returns 200 with `{ "valid": false, "expiry": null }` for unknown/expired/inactive sessions.
|
|
383
|
+
|
|
384
|
+
**Example Request:**
|
|
385
|
+
```javascript
|
|
386
|
+
fetch('/mbkauthe/api/verifySession', {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: { 'Content-Type': 'application/json' },
|
|
389
|
+
body: JSON.stringify({ sessionId: '550e8400-e29b-41d4-a716-446655440000' })
|
|
390
|
+
}).then(r => r.json()).then(console.log);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
304
395
|
#### `GET /mbkauthe/2fa`
|
|
305
396
|
|
|
306
397
|
Renders the Two-Factor Authentication verification page.
|
package/lib/routes/misc.js
CHANGED
|
@@ -4,9 +4,9 @@ import rateLimit from 'express-rate-limit';
|
|
|
4
4
|
import { mbkautheVar, packageJson, appVersion } from "#config.js";
|
|
5
5
|
import { renderError } from "#response.js";
|
|
6
6
|
import { authenticate, validateSession, validateApiSession } from "../middleware/auth.js";
|
|
7
|
-
import { ErrorCodes, ErrorMessages } from "../utils/errors.js";
|
|
7
|
+
import { ErrorCodes, ErrorMessages, createErrorResponse } from "../utils/errors.js";
|
|
8
8
|
import { dblogin } from "#pool.js";
|
|
9
|
-
import { clearSessionCookies } from "#cookies.js";
|
|
9
|
+
import { clearSessionCookies, decryptSessionId } from "#cookies.js";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
import path from "path";
|
|
12
12
|
import fs from "fs";
|
|
@@ -236,6 +236,107 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
|
|
|
236
236
|
}
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// UUID helper used by session endpoints
|
|
240
|
+
const isUuid = (val) => typeof val === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val);
|
|
241
|
+
|
|
242
|
+
// POST /api/checkSession — accept sessionId in request body { sessionId: "<uuid>" }
|
|
243
|
+
router.post('/api/checkSession', LoginLimit, async (req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
const { sessionId: rawSessionId, isEncrypt, isEncryt } = req.body || {};
|
|
246
|
+
let sessionId = rawSessionId;
|
|
247
|
+
if (!sessionId) {
|
|
248
|
+
return res.status(400).json(createErrorResponse(400, ErrorCodes.MISSING_REQUIRED_FIELD));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If body indicates the sessionId is encrypted, decode (if URL-encoded) then decrypt it first
|
|
252
|
+
const encryptedFlag = isEncrypt === true || isEncrypt === 'true' || isEncryt === true || isEncryt === 'true';
|
|
253
|
+
if (encryptedFlag) {
|
|
254
|
+
// Some clients URL-encode cookie values when posting; safely try to decode first
|
|
255
|
+
let toDecrypt = typeof sessionId === 'string' ? sessionId : String(sessionId);
|
|
256
|
+
try {
|
|
257
|
+
toDecrypt = decodeURIComponent(toDecrypt);
|
|
258
|
+
} catch (decodeErr) {
|
|
259
|
+
// ignore decode errors and continue with original value
|
|
260
|
+
}
|
|
261
|
+
const decrypted = decryptSessionId(toDecrypt);
|
|
262
|
+
if (!decrypted || !isUuid(decrypted)) {
|
|
263
|
+
return res.status(400).json(createErrorResponse(400, ErrorCodes.SESSION_INVALID));
|
|
264
|
+
}
|
|
265
|
+
sessionId = decrypted;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await dblogin.query({
|
|
269
|
+
name: 'check-session-validity-by-id',
|
|
270
|
+
text: `SELECT s.expires_at, u."Active" FROM "Sessions" s JOIN "Users" u ON s."UserName" = u."UserName" WHERE s.id = $1 LIMIT 1`,
|
|
271
|
+
values: [sessionId]
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (result.rows.length === 0) {
|
|
275
|
+
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const row = result.rows[0];
|
|
279
|
+
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
280
|
+
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
|
|
284
|
+
return res.status(200).json({ sessionValid: true, expiry });
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error('[mbkauthe] checkSession (body) error:', err);
|
|
287
|
+
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// POST /api/verifySession — returns details about sessionId provided in body
|
|
292
|
+
router.post('/api/verifySession', LoginLimit, async (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const { sessionId: rawSessionId, isEncrypt, isEncryt } = req.body || {};
|
|
295
|
+
let sessionId = rawSessionId;
|
|
296
|
+
if (!sessionId) {
|
|
297
|
+
return res.status(400).json(createErrorResponse(400, ErrorCodes.MISSING_REQUIRED_FIELD));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// If body indicates the sessionId is encrypted, decode (if URL-encoded) then decrypt it first
|
|
301
|
+
const encryptedFlag = isEncrypt === true || isEncrypt === 'true' || isEncryt === true || isEncryt === 'true';
|
|
302
|
+
if (encryptedFlag) {
|
|
303
|
+
// Some clients URL-encode cookie values when posting; safely try to decode first
|
|
304
|
+
let toDecrypt = typeof sessionId === 'string' ? sessionId : String(sessionId);
|
|
305
|
+
try {
|
|
306
|
+
toDecrypt = decodeURIComponent(toDecrypt);
|
|
307
|
+
} catch (decodeErr) {
|
|
308
|
+
// ignore decode errors and continue with original value
|
|
309
|
+
}
|
|
310
|
+
const decrypted = decryptSessionId(toDecrypt);
|
|
311
|
+
if (!decrypted || !isUuid(decrypted)) {
|
|
312
|
+
return res.status(400).json(createErrorResponse(400, ErrorCodes.SESSION_INVALID));
|
|
313
|
+
}
|
|
314
|
+
sessionId = decrypted;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
|
|
318
|
+
FROM "Sessions" s
|
|
319
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
320
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
321
|
+
const result = await dblogin.query({ name: 'verify-session', text: query, values: [sessionId] });
|
|
322
|
+
|
|
323
|
+
if (result.rows.length === 0) {
|
|
324
|
+
return res.status(200).json({ valid: false, expiry: null });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const row = result.rows[0];
|
|
328
|
+
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
329
|
+
return res.status(200).json({ valid: false, expiry: null });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
|
|
333
|
+
return res.status(200).json({ valid: true, expiry, username: row.UserName, role: row.Role });
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error('[mbkauthe] verifySession error:', err);
|
|
336
|
+
return res.status(200).json({ valid: false, expiry: null });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
239
340
|
// Error codes page
|
|
240
341
|
router.get("/ErrorCode", (req, res) => {
|
|
241
342
|
try {
|
|
@@ -427,4 +528,4 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
|
|
|
427
528
|
}
|
|
428
529
|
});
|
|
429
530
|
|
|
430
|
-
export default router;
|
|
531
|
+
export default router;
|
package/package.json
CHANGED
|
@@ -1,45 +1,76 @@
|
|
|
1
|
-
<div class="header-actions">
|
|
1
|
+
<div class="mbkauthe-header-actions">
|
|
2
2
|
{{#if userLoggedIn}}
|
|
3
|
-
<div class="profile-menu">
|
|
4
|
-
<button class="profile-trigger" type="button" aria-expanded="false" aria-haspopup="true">
|
|
5
|
-
<img src="/mbkauthe/user/profilepic?u={{username}}" alt="Profile" class="profile-avatar">
|
|
3
|
+
<div class="mbkauthe-profile-menu">
|
|
4
|
+
<button class="mbkauthe-profile-trigger" type="button" aria-expanded="false" aria-haspopup="true">
|
|
5
|
+
<img src="/mbkauthe/user/profilepic?u={{username}}" alt="Profile" class="mbkauthe-profile-avatar">
|
|
6
6
|
</button>
|
|
7
|
-
<div class="profile-dropdown" role="menu" hidden>
|
|
8
|
-
<a class="profile-item" role="menuitem" title="{{username}}">
|
|
7
|
+
<div class="mbkauthe-profile-dropdown" role="menu" hidden>
|
|
8
|
+
<a class="mbkauthe-profile-item" role="menuitem" title="{{username}}">
|
|
9
9
|
<span>@{{username}}</span>
|
|
10
10
|
</a>
|
|
11
|
-
<a class="profile-item" href="https://portal.mbktech.org/user/settings" role="menuitem"
|
|
11
|
+
<a class="mbkauthe-profile-item" href="https://portal.mbktech.org/user/settings" role="menuitem"
|
|
12
|
+
title="Settings">
|
|
12
13
|
<i class="fas fa-cog"></i>
|
|
13
14
|
<span>Settings</span>
|
|
14
15
|
</a>
|
|
15
|
-
<a class="profile-item" href="/mbkauthe/accounts" role="menuitem"
|
|
16
|
+
<a class="mbkauthe-profile-item" href="/mbkauthe/accounts" role="menuitem"
|
|
17
|
+
title="Switch or Add another Account">
|
|
16
18
|
<i class="fa fa-user-group"></i>
|
|
17
19
|
<span>Switch account</span>
|
|
18
20
|
</a>
|
|
19
|
-
<button class="profile-item" type="button" data-action="logout" role="menuitem" title="Logout">
|
|
21
|
+
<button class="mbkauthe-profile-item" type="button" data-action="logout" role="menuitem" title="Logout">
|
|
20
22
|
<i class="fas fa-sign-out-alt"></i>
|
|
21
23
|
<span>Logout</span>
|
|
22
24
|
</button>
|
|
23
25
|
</div>
|
|
24
26
|
</div>
|
|
25
27
|
{{else}}
|
|
26
|
-
<a class="btn-login" style="text-decoration: none;" href="/mbkauthe/login"><i
|
|
28
|
+
<a class="mbkauthe-btn-login" style="text-decoration: none;" href="/mbkauthe/login"><i
|
|
29
|
+
class="fas fa-sign-in-alt"></i>
|
|
27
30
|
Login</a>
|
|
28
31
|
{{/if}}
|
|
29
32
|
</div>
|
|
30
33
|
<style>
|
|
31
|
-
.
|
|
34
|
+
.mbkauthe-btn-login {
|
|
35
|
+
width: 100%;
|
|
36
|
+
padding: 8px;
|
|
37
|
+
border-radius: var(--border-radius);
|
|
38
|
+
background: var(--accent);
|
|
39
|
+
color: var(--dark);
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
font-size: 1.2rem;
|
|
42
|
+
border: .1rem solid var(--accent);
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
transition: all 0.4s cubic-bezier(.4, 0, .2, 1);
|
|
45
|
+
box-shadow: var(--box-shadow);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.mbkauthe-btn-login:hover {
|
|
49
|
+
background: var(--dark);
|
|
50
|
+
color: var(--accent);
|
|
51
|
+
box-shadow: 0 6px 20px rgba(0, 184, 148, 0.3);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.mbkauthe-btn-login:disabled {
|
|
55
|
+
background: var(--dark);
|
|
56
|
+
color: var(--accent);
|
|
57
|
+
cursor: not-allowed;
|
|
58
|
+
transform: none;
|
|
59
|
+
box-shadow: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.mbkauthe-header-actions {
|
|
32
63
|
display: flex;
|
|
33
64
|
align-items: center;
|
|
34
65
|
gap: 0.75rem;
|
|
35
66
|
margin-left: auto;
|
|
36
67
|
}
|
|
37
68
|
|
|
38
|
-
.profile-menu {
|
|
69
|
+
.mbkauthe-profile-menu {
|
|
39
70
|
position: relative;
|
|
40
71
|
}
|
|
41
72
|
|
|
42
|
-
.profile-trigger {
|
|
73
|
+
.mbkauthe-profile-trigger {
|
|
43
74
|
width: 45px;
|
|
44
75
|
height: 45px;
|
|
45
76
|
border-radius: 999px;
|
|
@@ -52,17 +83,17 @@
|
|
|
52
83
|
transition: var(--transition);
|
|
53
84
|
}
|
|
54
85
|
|
|
55
|
-
.profile-trigger:hover {
|
|
86
|
+
.mbkauthe-profile-trigger:hover {
|
|
56
87
|
border-color: var(--accent);
|
|
57
88
|
box-shadow: 0 10px 30px rgba(0, 184, 148, 0.2);
|
|
58
89
|
}
|
|
59
90
|
|
|
60
|
-
.profile-trigger:focus-visible {
|
|
91
|
+
.mbkauthe-profile-trigger:focus-visible {
|
|
61
92
|
outline: 2px solid var(--accent);
|
|
62
93
|
outline-offset: 2px;
|
|
63
94
|
}
|
|
64
95
|
|
|
65
|
-
.profile-avatar {
|
|
96
|
+
.mbkauthe-profile-avatar {
|
|
66
97
|
width: 40px;
|
|
67
98
|
height: 40px;
|
|
68
99
|
border-radius: 999px;
|
|
@@ -70,34 +101,46 @@
|
|
|
70
101
|
background: var(--dark);
|
|
71
102
|
}
|
|
72
103
|
|
|
73
|
-
|
|
104
|
+
/* Theme-aware dropdown variables and improved visuals */
|
|
105
|
+
.mbkauthe-profile-dropdown {
|
|
106
|
+
--mbk-bg: var(--mbkauthe-dropdown-bg, #ffffff);
|
|
107
|
+
--mbk-border: var(--mbkauthe-dropdown-border, rgba(15, 23, 42, 0.06));
|
|
108
|
+
--mbk-item-hover: var(--mbkauthe-item-hover, rgba(15, 23, 42, 0.03));
|
|
109
|
+
--mbk-text: var(--mbkauthe-text, var(--text, #0f172a));
|
|
110
|
+
|
|
74
111
|
position: absolute;
|
|
75
112
|
right: 0;
|
|
76
113
|
top: calc(100% + 0.5rem);
|
|
77
|
-
background: var(--
|
|
78
|
-
border: 1px solid
|
|
114
|
+
background: var(--mbk-bg);
|
|
115
|
+
border: 1px solid var(--mbk-border);
|
|
79
116
|
border-radius: var(--border-radius);
|
|
80
117
|
min-width: 190px;
|
|
81
|
-
box-shadow: 0
|
|
118
|
+
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.12);
|
|
82
119
|
padding: 0;
|
|
83
120
|
opacity: 0;
|
|
84
121
|
visibility: hidden;
|
|
85
122
|
transform: translateY(-6px);
|
|
86
123
|
transition: var(--transition);
|
|
87
124
|
z-index: 1000;
|
|
125
|
+
backdrop-filter: blur(6px);
|
|
126
|
+
-webkit-backdrop-filter: blur(6px);
|
|
127
|
+
color: var(--mbk-text);
|
|
128
|
+
overflow: hidden;
|
|
88
129
|
}
|
|
89
130
|
|
|
90
|
-
.profile-dropdown.open {
|
|
131
|
+
.mbkauthe-profile-dropdown.open {
|
|
132
|
+
display: block;
|
|
91
133
|
opacity: 1;
|
|
92
134
|
visibility: visible;
|
|
93
135
|
transform: translateY(0);
|
|
136
|
+
border-radius: 3%;
|
|
94
137
|
}
|
|
95
138
|
|
|
96
|
-
.profile-item {
|
|
139
|
+
.mbkauthe-profile-item {
|
|
97
140
|
width: 100%;
|
|
98
141
|
display: block;
|
|
99
142
|
padding: 0.85rem 1rem;
|
|
100
|
-
color: var(--text);
|
|
143
|
+
color: var(--mbk-text);
|
|
101
144
|
background: transparent;
|
|
102
145
|
border: none;
|
|
103
146
|
text-decoration: none;
|
|
@@ -105,61 +148,103 @@
|
|
|
105
148
|
font-weight: 600;
|
|
106
149
|
letter-spacing: 0.01em;
|
|
107
150
|
cursor: pointer;
|
|
108
|
-
transition:
|
|
151
|
+
transition: background 0.12s ease, color 0.12s ease, transform 0.12s ease;
|
|
109
152
|
}
|
|
110
153
|
|
|
111
|
-
.profile-item:hover {
|
|
112
|
-
background:
|
|
113
|
-
color: var(--accent);
|
|
154
|
+
.mbkauthe-profile-item:hover {
|
|
155
|
+
background: var(--mbk-item-hover);
|
|
156
|
+
color: var(--color-accent, var(--accent, #0d9488));
|
|
114
157
|
}
|
|
115
158
|
|
|
116
|
-
.profile-item:
|
|
159
|
+
.mbkauthe-profile-item:focus-visible {
|
|
160
|
+
outline: 2px solid color-mix(in srgb, var(--color-accent, var(--accent, #0d9488)) 40%, transparent 60%);
|
|
161
|
+
outline-offset: 2px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.mbkauthe-profile-item:disabled {
|
|
117
165
|
opacity: 0.6;
|
|
118
166
|
cursor: not-allowed;
|
|
119
167
|
}
|
|
168
|
+
|
|
169
|
+
/* Make trigger adapt to theme */
|
|
170
|
+
.mbkauthe-profile-trigger {
|
|
171
|
+
transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.mbkauthe-profile-trigger:hover {
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* prefers-color-scheme fallbacks scoped to component */
|
|
178
|
+
@media (prefers-color-scheme: dark) {
|
|
179
|
+
.mbkauthe-profile-dropdown {
|
|
180
|
+
--mbk-bg: #0b1220;
|
|
181
|
+
--mbk-border: rgba(255, 255, 255, 0.06);
|
|
182
|
+
--mbk-item-hover: rgba(255, 255, 255, 0.03);
|
|
183
|
+
--mbk-text: #e6eef6;
|
|
184
|
+
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.7);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.mbkauthe-profile-trigger {
|
|
188
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
189
|
+
background: rgba(255, 255, 255, 0.02);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@media (prefers-color-scheme: light) {
|
|
194
|
+
.mbkauthe-profile-dropdown {
|
|
195
|
+
--mbk-bg: #ffffff;
|
|
196
|
+
--mbk-border: rgba(15, 23, 42, 0.06);
|
|
197
|
+
--mbk-item-hover: rgba(15, 23, 42, 0.03);
|
|
198
|
+
--mbk-text: var(--dark-color-ml, #0f172a);
|
|
199
|
+
box-shadow: 0 8px 22px rgba(2, 6, 23, 0.08);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.mbkauthe-profile-trigger {
|
|
203
|
+
border: 1px solid rgba(2, 6, 23, 0.06);
|
|
204
|
+
background: rgba(255, 255, 255, 0.96);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
120
207
|
</style>
|
|
121
208
|
<script>
|
|
122
209
|
(() => {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
210
|
+
const mbkTrigger = document.querySelector('.mbkauthe-profile-trigger');
|
|
211
|
+
const mbkDropdown = document.querySelector('.mbkauthe-profile-dropdown');
|
|
125
212
|
|
|
126
|
-
if (!
|
|
213
|
+
if (!mbkTrigger || !mbkDropdown) {
|
|
127
214
|
return;
|
|
128
215
|
}
|
|
129
216
|
|
|
130
217
|
const closeMenu = () => {
|
|
131
|
-
if (!
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
218
|
+
if (!mbkDropdown.classList.contains('open')) return;
|
|
219
|
+
mbkDropdown.classList.remove('open');
|
|
220
|
+
mbkTrigger.setAttribute('aria-expanded', 'false');
|
|
135
221
|
// After transition, hide to prevent FOUC on next paint
|
|
136
222
|
const onTransition = (e) => {
|
|
137
223
|
if (e.propertyName === 'opacity') {
|
|
138
|
-
|
|
139
|
-
|
|
224
|
+
mbkDropdown.setAttribute('hidden', '');
|
|
225
|
+
mbkDropdown.removeEventListener('transitionend', onTransition);
|
|
140
226
|
}
|
|
141
227
|
};
|
|
142
228
|
|
|
143
|
-
|
|
229
|
+
mbkDropdown.addEventListener('transitionend', onTransition);
|
|
144
230
|
// Fallback in case transitionend doesn't fire
|
|
145
231
|
setTimeout(() => {
|
|
146
|
-
if (!
|
|
147
|
-
|
|
232
|
+
if (!mbkDropdown.classList.contains('open')) {
|
|
233
|
+
mbkDropdown.setAttribute('hidden', '');
|
|
148
234
|
}
|
|
149
235
|
}, 350);
|
|
150
236
|
};
|
|
151
237
|
|
|
152
|
-
|
|
238
|
+
mbkTrigger.addEventListener('click', (event) => {
|
|
153
239
|
event.stopPropagation();
|
|
154
|
-
const isOpening = !
|
|
155
|
-
|
|
240
|
+
const isOpening = !mbkDropdown.classList.contains('open');
|
|
156
241
|
if (isOpening) {
|
|
157
242
|
// Reveal then start transition to open state
|
|
158
|
-
|
|
243
|
+
mbkDropdown.removeAttribute('hidden');
|
|
159
244
|
// Ensure the class add happens on next frame for transition to work
|
|
160
245
|
requestAnimationFrame(() => {
|
|
161
|
-
|
|
162
|
-
|
|
246
|
+
mbkDropdown.classList.add('open');
|
|
247
|
+
mbkTrigger.setAttribute('aria-expanded', 'true');
|
|
163
248
|
});
|
|
164
249
|
} else {
|
|
165
250
|
// Close the menu
|
|
@@ -168,7 +253,7 @@
|
|
|
168
253
|
});
|
|
169
254
|
|
|
170
255
|
document.addEventListener('click', (event) => {
|
|
171
|
-
if (!
|
|
256
|
+
if (!mbkDropdown.contains(event.target) && !mbkTrigger.contains(event.target)) {
|
|
172
257
|
closeMenu();
|
|
173
258
|
}
|
|
174
259
|
});
|
|
@@ -179,7 +264,7 @@
|
|
|
179
264
|
}
|
|
180
265
|
});
|
|
181
266
|
|
|
182
|
-
const logoutButton =
|
|
267
|
+
const logoutButton = mbkDropdown.querySelector('[data-action="logout"]');
|
|
183
268
|
if (logoutButton) {
|
|
184
269
|
logoutButton.addEventListener('click', async () => {
|
|
185
270
|
const originalLabel = logoutButton.textContent;
|
|
@@ -215,7 +300,7 @@
|
|
|
215
300
|
|
|
216
301
|
// Populate the "Switch account" link with encoded current page URL in its `redirect` param
|
|
217
302
|
(function setSwitchAccountRedirect() {
|
|
218
|
-
const switchAccountLink =
|
|
303
|
+
const switchAccountLink = mbkDropdown.querySelector('a[href^="/mbkauthe/accounts"]');
|
|
219
304
|
if (!switchAccountLink) return;
|
|
220
305
|
|
|
221
306
|
try {
|