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 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, {
@@ -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 = {};
@@ -183,6 +183,7 @@ class ExpressServer {
183
183
  }
184
184
  const query = req.query;
185
185
  const adminUser = req.adminUser;
186
+ // lower request headers
186
187
  const headers = req.headers;
187
188
  const cookies = yield parseExpressCookie(req);
188
189
  const response = {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.53-next.0",
3
+ "version": "1.3.53",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -199,6 +199,7 @@ class ExpressServer implements IExpressHttpServer {
199
199
 
200
200
  const query = req.query;
201
201
  const adminUser = req.adminUser;
202
+ // lower request headers
202
203
  const headers = req.headers;
203
204
  const cookies = await parseExpressCookie(req);
204
205
 
package/spa/src/App.vue CHANGED
@@ -29,7 +29,8 @@
29
29
 
30
30
 
31
31
 
32
- <span @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">
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
- <li v-for="c in coreStore?.config?.globalInjections?.userMenu || []">
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),
@@ -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.