adminforth 1.3.53-next.0 → 1.3.54-next.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.
- package/dist/index.js +11 -2
- package/dist/modules/utils.js +96 -0
- package/dist/servers/express.js +1 -0
- package/index.ts +12 -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, {
|
|
@@ -137,6 +137,15 @@ class AdminForth {
|
|
|
137
137
|
}
|
|
138
138
|
getUserByPk(pk) {
|
|
139
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
// if database discovery is not done, throw
|
|
141
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
142
|
+
if (this.statuses.dbDiscover === 'running') {
|
|
143
|
+
throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n' +
|
|
144
|
+
'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
|
|
145
|
+
}
|
|
146
|
+
throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n' +
|
|
147
|
+
'Call admin.discoverDatabases() first and await it before using data API');
|
|
148
|
+
}
|
|
140
149
|
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
|
|
141
150
|
if (!resource) {
|
|
142
151
|
const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
|
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 {
|
|
@@ -197,6 +197,16 @@ class AdminForth implements IAdminForth {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
async getUserByPk(pk: string) {
|
|
200
|
+
// if database discovery is not done, throw
|
|
201
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
202
|
+
if (this.statuses.dbDiscover === 'running') {
|
|
203
|
+
throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n'+
|
|
204
|
+
'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
|
|
205
|
+
}
|
|
206
|
+
throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n'+
|
|
207
|
+
'Call admin.discoverDatabases() first and await it before using data API');
|
|
208
|
+
}
|
|
209
|
+
|
|
200
210
|
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
|
|
201
211
|
if (!resource) {
|
|
202
212
|
const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
|
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.
|