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.
Files changed (161) hide show
  1. package/admin/assets/index.bb5198ec.js +281 -0
  2. package/admin/assets/index.dcc78777.css +1 -0
  3. package/admin/assets/sha512.9dfe82e1.js +8 -0
  4. package/admin/index.html +3 -1
  5. package/admin/{public/logo.svg → logo.svg} +0 -0
  6. package/frontend/assets/index.27a78796.js +85 -0
  7. package/frontend/assets/index.93366732.css +1 -0
  8. package/frontend/assets/sha512.6af42937.js +8 -0
  9. package/frontend/{public/fontello.css → fontello.css} +0 -0
  10. package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
  11. package/frontend/index.html +3 -1
  12. package/package.json +1 -1
  13. package/src/QuickZipStream.js +285 -0
  14. package/src/ThrottledStream.js +93 -0
  15. package/src/adminApis.js +169 -0
  16. package/src/api.accounts.js +59 -0
  17. package/src/api.auth.js +128 -0
  18. package/src/api.file_list.js +103 -0
  19. package/src/api.helpers.js +32 -0
  20. package/src/api.monitor.js +102 -0
  21. package/src/api.plugins.js +127 -0
  22. package/src/api.vfs.js +164 -0
  23. package/src/apiMiddleware.js +120 -0
  24. package/src/block.js +33 -0
  25. package/src/commands.js +124 -0
  26. package/src/config.js +168 -0
  27. package/src/connections.js +57 -0
  28. package/src/const.js +83 -0
  29. package/src/crypt.js +21 -0
  30. package/src/debounceAsync.js +48 -0
  31. package/src/events.js +9 -0
  32. package/src/frontEndApis.js +38 -0
  33. package/src/github.js +102 -0
  34. package/src/index.js +56 -0
  35. package/src/listen.js +235 -0
  36. package/src/log.js +137 -0
  37. package/src/middlewares.js +175 -0
  38. package/src/misc.js +160 -0
  39. package/src/pbkdf2.js +74 -0
  40. package/src/perm.js +181 -0
  41. package/src/plugins.js +343 -0
  42. package/src/serveFile.js +105 -0
  43. package/src/serveGuiFiles.js +113 -0
  44. package/src/sse.js +29 -0
  45. package/src/throttler.js +91 -0
  46. package/src/update.js +69 -0
  47. package/src/util-files.js +148 -0
  48. package/src/util-generators.js +30 -0
  49. package/src/util-http.js +30 -0
  50. package/src/vfs.js +230 -0
  51. package/src/watchLoad.js +73 -0
  52. package/src/zip.js +72 -0
  53. package/admin/.DS_Store +0 -0
  54. package/admin/.eslintrc +0 -8
  55. package/admin/.gitignore +0 -23
  56. package/admin/package.json +0 -67
  57. package/admin/src/AccountForm.ts +0 -92
  58. package/admin/src/AccountsPage.ts +0 -143
  59. package/admin/src/App.ts +0 -83
  60. package/admin/src/ArrayField.ts +0 -84
  61. package/admin/src/ConfigPage.ts +0 -279
  62. package/admin/src/FileField.ts +0 -52
  63. package/admin/src/FileForm.ts +0 -148
  64. package/admin/src/FilePicker.ts +0 -166
  65. package/admin/src/HomePage.ts +0 -96
  66. package/admin/src/InstalledPlugins.ts +0 -158
  67. package/admin/src/LoginRequired.ts +0 -75
  68. package/admin/src/LogoutPage.ts +0 -27
  69. package/admin/src/LogsPage.ts +0 -75
  70. package/admin/src/MainMenu.ts +0 -74
  71. package/admin/src/MenuButton.ts +0 -38
  72. package/admin/src/MonitorPage.ts +0 -200
  73. package/admin/src/OnlinePlugins.ts +0 -101
  74. package/admin/src/PermField.ts +0 -80
  75. package/admin/src/PluginsPage.ts +0 -27
  76. package/admin/src/VfsMenuBar.ts +0 -58
  77. package/admin/src/VfsPage.ts +0 -124
  78. package/admin/src/VfsTree.ts +0 -95
  79. package/admin/src/addFiles.ts +0 -59
  80. package/admin/src/api.ts +0 -246
  81. package/admin/src/dialog.ts +0 -203
  82. package/admin/src/index.css +0 -21
  83. package/admin/src/index.ts +0 -10
  84. package/admin/src/md.ts +0 -31
  85. package/admin/src/misc.ts +0 -141
  86. package/admin/src/react-app-env.d.ts +0 -1
  87. package/admin/src/reportWebVitals.ts +0 -15
  88. package/admin/src/setupTests.ts +0 -5
  89. package/admin/src/state.ts +0 -40
  90. package/admin/src/theme.ts +0 -37
  91. package/admin/tsconfig.json +0 -26
  92. package/admin/vite.config.ts +0 -32
  93. package/frontend/.DS_Store +0 -0
  94. package/frontend/.eslintrc +0 -8
  95. package/frontend/.gitignore +0 -23
  96. package/frontend/package.json +0 -51
  97. package/frontend/src/App.ts +0 -25
  98. package/frontend/src/Breadcrumbs.ts +0 -43
  99. package/frontend/src/BrowseFiles.ts +0 -141
  100. package/frontend/src/Head.ts +0 -45
  101. package/frontend/src/UserPanel.ts +0 -52
  102. package/frontend/src/api.ts +0 -78
  103. package/frontend/src/components.ts +0 -54
  104. package/frontend/src/dialog.css +0 -76
  105. package/frontend/src/dialog.ts +0 -105
  106. package/frontend/src/icons.ts +0 -46
  107. package/frontend/src/index.scss +0 -307
  108. package/frontend/src/index.ts +0 -10
  109. package/frontend/src/login.ts +0 -50
  110. package/frontend/src/menu.ts +0 -188
  111. package/frontend/src/misc.ts +0 -54
  112. package/frontend/src/options.ts +0 -52
  113. package/frontend/src/react-app-env.d.ts +0 -1
  114. package/frontend/src/reportWebVitals.ts +0 -15
  115. package/frontend/src/setupTests.ts +0 -5
  116. package/frontend/src/state.ts +0 -82
  117. package/frontend/src/useAuthorized.ts +0 -17
  118. package/frontend/src/useFetchList.ts +0 -144
  119. package/frontend/src/useTheme.ts +0 -23
  120. package/frontend/tsconfig.json +0 -26
  121. package/frontend/vite.config.ts +0 -21
  122. package/src/QuickZipStream.ts +0 -279
  123. package/src/ThrottledStream.ts +0 -98
  124. package/src/adminApis.ts +0 -161
  125. package/src/api.accounts.ts +0 -78
  126. package/src/api.auth.ts +0 -131
  127. package/src/api.file_list.ts +0 -102
  128. package/src/api.helpers.ts +0 -30
  129. package/src/api.monitor.ts +0 -106
  130. package/src/api.plugins.ts +0 -139
  131. package/src/api.vfs.ts +0 -182
  132. package/src/apiMiddleware.ts +0 -124
  133. package/src/block.ts +0 -35
  134. package/src/commands.ts +0 -122
  135. package/src/config.ts +0 -166
  136. package/src/connections.ts +0 -60
  137. package/src/const.ts +0 -57
  138. package/src/crypt.ts +0 -16
  139. package/src/debounceAsync.ts +0 -51
  140. package/src/events.ts +0 -6
  141. package/src/frontEndApis.ts +0 -17
  142. package/src/github.ts +0 -102
  143. package/src/index.ts +0 -53
  144. package/src/listen.ts +0 -220
  145. package/src/log.ts +0 -128
  146. package/src/middlewares.ts +0 -176
  147. package/src/misc.ts +0 -149
  148. package/src/pbkdf2.ts +0 -83
  149. package/src/perm.ts +0 -194
  150. package/src/plugins.ts +0 -342
  151. package/src/serveFile.ts +0 -104
  152. package/src/serveGuiFiles.ts +0 -95
  153. package/src/sse.ts +0 -29
  154. package/src/throttler.ts +0 -106
  155. package/src/update.ts +0 -67
  156. package/src/util-files.ts +0 -137
  157. package/src/util-generators.ts +0 -29
  158. package/src/util-http.ts +0 -29
  159. package/src/vfs.ts +0 -258
  160. package/src/watchLoad.ts +0 -75
  161. 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
@@ -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
- }
@@ -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
-
@@ -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
- }