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