mbkauthe 4.7.2 → 4.8.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.
@@ -1,103 +1,230 @@
1
- import path from "path";
1
+ import path from "path";
2
2
  import { AsyncLocalStorage } from "async_hooks";
3
3
 
4
4
  const isDev = process.env.env === "dev" && process.env.dbLogs === "true";
5
5
  const requestContext = isDev ? new AsyncLocalStorage() : null;
6
6
 
7
- export const runWithRequestContext = (req, fn) => {
8
- if (!isDev || !requestContext) return fn();
9
- return requestContext.run({ req }, fn);
7
+ const GLOBAL_MAX_QUERY_LOG_ENTRIES = 1000;
8
+ const globalQueryState = {
9
+ totalCount: 0,
10
+ log: [],
10
11
  };
12
+ let autoPoolId = 0;
11
13
 
12
- export const getRequestContext = () => {
13
- if (!isDev || !requestContext) return undefined;
14
- return requestContext.getStore();
15
- };
14
+ const safeValue = (value, depth = 0, seen = new WeakSet()) => {
15
+ if (value == null) return value;
16
+ if (typeof value === "string") {
17
+ return value.length > 300 ? `${value.slice(0, 300)}...` : value;
18
+ }
19
+ if (typeof value === "number" || typeof value === "boolean") return value;
20
+ if (typeof value === "bigint") return value.toString();
21
+ if (value instanceof Date) return value.toISOString();
22
+ if (Buffer.isBuffer(value)) return `[buffer:${value.length}]`;
23
+
24
+ if (Array.isArray(value)) {
25
+ if (depth >= 4) return `[array:${value.length}]`;
26
+ const sample = value.slice(0, 8).map((v) => safeValue(v, depth + 1, seen));
27
+ if (value.length > 8) sample.push(`...(${value.length - 8} more)`);
28
+ return sample;
29
+ }
16
30
 
17
- export const attachDevQueryLogger = (pool) => {
18
- if (!isDev || !pool || pool.__mbkQueryLoggerInstalled) {
19
- return;
31
+ if (typeof value === "object") {
32
+ if (seen.has(value)) return "[circular]";
33
+ seen.add(value);
34
+
35
+ const keys = Object.keys(value);
36
+ if (depth >= 4) {
37
+ const head = keys.slice(0, 5).join(", ");
38
+ return keys.length > 5 ? `[object:${head}, ...]` : `[object:${head}]`;
39
+ }
40
+
41
+ const out = {};
42
+ const entries = Object.entries(value).slice(0, 20);
43
+ for (const [k, v] of entries) {
44
+ out[k] = safeValue(v, depth + 1, seen);
45
+ }
46
+ if (keys.length > 20) {
47
+ out.__truncated = `${keys.length - 20} more keys`;
48
+ }
49
+
50
+ seen.delete(value);
51
+ return out;
20
52
  }
21
53
 
22
- pool.__mbkQueryLoggerInstalled = true;
54
+ return String(value);
55
+ };
23
56
 
24
- // Simple counter for all DB requests made via this pool. This is intentionally lightweight.
25
- let dbQueryCount = 0;
26
- const dbQueryLog = [];
27
- const MAX_QUERY_LOG_ENTRIES = 1000;
57
+ const toWorkspacePath = (filePath) => {
58
+ const rel = path.relative(process.cwd(), filePath) || filePath;
59
+ return rel.replace(/\\/g, "/");
60
+ };
28
61
 
29
- const originalQuery = pool.query.bind(pool);
62
+ const buildCallsite = () => {
63
+ try {
64
+ const stack = new Error().stack || "";
65
+ const lines = stack.split("\n").map((l) => l.trim());
66
+ const frame = lines.find(
67
+ (line) =>
68
+ line.startsWith("at ") &&
69
+ !line.includes("/lib/utils/dbQueryLogger.js") &&
70
+ !line.includes("node:internal") &&
71
+ !line.includes("internal/process")
72
+ );
73
+
74
+ if (!frame) return null;
75
+
76
+ const withFunc = /^at\s+([^\s(]+)\s+\((.+):([0-9]+):([0-9]+)\)$/.exec(frame);
77
+ const noFunc = /^at\s+(.+):([0-9]+):([0-9]+)$/.exec(frame);
30
78
 
31
- const safeValue = (value, depth = 0, seen = new WeakSet()) => {
32
- if (value == null) return value;
33
- if (typeof value === "string") {
34
- return value.length > 300 ? `${value.slice(0, 300)}...` : value;
79
+ if (withFunc) {
80
+ return {
81
+ function: withFunc[1],
82
+ file: toWorkspacePath(withFunc[2]),
83
+ line: Number(withFunc[3]),
84
+ column: Number(withFunc[4]),
85
+ };
35
86
  }
36
- if (typeof value === "number" || typeof value === "boolean") return value;
37
- if (typeof value === "bigint") return value.toString();
38
- if (value instanceof Date) return value.toISOString();
39
- if (Buffer.isBuffer(value)) return `[buffer:${value.length}]`;
40
-
41
- if (Array.isArray(value)) {
42
- if (depth >= 4) return `[array:${value.length}]`;
43
- const sample = value.slice(0, 8).map((v) => safeValue(v, depth + 1, seen));
44
- if (value.length > 8) sample.push(`...(${value.length - 8} more)`);
45
- return sample;
87
+
88
+ if (noFunc) {
89
+ return {
90
+ function: null,
91
+ file: toWorkspacePath(noFunc[1]),
92
+ line: Number(noFunc[2]),
93
+ column: Number(noFunc[3]),
94
+ };
46
95
  }
96
+ } catch {
97
+ return null;
98
+ }
47
99
 
48
- if (typeof value === "object") {
49
- if (seen.has(value)) return "[circular]";
50
- seen.add(value);
100
+ return null;
101
+ };
51
102
 
52
- const keys = Object.keys(value);
53
- if (depth >= 4) {
54
- const head = keys.slice(0, 5).join(", ");
55
- return keys.length > 5 ? `[object:${head}, ...]` : `[object:${head}]`;
56
- }
103
+ const buildRequestContext = () => {
104
+ const store = getRequestContext();
105
+ const req = store?.req;
106
+ if (!req) return null;
107
+
108
+ const user = req.session?.user || null;
109
+ return {
110
+ method: req.method,
111
+ url: req.originalUrl || req.url,
112
+ ip: req.ip,
113
+ userId: user?.id || null,
114
+ username: user?.username || null,
115
+ };
116
+ };
57
117
 
58
- const out = {};
59
- const entries = Object.entries(value).slice(0, 20);
60
- for (const [k, v] of entries) {
61
- out[k] = safeValue(v, depth + 1, seen);
62
- }
63
- if (keys.length > 20) {
64
- out.__truncated = `${keys.length - 20} more keys`;
65
- }
118
+ const buildReturnValue = (result) => {
119
+ if (!result || typeof result !== "object") return undefined;
66
120
 
67
- seen.delete(value);
68
- return out;
121
+ const returnValue = {
122
+ command: result.command || undefined,
123
+ rowCount: typeof result.rowCount === "number" ? result.rowCount : undefined,
124
+ };
125
+
126
+ if (Array.isArray(result.rows)) {
127
+ const previewSize = 3;
128
+ returnValue.returnedRows = result.rows.length;
129
+ returnValue.rowsPreview = result.rows.slice(0, previewSize).map((row) => safeValue(row));
130
+ if (result.rows.length > previewSize) {
131
+ returnValue.rowsTruncated = true;
69
132
  }
133
+ }
70
134
 
71
- return String(value);
72
- };
135
+ return returnValue;
136
+ };
73
137
 
74
- const buildReturnValue = (result) => {
75
- if (!result || typeof result !== "object") return undefined;
138
+ const recordGlobalLog = (entry) => {
139
+ globalQueryState.totalCount += 1;
140
+ globalQueryState.log.push(entry);
141
+ if (globalQueryState.log.length > GLOBAL_MAX_QUERY_LOG_ENTRIES) {
142
+ globalQueryState.log.shift();
143
+ }
144
+ };
76
145
 
77
- const returnValue = {
78
- command: result.command || undefined,
79
- rowCount: typeof result.rowCount === "number" ? result.rowCount : undefined,
80
- };
146
+ export const getQueryCount = () => globalQueryState.totalCount;
147
+ export const getQueryLog = (options = {}) => {
148
+ const { limit } = options;
149
+ if (typeof limit === "number") {
150
+ return globalQueryState.log.slice(-limit);
151
+ }
152
+ return [...globalQueryState.log];
153
+ };
81
154
 
82
- if (Array.isArray(result.rows)) {
83
- const previewSize = 3;
84
- returnValue.returnedRows = result.rows.length;
85
- returnValue.rowsPreview = result.rows.slice(0, previewSize).map((row) => safeValue(row));
86
- if (result.rows.length > previewSize) {
87
- returnValue.rowsTruncated = true;
88
- }
155
+ export const resetQueryCount = () => {
156
+ globalQueryState.totalCount = 0;
157
+ };
158
+
159
+ export const resetQueryLog = () => {
160
+ globalQueryState.log.length = 0;
161
+ };
162
+
163
+ export const runWithRequestContext = (req, fn) => {
164
+ if (!isDev || !requestContext) return fn();
165
+ return requestContext.run({ req }, fn);
166
+ };
167
+
168
+ export const getRequestContext = () => {
169
+ if (!isDev || !requestContext) return undefined;
170
+ return requestContext.getStore();
171
+ };
172
+
173
+ const resolveLoggerPool = (item) => {
174
+ if (item && typeof item === 'object') {
175
+ if (item.pool && item.pool.query) {
176
+ return { pool: item.pool, name: item.name || item.pool.__mbkQueryLoggerName || item.pool.name };
177
+ }
178
+ if (item.query) {
179
+ // Don't force 'default' here; let getPoolName assign a non-default auto name.
180
+ return { pool: item, name: item.__mbkQueryLoggerName || item.name || item.options?.application_name || null };
181
+ }
182
+ }
183
+ return null;
184
+ };
185
+
186
+ const getPoolName = (pool, fallbackName) => {
187
+ if (fallbackName) return fallbackName;
188
+ if (pool.__mbkQueryLoggerName) return pool.__mbkQueryLoggerName;
189
+ if (pool.name) return pool.name;
190
+ if (pool.options && pool.options.application_name) return pool.options.application_name;
191
+ // never return "default"; always provide an actual pool name
192
+ autoPoolId += 1;
193
+ return `pool-${autoPoolId}`;
194
+ };
195
+
196
+ const attachSinglePool = (pool, poolName = null) => {
197
+ if (!pool) return;
198
+
199
+ if (pool.__mbkQueryLoggerInstalled) {
200
+ // Allow late explicit naming to override a previous auto name.
201
+ if (poolName && pool.__mbkQueryLoggerName !== poolName) {
202
+ pool.__mbkQueryLoggerName = poolName;
89
203
  }
204
+ return;
205
+ }
206
+
207
+ pool.__mbkQueryLoggerInstalled = true;
208
+ pool.__mbkQueryLoggerName = getPoolName(pool, poolName);
209
+
210
+ let dbQueryCount = 0;
211
+ const dbQueryLog = [];
212
+ const originalQuery = pool.query.bind(pool);
90
213
 
91
- return returnValue;
214
+ const recordPoolLog = (entry) => {
215
+ dbQueryCount += 1;
216
+ dbQueryLog.push(entry);
217
+ if (dbQueryLog.length > GLOBAL_MAX_QUERY_LOG_ENTRIES) {
218
+ dbQueryLog.shift();
219
+ }
220
+ recordGlobalLog(entry);
92
221
  };
93
222
 
94
223
  pool.query = (...args) => {
95
- dbQueryCount++;
96
-
97
- // `pg` supports (text, values, callback) or (config, callback).
98
224
  let queryText = "";
99
225
  let queryName = "";
100
226
  let queryValues;
227
+
101
228
  try {
102
229
  if (typeof args[0] === "string") {
103
230
  queryText = args[0];
@@ -116,66 +243,6 @@ export const attachDevQueryLogger = (pool) => {
116
243
  }
117
244
 
118
245
  const startTime = process.hrtime.bigint();
119
- const toWorkspacePath = (filePath) => {
120
- const rel = path.relative(process.cwd(), filePath) || filePath;
121
- return rel.replace(/\\/g, "/");
122
- };
123
-
124
- const buildCallsite = () => {
125
- try {
126
- const stack = new Error().stack || "";
127
- const lines = stack.split("\n").map((l) => l.trim());
128
- const frame = lines.find(
129
- (line) =>
130
- line.startsWith("at ") &&
131
- !line.includes("/lib/utils/dbQueryLogger.js") &&
132
- !line.includes("node:internal") &&
133
- !line.includes("internal/process")
134
- );
135
-
136
- if (!frame) return null;
137
-
138
- const withFunc = /^at\s+([^\s(]+)\s+\((.+):([0-9]+):([0-9]+)\)$/.exec(frame);
139
- const noFunc = /^at\s+(.+):([0-9]+):([0-9]+)$/.exec(frame);
140
-
141
- if (withFunc) {
142
- return {
143
- function: withFunc[1],
144
- file: toWorkspacePath(withFunc[2]),
145
- line: Number(withFunc[3]),
146
- column: Number(withFunc[4]),
147
- };
148
- }
149
-
150
- if (noFunc) {
151
- return {
152
- function: null,
153
- file: toWorkspacePath(noFunc[1]),
154
- line: Number(noFunc[2]),
155
- column: Number(noFunc[3]),
156
- };
157
- }
158
- } catch {
159
- return null;
160
- }
161
- return null;
162
- };
163
-
164
- const buildRequestContext = () => {
165
- const store = getRequestContext();
166
- const req = store?.req;
167
- if (!req) return null;
168
-
169
- const user = req.session?.user || null;
170
- return {
171
- method: req.method,
172
- url: req.originalUrl || req.url,
173
- ip: req.ip,
174
- userId: user?.id || null,
175
- username: user?.username || null,
176
- };
177
- };
178
-
179
246
  const callsiteSnapshot = buildCallsite();
180
247
 
181
248
  const recordLog = (success, error, result) => {
@@ -183,7 +250,7 @@ export const attachDevQueryLogger = (pool) => {
183
250
  const request = buildRequestContext();
184
251
  const returnValue = buildReturnValue(result);
185
252
 
186
- dbQueryLog.push({
253
+ const entry = {
187
254
  time: new Date().toISOString(),
188
255
  query: queryText,
189
256
  name: queryName || undefined,
@@ -194,16 +261,15 @@ export const attachDevQueryLogger = (pool) => {
194
261
  returnValue,
195
262
  request,
196
263
  pool: {
264
+ name: pool.__mbkQueryLoggerName,
197
265
  total: pool.totalCount,
198
266
  idle: pool.idleCount,
199
267
  waiting: pool.waitingCount,
200
268
  },
201
269
  callsite: callsiteSnapshot,
202
- });
270
+ };
203
271
 
204
- if (dbQueryLog.length > MAX_QUERY_LOG_ENTRIES) {
205
- dbQueryLog.shift();
206
- }
272
+ recordPoolLog(entry);
207
273
  };
208
274
 
209
275
  try {
@@ -245,3 +311,14 @@ export const attachDevQueryLogger = (pool) => {
245
311
  dbQueryLog.length = 0;
246
312
  };
247
313
  };
314
+
315
+ export const attachDevQueryLogger = (poolOrPools) => {
316
+ if (!isDev || !poolOrPools) return;
317
+
318
+ const inputs = Array.isArray(poolOrPools) ? poolOrPools : [poolOrPools];
319
+ for (const item of inputs) {
320
+ const resolved = resolveLoggerPool(item);
321
+ if (!resolved) continue;
322
+ attachSinglePool(resolved.pool, resolved.name);
323
+ }
324
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "4.7.2",
3
+ "version": "4.8.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/test.spec.js CHANGED
@@ -107,10 +107,22 @@ describe('mbkauthe Routes', () => {
107
107
  const response = await request(BASE_URL).get('/mbkauthe/test');
108
108
  expect([200, 302, 401, 403, 429]).toContain(response.status);
109
109
  });
110
+
111
+ test('GET /mbkauthe/test with curl UA returns JSON 401', async () => {
112
+ const response = await request(BASE_URL)
113
+ .get('/mbkauthe/test')
114
+ .set('User-Agent', 'curl/8.0.1')
115
+ .set('Accept', '*/*');
116
+
117
+ expect(response.status).toBe(401);
118
+ expect(response.headers['content-type']).toContain('application/json');
119
+ expect(response.body).toHaveProperty('success', false);
120
+ expect(response.body).toHaveProperty('errorCode');
121
+ });
110
122
  });
111
123
 
112
124
  describe('OAuth Routes', () => {
113
- test('GET /mbkauthe/api/github/login handles GitHub OAuth', async () => {
125
+ test('GET /mbkauthe/api/github/login handles GitHub App flow', async () => {
114
126
  const response = await request(BASE_URL)
115
127
  .get('/mbkauthe/api/github/login')
116
128
  .redirects(0);
@@ -123,7 +135,7 @@ describe('mbkauthe Routes', () => {
123
135
  }
124
136
  });
125
137
 
126
- test('GET /mbkauthe/api/github/login/callback handles callback', async () => {
138
+ test('GET /mbkauthe/api/github/login/callback handles GitHub App callback', async () => {
127
139
  const response = await request(BASE_URL).get('/mbkauthe/api/github/login/callback');
128
140
  expect([200, 302, 400, 401, 403, 429]).toContain(response.status);
129
141
  });
@@ -183,6 +183,16 @@
183
183
  padding: 0.1rem 0.45rem;
184
184
  }
185
185
 
186
+ .log-pool {
187
+ font-size: 0.75rem;
188
+ color: var(--text-light);
189
+ border: 1px solid var(--muted-border);
190
+ border-radius: 999px;
191
+ padding: 0.1rem 0.45rem;
192
+ background: color-mix(in srgb, var(--accent) 12%, transparent 88%);
193
+ margin: 0 0.25rem;
194
+ }
195
+
186
196
  .log-time {
187
197
  color: var(--text-light);
188
198
  font-size: 0.8rem;
@@ -402,6 +412,10 @@
402
412
  <input id="limit" name="limit" type="number" min="1" max="500" value="{{queryLimit}}" {{#unless isDev}}disabled{{/unless}} />
403
413
  <button type="submit" {{#unless isDev}}disabled{{/unless}}>Update</button>
404
414
  </form>
415
+ <label for="pool-filter">Pool</label>
416
+ <select id="pool-filter" {{#unless isDev}}disabled{{/unless}}>
417
+ <option value="">All Pools</option>
418
+ </select>
405
419
  <button id="reset-btn" type="button" {{#unless isDev}}disabled{{/unless}}>Reset</button>
406
420
  <button id="copy-btn" type="button" {{#unless isDev}}disabled{{/unless}}>Copy Logs</button>
407
421
  </div>
@@ -420,6 +434,7 @@
420
434
 
421
435
  <script>
422
436
  const limitInput = document.getElementById('limit');
437
+ const poolFilterEl = document.getElementById('pool-filter');
423
438
  const queryCountEl = document.getElementById('query-count');
424
439
  const queryLimitEl = document.getElementById('query-limit');
425
440
  const queryLogEl = document.getElementById('query-log');
@@ -428,6 +443,7 @@
428
443
  const resetNoticeEl = document.getElementById('reset-notice');
429
444
  const isDev = {{#if isDev}}true{{else}}false{{/if}};
430
445
  let latestQueryLog = [];
446
+ let currentPoolFilter = '';
431
447
 
432
448
  const showDisabledState = (message = 'DB logs are disabled.') => {
433
449
  queryCountEl.textContent = '-';
@@ -443,6 +459,35 @@
443
459
  .replace(/\|/g, '\\|')
444
460
  .replace(/\r?\n/g, '<br>');
445
461
 
462
+ const getUniquePoolNames = (log) => {
463
+ if (!Array.isArray(log)) return [];
464
+ const names = new Set();
465
+ for (const entry of log) {
466
+ const rawPoolName = entry?.pool?.name || 'default';
467
+ if (rawPoolName && rawPoolName !== 'default') {
468
+ names.add(rawPoolName);
469
+ }
470
+ }
471
+ return [...names].sort();
472
+ };
473
+
474
+ const renderPoolFilterOptions = (names) => {
475
+ if (!poolFilterEl) return;
476
+ const selected = poolFilterEl.value || '';
477
+ poolFilterEl.innerHTML = '<option value="">All Pools</option>' +
478
+ names.map((name) => `\n <option value="${escapeHtml(name)}"${name === selected ? ' selected' : ''}>${escapeHtml(name)}</option>`).join('');
479
+ };
480
+
481
+ const getFilteredLog = (log) => {
482
+ if (!Array.isArray(log) || !currentPoolFilter) return log;
483
+ return log.filter((entry) => (entry?.pool?.name || 'default') === currentPoolFilter);
484
+ };
485
+
486
+ poolFilterEl?.addEventListener('change', () => {
487
+ currentPoolFilter = poolFilterEl.value || '';
488
+ buildTable(getFilteredLog(latestQueryLog));
489
+ });
490
+
446
491
  const formatParamValues = (values, pretty = false) => {
447
492
  if (!Array.isArray(values) || values.length === 0) return '';
448
493
 
@@ -495,8 +540,10 @@
495
540
  `\nuser: ${entry.request.username || entry.request.userId || 'anonymous'}` +
496
541
  `\nip: ${entry.request.ip || ''}`
497
542
  : '';
498
- const poolText = entry.pool
499
- ? `total:${entry.pool.total} idle:${entry.pool.idle} waiting:${entry.pool.waiting}`
543
+ const rawPoolName = entry.pool?.name || 'default';
544
+ const poolName = rawPoolName === 'default' ? '' : rawPoolName;
545
+ const poolText = entry.pool && poolName
546
+ ? `name:${poolName} total:${entry.pool.total} idle:${entry.pool.idle} waiting:${entry.pool.waiting}`
500
547
  : '';
501
548
  const callsiteText = entry.callsite
502
549
  ? `${entry.callsite.function || '(anonymous)'}\n${entry.callsite.file}:${entry.callsite.line}:${entry.callsite.column}`
@@ -545,8 +592,10 @@
545
592
  `\nuser: ${entry.request.username || entry.request.userId || 'anonymous'}` +
546
593
  `\nip: ${entry.request.ip || ''}`
547
594
  : '';
548
- const poolText = entry.pool
549
- ? `total:${entry.pool.total} idle:${entry.pool.idle} waiting:${entry.pool.waiting}`
595
+ const rawPoolName = entry.pool?.name || 'default';
596
+ const poolName = rawPoolName === 'default' ? '' : rawPoolName;
597
+ const poolText = entry.pool && poolName
598
+ ? `name:${poolName} total:${entry.pool.total} idle:${entry.pool.idle} waiting:${entry.pool.waiting}`
550
599
  : '';
551
600
 
552
601
  const detailBlocks = [
@@ -563,6 +612,7 @@
563
612
  <div class="log-mainline">
564
613
  <span class="log-index">#${idx + 1}</span>
565
614
  ${statusBadge}
615
+ ${poolName ? `<span class="log-pool">${escapeHtml(poolName)}</span>` : ''}
566
616
  <span class="log-time">${escapeHtml(entry.time)}</span>
567
617
  </div>
568
618
  <button class="copy-row-btn" data-copy='${escapeHtml(JSON.stringify(entry))}'>Copy</button>
@@ -625,7 +675,8 @@
625
675
 
626
676
  queryCountEl.textContent = data.queryCount ?? '-';
627
677
  latestQueryLog = Array.isArray(data.queryLog) ? data.queryLog : [];
628
- buildTable(data.queryLog);
678
+ renderPoolFilterOptions(getUniquePoolNames(latestQueryLog));
679
+ buildTable(getFilteredLog(latestQueryLog));
629
680
  };
630
681
 
631
682
  resetBtn.addEventListener('click', () => {
@@ -396,12 +396,12 @@
396
396
 
397
397
  {{#if githubLoginEnabled }}
398
398
 
399
- // GitHub login: Navigate directly to GitHub OAuth flow
399
+ // GitHub login: Navigate directly to GitHub App flow
400
400
  async function startGithubLogin() {
401
401
  const urlParams = new URLSearchParams(window.location.search);
402
402
  const redirect = urlParams.get('redirect') || '{{customURL}}';
403
403
 
404
- // Navigate directly to the backend GET endpoint with redirect query
404
+ // Navigate directly to the backend GitHub App endpoint with redirect query
405
405
  window.location.href = `/mbkauthe/api/github/login?redirect=${encodeURIComponent(redirect)}`;
406
406
  }
407
407