admin/config: better field for https files
Massimo Melina committed
Apr 9, 2022 at 18:42 UTC
8dd1da5315271d3552bfb73907e5cffeeee71688
5 files changed
+104
-18
admin/src/ConfigPage.ts
+8
-4
@@ -2,13 +2,14 @@
2
3
import { Box, Button, FormHelperText, Link } from '@mui/material';
4
import { createElement as h, isValidElement, useEffect, useRef } from 'react';
5
-import { apiCall, useApi, useApiComp } from './api'
5
+import { apiCall, useApiComp } from './api'
6
import { state, useSnapState } from './state'
7
import { Info, Refresh } from '@mui/icons-material'
8
import { Dict, modifiedSx } from './misc'
9
import { subscribeKey } from 'valtio/utils'
10
import { Form, BoolField, NumberField, SelectField, FieldProps, Field } from './Form';
11
import StringStringField from './StringStringField'
12
+import FileField from './FileField'
13
import { alertDialog, closeDialog, confirmDialog, formDialog, newDialog, waitDialog } from './dialog'
14
import { proxyWarning } from './HomePage'
15
@@ -78,8 +79,8 @@ export default function ConfigPage() {
79
return v
80
}
81
},
81
- values.https_port >= 0 && { k: 'cert', label: "HTTPS certificate file" },
82
- values.https_port >= 0 && { k: 'private_key', label: "HTTPS private key file" },
82
+ values.https_port >= 0 && { k: 'cert', comp: FileField, label: "HTTPS certificate file" },
83
+ values.https_port >= 0 && { k: 'private_key', comp: FileField, label: "HTTPS private key file" },
84
{ k: 'max_kbps', ...maxSpeedDefaults, label: "Limit output KB/s", helperText: "Doesn't apply to localhost" },
85
{ k: 'max_kbps_per_ip', ...maxSpeedDefaults, label: "Limit output KB/s per-ip" },
86
...Object.entries(logLabels).map(a => ({ k: a[0], label: a[1] })),
@@ -178,7 +179,10 @@ function suggestMakingCert() {
179
h(Info), "You are enabling HTTPs. It needs a valid certificate + private key to work."
180
),
181
h(Box, { mt: 4, display: 'flex', gap: 1, justifyContent: 'space-around', },
181
- h(Button, { variant: 'contained', onClick(){ closeDialog(); makeCertAndSave() } }, "Help me!"),
182
+ h(Button, { variant: 'contained', onClick(){
183
+ closeDialog()
184
+ makeCertAndSave().then()
185
+ } }, "Help me!"),
186
h(Button, { onClick: closeDialog }, "I will handle the matter myself"),
187
),
188
)
admin/src/FileField.ts
new
+47
@@ -0,0 +1,47 @@
1
+import { FieldProps, StringField } from './Form'
2
+import { createElement as h } from 'react'
3
+import { InputAdornment } from '@mui/material'
4
+import { Eject } from '@mui/icons-material'
5
+import { IconBtn } from './misc'
6
+import { newDialog } from '@hfs/shared/lib/dialogs'
7
+import FilePicker from './FilePicker'
8
+import { apiCall } from './api'
9
+
10
+export default function FileField({ value, onChange, ...props }: FieldProps<string>) {
11
+ return h(StringField, {
12
+ ...props,
13
+ value,
14
+ onChange,
15
+ InputProps: {
16
+ endAdornment: h(InputAdornment, { position: 'end' },
17
+ h(IconBtn, {
18
+ icon: Eject,
19
+ title: "Browse files...",
20
+ edge: 'end',
21
+ onClick() {
22
+ const close = newDialog({
23
+ title: "Pick a file",
24
+ dialogProps: { sx:{ minWidth:'min(90vw, 40em)', minHeight: 'calc(100vh - 9em)' } },
25
+ Content,
26
+ })
27
+
28
+ function Content() {
29
+ return h(FilePicker, {
30
+ multiple: false,
31
+ from: value,
32
+ async onSelect(sel) {
33
+ let one = sel?.[0]
34
+ if (!one) return
35
+ const cwd = (await apiCall('get_cwd'))?.path
36
+ if (one.startsWith(cwd))
37
+ one = one.slice(cwd.length+1)
38
+ onChange(one, { was: value, event: 'picker' })
39
+ close()
40
+ }
41
+ })
42
+ }
43
+ },
44
+ }))
45
+ }
46
+ })
47
+}
admin/src/FilePicker.ts
+19
-12
@@ -1,7 +1,7 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { createElement as h, Fragment, useEffect, useMemo, useState } from 'react'
4
-import { useApi, useApiList } from './api'
4
+import { apiCall, useApiList } from './api'
5
import _ from 'lodash'
6
import {
7
Alert,
@@ -14,7 +14,7 @@ import {
14
TextField,
15
Typography
16
} from '@mui/material'
17
-import { enforceFinal, formatBytes, isWindowsDrive, spinner } from './misc'
17
+import { enforceFinal, formatBytes, isWindowsDrive, spinner, pathJoin, dirname, isAbsolutePath } from './misc'
18
import { ArrowUpward, Home } from '@mui/icons-material'
19
import { StringField } from './Form'
20
import { FileIcon, FolderIcon } from './VfsTree'
@@ -23,16 +23,24 @@ import AutoSizer from "react-virtualized-auto-sizer"
23
24
export interface DirEntry { n:string, s?:number, m?:string, c?:string, k?:'d' }
25
26
-export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }) {
27
- const [cwd, setCwd] = useState('')
26
+interface FilePickerProps {
27
+ onSelect:(v:string[])=>void
28
+ multiple?: boolean
29
+ from?: string
30
+}
31
+export default function FilePicker({ onSelect, multiple=true, from }: FilePickerProps) {
32
+ const passedDir = useMemo(() => from && dirname(from), [from])
33
+ const [cwd, setCwd] = useState(from && passedDir || '')
34
const [ready, setReady] = useState(false)
29
- const [gotCwd] = useApi('get_cwd')
35
useEffect(() => {
31
- if (gotCwd) {
32
- setCwd(gotCwd.path)
36
+ if (passedDir && isAbsolutePath(passedDir))
37
+ return setReady(true)
38
+ apiCall('get_cwd').then(res => {
39
+ if (typeof res.path === 'string')
40
+ setCwd(pathJoin(res.path, passedDir))
41
setReady(true)
34
- }
35
- }, [gotCwd])
42
+ })
43
+ }, [])
44
const { list, error, loading } = useApiList<DirEntry>(ready && 'ls', { path: cwd })
45
useEffect(() => {
46
setSel([])
@@ -92,7 +100,7 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
100
onSelect([cwdPostfixed + it.n])
101
}
102
},
95
- h(Checkbox, {
103
+ multiple && h(Checkbox, {
104
checked: sel.includes(it.n),
105
onClick(ev) {
106
const id = it.n
@@ -114,7 +122,7 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
122
}),
123
),
124
h(Box, { display:'flex', gap: 1 },
117
- h(Button, {
125
+ multiple && h(Button, {
126
variant: 'contained',
127
disabled: !sel.length,
128
sx: { minWidth: 'max-content' },
@@ -135,4 +143,3 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
143
)
144
)
145
}
138
-
admin/src/misc.ts
+27
-1
@@ -7,7 +7,7 @@ import { SxProps } from '@mui/system'
7
import { SvgIconComponent } from '@mui/icons-material'
8
import { alertDialog } from './dialog'
9
import { apiCall } from './api'
10
-import { useStateMounted } from '@hfs/shared'
10
+import { onlyTruthy, useStateMounted } from '@hfs/shared'
11
import {} from '@hfs/shared' // without this we get weird warnings by webpack
12
export * from '@hfs/shared'
13
@@ -68,3 +68,29 @@ export async function manipulateConfig(k: string, work:(data:any) => any) {
68
export function typedKeys<T>(o: T) {
69
return Object.keys(o) as (keyof T)[]
70
}
71
+
72
+export function dirname(s: string) {
73
+ let i = s.lastIndexOf('/')
74
+ if (i < 0)
75
+ i = s.lastIndexOf('\\')
76
+ return i < 0 ? '' : s.slice(0, i)
77
+}
78
+
79
+export function isAbsolutePath(s: string) {
80
+ return s && (s[0] === '/' || isWindowsDrive(s.slice(0,2)))
81
+}
82
+
83
+export function pathJoin(...args: any[]) {
84
+ const delimiter = findFirst(args, x => /\\|\//.exec('\\a/b')?.[0])
85
+ const good = onlyTruthy(args.map(x => x == null ? '' : String(x)))
86
+ return good.map((x, i) => i === good.length-1 || x.endsWith('\\') || x.endsWith('/') ? x : x + delimiter)
87
+ .join('')
88
+}
89
+
90
+export function findFirst<I=any, O=any>(a: I[], cb:(v:I)=>O): any {
91
+ for (const x of a) {
92
+ const ret = cb(x)
93
+ if (ret !== undefined)
94
+ return ret
95
+ }
96
+}
todo.md
+3
-1
@@ -7,10 +7,12 @@
7
- admin/fs: support insert/delete key
8
- admin/fs: button "copy url to clipboard"
9
- admin/monitor: show some info on what folder is browsing
10
+- anti-brute for logins
11
+- admin/stats: total bytes sent
12
+- admin/stats: list of IPs seen
13
- if specified config is a folder, check for file config.yaml inside
14
- frontend: ok button to inputDialogs
15
- admin: in a group, show linked accounts
13
-- admin/config: use filepicker for https files
16
- admin: warn in case of items with same name
17
- command line help --help
18
- admin/plugins