adminforth 1.3.52-next.1 → 1.3.52-next.3
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/restApi.js +4 -1
- package/dist/modules/utils.js +96 -0
- package/dist/servers/express.js +1 -0
- package/index.ts +2 -2
- package/modules/restApi.ts +6 -2
- package/modules/utils.ts +115 -0
- package/package.json +1 -1
- package/servers/express.ts +1 -0
- package/spa/src/App.vue +17 -9
- 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/restApi.js
CHANGED
|
@@ -160,9 +160,12 @@ export default class AdminForthRestAPI {
|
|
|
160
160
|
const dbUser = adminUser.dbUser;
|
|
161
161
|
username = dbUser[this.adminforth.config.auth.usernameField];
|
|
162
162
|
userFullName = dbUser[this.adminforth.config.auth.userFullNameField];
|
|
163
|
+
const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
164
|
+
const userPk = dbUser[userResource.columns.find((col) => col.primaryKey).name];
|
|
163
165
|
const userData = {
|
|
164
166
|
[this.adminforth.config.auth.usernameField]: username,
|
|
165
|
-
[this.adminforth.config.auth.userFullNameField]: userFullName
|
|
167
|
+
[this.adminforth.config.auth.userFullNameField]: userFullName,
|
|
168
|
+
pk: userPk,
|
|
166
169
|
};
|
|
167
170
|
const checkIsMenuItemVisible = (menuItem) => {
|
|
168
171
|
if (typeof menuItem.visible === 'function') {
|
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/restApi.ts
CHANGED
|
@@ -193,11 +193,15 @@ export default class AdminForthRestAPI {
|
|
|
193
193
|
|
|
194
194
|
const dbUser = adminUser.dbUser;
|
|
195
195
|
username = dbUser[this.adminforth.config.auth.usernameField];
|
|
196
|
-
userFullName =dbUser[this.adminforth.config.auth.userFullNameField];
|
|
196
|
+
userFullName = dbUser[this.adminforth.config.auth.userFullNameField];
|
|
197
|
+
const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
198
|
+
|
|
199
|
+
const userPk = dbUser[userResource.columns.find((col) => col.primaryKey).name];
|
|
197
200
|
|
|
198
201
|
const userData = {
|
|
199
202
|
[this.adminforth.config.auth.usernameField]: username,
|
|
200
|
-
[this.adminforth.config.auth.userFullNameField]: userFullName
|
|
203
|
+
[this.adminforth.config.auth.userFullNameField]: userFullName,
|
|
204
|
+
pk: userPk,
|
|
201
205
|
};
|
|
202
206
|
const checkIsMenuItemVisible = (menuItem) => {
|
|
203
207
|
if (typeof menuItem.visible === 'function') {
|
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
|
@@ -22,8 +22,6 @@
|
|
|
22
22
|
v-for="c in coreStore?.config?.globalInjections?.header || []"
|
|
23
23
|
:is="getCustomComponent(c)"
|
|
24
24
|
:meta="c.meta"
|
|
25
|
-
:record="coreStore.record"
|
|
26
|
-
:resource="coreStore.resource"
|
|
27
25
|
:adminUser="coreStore.adminUser"
|
|
28
26
|
/>
|
|
29
27
|
|
|
@@ -31,7 +29,8 @@
|
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
|
|
34
|
-
<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">
|
|
35
34
|
<IconMoonSolid class="w-5 h-5 text-blue-300" v-if="theme !== 'dark'" />
|
|
36
35
|
<IconSunSolid class="w-5 h-5 text-yellow-300" v-else />
|
|
37
36
|
</span>
|
|
@@ -54,12 +53,11 @@
|
|
|
54
53
|
</p>
|
|
55
54
|
</div>
|
|
56
55
|
<ul class="py-1" role="none">
|
|
57
|
-
|
|
56
|
+
|
|
57
|
+
<li v-for="c in coreStore?.config?.globalInjections?.userMenu || []" >
|
|
58
58
|
<component
|
|
59
59
|
:is="getCustomComponent(c)"
|
|
60
60
|
:meta="c.meta"
|
|
61
|
-
:record="coreStore.record"
|
|
62
|
-
:resource="coreStore.resource"
|
|
63
61
|
:adminUser="coreStore.adminUser"
|
|
64
62
|
/>
|
|
65
63
|
</li>
|
|
@@ -169,8 +167,6 @@
|
|
|
169
167
|
v-for="c in coreStore?.config?.globalInjections?.sidebar || []"
|
|
170
168
|
:is="getCustomComponent(c)"
|
|
171
169
|
:meta="c.meta"
|
|
172
|
-
:record="coreStore.record"
|
|
173
|
-
:resource="coreStore.resource"
|
|
174
170
|
:adminUser="coreStore.adminUser"
|
|
175
171
|
/>
|
|
176
172
|
</div>
|
|
@@ -250,7 +246,7 @@
|
|
|
250
246
|
<script setup lang="ts">
|
|
251
247
|
import { computed, onMounted, ref, watch, defineComponent, onBeforeMount } from 'vue';
|
|
252
248
|
import { RouterLink, RouterView } from 'vue-router';
|
|
253
|
-
import { initFlowbite } from 'flowbite'
|
|
249
|
+
import { initFlowbite, Dropdown } from 'flowbite'
|
|
254
250
|
import './index.scss'
|
|
255
251
|
import { useCoreStore } from '@/stores/core';
|
|
256
252
|
import { useUserStore } from '@/stores/user';
|
|
@@ -372,6 +368,14 @@ watch([loggedIn, routerIsReady, loginRedirectCheckIsReady], ([l,r,lr]) => {
|
|
|
372
368
|
if (l && r && lr) {
|
|
373
369
|
setTimeout(() => {
|
|
374
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
|
+
}
|
|
375
379
|
});
|
|
376
380
|
}
|
|
377
381
|
})
|
|
@@ -407,4 +411,8 @@ function closeCTA() {
|
|
|
407
411
|
const hash = ctaBadge.value.hash;
|
|
408
412
|
window.localStorage.setItem(`ctaBadge-${hash}`, '1');
|
|
409
413
|
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
410
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.
|