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 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, {
@@ -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') {
@@ -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 {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.52-next.1",
3
+ "version": "1.3.52-next.3",
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
@@ -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 @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">
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
- <li v-for="c in coreStore?.config?.globalInjections?.userMenu || []">
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),
@@ -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.