@samitouri / QOSami-HFS / commits / 39fe1fde

new "admin" permission value

Massimo Melina committed May 13, 2026 at 13:56 UTC 39fe1fdec1759b9b1ee2e6688e9c62013d234e15
6 files changed +50 -46
admin/src/FileForm.ts
+13 -11
@@ -8,9 +8,9 @@ import {
8 } from '@hfs/mui-grid-form'
9 import { apiCall, UseApi, useApiEx } from './api'
10 import {
11 - basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys,
12 - onlyTruthy, prefix, VfsPerms, wantArray, WhoVfs, WhoObject, matches, xlate, md, Callback, MASK_IN_TESTS,
13 - useRequestRender, splitAt, IMAGE_FILEMASK, copyTextToClipboard, normalizeHost, CFG, try_, WHO_ANY_ACCOUNT,
11 + basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys, useRequestRender, try_,
12 + onlyTruthy, prefix, VfsPerms, wantArray, WhoVfs, WhoObject, matches, xlate, md, Callback, copyTextToClipboard,
13 + normalizeHost, splitAt, IMAGE_FILEMASK, CFG, MASK_IN_TESTS, WHO_ANY_ACCOUNT, WHO_ADMIN, WHO_NO_ONE, WHO_ANYONE,
14 } from './misc'
15 import { isModifiedConfig } from './AccountForm'
16 import { Btn, Flex, IconBtn, LinkBtn, propsForModifiedValues, useBreakpoint, wikiLink } from './mui'
@@ -231,7 +231,7 @@ export default function FileForm({ file, addToBar, statusApi, accountsApi, saved
231 while (typeof inherit === 'string' && _.get(show, inherit) === false) // is 'inherit' referring to another permission that is not displayed?
232 inherit = _.get(values, inherit)
233 // non-permission who values (like WHO_ANY_ACCOUNT) are not valid keys for inherited lookup
234 - ?? (inherit !== WHO_ANY_ACCOUNT ? getInheritedPerms(file)?.[inherit] : undefined)
234 + ?? (inherit !== WHO_ANY_ACCOUNT && inherit !== WHO_ADMIN ? getInheritedPerms(file)?.[inherit] : undefined)
235 ?? _.get(defaultPerms, inherit)! // then show its value instead
236 return {
237 comp: WhoField,
@@ -282,9 +282,10 @@ export function WhoField({ value, onChange, parent, inherit, accountsApi, helper
282 const options = useMemo(() =>
283 onlyTruthy([
284 offerInheritance && { value: null, label: defaultLabel },
285 - { value: true },
286 - { value: false },
287 - { value: '*' },
285 + { value: WHO_NO_ONE },
286 + { value: WHO_ANY_ACCOUNT },
287 + { value: WHO_ADMIN },
288 + { value: WHO_ANYONE },
289 ...otherPerms || [],
290 { value: [], label: "Select accounts" },
291 ].map(x => x && !hideValues?.includes(x.value)
@@ -346,10 +347,11 @@ export function WhoField({ value, onChange, parent, inherit, accountsApi, helper
347 function who2desc(who: any) {
348 return who === false ? "No one"
349 : who === true ? "Anyone"
349 - : who === '*' ? "Any logged-in account"
350 - : Array.isArray(who) ? who.join(', ')
351 - : typeof who === 'string' ? `As "can ${perm2word(who)}"`
352 - : "*UNKNOWN*" + JSON.stringify(who)
350 + : who === WHO_ANY_ACCOUNT ? "Any logged-in account"
351 + : who === WHO_ADMIN ? "Any admin"
352 + : Array.isArray(who) ? who.join(', ')
353 + : typeof who === 'string' ? `As "can ${perm2word(who)}"`
354 + : "*UNKNOWN*" + JSON.stringify(who)
355 }
356
357 interface LinkFieldProps extends FieldProps<string> {
admin/src/OptionsPage.ts
+3 -3
@@ -153,12 +153,12 @@ export default function OptionsPage() {
153 { k : CFG.max_downloads_per_ip, ...maxDownloadsDefaults, label: "Max downloads per-IP" },
154 { k : CFG.max_downloads_per_account, ...maxDownloadsDefaults, label: "Max downloads per-account", helperText: "Overrides other limits" },
155
156 - { k: 'admin_net', comp: NetmaskField, label: "Admin-panel accessible from", placeholder: "any address",
156 + { k: 'admin_net', comp: NetmaskField, xs: 12, sm: 6, label: "Admin-panel accessible from", placeholder: "any address",
157 helperText: "IP address of browser machine"
158 },
159 - { k: 'localhost_admin', comp: BoolField, xs: 12, sm: 6, label: "Unprotected Admin-panel on localhost",
159 + { k: 'localhost_admin', comp: BoolField, xs: 12, sm: 6, label: "Consider localhost access as Admin",
160 getError: x => !x && admins?.length===0 && "First create at least one admin account",
161 - helperText: "Access without entering credentials"
161 + helperText: "Access admin-panel without entering credentials"
162 },
163
164 { k: 'proxies', comp: NumberField, xs: 12, sm: 4, md: 4, max: 9, label: "Number of incoming HTTP proxies", placeholder: "none",
config.md
+20 -19
@@ -48,6 +48,7 @@ Some properties use a `Who` descriptor, with one of these values:
48 - `true`: anyone can, even people who didn't log in.
49 - `false`: no one can.
50 - `"*"`: any account can, i.e. anyone who logged in.
51 +- `"admin"`: any account with admin-panel access.
52 - `[ frank, peter ]`: the list of accounts who can.
53
54 - `port` where to accept http connections. Default is 80.
@@ -90,7 +91,7 @@ Some properties use a `Who` descriptor, with one of these values:
91 - `force_lang` force translation for frontend. Default is none, meaning *let browser decide*.
92 - `admin_net` net-mask specifying what addresses are allowed to access Admin-panel. Default is any.
93 - `title` text displayed in the tab of your browser. Default is "File server".
93 -- `file_menu_on_link` if to display file-menu when clicking on link, or have a dedicated button instead. Default is true.
94 +- `file_menu_on_link` if to display file-menu when clicking on a link, or have a dedicated button instead. Default is true.
95 - `min_available_mb` refuse to accept uploads if available disk space is below this threshold. Default is 100.
96 - `dont_overwrite_uploading` uploading a file with name already present in the folder will have a number appended in the name if this is enabled.
97 Default is true. Affects the frontend only, but you can get the same effect using the `?existing=rename` in the url.
@@ -108,14 +109,14 @@ Some properties use a `Who` descriptor, with one of these values:
109 - You can set `attr` to use only file's attributes, or `attr+ion` to store in file's attributes but fall-back to `descript.ion` as read-only.`
110 - `descript_ion_encoding` text encoding to be used for file `descript.ion`. [List of supported values](https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings). Default is `utf8`.
111 - `server_code` javascript code that works similarly to [a plugin](dev-plugins.md).
111 -- `tiles_size` starting value for frontend's tiles size. Default is 0.
112 +- `tiles_size` starting value for frontend's tile size. Default is 0.
113 - `auto_play_seconds` starting value for frontend's auto_play_seconds (used in Show). Default is 5.
114 - `theme` starting value for theme. Default is "auto".
115 - `sort_by` starting value for sort-by. Values can be: name, extension, size, time. Default is "name".
116 - `sort_numerics` starting value for sort-numeric-names. Default is false.
117 - `folders_first` starting value for sort-folders-first. Default is true.
118 - `invert_order` starting value for invert-order. Default is false.
118 -- `show_uploader` who can see who uploaded files. Value is a `Who` descriptor. Default is false.
119 +- `show_uploader` who can see who uploaded files. Value is a `Who` descriptor. Default is `"admin"`.
120 - `update_to_beta` includes beta versions searching for updates. Default is false.
121 - `roots` maps hosts (or mask of hosts) to a root different from the home folder. Default is none. E.g.
122 ```
@@ -125,13 +126,13 @@ Some properties use a `Who` descriptor, with one of these values:
126 ```
127 - `max_downloads` limit the number of concurrent downloads on the whole server. Default is unlimited.
128 - `max_downloads_per_ip` limit the number of concurrent downloads for the same IP address. Default is unlimited.
128 -- `max_downloads_per_account` limit the number of concurrent downloads for each account. This is enforced only for connections that are logged in, and will override other similar settings. Default is unlimited.
129 -- `geo_enable` when enabled, country is determined for each request/connection. Necessary database will be downloaded every month (2MB).
130 -- `geo_allow` set true if `geo_list` should be treated as white-list, set false for black-list. Default will ignore the list.
131 -- `geo_list` list of country codes to be used as white-list or black-list. Default is none.
129 +- `max_downloads_per_account` limit the number of concurrent downloads for each account. This is enforced only for connections that are logged in and will override other similar settings. Default is unlimited.
130 +- `geo_enable` when enabled, country is determined for each request/connection. The necessary database will be downloaded every month (2MB).
131 +- `geo_allow` set true if `geo_list` should be treated as whitelist, set false for blacklist. Default will ignore the list.
132 +- `geo_list` list of country codes to be used as whitelist or blacklist. Default is none.
133 - `geo_allow_unknown` set false to disconnect connections for which country cannot be determined. Works only if `geo_allow` is set. Default is true.
134 - `dynamic_dns_url` URL to be requested to keep a domain updated with your latest IP address.
134 - Optionally, you can append “>” followed by a regular expression to determine a successful answer, otherwise status code will be used.
135 + Optionally, you can append “>” followed by a regular expression to determine a successful answer; otherwise status code will be used.
136 Multiple URLs are supported, and you can specify one for each line.
137 - `outbound_proxy` if you need outgoing http(s) requests to pass through an HTTP proxy. E.g.: `http://user:password@localhost:8888`. Default is none.
138 Setting one will trigger a test request to google.com. You can skip this with env HFS_SKIP_PROXY_TEST=1 .
@@ -179,24 +180,24 @@ Valid keys in a node are:
180 - `accept`: valid only on upload folders; not enforced, just hinting the browser. E.g. `.zip,.rar`
181 - `default`: use this with a folder where you want to serve a file, instead of the standard page with the list of files.
182 The value must be the name of the file to serve. E.g.: `index.html`.
182 - The value must be an absolute or relative path in the VFS, not a path on disk. It works also with other type of files.
183 + The value must be an absolute or relative path in the VFS, not a path on the disk. It works also with other types of files.
184 Using this will make `mime` default to "auto".
185 - `can_read`: specify who can download this entry. Value is a `Who` descriptor, or a VFS-specific extension. Default is `true`.
186
187 VFS permissions also accept these extra forms:
187 - - `can_SOMETHING`: copy the permission from another permission. This is convenient to have same value for different permissions. E.g. `can_see`
188 + - `can_SOMETHING`: copy the permission from another permission. This is convenient to have the same value for different permissions. E.g. `can_see`
189 - `{ this?: ..., children?: ... }`: this form is useful only for folders. Each value uses the same permission descriptor. By using it, you can have
189 - different permission for the folder itself and its children. For example, having only the `this` property
190 + different permissions for the folder itself and its children. For example, having only the `this` property
191 will make the permission limited to the folder and not be inherited by children. Otherwise, having only
191 - the `children` will make the permission have no effect on the folder, but only on its content.
192 + the `children` will make the permission have no effect on the folder but only on its content.
193 - `this` specifies permission for this folder
194 - `children` specifies permission for the content.
194 -- `can_see`: specify who can see this element. Even if a user can download you can still make the file not appear in the list.
195 +- `can_see`: specify who can see this element. Even if a user can download, you can still make the file not appear in the list.
196 Value uses the same permission descriptor described above. Default is `can_read`.
196 -- `can_upload`: specify who can upload. Applies to folders with a source. Default is none.
197 +- `can_upload`: specify who can upload. Applies to folders with a source. Default is `"admin"`.
198 - `can_list`: specify who can see the content of a folder. Default is `can_read`.
199 - `can_archive`: specify who can get the zip a folder or a set of files. Default is `can_read`.
199 -- `can_delete`: specify who can delete. Applies to folders with a source. Default is none.
200 +- `can_delete`: specify who can delete. Applies to folders with a source. Default is `"admin"`.
201 - `masks`: maps a file mask to a set of properties as the one documented in this section. E.g.
202 ```
203 myfile.txt:
@@ -212,8 +213,8 @@ Valid keys in a node are:
213 If the mask ends with `|files|`, then it will match only files and not folders.
214 You can get the opposite effect with suffix `|folders|`.
215
215 -Permissions set on an inner element will override inherited permissions. This means that you can restrict access to folder1,
216 -and yet decide to give free access to folder1/subfolder2.
216 +Permissions set on an inner element will override inherited permissions.
217 +This means that you can restrict access to folder1 and yet decide to give free access to folder1/subfolder2.
218
219 #### Accounts
220
@@ -232,12 +233,12 @@ accounts:
233 group1:
234 ```
235
235 -As soon as the config is read HFS will encrypt passwords (if necessary) in a non-reversible way. It means that `password` property is replaced with an encrypted property: `srp`.
236 +As soon as the config is read, HFS will encrypt passwords (if necessary) in a non-reversible way. It means that `password` property is replaced with an encrypted property: `srp`.
237
238 As you can see in the example, `group1` has no password. This implies that you cannot log in as `group1`, but still `group1` exists and its purpose is to
239 gather multiple accounts and refer to them collectively as `group1`, so you can quickly share powers among several accounts.
240
240 -For each account entries, this is the list of properties you can have:
241 +For each account entry, this is the list of properties you can have:
242
243 - `ignore_limits` to ignore speed limits. Default is `false`.
244 - `redirect` provide a URL if you want the user to be redirected upon login. Default is none.
src/cross.ts
+6 -7
@@ -54,10 +54,9 @@ export interface VfsPerms {
54 export const WHO_ANYONE = true
55 export const WHO_NO_ONE = false
56 export const WHO_ANY_ACCOUNT = '*'
57 +export const WHO_ADMIN = 'admin'
58 type AccountList = string[]
58 -export type Who = typeof WHO_ANYONE
59 - | typeof WHO_NO_ONE
60 - | typeof WHO_ANY_ACCOUNT
59 +export type Who = typeof WHO_ANYONE | typeof WHO_NO_ONE | typeof WHO_ANY_ACCOUNT | typeof WHO_ADMIN
60 | AccountList // use false instead of empty array to keep the type boolean-able
61 export type WhoVfs = Who | keyof VfsPerms | WhoObject
62 export interface WhoObject { this?: WhoVfs, children?: WhoVfs }
@@ -68,12 +67,12 @@ export type Jsonify<T> = T extends string | number | boolean | null | undefined
67 never
68
69 export const defaultPerms: Required<VfsPerms> = {
71 - can_see: 'can_read',
70 can_read: WHO_ANYONE,
71 + can_see: 'can_read',
72 can_list: 'can_read',
74 - can_upload: WHO_NO_ONE,
75 - can_delete: WHO_NO_ONE,
76 - can_archive: 'can_read'
73 + can_archive: 'can_read',
74 + can_upload: WHO_ADMIN,
75 + can_delete: WHO_ADMIN,
76 }
77
78 export type VfsNodeAdminSend = {
src/frontEndApis.ts
+3 -3
@@ -18,7 +18,7 @@ import fs from 'fs'
18 import { mkdir, rename, copyFile, unlink } from 'fs/promises'
19 import { basename, dirname, join } from 'path'
20 import { getUploadMeta } from './upload'
21 -import { apiAssertTypes, CFG, moveStoredFileAttrs, pathDecode, pathEncode, popKey } from './misc'
21 +import { apiAssertTypes, CFG, moveStoredFileAttrs, pathDecode, pathEncode, popKey, Who, WHO_ADMIN } from './misc'
22 import { defineConfig } from './config'
23 import { getCommentFor, setCommentFor } from './comments'
24 import { SendListReadable } from './SendList'
@@ -26,7 +26,7 @@ import { ctxAdminAccess } from './adminApis'
26 import _ from 'lodash'
27
28 const partialFolderSize: any = {}
29 -const showUploader = defineConfig(CFG.show_uploader, false)
29 +const showUploader = defineConfig<Who>(CFG.show_uploader, WHO_ADMIN)
30
31 export const frontEndApis: ApiHandlers = {
32 get_file_list,
@@ -126,7 +126,7 @@ export const frontEndApis: ApiHandlers = {
126 return {}
127 },
128
129 - async get_folder_size_partial({ id }, ctx) {
129 + async get_folder_size_partial({ id }) {
130 apiAssertTypes({ string: { id } })
131 return partialFolderSize[id] || new ApiError(HTTP_NOT_FOUND)
132 },
src/vfs.ts
+5 -3
@@ -4,7 +4,7 @@ import fs from 'fs/promises'
4 import { basename, dirname, join, resolve } from 'path'
5 import {
6 makeMatcher, setHidden, onlyTruthy, isValidFileName, throw_, VfsPerms, WhoVfs, debounceAsync,
7 - isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, HTTP_SERVER_ERROR, try_, matches, Promisable,
7 + isWhoObject, WHO_ANY_ACCOUNT, WHO_ADMIN, defaultPerms, PERM_KEYS, HTTP_SERVER_ERROR, try_, matches, Promisable,
8 statWithTimeout, safeDecodeURIComponent, getUncHost, Who,
9 } from './misc'
10 import Koa from 'koa'
@@ -19,6 +19,7 @@ import fswin from 'fswin'
19 import { DESCRIPT_ION, DESCRIPT_ION_ALT, usingDescriptIon } from './comments'
20 import { walkDir } from './walkDir'
21 import { Readable } from 'node:stream'
22 +import { ctxAdminAccess } from './adminApis'
23
24 const showHiddenFiles = defineConfig('show_hidden_files', false)
25
@@ -285,7 +286,7 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerms, ct
286 if (isWhoObject(who))
287 who = who.this
288 who ??= defaultPerms[cur]
288 - if (typeof who !== 'string' || who === WHO_ANY_ACCOUNT)
289 + if (typeof who !== 'string' || who === WHO_ANY_ACCOUNT || who === WHO_ADMIN)
290 break
291 if (!max--) {
292 console.error(`Endless loop in permission ${perm}=${node[perm] ?? defaultPerms[perm]} for ${node.url || getNodeName(node)}`)
@@ -312,7 +313,8 @@ export function simpleWhoToError(who: Who, ctx: Koa.Context) {
313 return ctxBelongsTo(ctx, who) ? 0 : HTTP_UNAUTHORIZED
314 return typeof who === 'boolean' ? (who ? 0 : HTTP_FORBIDDEN)
315 : who === WHO_ANY_ACCOUNT ? (getCurrentUsername(ctx) ? 0 : HTTP_UNAUTHORIZED)
315 - : undefined
316 + : who === WHO_ADMIN ? (ctxAdminAccess(ctx) ? 0 : HTTP_UNAUTHORIZED)
317 + : undefined
318 }
319
320 function isWhoVfsPerms(who: WhoVfs | undefined): who is keyof VfsPerms {