admin/logs: "block IP" button

Massimo Melina committed May 3, 2024 at 11:24 UTC b1642858870f6fc26ebf936d3b90d299a5d5a998
6 files changed +42 -17
admin/src/LogsPage.ts
+6 -1
@@ -17,6 +17,7 @@ import { ClearAll, Download, Settings, SmartToy } from '@mui/icons-material'
17 import { ConfigForm } from './ConfigForm'
18 import { BoolField, SelectField } from '@hfs/mui-grid-form'
19 import { toast, useDialogBarColors } from './dialog'
20 +import { useBlockIp } from './useBlockIp'
21
22 export default function LogsPage() {
23 const [tab, setTab] = useState(0)
@@ -130,11 +131,15 @@ function LogFile({ file, addToFooter, hidden }: { hidden?: boolean, file: string
131 renderCell: ({ value }) => h(Fragment, {}, value.toLocaleDateString(), h('br'), value.toLocaleTimeString())
132 }
133 const rows = useMemo(() => showApi || list?.[0]?.uri === undefined ? list : list.filter(x => !x.uri.startsWith(API_URL)), [list, showApi]) //TODO TypeError: l.uri is undefined
134 + const blockIp = useBlockIp()
135 + const isConsole = file === 'console'
136 return hidden ? null : h(DataTable, {
137 error,
138 loading: connecting,
139 rows,
140 compact: true,
141 + actionsProps: { hideUnder: 'md' },
142 + actions: ({ row }) => [ !isConsole && blockIp.iconBtn(row.ip, "From log") ],
143 addToFooter: h(Box, {}, // 4 icons don't fit the tabs row on mobile
144 pauseButton,
145 showApiButton,
@@ -149,7 +154,7 @@ function LogFile({ file, addToFooter, hidden }: { hidden?: boolean, file: string
154 }, "Load whole log"),
155 addToFooter,
156 ),
152 - columns: file === 'console' ? [
157 + columns: isConsole ? [
158 tsColumn,
159 {
160 field: 'k',
admin/src/MonitorPage.ts
+5 -13
@@ -3,16 +3,17 @@
3 import _ from "lodash"
4 import { createElement as h, useMemo, Fragment, useState } from "react"
5 import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
6 -import { LinkOff, Lock, Block, FolderZip, Upload, Download, ChevronRight, ChevronLeft } from '@mui/icons-material'
6 +import { LinkOff, Lock, FolderZip, Upload, Download, ChevronRight, ChevronLeft } from '@mui/icons-material'
7 import { Box, Chip, ChipProps } from '@mui/material'
8 import { DataTable } from './DataTable'
9 -import { formatBytes, ipForUrl, manipulateConfig, CFG, formatSpeed, with_, createDurationFormatter, formatTimestamp,
9 +import { formatBytes, ipForUrl, CFG, formatSpeed, with_, createDurationFormatter, formatTimestamp,
10 formatPerc, md } from "./misc"
11 import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country, hTooltip } from './mui'
12 import { Field, SelectField } from '@hfs/mui-grid-form'
13 import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
14 import { agentIcons } from './LogsPage'
15 import { state, useSnapState } from './state'
16 +import { useBlockIp } from './useBlockIp'
17
18 export default function MonitorPage() {
19 return h(Fragment, {},
@@ -96,6 +97,7 @@ function Connections() {
97 const rows = useMemo(() =>
98 list?.filter((x: any) => !monitorOnlyFiles || x.op).map((x: any, id: number) => ({ id, ...x })),
99 [!pause && list, monitorOnlyFiles]) //eslint-disable-line
100 + const blockIp = useBlockIp()
101 return h(Fragment, {},
102 h(Box, { display: 'flex', alignItems: 'center' },
103 h(SelectField as Field<boolean>, {
@@ -203,22 +205,12 @@ function Connections() {
205 doneMessage: true,
206 onClick: () => apiCall('disconnect', _.pick(row, ['ip', 'port'])).then(x => x.result > 0)
207 }),
206 - h(IconBtn, {
207 - icon: Block,
208 - title: "Block IP",
209 - confirm: "Block address " + row.ip,
210 - disabled: row.ip === props?.you,
211 - onClick: () => blockIp(row.ip),
212 - }),
208 + blockIp.iconBtn(row.ip, "From monitoring", { disabled: row.ip === props?.you }),
209 ]
210 })
211 )
212 }
213
218 -function blockIp(ip: string) {
219 - return manipulateConfig('block', data => [...data, { ip }])
220 -}
221 -
214 function formatSpeedK(value: number | undefined) {
215 return value === undefined ? '' : formatSpeed(value * 1000, { digits: 1 })
216 }
admin/src/mui.ts
+1 -1
@@ -117,7 +117,7 @@ function useRefPass<T=unknown>(forwarded: ForwardedRef<any>) {
117 })
118 }
119
120 -interface IconBtnProps extends Omit<BtnProps, 'icon' | 'children'> { icon: SvgIconComponent }
120 +export interface IconBtnProps extends Omit<BtnProps, 'icon' | 'children'> { icon: SvgIconComponent }
121 export const IconBtn = forwardRef((props: IconBtnProps, ref: ForwardedRef<HTMLButtonElement>) =>
122 h(Btn, { ref, ...props }))
123
admin/src/useBlockIp.ts new
+26
@@ -0,0 +1,26 @@
1 +import { apiCall, useApi } from '@hfs/shared/api'
2 +import { createElement as h, useCallback } from 'react'
3 +import { BlockingRule } from '../../src/block'
4 +import { toast } from './dialog'
5 +import _ from 'lodash'
6 +import { IconBtn, IconBtnProps } from './mui'
7 +import { Block } from '@mui/icons-material'
8 +
9 +export function useBlockIp() {
10 + const { data, reload } = useApi('get_config', { only: ['block'] })
11 + const block = useCallback((ip: string, more: Partial<BlockingRule>={}, showResult=true) =>
12 + apiCall('set_config', { values: { block: [...data.block, { ip, ...more }] } })
13 + .then(reload).then(() => showResult && toast("Blocked", 'success')),
14 + [data, reload])
15 + const isBlocked = useCallback((ip: string) => _.find(data?.block, { ip }), [data])
16 + return {
17 + iconBtn: (ip: string, comment: string, options: Partial<IconBtnProps>={}) => h(IconBtn, {
18 + icon: Block,
19 + title: "Block IP",
20 + confirm: "Block address " + ip,
21 + ...isBlocked(ip) && { disabled: true, title: "Blocked" },
22 + ...options,
23 + onClick: () => block(ip, { comment }),
24 + }),
25 + }
26 +}
src/block.ts
+1 -1
@@ -5,7 +5,7 @@ import { disconnect, getConnections, normalizeIp } from './connections'
5 import { makeNetMatcher, MINUTE, onlyTruthy } from './misc'
6 import { Socket } from 'net'
7
8 -interface BlockingRule { ip: string, comment?: string, expire?: Date, disabled?: boolean }
8 +export interface BlockingRule { ip: string, comment?: string, expire?: Date, disabled?: boolean }
9
10 export const block = defineConfig('block', [] as BlockingRule[], rules => {
11 const now = new Date()
src/events.ts
+3 -1
@@ -3,4 +3,6 @@
3 import EventEmitter from 'events'
4
5 // app-wide events
6 -export default new EventEmitter().setMaxListeners(100)
\ No newline at end of file
6 +const ee = new EventEmitter().setMaxListeners(100)
7 +
8 +export default ee
\ No newline at end of file