adminforth 1.3.53-next.0 → 1.3.53
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/dist/index.js +2 -2
- package/dist/modules/utils.js +96 -0
- package/dist/servers/express.js +1 -0
- package/index.ts +2 -2
- package/modules/utils.ts +115 -0
- package/package.json +1 -1
- package/servers/express.ts +1 -0
- package/spa/src/App.vue +17 -3
- package/spa/src/composables/useStores.ts +1 -0
- package/types/FrontendAPI.ts +5 -0
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import PostgresConnector from './dataConnectors/postgres.js';
|
|
|
19
19
|
import SQLiteConnector from './dataConnectors/sqlite.js';
|
|
20
20
|
import CodeInjector from './modules/codeInjector.js';
|
|
21
21
|
import ExpressServer from './servers/express.js';
|
|
22
|
-
import { ADMINFORTH_VERSION, listify, suggestIfTypo } from './modules/utils.js';
|
|
22
|
+
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, getClinetIp } from './modules/utils.js';
|
|
23
23
|
import { AdminForthFilterOperators, AdminForthDataTypes, } from './types/AdminForthConfig.js';
|
|
24
24
|
import AdminForthPlugin from './basePlugin.js';
|
|
25
25
|
import ConfigValidator from './modules/configValidator.js';
|
|
@@ -30,7 +30,7 @@ import OperationalResource from './modules/operationalResource.js';
|
|
|
30
30
|
export * from './types/AdminForthConfig.js';
|
|
31
31
|
export { interpretResource };
|
|
32
32
|
export { AdminForthPlugin };
|
|
33
|
-
export { suggestIfTypo };
|
|
33
|
+
export { suggestIfTypo, RateLimiter, getClinetIp };
|
|
34
34
|
class AdminForth {
|
|
35
35
|
constructor(config) {
|
|
36
36
|
_AdminForth_defaultConfig.set(this, {
|
package/dist/modules/utils.js
CHANGED
|
@@ -323,3 +323,99 @@ export function suggestIfTypo(names, name) {
|
|
|
323
323
|
return result[0].item;
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
|
+
export function getClinetIp(headers) {
|
|
327
|
+
console.log('headers', headers);
|
|
328
|
+
return headers['CF-Connecting-IP'] ||
|
|
329
|
+
headers['x-forwarded-for'] ||
|
|
330
|
+
headers['x-real-ip'] ||
|
|
331
|
+
headers['x-client-ip'] ||
|
|
332
|
+
headers['x-proxy-user'] ||
|
|
333
|
+
headers['x-cluster-client-ip'] ||
|
|
334
|
+
headers['forwarded'] ||
|
|
335
|
+
headers['remote-addr'] ||
|
|
336
|
+
headers['client-ip'] ||
|
|
337
|
+
headers['x-client-ip'] ||
|
|
338
|
+
headers['x-real-ip'] || 'unknown';
|
|
339
|
+
}
|
|
340
|
+
export class RateLimiter {
|
|
341
|
+
/**
|
|
342
|
+
* Very dirty version of ratelimiter for demo purposes (should not be considered as production ready)
|
|
343
|
+
* Will be used as RateLimiter.checkRateLimit('key', '5/24h', clientIp)
|
|
344
|
+
* Stores counter in this class, in RAM, resets limits on app restart.
|
|
345
|
+
* Also it creates setTimeout for every call, so is not optimal for high load.
|
|
346
|
+
* @param key - key to store rate limit for
|
|
347
|
+
* @param limit - limit in format '5/24h' - 5 requests per 24 hours
|
|
348
|
+
* @param clientIp
|
|
349
|
+
*/
|
|
350
|
+
static checkRateLimit(key, limit, clientIp) {
|
|
351
|
+
if (!limit) {
|
|
352
|
+
throw new Error('Rate limit is not set');
|
|
353
|
+
}
|
|
354
|
+
if (!key) {
|
|
355
|
+
throw new Error('Rate limit key is not set');
|
|
356
|
+
}
|
|
357
|
+
if (!clientIp) {
|
|
358
|
+
throw new Error('Client IP is not set');
|
|
359
|
+
}
|
|
360
|
+
if (!limit.includes('/')) {
|
|
361
|
+
throw new Error('Rate limit should be in format count/period, like 5/24h');
|
|
362
|
+
}
|
|
363
|
+
// parse limit
|
|
364
|
+
const [count, period] = limit.split('/');
|
|
365
|
+
const [preiodAmount, periodType] = /(\d+)(\w+)/.exec(period).slice(1);
|
|
366
|
+
const preiodAmountNumber = parseInt(preiodAmount);
|
|
367
|
+
// get current time
|
|
368
|
+
const whenClear = new Date();
|
|
369
|
+
if (periodType === 'h') {
|
|
370
|
+
whenClear.setHours(whenClear.getHours() + preiodAmountNumber);
|
|
371
|
+
}
|
|
372
|
+
else if (periodType === 'd') {
|
|
373
|
+
whenClear.setDate(whenClear.getDate() + preiodAmountNumber);
|
|
374
|
+
}
|
|
375
|
+
else if (periodType === 'm') {
|
|
376
|
+
whenClear.setMinutes(whenClear.getMinutes() + preiodAmountNumber);
|
|
377
|
+
}
|
|
378
|
+
else if (periodType === 'y') {
|
|
379
|
+
whenClear.setFullYear(whenClear.getFullYear() + preiodAmountNumber);
|
|
380
|
+
}
|
|
381
|
+
else if (periodType === 's') {
|
|
382
|
+
whenClear.setSeconds(whenClear.getSeconds() + preiodAmountNumber);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
throw new Error(`Unsupported period type for rate limiting: ${periodType}`);
|
|
386
|
+
}
|
|
387
|
+
// get current counter
|
|
388
|
+
const counter = this.counterData[key] && this.counterData[key][clientIp] || 0;
|
|
389
|
+
if (counter >= count) {
|
|
390
|
+
return { error: true };
|
|
391
|
+
}
|
|
392
|
+
RateLimiter.incrementCounter(key, clientIp);
|
|
393
|
+
setTimeout(() => {
|
|
394
|
+
RateLimiter.decrementCounter(key, clientIp);
|
|
395
|
+
}, whenClear.getTime() - Date.now());
|
|
396
|
+
return { error: false };
|
|
397
|
+
}
|
|
398
|
+
static incrementCounter(key, ip) {
|
|
399
|
+
if (!RateLimiter.counterData[key]) {
|
|
400
|
+
RateLimiter.counterData[key] = {};
|
|
401
|
+
}
|
|
402
|
+
if (!RateLimiter.counterData[key][ip]) {
|
|
403
|
+
RateLimiter.counterData[key][ip] = 0;
|
|
404
|
+
}
|
|
405
|
+
RateLimiter.counterData[key][ip]++;
|
|
406
|
+
console.log('🔄️🔄️🔄️🔄️🔄️🔄️ incremented', key, ip, this.counterData[key][ip]);
|
|
407
|
+
}
|
|
408
|
+
static decrementCounter(key, ip) {
|
|
409
|
+
if (!RateLimiter.counterData[key]) {
|
|
410
|
+
RateLimiter.counterData[key] = {};
|
|
411
|
+
}
|
|
412
|
+
if (!RateLimiter.counterData[key][ip]) {
|
|
413
|
+
RateLimiter.counterData[key][ip] = 0;
|
|
414
|
+
}
|
|
415
|
+
if (RateLimiter.counterData[key][ip] > 0) {
|
|
416
|
+
RateLimiter.counterData[key][ip]--;
|
|
417
|
+
}
|
|
418
|
+
console.log('🔄️🔄️🔄️🔄️🔄️🔄️ decremented', key, ip, this.counterData[key][ip]);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
RateLimiter.counterData = {};
|
package/dist/servers/express.js
CHANGED
package/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import PostgresConnector from './dataConnectors/postgres.js';
|
|
|
5
5
|
import SQLiteConnector from './dataConnectors/sqlite.js';
|
|
6
6
|
import CodeInjector from './modules/codeInjector.js';
|
|
7
7
|
import ExpressServer from './servers/express.js';
|
|
8
|
-
import { ADMINFORTH_VERSION, listify, suggestIfTypo } from './modules/utils.js';
|
|
8
|
+
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, getClinetIp } from './modules/utils.js';
|
|
9
9
|
import {
|
|
10
10
|
type AdminForthConfig,
|
|
11
11
|
type IAdminForth,
|
|
@@ -29,7 +29,7 @@ import OperationalResource from './modules/operationalResource.js';
|
|
|
29
29
|
export * from './types/AdminForthConfig.js';
|
|
30
30
|
export { interpretResource };
|
|
31
31
|
export { AdminForthPlugin };
|
|
32
|
-
export { suggestIfTypo };
|
|
32
|
+
export { suggestIfTypo, RateLimiter, getClinetIp };
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class AdminForth implements IAdminForth {
|
package/modules/utils.ts
CHANGED
|
@@ -354,4 +354,119 @@ export function suggestIfTypo(names: string[], name: string): string {
|
|
|
354
354
|
if (result.length > 0) {
|
|
355
355
|
return result[0].item;
|
|
356
356
|
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
export function getClinetIp(headers: object) {
|
|
361
|
+
console.log('headers', headers);
|
|
362
|
+
return headers['CF-Connecting-IP'] ||
|
|
363
|
+
headers['x-forwarded-for'] ||
|
|
364
|
+
headers['x-real-ip'] ||
|
|
365
|
+
headers['x-client-ip'] ||
|
|
366
|
+
headers['x-proxy-user'] ||
|
|
367
|
+
headers['x-cluster-client-ip'] ||
|
|
368
|
+
headers['forwarded'] ||
|
|
369
|
+
headers['remote-addr'] ||
|
|
370
|
+
headers['client-ip'] ||
|
|
371
|
+
headers['x-client-ip'] ||
|
|
372
|
+
headers['x-real-ip'] || 'unknown';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
export class RateLimiter {
|
|
377
|
+
static counterData = {};
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Very dirty version of ratelimiter for demo purposes (should not be considered as production ready)
|
|
384
|
+
* Will be used as RateLimiter.checkRateLimit('key', '5/24h', clientIp)
|
|
385
|
+
* Stores counter in this class, in RAM, resets limits on app restart.
|
|
386
|
+
* Also it creates setTimeout for every call, so is not optimal for high load.
|
|
387
|
+
* @param key - key to store rate limit for
|
|
388
|
+
* @param limit - limit in format '5/24h' - 5 requests per 24 hours
|
|
389
|
+
* @param clientIp
|
|
390
|
+
*/
|
|
391
|
+
static checkRateLimit(key: string, limit: string, clientIp: string) {
|
|
392
|
+
|
|
393
|
+
if (!limit) {
|
|
394
|
+
throw new Error('Rate limit is not set');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!key) {
|
|
398
|
+
throw new Error('Rate limit key is not set');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!clientIp) {
|
|
402
|
+
throw new Error('Client IP is not set');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!limit.includes('/')) {
|
|
406
|
+
throw new Error('Rate limit should be in format count/period, like 5/24h');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// parse limit
|
|
410
|
+
const [count, period] = limit.split('/');
|
|
411
|
+
const [preiodAmount, periodType] = /(\d+)(\w+)/.exec(period).slice(1);
|
|
412
|
+
const preiodAmountNumber = parseInt(preiodAmount);
|
|
413
|
+
|
|
414
|
+
// get current time
|
|
415
|
+
const whenClear = new Date();
|
|
416
|
+
if (periodType === 'h') {
|
|
417
|
+
whenClear.setHours(whenClear.getHours() + preiodAmountNumber);
|
|
418
|
+
} else if (periodType === 'd') {
|
|
419
|
+
whenClear.setDate(whenClear.getDate() + preiodAmountNumber);
|
|
420
|
+
} else if (periodType === 'm') {
|
|
421
|
+
whenClear.setMinutes(whenClear.getMinutes() + preiodAmountNumber);
|
|
422
|
+
} else if (periodType === 'y') {
|
|
423
|
+
whenClear.setFullYear(whenClear.getFullYear() + preiodAmountNumber);
|
|
424
|
+
} else if (periodType === 's') {
|
|
425
|
+
whenClear.setSeconds(whenClear.getSeconds() + preiodAmountNumber);
|
|
426
|
+
} else {
|
|
427
|
+
throw new Error(`Unsupported period type for rate limiting: ${periodType}`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
// get current counter
|
|
432
|
+
const counter = this.counterData[key] && this.counterData[key][clientIp] || 0;
|
|
433
|
+
if (counter >= count) {
|
|
434
|
+
return { error: true };
|
|
435
|
+
}
|
|
436
|
+
RateLimiter.incrementCounter(key, clientIp);
|
|
437
|
+
setTimeout(() => {
|
|
438
|
+
RateLimiter.decrementCounter(key, clientIp);
|
|
439
|
+
}, whenClear.getTime() - Date.now());
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
return { error: false };
|
|
443
|
+
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
static incrementCounter(key: string, ip: string) {
|
|
447
|
+
if (!RateLimiter.counterData[key]) {
|
|
448
|
+
RateLimiter.counterData[key] = {};
|
|
449
|
+
}
|
|
450
|
+
if (!RateLimiter.counterData[key][ip]) {
|
|
451
|
+
RateLimiter.counterData[key][ip] = 0;
|
|
452
|
+
}
|
|
453
|
+
RateLimiter.counterData[key][ip]++;
|
|
454
|
+
console.log('🔄️🔄️🔄️🔄️🔄️🔄️ incremented', key, ip, this.counterData[key][ip]);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
static decrementCounter(key: string, ip: string) {
|
|
458
|
+
if (!RateLimiter.counterData[key]) {
|
|
459
|
+
RateLimiter.counterData[key] = {};
|
|
460
|
+
}
|
|
461
|
+
if (!RateLimiter.counterData[key][ip]) {
|
|
462
|
+
RateLimiter.counterData[key][ip] = 0;
|
|
463
|
+
}
|
|
464
|
+
if (RateLimiter.counterData[key][ip] > 0) {
|
|
465
|
+
RateLimiter.counterData[key][ip]--;
|
|
466
|
+
}
|
|
467
|
+
console.log('🔄️🔄️🔄️🔄️🔄️🔄️ decremented', key, ip, this.counterData[key][ip]);
|
|
468
|
+
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
357
472
|
}
|
package/package.json
CHANGED
package/servers/express.ts
CHANGED
package/spa/src/App.vue
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
<span
|
|
32
|
+
<span
|
|
33
|
+
@click="toggleTheme" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-lightHtml dark:text-darkSidebarTextHover dark:hover:bg-darkHtml dark:hover:text-darkSidebarTextActive" role="menuitem">
|
|
33
34
|
<IconMoonSolid class="w-5 h-5 text-blue-300" v-if="theme !== 'dark'" />
|
|
34
35
|
<IconSunSolid class="w-5 h-5 text-yellow-300" v-else />
|
|
35
36
|
</span>
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
</p>
|
|
53
54
|
</div>
|
|
54
55
|
<ul class="py-1" role="none">
|
|
55
|
-
|
|
56
|
+
|
|
57
|
+
<li v-for="c in coreStore?.config?.globalInjections?.userMenu || []" >
|
|
56
58
|
<component
|
|
57
59
|
:is="getCustomComponent(c)"
|
|
58
60
|
:meta="c.meta"
|
|
@@ -244,7 +246,7 @@
|
|
|
244
246
|
<script setup lang="ts">
|
|
245
247
|
import { computed, onMounted, ref, watch, defineComponent, onBeforeMount } from 'vue';
|
|
246
248
|
import { RouterLink, RouterView } from 'vue-router';
|
|
247
|
-
import { initFlowbite } from 'flowbite'
|
|
249
|
+
import { initFlowbite, Dropdown } from 'flowbite'
|
|
248
250
|
import './index.scss'
|
|
249
251
|
import { useCoreStore } from '@/stores/core';
|
|
250
252
|
import { useUserStore } from '@/stores/user';
|
|
@@ -366,6 +368,14 @@ watch([loggedIn, routerIsReady, loginRedirectCheckIsReady], ([l,r,lr]) => {
|
|
|
366
368
|
if (l && r && lr) {
|
|
367
369
|
setTimeout(() => {
|
|
368
370
|
initFlowbite();
|
|
371
|
+
|
|
372
|
+
const dd = new Dropdown(
|
|
373
|
+
document.querySelector('#dropdown-user') as HTMLElement,
|
|
374
|
+
document.querySelector('[data-dropdown-toggle="dropdown-user"]') as HTMLElement,
|
|
375
|
+
);
|
|
376
|
+
window.adminforth.closeUserMenuDropdown = () => {
|
|
377
|
+
dd.hide();
|
|
378
|
+
}
|
|
369
379
|
});
|
|
370
380
|
}
|
|
371
381
|
})
|
|
@@ -401,4 +411,8 @@ function closeCTA() {
|
|
|
401
411
|
const hash = ctaBadge.value.hash;
|
|
402
412
|
window.localStorage.setItem(`ctaBadge-${hash}`, '1');
|
|
403
413
|
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
404
418
|
</script>
|
|
@@ -47,6 +47,7 @@ export class FrontendAPI {
|
|
|
47
47
|
list: {
|
|
48
48
|
refresh: () => {/* will be redefined in list*/},
|
|
49
49
|
closeThreeDotsDropdown: () => {/* will be redefined in list*/},
|
|
50
|
+
closeUserMenuDropdown: () => {/* will be redefined in list*/},
|
|
50
51
|
setFilter: () => this.setListFilter.bind(this),
|
|
51
52
|
updateFilter: () => this.updateListFilter.bind(this),
|
|
52
53
|
clearFilters: () => this.clearListFilters.bind(this),
|
package/types/FrontendAPI.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface FrontendAPIInterface {
|
|
|
48
48
|
*/
|
|
49
49
|
closeThreeDotsDropdown(): void;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Close the user menu dropdown
|
|
53
|
+
*/
|
|
54
|
+
closeUserMenuDropdown(): void;
|
|
55
|
+
|
|
51
56
|
/**
|
|
52
57
|
* Set a filter in the list
|
|
53
58
|
* Works only when user located on the list page.
|