Skip to content
Merged

Next #396

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,55 @@ new AdminForth({

If you hide the logo with `showBrandLogoInSidebar: false`, components injected via `sidebarTop` will take the whole line width.

## Injection order

Most of injections accept an array of components. By defult the order of components is the same as in the array. You can use standard array methods e.g. `push`, `unshift`, `splice` to put item in desired place.

However, if you want to control the order of injections dynamically, which is very handly for plugins, you can use `meta.afOrder` property in the injection instantiation. The higher the number, the earlier the component will be rendered. For example

```ts title="/index.ts"
{
...
customization: {
globalInjections: {
userMenu: [
{
file: '@@/CustomUserMenuItem.vue',
meta: { afOrder: 10 }
},
{
file: '@@/AnotherCustomUserMenuItem.vue',
meta: { afOrder: 20 }
},
{
file: '@@/LastCustomUserMenuItem.vue',
meta: { afOrder: 5 }
},
]
}
}
...
}
```

## Order of components inserted by plugins

For plugins, the plugin developers encouraged to use `meta.afOrder` to control the order of injections and allow to pass it from plugin options.

For example "OAuth2 plugin", when registers a login button component for login page injection, uses `meta.afOrder` and sets it equal to 'YYY' passed in plugin options:

```ts title="/index.ts"
// plugin CODE
YYY.push({
file: '@@/..vue',
meta: {
afOrder: this.pluginOptions.YYY || 0
}
})
```

So you can jsut pass `YYY` option to the plugin to control the order of the injection.

## Custom scripts in head

If you want to inject tags in your html head:
Expand Down
16 changes: 15 additions & 1 deletion adminforth/modules/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,13 @@ export const styles = () => ({
lightUserMenuSettingsButtonDropdownItemText: "alias:lightBreadcrumbsHomepageText",
lightUserMenuSettingsButtonDropdownItemTextHover: "alias:lightBreadcrumbsHomepageTextHover",


lightUserMenuBackground: "#FFFFFF",
lightUserMenuBorder: "#f3f4f6",
lightUserMenuText: "#111827",
lightUserMenuItemBackground: "alias:lightUserMenuBackground",
lightUserMenuItemBackgroundHover: "alias:lightUserMenuBackground",
lightUserMenuItemText: "#000000",
lightUserMenuItemTextHover: "#000000",

// colors for dark theme
darkHtml: "#111827",
Expand Down Expand Up @@ -711,6 +717,14 @@ export const styles = () => ({
darkUserMenuSettingsButtonDropdownItemText: "#FFFFFF",
darkUserMenuSettingsButtonDropdownItemTextHover: "#FFFFFF",

darkUserMenuBackground: "alias:darkSidebar",
darkUserMenuBorder: "alias:darkSidebarDevider",
darkUserMenuText: "#FFFFFF",
darkUserMenuItemBackground: "alias:darkSidebar",
darkUserMenuItemBackgroundHover: "alias:darkSidebarItemHover",
darkUserMenuItemText: "#FFFFFF",
darkUserMenuItemTextHover: "#FFFFFF",

},
boxShadow: {
customLight: "0 4px 8px rgba(0, 0, 0, 0.1)", // Lighter shadow
Expand Down
105 changes: 33 additions & 72 deletions adminforth/modules/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from 'fs';
import Fuse from 'fuse.js';
import crypto from 'crypto';
import AdminForth, { AdminForthConfig } from '../index.js';
import { RateLimiterMemory, RateLimiterAbstract } from "rate-limiter-flexible";
// @ts-ignore-next-line


Expand Down Expand Up @@ -381,90 +382,50 @@ export function md5hash(str:string) {
}

export class RateLimiter {
static counterData = {};

/**
* Very dirty version of ratelimiter for demo purposes (should not be considered as production ready)
* Will be used as RateLimiter.checkRateLimit('key', '5/24h', clientIp)
* Stores counter in this class, in RAM, resets limits on app restart.
* Also it creates setTimeout for every call, so is not optimal for high load.
* @param key - key to store rate limit for
* @param limit - limit in format '5/24h' - 5 requests per 24 hours
* @param clientIp
*/
static checkRateLimit(key: string, limit: string, clientIp: string) {

if (!limit) {
throw new Error('Rate limit is not set');
}
// constructor, accepts string like 10/10m, or 20/10s, or 30/1d

if (!key) {
throw new Error('Rate limit key is not set');
}

if (!clientIp) {
throw new Error('Client IP is not set');
}
rateLimiter: RateLimiterAbstract;

if (!limit.includes('/')) {
throw new Error('Rate limit should be in format count/period, like 5/24h');
}

// parse limit
const [count, period] = limit.split('/');
const [preiodAmount, periodType] = /(\d+)(\w+)/.exec(period).slice(1);
const preiodAmountNumber = parseInt(preiodAmount);

// get current time
const whenClear = new Date();
if (periodType === 'h') {
whenClear.setHours(whenClear.getHours() + preiodAmountNumber);
} else if (periodType === 'd') {
whenClear.setDate(whenClear.getDate() + preiodAmountNumber);
} else if (periodType === 'm') {
whenClear.setMinutes(whenClear.getMinutes() + preiodAmountNumber);
} else if (periodType === 'y') {
whenClear.setFullYear(whenClear.getFullYear() + preiodAmountNumber);
} else if (periodType === 's') {
whenClear.setSeconds(whenClear.getSeconds() + preiodAmountNumber);
} else {
throw new Error(`Unsupported period type for rate limiting: ${periodType}`);
durStringToSeconds(rate: string): number {
if (!rate) {
throw new Error('Rate duration is required');
}


// get current counter
const counter = this.counterData[key] && this.counterData[key][clientIp] || 0;
if (counter >= count) {
return { error: true };

const period = rate.slice(-1);
const duration = parseInt(rate.slice(0, -1));
if (period === 's') {
return duration;
} else if (period === 'm') {
return duration * 60;
} else if (period === 'h') {
return duration * 60 * 60;
} else if (period === 'd') {
return duration * 60 * 60 * 24;
}
RateLimiter.incrementCounter(key, clientIp);
setTimeout(() => {
RateLimiter.decrementCounter(key, clientIp);
}, whenClear.getTime() - Date.now());
throw new Error(`Invalid rate duration period: ${period}`);
}

return { error: false };

constructor(rate: string) {
const [points, duration] = rate.split('/');
const durationSeconds = this.durStringToSeconds(duration);
const opts = {
points: parseInt(points),
duration: durationSeconds, // Per second
};
this.rateLimiter = new RateLimiterMemory(opts);
}

static incrementCounter(key: string, ip: string) {
if (!RateLimiter.counterData[key]) {
RateLimiter.counterData[key] = {};
}
if (!RateLimiter.counterData[key][ip]) {
RateLimiter.counterData[key][ip] = 0;
}
RateLimiter.counterData[key][ip]++;
}

static decrementCounter(key: string, ip: string) {
if (!RateLimiter.counterData[key]) {
RateLimiter.counterData[key] = {};
}
if (!RateLimiter.counterData[key][ip]) {
RateLimiter.counterData[key][ip] = 0;
}
if (RateLimiter.counterData[key][ip] > 0) {
RateLimiter.counterData[key][ip]--;
async consume(key: string) {
try {
await this.rateLimiter.consume(key);
return true;
} catch (rejRes) {
return false;
}
}

Expand Down
Loading