hfs 0.27.2 → 0.29.1-rc1
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/README.md +3 -2
- package/admin/.DS_Store +0 -0
- package/admin/.eslintrc +8 -0
- package/admin/.gitignore +23 -0
- package/admin/index.html +1 -3
- package/admin/package.json +66 -0
- package/admin/{logo.svg → public/logo.svg} +0 -0
- package/admin/src/AccountForm.ts +95 -0
- package/admin/src/AccountsPage.ts +144 -0
- package/admin/src/App.ts +83 -0
- package/admin/src/ArrayField.ts +86 -0
- package/admin/src/ConfigPage.ts +314 -0
- package/admin/src/FileField.ts +54 -0
- package/admin/src/FileForm.ts +147 -0
- package/admin/src/FilePicker.ts +166 -0
- package/admin/src/HomePage.ts +96 -0
- package/admin/src/InstalledPlugins.ts +163 -0
- package/admin/src/LoginRequired.ts +82 -0
- package/admin/src/LogoutPage.ts +29 -0
- package/admin/src/LogsPage.ts +80 -0
- package/admin/src/MainMenu.ts +74 -0
- package/admin/src/MenuButton.ts +38 -0
- package/admin/src/MonitorPage.ts +205 -0
- package/admin/src/OnlinePlugins.ts +103 -0
- package/admin/src/PermField.ts +80 -0
- package/admin/src/PluginsPage.ts +27 -0
- package/admin/src/VfsMenuBar.ts +58 -0
- package/admin/src/VfsPage.ts +124 -0
- package/admin/src/VfsTree.ts +99 -0
- package/admin/src/addFiles.ts +59 -0
- package/admin/src/api.ts +250 -0
- package/admin/src/dialog.ts +203 -0
- package/admin/src/index.css +22 -0
- package/admin/src/index.ts +10 -0
- package/admin/src/md.ts +31 -0
- package/admin/src/misc.ts +141 -0
- package/admin/src/react-app-env.d.ts +1 -0
- package/admin/src/reportWebVitals.ts +15 -0
- package/admin/src/setupTests.ts +5 -0
- package/admin/src/state.ts +40 -0
- package/admin/src/theme.ts +37 -0
- package/admin/tsconfig.json +26 -0
- package/admin/vite.config.ts +32 -0
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +8 -0
- package/frontend/.gitignore +23 -0
- package/frontend/index.html +1 -3
- package/frontend/package.json +50 -0
- package/frontend/{fontello.css → public/fontello.css} +0 -0
- package/frontend/{fontello.woff2 → public/fontello.woff2} +0 -0
- package/frontend/src/App.ts +25 -0
- package/frontend/src/Breadcrumbs.ts +43 -0
- package/frontend/src/BrowseFiles.ts +141 -0
- package/frontend/src/Head.ts +45 -0
- package/frontend/src/UserPanel.ts +57 -0
- package/frontend/src/api.ts +93 -0
- package/frontend/src/components.ts +55 -0
- package/frontend/src/dialog.css +83 -0
- package/frontend/src/dialog.ts +125 -0
- package/frontend/src/icons.ts +46 -0
- package/frontend/src/index.scss +324 -0
- package/frontend/src/index.ts +10 -0
- package/frontend/src/login.ts +63 -0
- package/frontend/src/menu.ts +187 -0
- package/frontend/src/misc.ts +54 -0
- package/frontend/src/options.ts +56 -0
- package/frontend/src/react-app-env.d.ts +1 -0
- package/frontend/src/reportWebVitals.ts +15 -0
- package/frontend/src/setupTests.ts +5 -0
- package/frontend/src/state.ts +84 -0
- package/frontend/src/upload.ts +306 -0
- package/frontend/src/useAuthorized.ts +19 -0
- package/frontend/src/useFetchList.ts +162 -0
- package/frontend/src/useTheme.ts +23 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/vite.config.ts +21 -0
- package/package.json +8 -6
- package/plugins/vhosting/plugin.js +1 -1
- package/src/QuickZipStream.ts +279 -0
- package/src/ThrottledStream.ts +98 -0
- package/src/adminApis.ts +161 -0
- package/src/api.accounts.ts +78 -0
- package/src/api.auth.ts +142 -0
- package/src/api.file_list.ts +109 -0
- package/src/api.helpers.ts +30 -0
- package/src/api.monitor.ts +109 -0
- package/src/api.plugins.ts +141 -0
- package/src/api.vfs.ts +182 -0
- package/src/apiMiddleware.ts +131 -0
- package/src/block.ts +37 -0
- package/src/commands.ts +124 -0
- package/src/config.ts +167 -0
- package/src/connections.ts +60 -0
- package/src/const.ts +69 -0
- package/src/crypt.ts +16 -0
- package/src/debounceAsync.ts +53 -0
- package/src/events.ts +6 -0
- package/src/frontEndApis.ts +37 -0
- package/src/github.ts +107 -0
- package/src/index.ts +55 -0
- package/src/listen.ts +223 -0
- package/src/log.ts +128 -0
- package/src/middlewares.ts +183 -0
- package/src/misc.ts +176 -0
- package/src/pbkdf2.ts +83 -0
- package/src/perm.ts +197 -0
- package/src/plugins.ts +340 -0
- package/src/serveFile.ts +114 -0
- package/src/serveGuiFiles.ts +95 -0
- package/src/sse.ts +30 -0
- package/src/throttler.ts +106 -0
- package/src/update.ts +69 -0
- package/src/upload.ts +98 -0
- package/src/util-files.ts +141 -0
- package/src/util-generators.ts +31 -0
- package/src/util-http.ts +32 -0
- package/src/util-os.ts +38 -0
- package/src/vfs.ts +264 -0
- package/src/watchLoad.ts +75 -0
- package/src/zip.ts +71 -0
- package/admin/assets/index-509bb1d6.js +0 -415
- package/admin/assets/index-60a380a7.css +0 -1
- package/admin/assets/sha512-738f0943.js +0 -8
- package/frontend/assets/index-6e178dfd.css +0 -1
- package/frontend/assets/index-aea7654e.js +0 -85
- package/frontend/assets/sha512-bf915587.js +0 -8
- package/src/QuickZipStream.js +0 -285
- package/src/ThrottledStream.js +0 -93
- package/src/adminApis.js +0 -169
- package/src/api.accounts.js +0 -59
- package/src/api.auth.js +0 -128
- package/src/api.file_list.js +0 -110
- package/src/api.helpers.js +0 -32
- package/src/api.monitor.js +0 -104
- package/src/api.plugins.js +0 -128
- package/src/api.vfs.js +0 -167
- package/src/apiMiddleware.js +0 -123
- package/src/block.js +0 -34
- package/src/commands.js +0 -125
- package/src/config.js +0 -168
- package/src/connections.js +0 -57
- package/src/const.js +0 -94
- package/src/crypt.js +0 -21
- package/src/debounceAsync.js +0 -49
- package/src/events.js +0 -9
- package/src/frontEndApis.js +0 -38
- package/src/github.js +0 -104
- package/src/index.js +0 -57
- package/src/listen.js +0 -235
- package/src/log.js +0 -137
- package/src/middlewares.js +0 -195
- package/src/misc.js +0 -160
- package/src/pbkdf2.js +0 -74
- package/src/perm.js +0 -183
- package/src/plugins.js +0 -343
- package/src/serveFile.js +0 -105
- package/src/serveGuiFiles.js +0 -113
- package/src/sse.js +0 -30
- package/src/throttler.js +0 -91
- package/src/update.js +0 -70
- package/src/util-files.js +0 -163
- package/src/util-generators.js +0 -31
- package/src/util-http.js +0 -32
- package/src/vfs.js +0 -232
- package/src/watchLoad.js +0 -73
- package/src/zip.js +0 -73
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ You won't find all previous features here (yet), but still we got:
|
|
|
34
34
|
- search
|
|
35
35
|
- accounts
|
|
36
36
|
- resumable downloads
|
|
37
|
-
-
|
|
37
|
+
- resumable uploads
|
|
38
38
|
- download folders as zip archive
|
|
39
39
|
- simple website serving
|
|
40
40
|
- plug-ins
|
|
@@ -145,7 +145,7 @@ an *env* called `HFS_CONFIG`. Any relative path provided is relative to the *cwd
|
|
|
145
145
|
- `https_port` listen on a specific port. Default is 443.
|
|
146
146
|
- `cert` use this file for https certificate. Minimum to start https is to give a cert and a private_key. Default is none.
|
|
147
147
|
- `private_key` use this file for https private key. Default is none.
|
|
148
|
-
- `allowed_referer` you can decide what domains can link to your files. Wildcards supported. Default is any.
|
|
148
|
+
- `allowed_referer` you can decide what domains can link to your files. Wildcards supported. Default is empty, meaning any.
|
|
149
149
|
- `block` a list of rules that will block connections. E.g.:
|
|
150
150
|
```
|
|
151
151
|
block:
|
|
@@ -157,6 +157,7 @@ an *env* called `HFS_CONFIG`. Any relative path provided is relative to the *cwd
|
|
|
157
157
|
- `custom_header` provide HTML code to be put at the top of your Frontend. Default is none.
|
|
158
158
|
- `localhost_admin` should Admin be accessed without credentials when on localhost. Default is true.
|
|
159
159
|
- `proxies` number of proxies between server and clients to be trusted about providing clients' IP addresses. Default is 0.
|
|
160
|
+
- `keep_unfinished_uploads` should unfinished uploads be deleted immediately when interrupted. Default is true.
|
|
160
161
|
|
|
161
162
|
#### Virtual File System (VFS)
|
|
162
163
|
|
package/admin/.DS_Store
ADDED
|
Binary file
|
package/admin/.eslintrc
ADDED
package/admin/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.js
|
|
7
|
+
|
|
8
|
+
# testing
|
|
9
|
+
/coverage
|
|
10
|
+
|
|
11
|
+
# production
|
|
12
|
+
/build
|
|
13
|
+
|
|
14
|
+
# misc
|
|
15
|
+
.DS_Store
|
|
16
|
+
.env.local
|
|
17
|
+
.env.development.local
|
|
18
|
+
.env.test.local
|
|
19
|
+
.env.production.local
|
|
20
|
+
|
|
21
|
+
npm-debug.log*
|
|
22
|
+
yarn-debug.log*
|
|
23
|
+
yarn-error.log*
|
package/admin/index.html
CHANGED
|
@@ -7,11 +7,9 @@
|
|
|
7
7
|
<script async src='https://unpkg.com/node-forge@1.3.1/dist/forge.min.js'></script>
|
|
8
8
|
<script>SESSION = _HFS_SESSION_</script>
|
|
9
9
|
<title>HFS Admin</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-509bb1d6.js"></script>
|
|
11
|
-
<link rel="stylesheet" href="/assets/index-60a380a7.css">
|
|
12
10
|
</head>
|
|
13
11
|
<body>
|
|
14
12
|
<div id="root"></div>
|
|
15
|
-
|
|
13
|
+
<script type="module" src="/src/index.ts"></script>
|
|
16
14
|
</body>
|
|
17
15
|
</html>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hfs/admin",
|
|
3
|
+
"private": true,
|
|
4
|
+
"proxy": "http://localhost",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "vite",
|
|
7
|
+
"build": "tsc && vite build",
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"test-dep": "npm audit --production"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@emotion/react": "^11.10.0",
|
|
13
|
+
"@hfs/mui-grid-form": "*",
|
|
14
|
+
"@hfs/shared": "*",
|
|
15
|
+
"@emotion/styled": "^11.10.0",
|
|
16
|
+
"@mui/icons-material": "^5.8.4",
|
|
17
|
+
"@mui/lab": "^5.0.0-alpha.94",
|
|
18
|
+
"@mui/material": "^5.10.0",
|
|
19
|
+
"@mui/x-data-grid": "^5.15.1",
|
|
20
|
+
"react": "^18.2.0",
|
|
21
|
+
"react-dom": "^18.2.0",
|
|
22
|
+
"react-router-dom": "^6.2.1",
|
|
23
|
+
"react-window": "^1.8.6",
|
|
24
|
+
"tssrp6a": "^3.0.0",
|
|
25
|
+
"valtio": "^1.2.9",
|
|
26
|
+
"immer": "^9.0.15",
|
|
27
|
+
"web-vitals": "^2.1.4"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^16.11.21",
|
|
31
|
+
"@types/react": "^18.0.15",
|
|
32
|
+
"@types/react-dom": "^18.0.6",
|
|
33
|
+
"@types/react-window": "^1.8.5",
|
|
34
|
+
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
|
35
|
+
"vite": "^4.0.4",
|
|
36
|
+
"vite-plugin-babel-import": "github:rejetto/vite-plugin-babel-import"
|
|
37
|
+
},
|
|
38
|
+
"eslintConfig": {
|
|
39
|
+
"extends": [
|
|
40
|
+
"react-app",
|
|
41
|
+
"react-app/jest"
|
|
42
|
+
],
|
|
43
|
+
"overrides": [
|
|
44
|
+
{
|
|
45
|
+
"files": [
|
|
46
|
+
"*.ts"
|
|
47
|
+
],
|
|
48
|
+
"rules": {
|
|
49
|
+
"no-mixed-operators": "off"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"browserslist": {
|
|
55
|
+
"production": [
|
|
56
|
+
">0.2%",
|
|
57
|
+
"not dead",
|
|
58
|
+
"not op_mini all"
|
|
59
|
+
],
|
|
60
|
+
"development": [
|
|
61
|
+
"last 1 chrome version",
|
|
62
|
+
"last 1 firefox version",
|
|
63
|
+
"last 1 safari version"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
+
|
|
3
|
+
import { createElement as h, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { BoolField, Form, MultiSelectField } from '@hfs/mui-grid-form'
|
|
5
|
+
import { Box, Button } from '@mui/material'
|
|
6
|
+
import { apiCall } from './api'
|
|
7
|
+
import { alertDialog } from './dialog'
|
|
8
|
+
import { isEqualLax, modifiedSx } from './misc'
|
|
9
|
+
import { Account, account2icon } from './AccountsPage'
|
|
10
|
+
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
|
|
11
|
+
|
|
12
|
+
interface FormProps { account: Account, groups: string[], done: (username: string)=>void, close: ()=>void }
|
|
13
|
+
export default function AccountForm({ account, done, groups, close }: FormProps) {
|
|
14
|
+
const [values, setValues] = useState<Account & { password?: string, password2?: string }>(account)
|
|
15
|
+
const [belongsOptions, setBelongOptions] = useState<string[]>([])
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setValues(account)
|
|
18
|
+
setBelongOptions(groups.filter(x => x !== account.username ))
|
|
19
|
+
ref.current?.querySelector('input')?.focus()
|
|
20
|
+
}, [JSON.stringify(account)]) //eslint-disable-line
|
|
21
|
+
const add = !account.username
|
|
22
|
+
const group = !values.hasPassword
|
|
23
|
+
const ref = useRef<HTMLFormElement>()
|
|
24
|
+
return h(Form, {
|
|
25
|
+
formRef: ref,
|
|
26
|
+
values,
|
|
27
|
+
set(v, k) {
|
|
28
|
+
setValues({ ...values, [k]: v })
|
|
29
|
+
},
|
|
30
|
+
addToBar: [
|
|
31
|
+
h(Button, { onClick: close, sx: { ml: 2 } }, "Close"),
|
|
32
|
+
h(Box, { flex:1 }),
|
|
33
|
+
account2icon(values, { fontSize: 'large', sx: { p: 1 }})
|
|
34
|
+
],
|
|
35
|
+
fields: [
|
|
36
|
+
{ k: 'username', label: group ? 'Group name' : undefined, autoComplete: 'off', required: true, xl: group ? 12 : 4,
|
|
37
|
+
getError: v => v !== account.username && apiCall('get_account', { username: v })
|
|
38
|
+
.then(got => got.username === account.username ? "usernames are case-insensitive" : "already used", () => false),
|
|
39
|
+
},
|
|
40
|
+
!group && { k: 'password', md: 6, xl: 4, type: 'password', autoComplete: 'new-password', required: add,
|
|
41
|
+
label: add ? "Password" : "Change password"
|
|
42
|
+
},
|
|
43
|
+
!group && { k: 'password2', md: 6, xl: 4, type: 'password', autoComplete: 'new-password', label: 'Repeat password',
|
|
44
|
+
getError: (x, { values }) => (x||'') !== (values.password||'') && "Enter same password" },
|
|
45
|
+
{ k: 'ignore_limits', comp: BoolField, xl: 6,
|
|
46
|
+
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
|
|
47
|
+
{ k: 'admin', comp: BoolField, xl: 6, fromField: (v:boolean) => v||null, label: "Permission to access Admin-panel",
|
|
48
|
+
helperText: "To access THIS interface you are using right now",
|
|
49
|
+
...!account.admin && account.adminActualAccess && { value: true, helperText: "This permission is inherited" },
|
|
50
|
+
},
|
|
51
|
+
{ k: 'belongs', comp: MultiSelectField, label: "Inherits from", options: belongsOptions,
|
|
52
|
+
helperText: "Specify groups to inherit permissions from."
|
|
53
|
+
+ (belongsOptions.length ? '' : " There are no groups available, create one first.")
|
|
54
|
+
},
|
|
55
|
+
{ k: 'redirect', helperText: "If you want this account to be redirected to a specific folder/address at login time" },
|
|
56
|
+
],
|
|
57
|
+
onError: alertDialog,
|
|
58
|
+
save: {
|
|
59
|
+
sx: modifiedSx( !isEqualLax(values, account)),
|
|
60
|
+
async onClick() {
|
|
61
|
+
const { password='', password2, adminActualAccess, ...withoutPassword } = values
|
|
62
|
+
const { username } = values
|
|
63
|
+
if (add) {
|
|
64
|
+
const got = await apiCall('add_account', withoutPassword)
|
|
65
|
+
if (password)
|
|
66
|
+
try { await apiNewPassword(username, password) }
|
|
67
|
+
catch(e) {
|
|
68
|
+
apiCall('del_account', { username }).then() // best effort, don't wait
|
|
69
|
+
throw e
|
|
70
|
+
}
|
|
71
|
+
done(got.username)
|
|
72
|
+
return alertDialog("Account created", 'success')
|
|
73
|
+
}
|
|
74
|
+
const got = await apiCall('set_account', {
|
|
75
|
+
username: account.username,
|
|
76
|
+
changes: withoutPassword,
|
|
77
|
+
})
|
|
78
|
+
if (password)
|
|
79
|
+
await apiNewPassword(username, password)
|
|
80
|
+
done(got.username)
|
|
81
|
+
return alertDialog("Account modified", 'success')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function apiNewPassword(username: string, password: string) {
|
|
88
|
+
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
|
|
89
|
+
const res = await createVerifierAndSalt(srp6aNimbusRoutines, username, password)
|
|
90
|
+
return apiCall('change_srp_others', { username, salt: String(res.s), verifier: String(res.v) }).catch(e => {
|
|
91
|
+
if (e.code !== 406) // 406 = server was configured to support clear text authentication
|
|
92
|
+
throw e
|
|
93
|
+
return apiCall('change_password_others', { username, newPassword: password }) // unencrypted version
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// This file is part of HFS - Copyright 2021-2023, 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
|
+
admin?: boolean
|
|
18
|
+
adminActualAccess?: boolean
|
|
19
|
+
ignore_limits?: boolean
|
|
20
|
+
redirect?: string
|
|
21
|
+
belongs?: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function AccountsPage() {
|
|
25
|
+
const { data, reload, element } = useApiEx('get_accounts')
|
|
26
|
+
const [sel, setSel] = useState<string[] | 'new-group' | 'new-user'>([])
|
|
27
|
+
const selectionMode = Array.isArray(sel)
|
|
28
|
+
useEffect(() => { // if accounts are reloaded, review the selection to remove elements that don't exist anymore
|
|
29
|
+
if (Array.isArray(data?.list) && selectionMode)
|
|
30
|
+
setSel( sel.filter(u => data.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
|
|
31
|
+
}, [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
|
|
32
|
+
if (element)
|
|
33
|
+
return element
|
|
34
|
+
const { list }: { list: Account[] } = data
|
|
35
|
+
return h(Grid, { container: true, maxWidth: '80em' },
|
|
36
|
+
h(Grid, { item: true, xs: 12 },
|
|
37
|
+
h(Box, {
|
|
38
|
+
display: 'flex',
|
|
39
|
+
flexWrap: 'wrap',
|
|
40
|
+
gap: 2,
|
|
41
|
+
mb: 2,
|
|
42
|
+
sx: {
|
|
43
|
+
position: 'sticky',
|
|
44
|
+
top: 0,
|
|
45
|
+
zIndex: 2,
|
|
46
|
+
backgroundColor: 'background.paper',
|
|
47
|
+
width: 'fit-content',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
h(MenuButton, {
|
|
51
|
+
variant: 'contained',
|
|
52
|
+
startIcon: h(PersonAdd),
|
|
53
|
+
items: [
|
|
54
|
+
{ children: "user", onClick: () => setSel('new-user') },
|
|
55
|
+
{ children: "group", onClick: () => setSel('new-group') }
|
|
56
|
+
]
|
|
57
|
+
}, 'Add'),
|
|
58
|
+
h(Button, {
|
|
59
|
+
disabled: !selectionMode || !sel.length,
|
|
60
|
+
startIcon: h(Delete),
|
|
61
|
+
async onClick(){
|
|
62
|
+
if (!selectionMode) return
|
|
63
|
+
if (!await confirmDialog(`You are going to delete ${sel.length} account(s)`))
|
|
64
|
+
return
|
|
65
|
+
const errors = onlyTruthy(await Promise.all(sel.map(username =>
|
|
66
|
+
apiCall('del_account', { username }).then(() => null, () => username) )))
|
|
67
|
+
if (errors.length)
|
|
68
|
+
return alertDialog(errors.length === sel.length ? "Request failed" : hList("Some accounts were not deleted", errors), 'error')
|
|
69
|
+
reload()
|
|
70
|
+
}
|
|
71
|
+
}, "Remove"),
|
|
72
|
+
h(Button, { onClick: reload, startIcon: h(Refresh) }, "Reload"),
|
|
73
|
+
list.length > 0 && h(Typography, { p: 1 }, `${list.length} account(s)`),
|
|
74
|
+
) ),
|
|
75
|
+
h(Grid, { item: true, md: 5 },
|
|
76
|
+
!list.length && h(Alert, { severity: 'info' }, md`To access administration _remotely_ you will need to create a user account with admin permission`),
|
|
77
|
+
h(TreeView, {
|
|
78
|
+
multiSelect: true,
|
|
79
|
+
sx: { pr: 4, pb: 2, minWidth: '15em' },
|
|
80
|
+
selected: selectionMode ? sel : [],
|
|
81
|
+
onNodeSelect(ev, ids) {
|
|
82
|
+
setSel(ids)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
list.map((ac: Account) =>
|
|
86
|
+
h(TreeItem, {
|
|
87
|
+
key: ac.username,
|
|
88
|
+
nodeId: ac.username,
|
|
89
|
+
label: h(Box, {
|
|
90
|
+
sx: {
|
|
91
|
+
display: 'flex',
|
|
92
|
+
flexWrap: 'wrap',
|
|
93
|
+
padding: '.2em 0',
|
|
94
|
+
gap: '.5em',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
account2icon(ac),
|
|
99
|
+
ac.adminActualAccess && iconTooltip(MilitaryTech, "Can login into Admin"),
|
|
100
|
+
ac.username,
|
|
101
|
+
Boolean(ac.belongs?.length) && h(Box, { sx: { color: 'text.secondary', fontSize: 'small' } },
|
|
102
|
+
'(', ac.belongs?.join(', '), ')')
|
|
103
|
+
),
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
),
|
|
108
|
+
sel.length > 0 // this clever test is true both when some accounts are selected and when we are in "new account" modes
|
|
109
|
+
&& h(Grid, { item: true, md: 7 },
|
|
110
|
+
h(Card, {},
|
|
111
|
+
h(CardContent, {},
|
|
112
|
+
selectionMode && sel.length > 1 ? h(Box, {},
|
|
113
|
+
h(Typography, {}, sel.length + " selected"),
|
|
114
|
+
h(List, {},
|
|
115
|
+
sel.map(username =>
|
|
116
|
+
h(ListItem, { key: username },
|
|
117
|
+
h(ListItemText, {}, username))))
|
|
118
|
+
) : h(AccountForm, {
|
|
119
|
+
account: selectionMode && list.find(x => x.username === sel[0])
|
|
120
|
+
|| { username: '', hasPassword: sel === 'new-user' },
|
|
121
|
+
groups: list.filter(x => !x.hasPassword).map( x => x.username ),
|
|
122
|
+
close(){ setSel([]) },
|
|
123
|
+
done(username) {
|
|
124
|
+
setSel([username])
|
|
125
|
+
reload()
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
)))
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hList(heading: string, list: any[]) {
|
|
133
|
+
return h(Fragment, {},
|
|
134
|
+
heading>'' && h(Typography, {}, heading),
|
|
135
|
+
h(List, {},
|
|
136
|
+
list.map((text,key) =>
|
|
137
|
+
h(ListItem, { key },
|
|
138
|
+
typeof text === 'string' ? h(ListItemText, {}, text) : text) ))
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function account2icon(ac: Account, props={}) {
|
|
143
|
+
return h(ac.hasPassword ? Person : Group, props)
|
|
144
|
+
}
|
package/admin/src/App.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// This file is part of HFS - Copyright 2021-2023, 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
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
+
|
|
3
|
+
import { createElement as h, Fragment, useMemo } from 'react'
|
|
4
|
+
import { IconBtn, setHidden } from './misc'
|
|
5
|
+
import { Add, Edit, Delete } from '@mui/icons-material'
|
|
6
|
+
import { confirmDialog, formDialog } from './dialog'
|
|
7
|
+
import { DataGrid, GridAlignment } from '@mui/x-data-grid'
|
|
8
|
+
import { FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
|
|
9
|
+
import { Box, FormHelperText, FormLabel } from '@mui/material'
|
|
10
|
+
|
|
11
|
+
export function ArrayField<T=any>({ label, helperText, fields, value, onChange, onError, getApi, ...rest }: FieldProps<T[]> & { fields: FieldDescriptor[], height?: number }) {
|
|
12
|
+
const rows = useMemo(() => (value||[]).map((x,$idx) =>
|
|
13
|
+
setHidden({ ...x } as any, 'id' in x ? { $idx } : { id: $idx })),
|
|
14
|
+
[JSON.stringify(value)]) //eslint-disable-line
|
|
15
|
+
const columns = useMemo(() => {
|
|
16
|
+
return [
|
|
17
|
+
...fields.map(f => ({
|
|
18
|
+
field: f.k,
|
|
19
|
+
headerName: f.headerName ?? (typeof f.label === 'string' ? f.label : labelFromKey(f.k)),
|
|
20
|
+
disableColumnMenu: true,
|
|
21
|
+
...f.$width >= 8 ? { width: f.$width } : { flex: f.$width || 1 },
|
|
22
|
+
...f.$column,
|
|
23
|
+
})),
|
|
24
|
+
{
|
|
25
|
+
field: '',
|
|
26
|
+
width: 80,
|
|
27
|
+
disableColumnMenu: true,
|
|
28
|
+
sortable: false,
|
|
29
|
+
align: 'center' as GridAlignment,
|
|
30
|
+
headerAlign: 'center' as GridAlignment,
|
|
31
|
+
renderHeader(){
|
|
32
|
+
return h(IconBtn, {
|
|
33
|
+
icon: Add,
|
|
34
|
+
title: "Add",
|
|
35
|
+
onClick: (event:any) =>
|
|
36
|
+
formDialog({ fields }).then(o => // @ts-ignore
|
|
37
|
+
o && onChange([...value||[], o], { was: value, event }))
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
renderCell({ row }: any) {
|
|
41
|
+
const { $idx=row.id } = row
|
|
42
|
+
return h('div', {},
|
|
43
|
+
h(IconBtn, {
|
|
44
|
+
icon: Edit,
|
|
45
|
+
title: "Modify",
|
|
46
|
+
onClick: (event:any) =>
|
|
47
|
+
formDialog({ fields, values: row }).then(newRec => {
|
|
48
|
+
if (!newRec) return
|
|
49
|
+
const newValue = value!.map((oldRec, i) => i === $idx ? newRec : oldRec)
|
|
50
|
+
onChange(newValue, { was: value, event })
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
h(IconBtn, {
|
|
54
|
+
icon: Delete,
|
|
55
|
+
title: "Delete",
|
|
56
|
+
onClick: (event:any) =>
|
|
57
|
+
confirmDialog("Delete?").then(ok => {
|
|
58
|
+
if (!ok) return
|
|
59
|
+
const newValue = value!.filter((rec, i) => i !== $idx)
|
|
60
|
+
onChange(newValue, { was: value, event })
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}, [fields, value, onChange])
|
|
68
|
+
return h(Fragment, {},
|
|
69
|
+
label && h(FormLabel, { sx: { ml: 1 } }, label),
|
|
70
|
+
helperText && h(FormHelperText, {}, helperText),
|
|
71
|
+
h(Box, { height: '20em', ...rest },
|
|
72
|
+
h(DataGrid, {
|
|
73
|
+
columns,
|
|
74
|
+
rows,
|
|
75
|
+
hideFooterSelectedRowCount: true,
|
|
76
|
+
hideFooter: true,
|
|
77
|
+
componentsProps: {
|
|
78
|
+
pagination: {
|
|
79
|
+
showFirstButton: true,
|
|
80
|
+
showLastButton: true,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
}
|