hfs 0.26.8 → 0.27.2
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 +15 -2
- package/admin/assets/index-509bb1d6.js +415 -0
- package/admin/assets/index-60a380a7.css +1 -0
- package/admin/assets/sha512-738f0943.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index-6e178dfd.css +1 -0
- package/frontend/assets/index-aea7654e.js +85 -0
- package/frontend/assets/sha512-bf915587.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 +4 -2
- package/package.json +2 -6
- package/plugins/vhosting/plugin.js +23 -20
- 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 +110 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +104 -0
- package/src/api.plugins.js +128 -0
- package/src/api.vfs.js +167 -0
- package/src/apiMiddleware.js +123 -0
- package/src/block.js +34 -0
- package/src/commands.js +125 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +94 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +49 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +104 -0
- package/src/index.js +57 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +195 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +183 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +30 -0
- package/src/throttler.js +91 -0
- package/src/update.js +70 -0
- package/src/util-files.js +163 -0
- package/src/util-generators.js +31 -0
- package/src/util-http.js +32 -0
- package/src/vfs.js +232 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +73 -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
package/src/watchLoad.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.watchLoad = void 0;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
11
|
+
const misc_1 = require("./misc");
|
|
12
|
+
function watchLoad(path, parser, { failedOnFirstAttempt } = {}) {
|
|
13
|
+
let doing = false;
|
|
14
|
+
let watcher;
|
|
15
|
+
const debounced = (0, misc_1.debounceAsync)(load, 500, { leading: true });
|
|
16
|
+
let retry;
|
|
17
|
+
let saving;
|
|
18
|
+
let lastStats;
|
|
19
|
+
init().then(ok => ok || (failedOnFirstAttempt === null || failedOnFirstAttempt === void 0 ? void 0 : failedOnFirstAttempt()));
|
|
20
|
+
return {
|
|
21
|
+
unwatch() {
|
|
22
|
+
watcher === null || watcher === void 0 ? void 0 : watcher.close();
|
|
23
|
+
clearTimeout(retry);
|
|
24
|
+
watcher = undefined;
|
|
25
|
+
},
|
|
26
|
+
save(...args) {
|
|
27
|
+
return Promise.resolve(saving).then(() => // wait in case another is ongoing
|
|
28
|
+
saving = promises_1.default.writeFile(...args).finally(() => // save but also keep track of the current operation
|
|
29
|
+
saving = undefined)); // clear
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
async function init() {
|
|
33
|
+
try {
|
|
34
|
+
debounced().then();
|
|
35
|
+
watcher = (0, fs_1.watch)(path, () => {
|
|
36
|
+
if (!saving)
|
|
37
|
+
debounced().then();
|
|
38
|
+
});
|
|
39
|
+
return true; // used actually just by the first invocation
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
retry = setTimeout(init, 3000); // manual watching until watch is successful
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function load() {
|
|
46
|
+
if (doing)
|
|
47
|
+
return;
|
|
48
|
+
doing = true;
|
|
49
|
+
let data;
|
|
50
|
+
try {
|
|
51
|
+
try { // I've seen watch() firing 'change' without any change, so we'll check if any change is detectable before going on
|
|
52
|
+
const stats = await promises_1.default.stat(path);
|
|
53
|
+
if (stats.mtimeMs === (lastStats === null || lastStats === void 0 ? void 0 : lastStats.mtimeMs))
|
|
54
|
+
return;
|
|
55
|
+
lastStats = stats;
|
|
56
|
+
data = await (0, misc_1.readFileBusy)(path);
|
|
57
|
+
console.debug('loaded', path);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
if (e.code === 'EPERM')
|
|
61
|
+
console.error("missing permissions on file", path); // warn user, who could be clueless about this problem
|
|
62
|
+
return; // ignore read errors
|
|
63
|
+
}
|
|
64
|
+
if (path.endsWith('.yaml'))
|
|
65
|
+
data = yaml_1.default.parse(data);
|
|
66
|
+
await parser(data);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
doing = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.watchLoad = watchLoad;
|
package/src/zip.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.zipStreamFromFolder = void 0;
|
|
8
|
+
const vfs_1 = require("./vfs");
|
|
9
|
+
const misc_1 = require("./misc");
|
|
10
|
+
const QuickZipStream_1 = require("./QuickZipStream");
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
const serveFile_1 = require("./serveFile");
|
|
16
|
+
const const_1 = require("./const");
|
|
17
|
+
async function zipStreamFromFolder(node, ctx) {
|
|
18
|
+
ctx.status = const_1.HTTP_OK;
|
|
19
|
+
ctx.mime = 'zip';
|
|
20
|
+
const name = (0, vfs_1.getNodeName)(node);
|
|
21
|
+
ctx.attachment((name || 'archive') + '.zip');
|
|
22
|
+
const filter = (0, misc_1.pattern2filter)(String(ctx.query.search || ''));
|
|
23
|
+
const { list } = ctx.query;
|
|
24
|
+
const walker = !list ? (0, vfs_1.walkNode)(node, ctx, Infinity)
|
|
25
|
+
: (async function* () {
|
|
26
|
+
for await (const el of String(list).split('*')) { // we are using * as separator because it cannot be used in a file name and doesn't need url encoding
|
|
27
|
+
const subNode = await (0, vfs_1.urlToNode)(el, ctx, node);
|
|
28
|
+
if (!subNode || !(0, vfs_1.hasPermission)(subNode, 'can_read', ctx))
|
|
29
|
+
continue;
|
|
30
|
+
if (await (0, vfs_1.nodeIsDirectory)(subNode)) { // a directory needs to walked
|
|
31
|
+
yield* (0, vfs_1.walkNode)(subNode, ctx, Infinity, el + '/');
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let folder = (0, path_1.dirname)(el);
|
|
35
|
+
folder = folder === '.' ? '' : folder + '/';
|
|
36
|
+
yield { ...subNode, name: folder + (0, vfs_1.getNodeName)(subNode) }; // reflect relative path in archive, otherwise way may have name-clashes
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
const mappedWalker = (0, misc_1.filterMapGenerator)(walker, async (el) => {
|
|
40
|
+
const { source } = el;
|
|
41
|
+
const name = (0, vfs_1.getNodeName)(el);
|
|
42
|
+
if (!source || ctx.req.aborted || !filter(name))
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
const st = await promises_1.default.stat(source);
|
|
46
|
+
if (!st || !st.isFile())
|
|
47
|
+
return;
|
|
48
|
+
return {
|
|
49
|
+
path: name,
|
|
50
|
+
size: st.size,
|
|
51
|
+
ts: st.mtime || st.ctime,
|
|
52
|
+
mode: st.mode,
|
|
53
|
+
sourcePath: source,
|
|
54
|
+
getData: () => (0, fs_1.createReadStream)(source, { start: 0, end: Math.max(0, st.size - 1) })
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (_a) { }
|
|
58
|
+
});
|
|
59
|
+
const zip = new QuickZipStream_1.QuickZipStream(mappedWalker);
|
|
60
|
+
const time = 1000 * zipSeconds.get();
|
|
61
|
+
const size = await zip.calculateSize(time);
|
|
62
|
+
ctx.response.length = size;
|
|
63
|
+
const range = (0, serveFile_1.getRange)(ctx, size); // keep var size as ctx.response.length won't preserve a NaN
|
|
64
|
+
if (ctx.status >= 400)
|
|
65
|
+
return;
|
|
66
|
+
if (range)
|
|
67
|
+
zip.applyRange(range.start, range.end);
|
|
68
|
+
ctx.body = zip;
|
|
69
|
+
ctx.req.on('close', () => zip.destroy());
|
|
70
|
+
ctx.state.archive = 'zip';
|
|
71
|
+
}
|
|
72
|
+
exports.zipStreamFromFolder = zipStreamFromFolder;
|
|
73
|
+
const zipSeconds = (0, config_1.defineConfig)('zip_calculate_size_for_seconds', 1);
|
package/admin/.DS_Store
DELETED
|
Binary file
|
package/admin/.eslintrc
DELETED
package/admin/.gitignore
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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/package.json
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
"js-sha512": "^0.8.0",
|
|
21
|
-
"react": "^18.2.0",
|
|
22
|
-
"react-dom": "^18.2.0",
|
|
23
|
-
"react-router-dom": "^6.2.1",
|
|
24
|
-
"react-window": "^1.8.6",
|
|
25
|
-
"tssrp6a": "^3.0.0",
|
|
26
|
-
"valtio": "^1.2.9",
|
|
27
|
-
"immer": "^9.0.15",
|
|
28
|
-
"web-vitals": "^2.1.4"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/node": "^16.11.21",
|
|
32
|
-
"@types/react": "^18.0.15",
|
|
33
|
-
"@types/react-dom": "^18.0.6",
|
|
34
|
-
"@types/react-window": "^1.8.5",
|
|
35
|
-
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
|
36
|
-
"vite": "^3.0.0",
|
|
37
|
-
"vite-plugin-babel-import": "github:rejetto/vite-plugin-babel-import"
|
|
38
|
-
},
|
|
39
|
-
"eslintConfig": {
|
|
40
|
-
"extends": [
|
|
41
|
-
"react-app",
|
|
42
|
-
"react-app/jest"
|
|
43
|
-
],
|
|
44
|
-
"overrides": [
|
|
45
|
-
{
|
|
46
|
-
"files": [
|
|
47
|
-
"*.ts"
|
|
48
|
-
],
|
|
49
|
-
"rules": {
|
|
50
|
-
"no-mixed-operators": "off"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
]
|
|
54
|
-
},
|
|
55
|
-
"browserslist": {
|
|
56
|
-
"production": [
|
|
57
|
-
">0.2%",
|
|
58
|
-
"not dead",
|
|
59
|
-
"not op_mini all"
|
|
60
|
-
],
|
|
61
|
-
"development": [
|
|
62
|
-
"last 1 chrome version",
|
|
63
|
-
"last 1 firefox version",
|
|
64
|
-
"last 1 safari version"
|
|
65
|
-
]
|
|
66
|
-
}
|
|
67
|
-
}
|
package/admin/src/AccountForm.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { createElement as h, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { BoolField, Form, MultiSelectField } from '@hfs/mui-grid-form'
|
|
3
|
-
import { Box, Button } from '@mui/material'
|
|
4
|
-
import { apiCall } from './api'
|
|
5
|
-
import { alertDialog } from './dialog'
|
|
6
|
-
import { isEqualLax, modifiedSx } from './misc'
|
|
7
|
-
import { Account, account2icon } from './AccountsPage'
|
|
8
|
-
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
|
|
9
|
-
|
|
10
|
-
interface FormProps { account: Account, groups: string[], done: (username: string)=>void, close: ()=>void }
|
|
11
|
-
export default function AccountForm({ account, done, groups, close }: FormProps) {
|
|
12
|
-
const [values, setValues] = useState<Account & { password?: string, password2?: string }>(account)
|
|
13
|
-
const [belongsOptions, setBelongOptions] = useState<string[]>([])
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
setValues(account)
|
|
16
|
-
setBelongOptions(groups.filter(x => x !== account.username ))
|
|
17
|
-
ref.current?.querySelector('input')?.focus()
|
|
18
|
-
}, [JSON.stringify(account)]) //eslint-disable-line
|
|
19
|
-
const add = !account.username
|
|
20
|
-
const group = !values.hasPassword
|
|
21
|
-
const ref = useRef<HTMLFormElement>()
|
|
22
|
-
return h(Form, {
|
|
23
|
-
formRef: ref,
|
|
24
|
-
values,
|
|
25
|
-
set(v, k) {
|
|
26
|
-
setValues({ ...values, [k]: v })
|
|
27
|
-
},
|
|
28
|
-
addToBar: [
|
|
29
|
-
h(Button, { onClick: close, sx: { ml: 2 } }, "Close"),
|
|
30
|
-
h(Box, { flex:1 }),
|
|
31
|
-
account2icon(values, { fontSize: 'large', sx: { p: 1 }})
|
|
32
|
-
],
|
|
33
|
-
fields: [
|
|
34
|
-
{ k: 'username', label: group ? 'Group name' : undefined, autoComplete: 'off', required: true, xl: group ? 12 : 4,
|
|
35
|
-
getError: v => v !== account.username && apiCall('get_account', { username: v }).then(() => "already used", () => false),
|
|
36
|
-
},
|
|
37
|
-
!group && { k: 'password', md: 6, xl: 4, type: 'password', autoComplete: 'new-password', required: add,
|
|
38
|
-
label: add ? "Password" : "Change password"
|
|
39
|
-
},
|
|
40
|
-
!group && { k: 'password2', md: 6, xl: 4, type: 'password', autoComplete: 'new-password', label: 'Repeat password',
|
|
41
|
-
getError: (x, { values }) => (x||'') !== (values.password||'') && "Enter same password" },
|
|
42
|
-
{ k: 'ignore_limits', comp: BoolField, xl: 6,
|
|
43
|
-
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
|
|
44
|
-
{ k: 'admin', comp: BoolField, xl: 6, fromField: (v:boolean) => v||null, label: "Permission to access Admin interface",
|
|
45
|
-
helperText: "To access THIS interface you are using right now",
|
|
46
|
-
...account.adminActualAccess && { value: true, disabled: true, helperText: "This permission is inherited" },
|
|
47
|
-
},
|
|
48
|
-
{ k: 'belongs', comp: MultiSelectField, label: "Inherits from", options: belongsOptions,
|
|
49
|
-
helperText: "Specify groups to inherit permissions from."
|
|
50
|
-
+ (belongsOptions.length ? '' : " There are no groups available, create one first.")
|
|
51
|
-
},
|
|
52
|
-
{ k: 'redirect', helperText: "If you want this account to be redirected to a specific folder/address at login time" },
|
|
53
|
-
],
|
|
54
|
-
onError: alertDialog,
|
|
55
|
-
save: {
|
|
56
|
-
sx: modifiedSx( !isEqualLax(values, account)),
|
|
57
|
-
async onClick() {
|
|
58
|
-
const { password='', password2, adminActualAccess, ...withoutPassword } = values
|
|
59
|
-
const { username } = values
|
|
60
|
-
if (add) {
|
|
61
|
-
await apiCall('add_account', withoutPassword)
|
|
62
|
-
if (password)
|
|
63
|
-
try { await apiNewPassword(username, password) }
|
|
64
|
-
catch(e) {
|
|
65
|
-
apiCall('del_account', { username }).then() // best effort, don't wait
|
|
66
|
-
throw e
|
|
67
|
-
}
|
|
68
|
-
done(username)
|
|
69
|
-
return alertDialog("Account created", 'success')
|
|
70
|
-
}
|
|
71
|
-
await apiCall('set_account', {
|
|
72
|
-
username: account.username,
|
|
73
|
-
changes: withoutPassword,
|
|
74
|
-
})
|
|
75
|
-
if (password)
|
|
76
|
-
await apiNewPassword(username, password)
|
|
77
|
-
done(username)
|
|
78
|
-
return alertDialog("Account modified", 'success')
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function apiNewPassword(username: string, password: string) {
|
|
85
|
-
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
|
|
86
|
-
const res = await createVerifierAndSalt(srp6aNimbusRoutines, username, password)
|
|
87
|
-
return apiCall('change_srp_others', { username, salt: String(res.s), verifier: String(res.v) }).catch(e => {
|
|
88
|
-
if (e.code !== 406) // 406 = server was configured to support clear text authentication
|
|
89
|
-
throw e
|
|
90
|
-
return apiCall('change_password_others', { username, newPassword: password }) // unencrypted version
|
|
91
|
-
})
|
|
92
|
-
}
|
|
@@ -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
|
-
}
|