@samitouri / QOSami-HFS / commits / b06f8323

fix: admin/internet: verify-again after fix-it button could show the wrong message

Massimo Melina committed Dec 23, 2023 at 17:48 UTC b06f832312c5cfcbc5c6d8de7b6e30ee9864db60
2 files changed +49 -49
admin/src/InternetPage.ts
+45 -46
@@ -3,7 +3,8 @@ import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, Linea
3 import { CardMembership, HomeWorkTwoTone, Lock, Public, PublicTwoTone, RouterTwoTone, Send, Storage,
4 SvgIconComponent } from '@mui/icons-material'
5 import { apiCall, useApiEx } from './api'
6 -import { closeDialog, DAY, formatTimestamp, wait, wantArray, with_, PORT_DISABLED, isIP, CFG } from './misc'
6 +import { closeDialog, DAY, formatTimestamp, wait, wantArray, with_, PORT_DISABLED, isIP, CFG,
7 + useRequestRender } from './misc'
8 import { Flex, LinkBtn, Btn } from './mui'
9 import { alertDialog, confirmDialog, promptDialog, toast, waitDialog } from './dialog'
10 import { BoolField, Form, MultiSelectField, NumberField, SelectField } from '@hfs/mui-grid-form'
@@ -26,21 +27,21 @@ export default function InternetPage() {
27 const [checkResult, setCheckResult] = useState<boolean | undefined>()
28 const [checking, setChecking] = useState(false)
29 const [mapping, setMapping] = useState(false)
29 - const [verifyAgain, setVerifyAgain] = useState(false)
30 const status = useApiEx('get_status')
31 const config = useApiEx('get_config', { only: ['base_url'] })
32 const localColor = with_([status.data?.http?.error, status.data?.https?.error], ([h, s]) =>
33 h && s ? 'error' : h || s ? 'warning' : 'success')
34 type GetNat = Awaited<ReturnType<typeof getNatInfo>>
35 - const { data: nat, reload: reloadNat, error, loading, element } = useApiEx<GetNat>('get_nat')
36 - const port = nat?.internalPort
37 - const wrongMap = nat?.mapped && nat.mapped.private.port !== port && nat.mapped.private.port
38 - const doubleNat = nat?.externalIp && nat?.publicIps && !nat.publicIps.includes(nat.externalIp)
35 + const nat = useApiEx<GetNat>('get_nat')
36 + const { data } = nat
37 + const port = data?.internalPort
38 + const wrongMap = data?.mapped && data.mapped.private.port !== port && data.mapped.private.port
39 + const doubleNat = data?.externalIp && data?.publicIps && !data.publicIps.includes(data.externalIp)
40 + const verifyAgain = useRequestRender()
41 useEffect(() => {
40 - if (!verifyAgain || !nat || loading) return
41 - verify().then()
42 - setVerifyAgain(false)
43 - }, [verifyAgain, nat, loading])
42 + if (verifyAgain.state) // skip first
43 + verify(true).then()
44 + }, [verifyAgain.state])
45 return h(Flex, { vert: true, gap: '2em', maxWidth: '40em' },
46 h(Alert, { severity: 'info' }, "This page makes sure your site is working correctly on the Internet"),
47 baseUrlBox(),
@@ -99,7 +100,7 @@ export default function InternetPage() {
100 const { https } = status.data ||{}
101 const disabled = https?.port === PORT_DISABLED
102 const error = https?.error
102 - return element || status.element || h(TitleCard, { title: "HTTPS", icon: Lock, color: https?.listening && !error ? 'success' : 'warning' },
103 + return nat.element || status.element || h(TitleCard, { title: "HTTPS", icon: Lock, color: https?.listening && !error ? 'success' : 'warning' },
104 error ? h(Alert, { severity: 'warning' }, error) :
105 (disabled && h(LinkBtn, { onClick: notEnabled }, "Not enabled")),
106 cert.element || with_(cert.data, c => c.none ? h(LinkBtn, { onClick: () => suggestMakingCert().then(cert.reload) }, "No certificate configured") : h(Box, {},
@@ -198,38 +199,40 @@ export default function InternetPage() {
199 }
200
201 function networkBox() {
201 - if (error) return element
202 - if (!nat) return h(CircularProgress)
203 - const direct = nat?.publicIps.includes(nat?.localIp)
202 + if (nat.error) return nat.element
203 + if (!data) return h(CircularProgress)
204 + const direct = data?.publicIps.includes(data?.localIp)
205 return h(Flex, { justifyContent: 'space-around' },
205 - h(Device, { name: "Server", icon: direct ? Storage : HomeWorkTwoTone, color: localColor, ip: nat?.localIp,
206 + h(Device, { name: "Server", icon: direct ? Storage : HomeWorkTwoTone, color: localColor, ip: data?.localIp,
207 below: port && h(Box, { fontSize: 'smaller' }, "port ", port),
208 }),
209 !direct && h(Sep),
210 !direct && h(Device, {
210 - name: "Router", icon: RouterTwoTone, ip: nat?.gatewayIp,
211 - color: nat?.mapped && (wrongMap ? 'warning' : 'success'),
211 + name: "Router", icon: RouterTwoTone, ip: data?.gatewayIp,
212 + color: data?.mapped && (wrongMap ? 'warning' : 'success'),
213 below: mapping ? h(LinearProgress, { sx: { height: '1em' } })
214 : h(LinkBtn, { fontSize: 'smaller', display: 'block', onClick: configure },
214 - "port ", wrongMap ? 'is wrong' : nat?.externalPort || "unknown"),
215 + "port ", wrongMap ? 'is wrong' : data?.externalPort || "unknown"),
216 }),
217 h(Sep),
217 - h(Device, { name: "Internet", icon: PublicTwoTone, ip: nat?.publicIps,
218 + h(Device, { name: "Internet", icon: PublicTwoTone, ip: data?.publicIps,
219 color: checkResult ? 'success' : checkResult === false ? 'error' : doubleNat ? 'warning' : undefined,
220 below: checking ? h(LinearProgress, { sx: { height: '1em' } }) : h(Box, { fontSize: 'smaller' },
221 doubleNat && h(LinkBtn, { display: 'block', onClick: () => alertDialog(MSG_ISP, 'warning') }, "Double NAT"),
222 checkResult ? "Working!" : checkResult === false ? "Failed!" : '',
223 ' ',
223 - nat?.publicIps.length > 0 && nat.internalPort && h(LinkBtn, { onClick: verify }, "Verify")
224 + data?.publicIps.length > 0 && data.internalPort && h(LinkBtn, { onClick: () => verify() }, "Verify")
225 )
226 }),
227 )
228 }
229
229 - async function verify(): Promise<any> {
230 - if (!nat) return // shut up ts
230 + async function verify(again=false): Promise<any> {
231 + await nat.loading
232 + const data = nat.getData() // fresh data
233 + if (!data) return
234 setCheckResult(undefined)
232 - if (!verifyAgain && !await confirmDialog("This test will check if your server is working properly on the Internet")) return
235 + if (!again && !await confirmDialog("This test will check if your server is working properly on the Internet")) return
236 setChecking(true)
237 try {
238 const url = config.data?.base_url
@@ -251,27 +254,27 @@ export default function InternetPage() {
254 }
255 setCheckResult(false)
256 if (wrongMap)
254 - return fixPort().then(retry)
257 + return fixPort().then(verifyAgain)
258 if (doubleNat)
259 return alertDialog(MSG_ISP, 'warning')
260 const msg = "We couldn't reach your server from the Internet. "
258 - if (nat.upnp && !nat.mapped)
259 - return confirmDialog(msg + "Try port-forwarding on your router", { confirmText: "Fix it" }).then(go => {
261 + if (data.upnp && !data!.mapped)
262 + return confirmDialog(msg + "Try port-forwarding on your router", { confirmText: "Fix it" }).then(async go => {
263 if (!go) return
261 - try { mapPort(nat.internalPort!, '', '') }
262 - catch { mapPort(HIGHER_PORT, '') }
263 - toast("Port forwarded, now verify again", 'success')
264 - retry()
264 + try { await mapPort(data!.internalPort!, '', '') }
265 + catch { await mapPort(HIGHER_PORT, '') }
266 + toast("Port forwarded, now we verify again", 'success')
267 + verifyAgain()
268 })
269 const cfg = await apiCall('get_config', { only: [CFG.geo_enable, CFG.geo_allow] })
270 const { close } = alertDialog(h(Box, {}, msg + "Possible causes:", h('ul', {},
271 cfg[CFG.geo_enable] && cfg[CFG.geo_allow] != null && h('li', {}, "You may be blocking a country from where the test is performed"),
269 - !nat.upnp && h('li', {}, "Your router may need to be configured. ", h(Link, { href: PORT_FORWARD_URL, target: 'help' }, "How?")),
272 + !data.upnp && h('li', {}, "Your router may need to be configured. ", h(Link, { href: PORT_FORWARD_URL, target: 'help' }, "How?")),
273 h('li', {}, "There could be a firewall, try configuring or disabling it."),
271 - (nat.externalPort || nat.internalPort!) <= 1024 && h('li', {},
274 + (data.externalPort || data.internalPort!) <= 1024 && h('li', {},
275 "Your Internet Provider may be blocking ports under 1024. ",
273 - nat.upnp && h(Button, { size: 'small', onClick() { close(); mapPort(HIGHER_PORT).then(retry) } }, "Try " + HIGHER_PORT) ),
274 - nat.mapped && h('li', {}, "A bug in your modem/router, try rebooting it."),
276 + data.upnp && h(Button, { size: 'small', onClick() { close(); mapPort(HIGHER_PORT).then(verifyAgain) } }, "Try " + HIGHER_PORT) ),
277 + data.mapped && h('li', {}, "A bug in your modem/router, try rebooting it."),
278 h('li', {}, MSG_ISP),
279 )), 'warning')
280 }
@@ -281,23 +284,19 @@ export default function InternetPage() {
284 finally {
285 setChecking(false)
286 }
284 -
285 - function retry() {
286 - setVerifyAgain(true)
287 - }
287 }
288
289 async function configure() {
291 - if (!nat) return // shut up ts
290 + if (!data) return // shut up ts
291 if (wrongMap)
292 return await confirmDialog(`There is a port-forwarding but it is pointing to the wrong port (${wrongMap})`, { confirmText: "Fix it" })
293 && fixPort()
295 - if (!nat.upnp)
294 + if (!data.upnp)
295 return alertDialog(h(Box, { lineHeight: 1.5 }, md(`We cannot help you configuring your router because UPnP is not available.\nFind more help [on this website](${PORT_FORWARD_URL}).`)), 'info')
296 const res = await promptDialog(md(`This will ask the router to map your port, so that it can be reached from the Internet.\nYou can set the same number of the local network (${port}), or a different one.`), {
298 - value: nat.externalPort || port,
297 + value: data.externalPort || port,
298 field: { label: "Port seen from the Internet", comp: NumberField },
300 - addToBar: nat.mapped && [h(Button, { color: 'warning', onClick: remove }, "Remove")],
299 + addToBar: data.mapped && [h(Button, { color: 'warning', onClick: remove }, "Remove")],
300 dialogProps: { sx: { maxWidth: '20em' } },
301 })
302 if (res)
@@ -310,21 +309,21 @@ export default function InternetPage() {
309 }
310
311 function fixPort() {
313 - if (!nat?.externalPort) return alertDialog("externalPort not found", 'error')
314 - return mapPort(nat.externalPort, "Forwarding corrected")
312 + if (!data?.externalPort) return alertDialog("externalPort not found", 'error')
313 + return mapPort(data.externalPort, "Forwarding corrected")
314 }
315
316 async function mapPort(external: number, msg='', errMsg="Operation failed") {
317 setMapping(true)
318 try {
319 await apiCall('map_port', { external })
321 - reloadNat()
320 + nat.reload()
321 if (msg) toast(msg, 'success')
322 setCheckResult(undefined) // things have changed, invalidate check result
323 }
324 catch(e) {
325 if (errMsg) {
327 - const low = external && Math.min(external, nat!.internalPort!) < 1024
326 + const low = external && Math.min(external, data!.internalPort!) < 1024
327 const msg = errMsg + (low ? ". Some routers refuse to work with ports under 1024." : '')
328 await alertDialog(msg, 'error')
329 }
shared/api.ts
+4 -3
@@ -80,6 +80,7 @@ export function useApi<T=any>(cmd: string | Falsy, params?: object, options: Api
80 const [forcer, setForcer] = useStateMounted(0)
81 const loadingRef = useRef<ReturnType<typeof apiCall>>()
82 const reloadingRef = useRef<any>()
83 + const dataRef = useRef<T>()
84 useEffect(() => {
85 loadingRef.current?.abort()
86 setData(undefined)
@@ -88,10 +89,10 @@ export function useApi<T=any>(cmd: string | Falsy, params?: object, options: Api
89 let req: undefined | ReturnType<typeof apiCall>
90 const wholePromise = wait(0) // postpone a bit, so that if it is aborted immediately, it is never really fired (happens mostly in dev mode)
91 .then(() => !cmd || aborted ? undefined : req = apiCall<T>(cmd, params, options))
91 - .then(res => aborted || setData(res), err => {
92 + .then(res => aborted || setData(dataRef.current = res), err => {
93 if (aborted) return
94 setError(err)
94 - setData(undefined)
95 + setData(dataRef.current = undefined)
96 })
97 .finally(() => loadingRef.current = reloadingRef.current = undefined)
98 loadingRef.current = Object.assign(wholePromise, {
@@ -107,7 +108,7 @@ export function useApi<T=any>(cmd: string | Falsy, params?: object, options: Api
108 setForcer(v => v + 1)
109 reloadingRef.current = pendingPromise()
110 }, [setForcer])
110 - return { data, setData, error, reload, loading: Boolean(loadingRef.current || reloadingRef.current) }
111 + return { data, setData, error, reload, loading: loadingRef.current || reloadingRef.current, getData: () => dataRef.current, }
112 }
113
114 type EventHandler = (type:string, data?:any) => void