fix: admin/files: changing protocol of base url was not saved

Massimo Melina committed Jun 10, 2023 at 23:32 UTC 93bdf6cee39dcbae12b6b4b2509150dda930c1cf
4 files changed +48 -32
admin/src/FileForm.ts
+36 -26
@@ -16,6 +16,7 @@ import {
16 import { apiCall, useApiEx } from './api'
17 import {
18 basename,
19 + Btn,
20 formatBytes,
21 IconBtn,
22 isEqualLax,
@@ -30,9 +31,9 @@ import { reloadVfs, VfsNode, VfsPerms, Who } from './VfsPage'
31 import md from './md'
32 import _ from 'lodash'
33 import FileField from './FileField'
33 -import { alertDialog, useDialogBarColors } from './dialog'
34 +import { alertDialog, toast, useDialogBarColors } from './dialog'
35 import yaml from 'yaml'
35 -import { Check, ContentCopy, Delete, Edit } from '@mui/icons-material'
36 +import { Check, ContentCopy, Delete, Edit, Save } from '@mui/icons-material'
37
38 interface Account { username: string }
39
@@ -41,12 +42,12 @@ interface FileFormProps {
42 anyMask?: boolean
43 defaultPerms: VfsPerms
44 addToBar?: ReactNode
44 - urls: string[] | false
45 + statusApi: any
46 }
47
48 const ACCEPT_LINK = "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept"
49
49 -export default function FileForm({ file, anyMask, defaultPerms, addToBar, urls }: FileFormProps) {
50 +export default function FileForm({ file, anyMask, defaultPerms, addToBar, statusApi }: FileFormProps) {
51 const { parent, children, isRoot, byMasks, ...rest } = file
52 const [values, setValues] = useState(rest)
53 useEffect(() => {
@@ -111,7 +112,7 @@ export default function FileForm({ file, anyMask, defaultPerms, addToBar, urls }
112 fields: [
113 isRoot ? h(Alert,{ severity: 'info' }, "This is Home, the root of your shared files. Options set here will be applied to all files.")
114 : { k: 'name', required: true, helperText: hasSource && "You can decide a name that's different from the one on your disk" },
114 - { k: 'id', comp: LinkField, urls },
115 + { k: 'id', comp: LinkField, statusApi },
116 { k: 'source', label: "Source on disk", comp: FileField, files: !isDir, folders: isDir, multiline: true,
117 placeholder: "Not on disk, this is a virtual folder",
118 },
@@ -219,15 +220,18 @@ function who2desc(who: any) {
220 }
221
222 interface LinkFieldProps extends FieldProps<string> {
222 - urls: string[]
223 + statusApi: any // receive status from parent, to avoid asking server at each click on a file
224 }
224 -function LinkField({ value, urls }: LinkFieldProps) {
225 - const { data, error, reload } = useApiEx('get_config', { only: ['base_url'] })
226 - const base: string | undefined = data?.base_url
227 - const link = (base || (urls ? urls[0] : '')) + value
228 - return h(Box, { display: 'flex', },
225 +function LinkField({ value, statusApi }: LinkFieldProps) {
226 + const { data: status, reload, error } = statusApi
227 + const urls: string[] = status?.suggestedUrls
228 + const base: string = status?.baseUrl
229 + const link = (base || urls?.[0] || '') + value
230 + return h(Box, { display: 'flex' },
231 + !urls ? 'error' : // check data is ok
232 h(DisplayField, {
230 - label: "Link", value: link,
233 + label: "Link",
234 + value: link,
235 error,
236 end: h(Box, {},
237 h(IconBtn, {
@@ -235,23 +239,18 @@ function LinkField({ value, urls }: LinkFieldProps) {
239 title: "Copy",
240 onClick: () => navigator.clipboard.writeText(link)
241 }),
238 - h(IconBtn, {
239 - icon: Edit,
240 - title: "Change",
241 - onClick: edit,
242 - }),
242 + h(IconBtn, { icon: Edit, title: "Change", onClick: edit }),
243 )
244 }),
245 )
246
247 function edit() {
248 - const startingProto = new URL(base || urls[0]).protocol + '//'
249 - newDialog({
248 + const close = newDialog({
249 title: "Change link",
251 - onClose: reload,
250 Content() {
253 - const [v, setV] = useState(base)
254 - const [proto, setProto] = useState(startingProto)
251 + const [v, setV] = useState(base || '')
252 + const proto = new URL(v || urls[0]).protocol + '//'
253 + const host = urls.includes(v) ? '' : v.slice(proto.length)
254 return h(Box, { display: 'flex', flexDirection: 'column' },
255 h(Box, { mb: 2 }, "You can choose a different base address for your links"),
256 h(MenuList, {},
@@ -264,11 +263,11 @@ function LinkField({ value, urls }: LinkFieldProps) {
263 h(StringField, {
264 label: "Custom IP or domain",
265 helperText: md("You can type any address but *you* are responsible to make the address work.\nThis functionality is just to help you copy the link in case you have a domain or a complex network configuration."),
267 - value: !v || urls.includes(v) ? '' : v.slice(proto.length),
266 + value: host,
267 onChange: v => set(prefix(proto, v)),
268 start: h(SelectField as Field<string>, {
269 value: proto,
271 - onChange: setProto,
270 + onChange: v => host ? set(v + host) : toast("Enter domain first"),
271 options: ['http://','https://'],
272 size: 'small',
273 variant: 'standard',
@@ -276,12 +275,23 @@ function LinkField({ value, urls }: LinkFieldProps) {
275 }),
276 sx: { mt: 2 }
277 }),
278 + h(Box, { mt: 2, textAlign: 'right' },
279 + h(Btn, {
280 + icon: Save,
281 + children: "Save",
282 + async onClick() {
283 + if (v !== base) {
284 + await apiCall('set_config', { values: { base_url: v } })
285 + await reload()
286 + }
287 + close()
288 + },
289 + }) ),
290 )
291
281 - async function set(u: string) {
292 + function set(u: string) {
293 if (u.endsWith('/'))
294 u = u.slice(0, -1)
284 - await apiCall('set_config', { values: { base_url: u } })
295 setV(u)
296 }
297 }
admin/src/VfsPage.ts
+9 -4
@@ -38,9 +38,14 @@ export default function VfsPage() {
38 [vfs])
39 const sideBreakpoint = 'md'
40 const isSideBreakpoint = useBreakpoint(sideBreakpoint)
41 - const [status] = useApi('get_status')
42 - const urls = useMemo(() => _.sortBy(status?.urls.https || status?.urls.http, url => url.includes('[')), // ipv4 first
43 - [status])
41 + const statusApi = useApiEx('get_status')
42 + const { data: status } = statusApi
43 + const urls = useMemo(() => {
44 + const b = status?.baseUrl
45 + const ret = _.sortBy(status?.urls.https || status?.urls.http, url => url.includes('[') && url !== b) // ipv4 first
46 + if (status) status.suggestedUrls = ret // store it for Link component
47 + return b && !ret.includes(b) ? [b, ...ret] : ret
48 + }, [status])
49
50 function close() {
51 state.selectedFiles = []
@@ -55,7 +60,7 @@ export default function VfsPage() {
60 }),
61 defaultPerms: data?.defaultPerms as VfsPerms,
62 anyMask,
58 - urls,
63 + statusApi,
64 file: selectedFiles[0] as VfsNode // it's actually Snapshot<VfsNode> but it's easier this way
65 })
66 : h(Fragment, {},
src/adminApis.ts
+2 -1
@@ -22,7 +22,7 @@ import { debounceAsync, isLocalHost, makeNetMatcher, onOff, waitFor } from './mi
22 import events from './events'
23 import { accountCanLoginAdmin, accountsConfig, getFromAccount } from './perm'
24 import Koa from 'koa'
25 -import { getProxyDetected } from './middlewares'
25 +import { baseUrl, getProxyDetected } from './middlewares'
26 import { writeFile } from 'fs/promises'
27 import { createReadStream } from 'fs'
28 import * as readline from 'readline'
@@ -89,6 +89,7 @@ export const adminApis: ApiHandlers = {
89 compatibleApiVersion: COMPATIBLE_API_VERSION,
90 ...await getServerStatus(),
91 urls: getUrls(),
92 + baseUrl: baseUrl.get(), // can be retrieved with get_config, but it's very handy with urls and low overhead. Case is different because the context is
93 update: !updateSupported() ? false : await localUpdateAvailable() ? 'local' : true,
94 proxyDetected: getProxyDetected(),
95 frpDetected: localhostAdmin.get() && !getProxyDetected()
src/middlewares.ts
+1 -1
@@ -170,7 +170,7 @@ const errorMessages = {
170 [HTTP_FORBIDDEN]: "Forbidden",
171 }
172
173 -const baseUrl = defineConfig('base_url', '')
173 +export const baseUrl = defineConfig('base_url', '')
174
175 async function sendFolderList(node: VfsNode, ctx: Koa.Context) {
176 let { depth=0, folders, prepend } = ctx.query