mbkauthe 3.5.0 → 4.0.1
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 +54 -304
- package/docs/api.md +2 -2
- package/docs/db.md +26 -20
- package/docs/db.sql +116 -0
- package/docs/env.md +10 -0
- package/index.d.ts +3 -3
- package/index.js +4 -1
- package/lib/config/cookies.js +6 -0
- package/lib/config/index.js +20 -4
- package/lib/config/security.js +1 -1
- package/lib/middleware/auth.js +64 -30
- package/lib/middleware/index.js +37 -28
- package/lib/routes/auth.js +59 -36
- package/lib/routes/misc.js +22 -9
- package/package.json +1 -1
- package/views/Error/dError.handlebars +175 -88
- package/views/loginmbkauthe.handlebars +0 -2
package/lib/routes/misc.js
CHANGED
|
@@ -79,10 +79,15 @@ router.get('/test', validateSession, LoginLimit, async (req, res) => {
|
|
|
79
79
|
<p class="success">✅ Authentication successful! User is logged in.</p>
|
|
80
80
|
<p>Welcome, <strong>${req.session.user.username}</strong>! Your role: <strong>${req.session.user.role}</strong></p>
|
|
81
81
|
<div class="user-info">
|
|
82
|
+
Username: ${req.session.user.username}<br>
|
|
83
|
+
Role: ${req.session.user.role}<br>
|
|
84
|
+
Full Name: ${req.session.user.fullname || 'N/A'}<br>
|
|
82
85
|
User ID: ${req.session.user.id}<br>
|
|
83
86
|
Session ID: ${req.session.user.sessionId.slice(0, 5)}...
|
|
84
87
|
</div>
|
|
85
88
|
<button onclick="logout()">Logout</button>
|
|
89
|
+
<a href="https://portal.mbktech.org/">Web Portal</a>
|
|
90
|
+
<a href="https://portal.mbktech.org/user/settings">User Settings</a>
|
|
86
91
|
<a href="/mbkauthe/info">Info Page</a>
|
|
87
92
|
<a href="/mbkauthe/login">Login Page</a>
|
|
88
93
|
</div>
|
|
@@ -104,19 +109,27 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
|
|
|
104
109
|
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
const
|
|
108
|
-
const result = await dblogin.query({ name: 'check-session-validity', text: 'SELECT "SessionId", "Active" FROM "Users" WHERE id = $1', values: [id] });
|
|
112
|
+
const result = await dblogin.query({ name: 'check-session-validity', text: `SELECT s.expires_at, u."Active" FROM "Sessions" s JOIN "Users" u ON s."UserName" = u."UserName" WHERE s.id = $1 LIMIT 1`, values: [sessionId] });
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
if (!dbSessionId || dbSessionId !== normalizedSessionId || !result.rows[0].Active) {
|
|
114
|
+
if (result.rows.length === 0) {
|
|
112
115
|
req.session.destroy(() => { });
|
|
113
116
|
clearSessionCookies(res);
|
|
114
117
|
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
const row = result.rows[0];
|
|
121
|
+
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
122
|
+
req.session.destroy(() => { });
|
|
123
|
+
clearSessionCookies(res);
|
|
124
|
+
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Determine expiry: prefer application session expiry if present else fallback to connect-pg-simple expire
|
|
128
|
+
let expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
|
|
129
|
+
if (!expiry) {
|
|
130
|
+
const sessResult = await dblogin.query({ name: 'get-session-expiry', text: 'SELECT expire FROM "session" WHERE sid = $1', values: [req.sessionID] });
|
|
131
|
+
expiry = sessResult.rows.length > 0 && sessResult.rows[0].expire ? new Date(sessResult.rows[0].expire).toISOString() : null;
|
|
132
|
+
}
|
|
120
133
|
|
|
121
134
|
return res.status(200).json({ sessionValid: true, expiry });
|
|
122
135
|
} catch (err) {
|
|
@@ -287,8 +300,8 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
|
|
|
287
300
|
// Run both operations in parallel for better performance
|
|
288
301
|
await Promise.all([
|
|
289
302
|
dblogin.query({
|
|
290
|
-
name: 'terminate-all-
|
|
291
|
-
text: '
|
|
303
|
+
name: 'terminate-all-app-sessions',
|
|
304
|
+
text: 'DELETE FROM "Sessions"'
|
|
292
305
|
}),
|
|
293
306
|
dblogin.query({
|
|
294
307
|
name: 'terminate-all-db-sessions',
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
{{> head pageCode=code pageError=error ogUrl="/error" extraStyles="<style>
|
|
5
5
|
.login-box {
|
|
6
6
|
max-width: 600px;
|
|
7
|
+
padding: 2.5rem;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
.status-code {
|
|
@@ -35,7 +36,6 @@
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
.status-content {
|
|
38
|
-
margin-bottom: 1.5rem;
|
|
39
39
|
text-align: center;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
font-size: 1.2rem;
|
|
44
44
|
margin: 0.5rem 0;
|
|
45
45
|
color: var(--warning);
|
|
46
|
+
line-height: 1.4;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
.status-links {
|
|
49
50
|
display: flex;
|
|
50
51
|
justify-content: center;
|
|
51
|
-
margin-top: 1rem;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
.status-link {
|
|
@@ -57,90 +57,146 @@
|
|
|
57
57
|
transition: var(--transition);
|
|
58
58
|
margin: 0 auto;
|
|
59
59
|
font-size: 1rem;
|
|
60
|
-
font-weight:
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
padding: 0.5rem 0.75rem;
|
|
62
|
+
border-radius: var(--border-radius);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
.status-link:hover {
|
|
64
|
-
text-decoration:
|
|
66
|
+
text-decoration: none;
|
|
67
|
+
background: rgba(255,255,255,0.02);
|
|
65
68
|
color: var(--secondary);
|
|
69
|
+
transform: translateY(-2px);
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
/* Details wrapper: improved layout, accessibility and animation */
|
|
68
73
|
.details-wrapper {
|
|
69
|
-
margin-top: 1.
|
|
70
|
-
background: rgba(255, 255, 255, .
|
|
74
|
+
margin-top: 1.25rem;
|
|
75
|
+
background: rgba(255, 255, 255, .03);
|
|
71
76
|
border-radius: var(--border-radius);
|
|
72
|
-
padding:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
border: 1px solid rgba(255, 255, 255, .1);
|
|
77
|
+
padding: 0;
|
|
78
|
+
transition: box-shadow .25s ease, transform .25s ease;
|
|
79
|
+
border: 1px solid rgba(255, 255, 255, .06);
|
|
80
|
+
overflow: visible;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
.details-
|
|
80
|
-
background: rgba(255, 255, 255, .08);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.details-header {
|
|
83
|
+
.details-toggle {
|
|
84
84
|
display: flex;
|
|
85
|
-
justify-content: space-between;
|
|
86
85
|
align-items: center;
|
|
86
|
+
justify-content: space-between;
|
|
87
|
+
width: 100%;
|
|
88
|
+
background: transparent;
|
|
89
|
+
border: 0;
|
|
87
90
|
color: var(--light);
|
|
88
|
-
font-size: 1.
|
|
91
|
+
font-size: 1.05rem;
|
|
89
92
|
font-weight: 600;
|
|
93
|
+
padding: 0.85rem 1rem;
|
|
94
|
+
cursor: pointer;
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
.details-
|
|
93
|
-
|
|
97
|
+
.details-toggle:focus {
|
|
98
|
+
outline: 3px solid rgba(100, 150, 255, .18);
|
|
99
|
+
outline-offset: 2px;
|
|
100
|
+
border-radius: var(--border-radius);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.details-toggle .chev {
|
|
104
|
+
transition: transform .25s ease, color .2s ease;
|
|
105
|
+
color: var(--muted);
|
|
94
106
|
}
|
|
95
107
|
|
|
96
|
-
.details-wrapper:hover .details-
|
|
108
|
+
.details-wrapper:hover .details-toggle .chev {
|
|
97
109
|
color: var(--accent);
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
.error-details-wrapper {
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
max-height: 0;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
opacity: 0;
|
|
116
|
+
transition: max-height .33s cubic-bezier(.2,.9,.2,1), opacity .25s ease;
|
|
117
|
+
padding: 0 1rem;
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
.details-wrapper.active .error-details-wrapper {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.details-wrapper.active .details-header i {
|
|
110
|
-
transform: rotate(180deg);
|
|
121
|
+
max-height: 480px; /* large enough for content */
|
|
122
|
+
opacity: 1;
|
|
123
|
+
padding: 0.9rem 1rem 1.25rem 1rem;
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
.error-details {
|
|
114
127
|
width: 100%;
|
|
115
|
-
height:
|
|
116
|
-
background: rgba(0, 0, 0, .
|
|
117
|
-
border: 1px solid rgba(255, 255, 255, .
|
|
118
|
-
border-radius: var(--border-radius);
|
|
128
|
+
min-height: 140px;
|
|
129
|
+
background: rgba(0, 0, 0, .28);
|
|
130
|
+
border: 1px solid rgba(255, 255, 255, .08);
|
|
131
|
+
border-radius: calc(var(--border-radius) - 2px);
|
|
119
132
|
color: var(--text);
|
|
120
|
-
padding:
|
|
121
|
-
|
|
133
|
+
padding: 0.9rem;
|
|
134
|
+
padding-right: 3.6rem; /* leave space for overlay button */
|
|
135
|
+
box-sizing: border-box;
|
|
136
|
+
resize: vertical;
|
|
122
137
|
font-size: 0.9rem;
|
|
123
|
-
font-family: monospace;
|
|
138
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', monospace;
|
|
139
|
+
line-height: 1.45;
|
|
124
140
|
}
|
|
125
141
|
|
|
142
|
+
.error-details-container {
|
|
143
|
+
position: relative;
|
|
144
|
+
width: 100%;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Overlay copy button positioned inside the details container */
|
|
126
148
|
.copy-btn {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
position: absolute;
|
|
150
|
+
top: 0.6rem;
|
|
151
|
+
right: 0.65rem;
|
|
152
|
+
background: rgba(0,0,0,0.55);
|
|
153
|
+
color: var(--text);
|
|
154
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
155
|
+
border-radius: 6px;
|
|
156
|
+
padding: 0.45rem 0.6rem;
|
|
133
157
|
cursor: pointer;
|
|
134
|
-
transition:
|
|
135
|
-
font-size:
|
|
136
|
-
font-weight:
|
|
137
|
-
box-shadow:
|
|
158
|
+
transition: transform .12s ease, box-shadow .12s ease, background .12s ease;
|
|
159
|
+
font-size: 0.95rem;
|
|
160
|
+
font-weight: 700;
|
|
161
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.18);
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
gap: 0.45rem;
|
|
164
|
+
align-items: center;
|
|
165
|
+
z-index: 6;
|
|
166
|
+
backdrop-filter: blur(4px);
|
|
138
167
|
}
|
|
139
168
|
|
|
140
169
|
.copy-btn:hover {
|
|
141
|
-
background: var(--
|
|
170
|
+
background: var(--accent);
|
|
171
|
+
color: var(--dark);
|
|
142
172
|
transform: translateY(-2px);
|
|
143
|
-
box-shadow: 0
|
|
173
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.copy-feedback {
|
|
177
|
+
position: absolute;
|
|
178
|
+
top: 2.4rem;
|
|
179
|
+
right: 0.6rem;
|
|
180
|
+
background: rgba(0,0,0,0.65);
|
|
181
|
+
color: var(--accent);
|
|
182
|
+
padding: 0.25rem 0.5rem;
|
|
183
|
+
font-weight: 700;
|
|
184
|
+
border-radius: 4px;
|
|
185
|
+
font-size: 0.85rem;
|
|
186
|
+
display: none;
|
|
187
|
+
z-index: 6;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.sr-only {
|
|
191
|
+
position: absolute !important;
|
|
192
|
+
width: 1px !important;
|
|
193
|
+
height: 1px !important;
|
|
194
|
+
padding: 0 !important;
|
|
195
|
+
margin: -1px !important;
|
|
196
|
+
overflow: hidden !important;
|
|
197
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
198
|
+
white-space: nowrap !important;
|
|
199
|
+
border: 0 !important;
|
|
144
200
|
}
|
|
145
201
|
|
|
146
202
|
@media (max-width: 768px) {
|
|
@@ -149,8 +205,10 @@
|
|
|
149
205
|
}
|
|
150
206
|
|
|
151
207
|
.status-title {
|
|
152
|
-
font-size: 1.
|
|
208
|
+
font-size: 1.25rem;
|
|
153
209
|
}
|
|
210
|
+
|
|
211
|
+
.login-box { padding: 1.5rem; }
|
|
154
212
|
}
|
|
155
213
|
|
|
156
214
|
@media (max-width: 576px) {
|
|
@@ -186,17 +244,19 @@
|
|
|
186
244
|
</div>
|
|
187
245
|
|
|
188
246
|
{{#if details}}
|
|
189
|
-
<div class="details-wrapper"
|
|
190
|
-
<
|
|
191
|
-
<span>Show
|
|
192
|
-
<i class="fas fa-chevron-down"></i>
|
|
193
|
-
</
|
|
194
|
-
<div class="error-details-wrapper">
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
247
|
+
<div class="details-wrapper">
|
|
248
|
+
<button class="details-toggle" type="button" aria-expanded="false" aria-controls="errorDetails">
|
|
249
|
+
<span>Show error details</span>
|
|
250
|
+
<i class="fas fa-chevron-down chev" aria-hidden="true"></i>
|
|
251
|
+
</button>
|
|
252
|
+
<div class="error-details-wrapper" id="errorDetails" hidden>
|
|
253
|
+
<div class="error-details-container">
|
|
254
|
+
<textarea class="error-details" readonly>{{details}}</textarea>
|
|
255
|
+
<button class="copy-btn" type="button" aria-label="Copy error details">
|
|
256
|
+
<i class="fas fa-copy" aria-hidden="true"></i><span class="sr-only"> Copy</span>
|
|
257
|
+
</button>
|
|
258
|
+
<span class="copy-feedback" aria-live="polite">Copied!</span>
|
|
259
|
+
</div>
|
|
200
260
|
</div>
|
|
201
261
|
</div>
|
|
202
262
|
{{/if}}
|
|
@@ -207,36 +267,63 @@
|
|
|
207
267
|
{{> versionInfo}}
|
|
208
268
|
|
|
209
269
|
<script>
|
|
210
|
-
function toggleDetailsDropdown(element) {
|
|
211
|
-
const errorDetailsWrapper = element.querySelector('.error-details-wrapper');
|
|
212
|
-
const icon = element.querySelector('.details-header i');
|
|
213
|
-
|
|
214
|
-
if (errorDetailsWrapper.style.display === 'block') {
|
|
215
|
-
errorDetailsWrapper.style.display = 'none';
|
|
216
|
-
icon.style.transform = 'rotate(0deg)';
|
|
217
|
-
} else {
|
|
218
|
-
errorDetailsWrapper.style.display = 'block';
|
|
219
|
-
icon.style.transform = 'rotate(180deg)';
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function copyErrorDetails(button) {
|
|
224
|
-
const details = button.previousElementSibling.value;
|
|
225
|
-
navigator.clipboard.writeText(details).then(() => {
|
|
226
|
-
button.textContent = 'Copied!';
|
|
227
|
-
setTimeout(() => {
|
|
228
|
-
button.textContent = 'Copy';
|
|
229
|
-
}, 2000);
|
|
230
|
-
}).catch(err => {
|
|
231
|
-
console.error('[mbkauthe] Failed to copy: ', err);
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
270
|
document.addEventListener('DOMContentLoaded', () => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
wrapper.
|
|
239
|
-
|
|
271
|
+
document.querySelectorAll('.details-wrapper').forEach(wrapper => {
|
|
272
|
+
const toggle = wrapper.querySelector('.details-toggle');
|
|
273
|
+
const details = wrapper.querySelector('.error-details-wrapper');
|
|
274
|
+
const icon = wrapper.querySelector('.chev');
|
|
275
|
+
const copyBtn = wrapper.querySelector('.copy-btn');
|
|
276
|
+
const feedback = wrapper.querySelector('.copy-feedback');
|
|
277
|
+
|
|
278
|
+
if (!toggle || !details) return;
|
|
279
|
+
|
|
280
|
+
// initialize hidden state and maxHeight for animation
|
|
281
|
+
details.style.maxHeight = details.hidden ? '0px' : details.scrollHeight + 'px';
|
|
282
|
+
|
|
283
|
+
toggle.addEventListener('click', (e) => {
|
|
284
|
+
e.stopPropagation();
|
|
285
|
+
const isOpen = wrapper.classList.toggle('active');
|
|
286
|
+
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
287
|
+
details.hidden = !isOpen;
|
|
288
|
+
|
|
289
|
+
if (isOpen) {
|
|
290
|
+
details.style.maxHeight = details.scrollHeight + 'px';
|
|
291
|
+
if (icon) icon.style.transform = 'rotate(180deg)';
|
|
292
|
+
} else {
|
|
293
|
+
details.style.maxHeight = '0px';
|
|
294
|
+
if (icon) icon.style.transform = 'rotate(0deg)';
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (copyBtn) {
|
|
299
|
+
copyBtn.addEventListener('click', async (e) => {
|
|
300
|
+
e.stopPropagation();
|
|
301
|
+
const textarea = wrapper.querySelector('.error-details');
|
|
302
|
+
try {
|
|
303
|
+
await navigator.clipboard.writeText(textarea.value);
|
|
304
|
+
if (feedback) {
|
|
305
|
+
feedback.style.display = 'inline';
|
|
306
|
+
setTimeout(() => { feedback.style.display = 'none'; }, 2000);
|
|
307
|
+
} else {
|
|
308
|
+
const orig = copyBtn.innerHTML;
|
|
309
|
+
copyBtn.innerHTML = '<i class="fas fa-check" aria-hidden="true"></i> Copied';
|
|
310
|
+
setTimeout(() => { copyBtn.innerHTML = orig; }, 2000);
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error('[mbkauthe] Failed to copy: ', err);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// close the details when user clicks outside
|
|
319
|
+
document.addEventListener('click', (e) => {
|
|
320
|
+
if (!wrapper.contains(e.target) && wrapper.classList.contains('active')) {
|
|
321
|
+
wrapper.classList.remove('active');
|
|
322
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
323
|
+
details.hidden = true;
|
|
324
|
+
if (icon) icon.style.transform = 'rotate(0deg)';
|
|
325
|
+
details.style.maxHeight = '0px';
|
|
326
|
+
}
|
|
240
327
|
});
|
|
241
328
|
});
|
|
242
329
|
});
|
|
@@ -167,8 +167,6 @@
|
|
|
167
167
|
window.location.href = `/mbkauthe/2fa${redirectQuery}`;
|
|
168
168
|
} else {
|
|
169
169
|
loginButtonText.textContent = 'Success! Redirecting...';
|
|
170
|
-
sessionStorage.setItem('sessionId', data.sessionId);
|
|
171
|
-
|
|
172
170
|
if (rememberMe) {
|
|
173
171
|
setCookie('rememberedUsername', username, 30); // 30 days
|
|
174
172
|
} else {
|