hfs 0.26.8 → 0.26.9
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/admin/assets/index.bb5198ec.js +281 -0
- package/admin/assets/index.dcc78777.css +1 -0
- package/admin/assets/sha512.9dfe82e1.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index.27a78796.js +85 -0
- package/frontend/assets/index.93366732.css +1 -0
- package/frontend/assets/sha512.6af42937.js +8 -0
- package/frontend/{public/fontello.css → fontello.css} +0 -0
- package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
- package/frontend/index.html +3 -1
- package/package.json +1 -1
- package/src/QuickZipStream.js +285 -0
- package/src/ThrottledStream.js +93 -0
- package/src/adminApis.js +169 -0
- package/src/api.accounts.js +59 -0
- package/src/api.auth.js +128 -0
- package/src/api.file_list.js +103 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +102 -0
- package/src/api.plugins.js +127 -0
- package/src/api.vfs.js +164 -0
- package/src/apiMiddleware.js +120 -0
- package/src/block.js +33 -0
- package/src/commands.js +124 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +83 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +48 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +102 -0
- package/src/index.js +56 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +175 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +181 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +29 -0
- package/src/throttler.js +91 -0
- package/src/update.js +69 -0
- package/src/util-files.js +148 -0
- package/src/util-generators.js +30 -0
- package/src/util-http.js +30 -0
- package/src/vfs.js +230 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +72 -0
- package/admin/.DS_Store +0 -0
- package/admin/.eslintrc +0 -8
- package/admin/.gitignore +0 -23
- package/admin/package.json +0 -67
- package/admin/src/AccountForm.ts +0 -92
- package/admin/src/AccountsPage.ts +0 -143
- package/admin/src/App.ts +0 -83
- package/admin/src/ArrayField.ts +0 -84
- package/admin/src/ConfigPage.ts +0 -279
- package/admin/src/FileField.ts +0 -52
- package/admin/src/FileForm.ts +0 -148
- package/admin/src/FilePicker.ts +0 -166
- package/admin/src/HomePage.ts +0 -96
- package/admin/src/InstalledPlugins.ts +0 -158
- package/admin/src/LoginRequired.ts +0 -75
- package/admin/src/LogoutPage.ts +0 -27
- package/admin/src/LogsPage.ts +0 -75
- package/admin/src/MainMenu.ts +0 -74
- package/admin/src/MenuButton.ts +0 -38
- package/admin/src/MonitorPage.ts +0 -200
- package/admin/src/OnlinePlugins.ts +0 -101
- package/admin/src/PermField.ts +0 -80
- package/admin/src/PluginsPage.ts +0 -27
- package/admin/src/VfsMenuBar.ts +0 -58
- package/admin/src/VfsPage.ts +0 -124
- package/admin/src/VfsTree.ts +0 -95
- package/admin/src/addFiles.ts +0 -59
- package/admin/src/api.ts +0 -246
- package/admin/src/dialog.ts +0 -203
- package/admin/src/index.css +0 -21
- package/admin/src/index.ts +0 -10
- package/admin/src/md.ts +0 -31
- package/admin/src/misc.ts +0 -141
- package/admin/src/react-app-env.d.ts +0 -1
- package/admin/src/reportWebVitals.ts +0 -15
- package/admin/src/setupTests.ts +0 -5
- package/admin/src/state.ts +0 -40
- package/admin/src/theme.ts +0 -37
- package/admin/tsconfig.json +0 -26
- package/admin/vite.config.ts +0 -32
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +0 -8
- package/frontend/.gitignore +0 -23
- package/frontend/package.json +0 -51
- package/frontend/src/App.ts +0 -25
- package/frontend/src/Breadcrumbs.ts +0 -43
- package/frontend/src/BrowseFiles.ts +0 -141
- package/frontend/src/Head.ts +0 -45
- package/frontend/src/UserPanel.ts +0 -52
- package/frontend/src/api.ts +0 -78
- package/frontend/src/components.ts +0 -54
- package/frontend/src/dialog.css +0 -76
- package/frontend/src/dialog.ts +0 -105
- package/frontend/src/icons.ts +0 -46
- package/frontend/src/index.scss +0 -307
- package/frontend/src/index.ts +0 -10
- package/frontend/src/login.ts +0 -50
- package/frontend/src/menu.ts +0 -188
- package/frontend/src/misc.ts +0 -54
- package/frontend/src/options.ts +0 -52
- package/frontend/src/react-app-env.d.ts +0 -1
- package/frontend/src/reportWebVitals.ts +0 -15
- package/frontend/src/setupTests.ts +0 -5
- package/frontend/src/state.ts +0 -82
- package/frontend/src/useAuthorized.ts +0 -17
- package/frontend/src/useFetchList.ts +0 -144
- package/frontend/src/useTheme.ts +0 -23
- package/frontend/tsconfig.json +0 -26
- package/frontend/vite.config.ts +0 -21
- package/src/QuickZipStream.ts +0 -279
- package/src/ThrottledStream.ts +0 -98
- package/src/adminApis.ts +0 -161
- package/src/api.accounts.ts +0 -78
- package/src/api.auth.ts +0 -131
- package/src/api.file_list.ts +0 -102
- package/src/api.helpers.ts +0 -30
- package/src/api.monitor.ts +0 -106
- package/src/api.plugins.ts +0 -139
- package/src/api.vfs.ts +0 -182
- package/src/apiMiddleware.ts +0 -124
- package/src/block.ts +0 -35
- package/src/commands.ts +0 -122
- package/src/config.ts +0 -166
- package/src/connections.ts +0 -60
- package/src/const.ts +0 -57
- package/src/crypt.ts +0 -16
- package/src/debounceAsync.ts +0 -51
- package/src/events.ts +0 -6
- package/src/frontEndApis.ts +0 -17
- package/src/github.ts +0 -102
- package/src/index.ts +0 -53
- package/src/listen.ts +0 -220
- package/src/log.ts +0 -128
- package/src/middlewares.ts +0 -176
- package/src/misc.ts +0 -149
- package/src/pbkdf2.ts +0 -83
- package/src/perm.ts +0 -194
- package/src/plugins.ts +0 -342
- package/src/serveFile.ts +0 -104
- package/src/serveGuiFiles.ts +0 -95
- package/src/sse.ts +0 -29
- package/src/throttler.ts +0 -106
- package/src/update.ts +0 -67
- package/src/util-files.ts +0 -137
- package/src/util-generators.ts +0 -29
- package/src/util-http.ts +0 -29
- package/src/vfs.ts +0 -258
- package/src/watchLoad.ts +0 -75
- package/src/zip.ts +0 -69
|
@@ -1,143 +0,0 @@
|
|
|
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, useState, useEffect, Fragment } from "react"
|
|
4
|
-
import { apiCall, useApiEx } from './api'
|
|
5
|
-
import { Alert, Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
|
|
6
|
-
import { Delete, Group, MilitaryTech, Person, PersonAdd, Refresh } from '@mui/icons-material'
|
|
7
|
-
import { alertDialog, confirmDialog } from './dialog'
|
|
8
|
-
import { iconTooltip, onlyTruthy } from './misc'
|
|
9
|
-
import { TreeItem, TreeView } from '@mui/lab'
|
|
10
|
-
import MenuButton from './MenuButton'
|
|
11
|
-
import AccountForm from './AccountForm'
|
|
12
|
-
import md from './md'
|
|
13
|
-
|
|
14
|
-
export interface Account {
|
|
15
|
-
username: string
|
|
16
|
-
hasPassword?: boolean
|
|
17
|
-
adminActualAccess?: boolean
|
|
18
|
-
ignore_limits?: boolean
|
|
19
|
-
redirect?: string
|
|
20
|
-
belongs?: string[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export default function AccountsPage() {
|
|
24
|
-
const { data, reload, element } = useApiEx('get_accounts')
|
|
25
|
-
const [sel, setSel] = useState<string[] | 'new-group' | 'new-user'>([])
|
|
26
|
-
const selectionMode = Array.isArray(sel)
|
|
27
|
-
useEffect(() => { // if accounts are reloaded, review the selection to remove elements that don't exist anymore
|
|
28
|
-
if (Array.isArray(data?.list) && selectionMode)
|
|
29
|
-
setSel( sel.filter(u => data.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
|
|
30
|
-
}, [data]) //eslint-disable-line -- Don't fall for its suggestion to add `sel` here: we modify it and declaring it as a dependency would cause a logical loop
|
|
31
|
-
if (element)
|
|
32
|
-
return element
|
|
33
|
-
const { list }: { list: Account[] } = data
|
|
34
|
-
return h(Grid, { container: true, maxWidth: '80em' },
|
|
35
|
-
h(Grid, { item: true, xs: 12 },
|
|
36
|
-
h(Box, {
|
|
37
|
-
display: 'flex',
|
|
38
|
-
flexWrap: 'wrap',
|
|
39
|
-
gap: 2,
|
|
40
|
-
mb: 2,
|
|
41
|
-
sx: {
|
|
42
|
-
position: 'sticky',
|
|
43
|
-
top: 0,
|
|
44
|
-
zIndex: 2,
|
|
45
|
-
backgroundColor: 'background.paper',
|
|
46
|
-
width: 'fit-content',
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
h(MenuButton, {
|
|
50
|
-
variant: 'contained',
|
|
51
|
-
startIcon: h(PersonAdd),
|
|
52
|
-
items: [
|
|
53
|
-
{ children: "user", onClick: () => setSel('new-user') },
|
|
54
|
-
{ children: "group", onClick: () => setSel('new-group') }
|
|
55
|
-
]
|
|
56
|
-
}, 'Add'),
|
|
57
|
-
h(Button, {
|
|
58
|
-
disabled: !selectionMode || !sel.length,
|
|
59
|
-
startIcon: h(Delete),
|
|
60
|
-
async onClick(){
|
|
61
|
-
if (!selectionMode) return
|
|
62
|
-
if (!await confirmDialog(`You are going to delete ${sel.length} account(s)`))
|
|
63
|
-
return
|
|
64
|
-
const errors = onlyTruthy(await Promise.all(sel.map(username =>
|
|
65
|
-
apiCall('del_account', { username }).then(() => null, () => username) )))
|
|
66
|
-
if (errors.length)
|
|
67
|
-
return alertDialog(errors.length === sel.length ? "Request failed" : hList("Some accounts were not deleted", errors), 'error')
|
|
68
|
-
reload()
|
|
69
|
-
}
|
|
70
|
-
}, "Remove"),
|
|
71
|
-
h(Button, { onClick: reload, startIcon: h(Refresh) }, "Reload"),
|
|
72
|
-
list.length > 0 && h(Typography, { p: 1 }, `${list.length} account(s)`),
|
|
73
|
-
) ),
|
|
74
|
-
h(Grid, { item: true, md: 5 },
|
|
75
|
-
!list.length && h(Alert, { severity: 'info' }, md`To access administration _remotely_ you will need to create a user account with admin permission`),
|
|
76
|
-
h(TreeView, {
|
|
77
|
-
multiSelect: true,
|
|
78
|
-
sx: { pr: 4, pb: 2, minWidth: '15em' },
|
|
79
|
-
selected: selectionMode ? sel : [],
|
|
80
|
-
onNodeSelect(ev, ids) {
|
|
81
|
-
setSel(ids)
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
list.map((ac: Account) =>
|
|
85
|
-
h(TreeItem, {
|
|
86
|
-
key: ac.username,
|
|
87
|
-
nodeId: ac.username,
|
|
88
|
-
label: h(Box, {
|
|
89
|
-
sx: {
|
|
90
|
-
display: 'flex',
|
|
91
|
-
flexWrap: 'wrap',
|
|
92
|
-
padding: '.2em 0',
|
|
93
|
-
gap: '.5em',
|
|
94
|
-
alignItems: 'center',
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
account2icon(ac),
|
|
98
|
-
ac.adminActualAccess && iconTooltip(MilitaryTech, "Can login into Admin"),
|
|
99
|
-
ac.username,
|
|
100
|
-
Boolean(ac.belongs?.length) && h(Box, { sx: { color: 'text.secondary', fontSize: 'small' } },
|
|
101
|
-
'(', ac.belongs?.join(', '), ')')
|
|
102
|
-
),
|
|
103
|
-
})
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
-
),
|
|
107
|
-
sel.length > 0 // this clever test is true both when some accounts are selected and when we are in "new account" modes
|
|
108
|
-
&& h(Grid, { item: true, md: 7 },
|
|
109
|
-
h(Card, {},
|
|
110
|
-
h(CardContent, {},
|
|
111
|
-
selectionMode && sel.length > 1 ? h(Box, {},
|
|
112
|
-
h(Typography, {}, sel.length + " selected"),
|
|
113
|
-
h(List, {},
|
|
114
|
-
sel.map(username =>
|
|
115
|
-
h(ListItem, { key: username },
|
|
116
|
-
h(ListItemText, {}, username))))
|
|
117
|
-
) : h(AccountForm, {
|
|
118
|
-
account: selectionMode && list.find(x => x.username === sel[0])
|
|
119
|
-
|| { username: '', hasPassword: sel === 'new-user' },
|
|
120
|
-
groups: list.filter(x => !x.hasPassword).map( x => x.username ),
|
|
121
|
-
close(){ setSel([]) },
|
|
122
|
-
done(username) {
|
|
123
|
-
setSel([username])
|
|
124
|
-
reload()
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
)))
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function hList(heading: string, list: any[]) {
|
|
132
|
-
return h(Fragment, {},
|
|
133
|
-
heading>'' && h(Typography, {}, heading),
|
|
134
|
-
h(List, {},
|
|
135
|
-
list.map((text,key) =>
|
|
136
|
-
h(ListItem, { key },
|
|
137
|
-
typeof text === 'string' ? h(ListItemText, {}, text) : text) ))
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function account2icon(ac: Account, props={}) {
|
|
142
|
-
return h(ac.hasPassword ? Person : Group, props)
|
|
143
|
-
}
|
package/admin/src/App.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
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, useState } from 'react'
|
|
4
|
-
import { HashRouter, Routes, Route, useLocation } from 'react-router-dom'
|
|
5
|
-
import MainMenu, { getMenuLabel, mainMenu } from './MainMenu'
|
|
6
|
-
import { AppBar, Box, Drawer, IconButton, ThemeProvider, Toolbar, Typography } from '@mui/material'
|
|
7
|
-
import { Dialogs } from './dialog'
|
|
8
|
-
import { useMyTheme } from './theme'
|
|
9
|
-
import { useBreakpoint} from './misc'
|
|
10
|
-
import { LoginRequired } from './LoginRequired'
|
|
11
|
-
import { Menu } from '@mui/icons-material'
|
|
12
|
-
|
|
13
|
-
function App() {
|
|
14
|
-
return h(ThemeProvider, { theme: useMyTheme() },
|
|
15
|
-
h(ApplyTheme, {},
|
|
16
|
-
h(LoginRequired, {},
|
|
17
|
-
h(HashRouter, {}, h(Routed)) ) ) )
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function ApplyTheme(props:any) {
|
|
21
|
-
return h(Box, {
|
|
22
|
-
sx: {
|
|
23
|
-
bgcolor: 'background.default', color: 'text.primary',
|
|
24
|
-
display: 'flex', flexDirection: 'column',
|
|
25
|
-
minHeight: '100%', flex: 1,
|
|
26
|
-
},
|
|
27
|
-
...props
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function Routed() {
|
|
32
|
-
const loc = useLocation().pathname.slice(1)
|
|
33
|
-
const current = mainMenu.find(x => x.path === loc)
|
|
34
|
-
const title = current && (current.title || getMenuLabel(current))
|
|
35
|
-
const [open, setOpen] = useState(false)
|
|
36
|
-
const large = useBreakpoint('lg')
|
|
37
|
-
return h(Fragment, {},
|
|
38
|
-
!large && h(StickyBar, { title, openMenu: () => setOpen(true) }),
|
|
39
|
-
!large && h(Drawer, { anchor:'left', open, onClose(){ setOpen(false) } },
|
|
40
|
-
h(MainMenu, {
|
|
41
|
-
onSelect: () => setOpen(false)
|
|
42
|
-
})),
|
|
43
|
-
h(Box, { display: 'flex', flex: 1, }, // horizontal layout for menu-content
|
|
44
|
-
large && h(MainMenu),
|
|
45
|
-
h(Box, {
|
|
46
|
-
component: 'main',
|
|
47
|
-
sx: {
|
|
48
|
-
background: 'url(logo.svg) no-repeat right fixed',
|
|
49
|
-
backgroundSize: 'contain',
|
|
50
|
-
px: { xs: 2, md: 3 },
|
|
51
|
-
pb: '1em',
|
|
52
|
-
position: 'relative',
|
|
53
|
-
display: 'flex',
|
|
54
|
-
flexDirection: 'column',
|
|
55
|
-
width: '100%',
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
title && large && h(Typography, { variant:'h2', mb:2 }, title),
|
|
59
|
-
h(Routes, {}, mainMenu.map((it,idx) =>
|
|
60
|
-
h(Route, { key: idx, path: it.path, element: h(it.comp) })) )
|
|
61
|
-
),
|
|
62
|
-
h(Dialogs)
|
|
63
|
-
)
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function StickyBar({ title, openMenu }: { title?: string, openMenu: ()=>void }) {
|
|
68
|
-
return h(AppBar, { position: 'sticky', sx: { mb: 2 } },
|
|
69
|
-
h(Toolbar, {},
|
|
70
|
-
h(IconButton, {
|
|
71
|
-
size: 'large',
|
|
72
|
-
edge: 'start',
|
|
73
|
-
color: 'inherit',
|
|
74
|
-
sx: { mr: 2 },
|
|
75
|
-
'aria-label': "menu",
|
|
76
|
-
onClick: openMenu
|
|
77
|
-
}, h(Menu)),
|
|
78
|
-
title,
|
|
79
|
-
)
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export default App
|
package/admin/src/ArrayField.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { createElement as h, Fragment, useMemo } from 'react'
|
|
2
|
-
import { IconBtn, setHidden } from './misc'
|
|
3
|
-
import { Add, Edit, Delete } from '@mui/icons-material'
|
|
4
|
-
import { confirmDialog, formDialog } from './dialog'
|
|
5
|
-
import { DataGrid, GridAlignment } from '@mui/x-data-grid'
|
|
6
|
-
import { FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
|
|
7
|
-
import { Box, FormHelperText, FormLabel } from '@mui/material'
|
|
8
|
-
|
|
9
|
-
export function ArrayField<T=any>({ label, helperText, fields, value, onChange, onError, getApi, ...rest }: FieldProps<T[]> & { fields: FieldDescriptor[], height?: number }) {
|
|
10
|
-
const rows = useMemo(() => (value||[]).map((x,$idx) =>
|
|
11
|
-
setHidden({ ...x } as any, 'id' in x ? { $idx } : { id: $idx })),
|
|
12
|
-
[JSON.stringify(value)]) //eslint-disable-line
|
|
13
|
-
const columns = useMemo(() => {
|
|
14
|
-
return [
|
|
15
|
-
...fields.map(f => ({
|
|
16
|
-
field: f.k,
|
|
17
|
-
headerName: f.headerName ?? (typeof f.label === 'string' ? f.label : labelFromKey(f.k)),
|
|
18
|
-
disableColumnMenu: true,
|
|
19
|
-
...f.$width >= 8 ? { width: f.$width } : { flex: f.$width || 1 },
|
|
20
|
-
...f.$column,
|
|
21
|
-
})),
|
|
22
|
-
{
|
|
23
|
-
field: '',
|
|
24
|
-
width: 80,
|
|
25
|
-
disableColumnMenu: true,
|
|
26
|
-
sortable: false,
|
|
27
|
-
align: 'center' as GridAlignment,
|
|
28
|
-
headerAlign: 'center' as GridAlignment,
|
|
29
|
-
renderHeader(){
|
|
30
|
-
return h(IconBtn, {
|
|
31
|
-
icon: Add,
|
|
32
|
-
title: "Add",
|
|
33
|
-
onClick: (event:any) =>
|
|
34
|
-
formDialog({ fields }).then(o => // @ts-ignore
|
|
35
|
-
o && onChange([...value||[], o], { was: value, event }))
|
|
36
|
-
})
|
|
37
|
-
},
|
|
38
|
-
renderCell({ row }: any) {
|
|
39
|
-
const { $idx=row.id } = row
|
|
40
|
-
return h('div', {},
|
|
41
|
-
h(IconBtn, {
|
|
42
|
-
icon: Edit,
|
|
43
|
-
title: "Modify",
|
|
44
|
-
onClick: (event:any) =>
|
|
45
|
-
formDialog({ fields, values: row }).then(newRec => {
|
|
46
|
-
if (!newRec) return
|
|
47
|
-
const newValue = value!.map((oldRec, i) => i === $idx ? newRec : oldRec)
|
|
48
|
-
onChange(newValue, { was: value, event })
|
|
49
|
-
}),
|
|
50
|
-
}),
|
|
51
|
-
h(IconBtn, {
|
|
52
|
-
icon: Delete,
|
|
53
|
-
title: "Delete",
|
|
54
|
-
onClick: (event:any) =>
|
|
55
|
-
confirmDialog("Delete?").then(ok => {
|
|
56
|
-
if (!ok) return
|
|
57
|
-
const newValue = value!.filter((rec, i) => i !== $idx)
|
|
58
|
-
onChange(newValue, { was: value, event })
|
|
59
|
-
}),
|
|
60
|
-
}),
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
}, [fields, value, onChange])
|
|
66
|
-
return h(Fragment, {},
|
|
67
|
-
label && h(FormLabel, { sx: { ml: 1 } }, label),
|
|
68
|
-
helperText && h(FormHelperText, {}, helperText),
|
|
69
|
-
h(Box, { height: '20em', ...rest },
|
|
70
|
-
h(DataGrid, {
|
|
71
|
-
columns,
|
|
72
|
-
rows,
|
|
73
|
-
hideFooterSelectedRowCount: true,
|
|
74
|
-
hideFooter: true,
|
|
75
|
-
componentsProps: {
|
|
76
|
-
pagination: {
|
|
77
|
-
showFirstButton: true,
|
|
78
|
-
showLastButton: true,
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
})
|
|
82
|
-
)
|
|
83
|
-
)
|
|
84
|
-
}
|
package/admin/src/ConfigPage.ts
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
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 { Box, Button, FormHelperText, Link } from '@mui/material';
|
|
4
|
-
import { createElement as h, useEffect, useRef } from 'react';
|
|
5
|
-
import { apiCall, useApi, useApiEx } 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, StringStringField, FieldProps, Field } from '@hfs/mui-grid-form';
|
|
11
|
-
import FileField from './FileField'
|
|
12
|
-
import { alertDialog, closeDialog, confirmDialog, formDialog, newDialog, toast, waitDialog } from './dialog'
|
|
13
|
-
import { proxyWarning } from './HomePage'
|
|
14
|
-
import _ from 'lodash';
|
|
15
|
-
import { proxy, useSnapshot } from 'valtio'
|
|
16
|
-
|
|
17
|
-
let loaded: Dict | undefined
|
|
18
|
-
let exposedReloadStatus: undefined | (() => void)
|
|
19
|
-
const pageState = proxy({
|
|
20
|
-
changes: {} as Dict
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
subscribeKey(state, 'config', recalculateChanges)
|
|
24
|
-
|
|
25
|
-
export const logLabels = {
|
|
26
|
-
log: "Access log file",
|
|
27
|
-
error_log: "Access error log file"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default function ConfigPage() {
|
|
31
|
-
const { data, reload: reloadConfig, element } = useApiEx('get_config', { omit: ['vfs'] })
|
|
32
|
-
let snap = useSnapState()
|
|
33
|
-
const { changes } = useSnapshot(pageState)
|
|
34
|
-
const statusApi = useApiEx(data && 'get_status')
|
|
35
|
-
const status = statusApi.data
|
|
36
|
-
const reloadStatus = exposedReloadStatus = statusApi.reload
|
|
37
|
-
useEffect(() => void(reloadStatus()), [data]) //eslint-disable-line
|
|
38
|
-
useEffect(() => () => exposedReloadStatus = undefined, []) // clear on unmount
|
|
39
|
-
|
|
40
|
-
const admins = useApi('get_admins')[0]?.list
|
|
41
|
-
|
|
42
|
-
if (element)
|
|
43
|
-
return element
|
|
44
|
-
if (statusApi.error)
|
|
45
|
-
return statusApi.element
|
|
46
|
-
const values = (loaded !== data) ? (state.config = loaded = data) : snap.config
|
|
47
|
-
const maxSpeedDefaults = {
|
|
48
|
-
comp: NumberField,
|
|
49
|
-
min: 1,
|
|
50
|
-
placeholder: "no limit",
|
|
51
|
-
}
|
|
52
|
-
return h(Form, {
|
|
53
|
-
sx: { maxWidth: '60em' },
|
|
54
|
-
values,
|
|
55
|
-
set(v, k) {
|
|
56
|
-
state.config[k] = v
|
|
57
|
-
},
|
|
58
|
-
stickyBar: true,
|
|
59
|
-
onError: alertDialog,
|
|
60
|
-
save: {
|
|
61
|
-
onClick: save,
|
|
62
|
-
sx: modifiedSx( Object.keys(changes).length>0),
|
|
63
|
-
},
|
|
64
|
-
barSx: { gap: 2 },
|
|
65
|
-
addToBar: [h(Button, {
|
|
66
|
-
onClick() {
|
|
67
|
-
reloadConfig()
|
|
68
|
-
reloadStatus()
|
|
69
|
-
},
|
|
70
|
-
startIcon: h(Refresh),
|
|
71
|
-
}, "Reload")],
|
|
72
|
-
defaults({ comp }) {
|
|
73
|
-
return comp === ServerPort ? { sm: 6, md: 3 }
|
|
74
|
-
: comp === NumberField ? { sm: 3 }
|
|
75
|
-
: { sm: 6 }
|
|
76
|
-
},
|
|
77
|
-
fields: [
|
|
78
|
-
{ k: 'max_kbps', ...maxSpeedDefaults, label: "Limit output KB/s", helperText: "Doesn't apply to localhost" },
|
|
79
|
-
{ k: 'max_kbps_per_ip', ...maxSpeedDefaults, label: "Limit output KB/s per-ip" },
|
|
80
|
-
{ k: 'port', comp: ServerPort, label:"HTTP port", status: status?.http||true, suggestedPort: 80 },
|
|
81
|
-
{ k: 'https_port', comp: ServerPort, label: "HTTPS port", status: status?.https||true, suggestedPort: 443,
|
|
82
|
-
onChange(v: number) {
|
|
83
|
-
if (v >= 0 && values.https_port < 0 && !values.cert)
|
|
84
|
-
suggestMakingCert()
|
|
85
|
-
return v
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
values.https_port >= 0 && { k: 'cert', comp: FileField, label: "HTTPS certificate file",
|
|
89
|
-
...with_(status?.https.error, e => isCertError(e) ? {
|
|
90
|
-
error: true,
|
|
91
|
-
helperText: [e, ' - ', h(Link, { key: 'fix', sx: { cursor: 'pointer' }, onClick: makeCertAndSave }, "make one")]
|
|
92
|
-
} : null)
|
|
93
|
-
},
|
|
94
|
-
values.https_port >= 0 && { k: 'private_key', comp: FileField, label: "HTTPS private key file",
|
|
95
|
-
...with_(status?.https.error, e => isKeyError(e) ? { error: true, helperText: e } : null)
|
|
96
|
-
},
|
|
97
|
-
{ k: 'open_browser_at_start', comp: BoolField },
|
|
98
|
-
{ k: 'localhost_admin', comp: BoolField, label: "Admin access for localhost connections",
|
|
99
|
-
getError: x => !x && admins?.length===0 && "First create at least one admin account",
|
|
100
|
-
helperText: "To access Admin without entering credentials"
|
|
101
|
-
},
|
|
102
|
-
{ k: 'log', label: logLabels.log, lg: 3, helperText: "Requests are logged here" },
|
|
103
|
-
{ k: 'error_log', label: logLabels.error_log, lg: 3, placeholder: "errors go to main log", helperText: "If you want errors in a different log" },
|
|
104
|
-
{ k: 'log_rotation', comp: SelectField, options: [{ value:'', label:"disabled" }, 'daily', 'weekly', 'monthly' ],
|
|
105
|
-
helperText: "To avoid an endlessly-growing single log file, you can opt for rotation"
|
|
106
|
-
},
|
|
107
|
-
{ k: 'proxies', comp: NumberField, min: 0, max: 9, sm: 6, lg: 6, label: "How many HTTP proxies between this server and users?",
|
|
108
|
-
error: proxyWarning(values, status),
|
|
109
|
-
helperText: "Wrong number will prevent detection of users' IP address"
|
|
110
|
-
},
|
|
111
|
-
{ k: 'allowed_referer', placeholder: "any",
|
|
112
|
-
helperText: values.allowed_referer ? "Leave empty to allow any" : "Use this to avoid direct links from other websites", },
|
|
113
|
-
{ k: 'zip_calculate_size_for_seconds', comp: NumberField, sm: 6, label: "Calculate ZIP size for seconds",
|
|
114
|
-
helperText: "If time is not enough, the browser will not show download percentage" },
|
|
115
|
-
{ k: 'custom_header', multiline: true, sx: { '& textarea': { fontFamily: 'monospace' } },
|
|
116
|
-
helperText: "Any HTML code here will be used as header for the Frontend"
|
|
117
|
-
},
|
|
118
|
-
{ k: 'mime', comp: StringStringField,
|
|
119
|
-
keyLabel: "Files", keyWidth: 7,
|
|
120
|
-
valueLabel: "Mime type", valueWidth: 4
|
|
121
|
-
},
|
|
122
|
-
{ k: 'block', label: "Blocked IPs", multiline: true, minRows:3, helperText: "Enter an IP address for each line. CIDR and * are supported.",
|
|
123
|
-
fromField: (all:string) => all.split('\n').map(s => s.trim()).filter(Boolean).map(ip => ({ ip })),
|
|
124
|
-
toField: (all: any) => !Array.isArray(all) ? '' : all.map(x => x?.ip).filter(Boolean).join('\n')
|
|
125
|
-
},
|
|
126
|
-
]
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
async function save() {
|
|
130
|
-
if (_.isEmpty(changes))
|
|
131
|
-
return toast("Nothing to save")
|
|
132
|
-
const loc = window.location
|
|
133
|
-
const newPort = loc.protocol === 'http:' ? changes.port : changes.https_port
|
|
134
|
-
if (newPort <= 0 && !await confirmDialog("You are switching off the server port and you will be disconnected"))
|
|
135
|
-
return
|
|
136
|
-
else if (newPort > 0 && !await confirmDialog("You are changing the port and you may be disconnected"))
|
|
137
|
-
return
|
|
138
|
-
if (loc.protocol === 'https:' && ('cert' in changes || 'private_key' in changes) && !await confirmDialog("You may disrupt https service, kicking you out"))
|
|
139
|
-
return
|
|
140
|
-
await apiCall('set_config', { values: changes })
|
|
141
|
-
if (newPort > 0) {
|
|
142
|
-
await alertDialog("You are being redirected but in some cases this may fail. Hold on tight!", 'warning')
|
|
143
|
-
return window.location.href = loc.protocol + '//' + loc.hostname + ':' + newPort + loc.pathname
|
|
144
|
-
}
|
|
145
|
-
setTimeout(reloadStatus, 'port' in changes || 'https_port' in changes ? 1000 : 0) // give some time to consider new ports
|
|
146
|
-
Object.assign(loaded!, changes) // since changes are recalculated subscribing state.config, but it depends on 'loaded' to (which cannot be subscribed), be sure to update loaded first
|
|
147
|
-
recalculateChanges()
|
|
148
|
-
toast("Changes applied", 'success')
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function recalculateChanges() {
|
|
153
|
-
const o: Dict = {}
|
|
154
|
-
if (state.config)
|
|
155
|
-
for (const [k, v] of Object.entries(state.config))
|
|
156
|
-
if (JSON.stringify(v) !== JSON.stringify(loaded?.[k]))
|
|
157
|
-
o[k] = v
|
|
158
|
-
pageState.changes = o
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function isCertError(error: any) {
|
|
162
|
-
return /certificate/.test(error)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function isKeyError(error: any) {
|
|
166
|
-
return /private key/.test(error)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function ServerPort({ label, value, onChange, getApi, status, suggestedPort=1, error, helperText }: FieldProps<number | null>) {
|
|
170
|
-
const lastCustom = useRef(suggestedPort)
|
|
171
|
-
if (value! > 0)
|
|
172
|
-
lastCustom.current = value!
|
|
173
|
-
const selectValue = Number(value! > 0 ? lastCustom.current : value) || 0
|
|
174
|
-
let errMsg = status?.error
|
|
175
|
-
if (errMsg)
|
|
176
|
-
if (isCertError(errMsg) || isKeyError(errMsg))
|
|
177
|
-
errMsg = undefined // never mind, we'll show this error elsewhere
|
|
178
|
-
else
|
|
179
|
-
error = true
|
|
180
|
-
return h(Box, {},
|
|
181
|
-
h(Box, { display: 'flex' },
|
|
182
|
-
h(SelectField as Field<number>, {
|
|
183
|
-
sx: { flexGrow: 1 },
|
|
184
|
-
label,
|
|
185
|
-
error,
|
|
186
|
-
value: selectValue,
|
|
187
|
-
options: [
|
|
188
|
-
{ label: "off", value: -1 },
|
|
189
|
-
{ label: "random", value: 0 },
|
|
190
|
-
{ label: "choose", value: lastCustom.current },
|
|
191
|
-
],
|
|
192
|
-
onChange,
|
|
193
|
-
}),
|
|
194
|
-
value! > 0 && h(NumberField, {
|
|
195
|
-
label: "Number",
|
|
196
|
-
fullWidth: false,
|
|
197
|
-
value,
|
|
198
|
-
onChange,
|
|
199
|
-
getApi,
|
|
200
|
-
error,
|
|
201
|
-
min: 1,
|
|
202
|
-
max: 65535,
|
|
203
|
-
helperText,
|
|
204
|
-
sx: { minWidth: '5.5em' }
|
|
205
|
-
}),
|
|
206
|
-
),
|
|
207
|
-
status && h(FormHelperText, { error },
|
|
208
|
-
status === true ? '...'
|
|
209
|
-
: errMsg ?? (status?.listening && "Correctly working on port " + status.port) )
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function suggestMakingCert() {
|
|
214
|
-
newDialog({
|
|
215
|
-
Content: () => h(Box, {},
|
|
216
|
-
h(Box, { display: 'flex', gap: 1 },
|
|
217
|
-
h(Info), "You are enabling HTTPs. It needs a valid certificate + private key to work."
|
|
218
|
-
),
|
|
219
|
-
h(Box, { mt: 4, display: 'flex', gap: 1, justifyContent: 'space-around', },
|
|
220
|
-
h(Button, { variant: 'contained', onClick(){
|
|
221
|
-
closeDialog()
|
|
222
|
-
makeCertAndSave().then()
|
|
223
|
-
} }, "Help me!"),
|
|
224
|
-
h(Button, { onClick: closeDialog }, "I will handle the matter myself"),
|
|
225
|
-
),
|
|
226
|
-
)
|
|
227
|
-
})
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export async function makeCertAndSave() {
|
|
231
|
-
if (!window.crypto.subtle)
|
|
232
|
-
return alertDialog("Retry this procedure on localhost", 'warning')
|
|
233
|
-
const res = await formDialog<{ commonName: string }>({
|
|
234
|
-
fields: [
|
|
235
|
-
h(Box, { display: 'flex', gap: 1 }, h(Info), "We'll generate a basic certificate for you"),
|
|
236
|
-
{ k: 'commonName', label: "Enter a domain, or leave empty" }
|
|
237
|
-
],
|
|
238
|
-
save: { children: "Continue" },
|
|
239
|
-
})
|
|
240
|
-
if (!res) return
|
|
241
|
-
const close = waitDialog()
|
|
242
|
-
try {
|
|
243
|
-
const saved = await apiCall('save_pem', await makeCert(res))
|
|
244
|
-
await apiCall('set_config', { values: saved })
|
|
245
|
-
if (loaded) // when undefined we are not in this page
|
|
246
|
-
Object.assign(loaded, saved)
|
|
247
|
-
setTimeout(exposedReloadStatus!, 1000) // give some time for backend to apply
|
|
248
|
-
Object.assign(state.config, saved)
|
|
249
|
-
await alertDialog("Certificate saved", 'success')
|
|
250
|
-
}
|
|
251
|
-
finally { close() }
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function makeCert(attributes: Record<string, string>) {
|
|
255
|
-
// this relies on having loaded node-forge/dist/forge.min.js
|
|
256
|
-
const { pki } = (window as any).forge
|
|
257
|
-
const keys = pki.rsa.generateKeyPair(2048);
|
|
258
|
-
const cert = pki.createCertificate();
|
|
259
|
-
cert.publicKey = keys.publicKey
|
|
260
|
-
cert.serialNumber = '01'
|
|
261
|
-
cert.validity.notBefore = new Date()
|
|
262
|
-
cert.validity.notAfter = new Date()
|
|
263
|
-
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1)
|
|
264
|
-
|
|
265
|
-
const attrs = Object.entries(attributes).map(x => ({ name: x[0], value: x[1] }))
|
|
266
|
-
cert.setSubject(attrs)
|
|
267
|
-
cert.setIssuer(attrs)
|
|
268
|
-
cert.sign(keys.privateKey)
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
cert: pki.certificateToPem(cert),
|
|
272
|
-
private_key: pki.privateKeyToPem(keys.privateKey),
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function with_<T,RT>(par:T, cb: (par:T) => RT) {
|
|
277
|
-
return cb(par)
|
|
278
|
-
}
|
|
279
|
-
|
package/admin/src/FileField.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { FieldProps, StringField } from '@hfs/mui-grid-form'
|
|
2
|
-
import { createElement as h } from 'react'
|
|
3
|
-
import { InputAdornment } from '@mui/material'
|
|
4
|
-
import { Eject } from '@mui/icons-material'
|
|
5
|
-
import { IconBtn, useBreakpoint } from './misc'
|
|
6
|
-
import { newDialog } from '@hfs/shared'
|
|
7
|
-
import FilePicker from './FilePicker'
|
|
8
|
-
import { apiCall } from './api'
|
|
9
|
-
|
|
10
|
-
export default function FileField({ value, onChange, files=true, folders=false, fileMask, defaultPath, title, ...props }: FieldProps<string>) {
|
|
11
|
-
const large = useBreakpoint('md')
|
|
12
|
-
return h(StringField, {
|
|
13
|
-
...props,
|
|
14
|
-
value,
|
|
15
|
-
onChange,
|
|
16
|
-
InputProps: {
|
|
17
|
-
endAdornment: h(InputAdornment, { position: 'end' },
|
|
18
|
-
h(IconBtn, {
|
|
19
|
-
icon: Eject,
|
|
20
|
-
title: "Browse files...",
|
|
21
|
-
edge: 'end',
|
|
22
|
-
onClick() {
|
|
23
|
-
const close = newDialog({
|
|
24
|
-
title: title ?? (files ? "Pick a file" : "Pick a folder"),
|
|
25
|
-
dialogProps: {
|
|
26
|
-
fullScreen: !large,
|
|
27
|
-
sx: { minWidth: 'min(90vw, 40em)', minHeight: 'calc(100vh - 9em)' }
|
|
28
|
-
},
|
|
29
|
-
Content() {
|
|
30
|
-
return h(FilePicker, {
|
|
31
|
-
multiple: false,
|
|
32
|
-
folders,
|
|
33
|
-
files,
|
|
34
|
-
fileMask,
|
|
35
|
-
from: value || defaultPath,
|
|
36
|
-
async onSelect(sel) {
|
|
37
|
-
let one = sel?.[0]
|
|
38
|
-
if (!one) return
|
|
39
|
-
const cwd = (await apiCall('get_cwd'))?.path
|
|
40
|
-
if (one.startsWith(cwd))
|
|
41
|
-
one = one.slice(cwd.length+1)
|
|
42
|
-
onChange(one, { was: value, event: 'picker' })
|
|
43
|
-
close()
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
},
|
|
49
|
-
}))
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
}
|