client/web: when readonly, add check for TS connection

When the viewing user is accessing a webclient not over Tailscale,
they must connect over Tailscale before being able to log into the
full management client, which is served over TS. This change adds
a check that the user is able to access the node's tailscale IP.
If not able to, the signin button is disabled. We'll also be adding
Copy here to help explain to the user that they must connect to
Tailscale before proceeding.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy 2023-11-15 19:10:26 -05:00 committed by Sonia Appasamy
parent 33147c4591
commit 4f409012c5

View File

@ -1,5 +1,5 @@
import cx from "classnames" import cx from "classnames"
import React, { useCallback, useState } from "react" import React, { useCallback, useEffect, useState } from "react"
import { AuthResponse, AuthType } from "src/hooks/auth" import { AuthResponse, AuthType } from "src/hooks/auth"
import { NodeData } from "src/hooks/node-data" import { NodeData } from "src/hooks/node-data"
import { ReactComponent as ChevronDown } from "src/icons/chevron-down.svg" import { ReactComponent as ChevronDown } from "src/icons/chevron-down.svg"
@ -81,6 +81,49 @@ function LoginPopoverContent({
auth: AuthResponse auth: AuthResponse
newSession: () => Promise<void> newSession: () => Promise<void>
}) { }) {
/**
* canConnectOverTS indicates whether the current viewer
* is able to hit the node's web client that's being served
* at http://${node.IP}:5252. If false, this means that the
* viewer must connect to the correct tailnet before being
* able to sign in.
*/
const [canConnectOverTS, setCanConnectOverTS] = useState<boolean>(false)
const [isRunningCheck, setIsRunningCheck] = useState<boolean>(false)
const checkTSConnection = useCallback(() => {
if (auth.viewerIdentity) {
setCanConnectOverTS(true) // already connected over ts
return
}
// Otherwise, test connection to the ts IP.
if (isRunningCheck) {
return // already checking
}
setIsRunningCheck(true)
fetch(`http://${node.IP}:5252/ok`, { mode: "no-cors" })
.then(() => {
setIsRunningCheck(false)
setCanConnectOverTS(true)
})
.catch(() => setIsRunningCheck(false))
}, [
auth.viewerIdentity,
isRunningCheck,
setCanConnectOverTS,
setIsRunningCheck,
])
/**
* Checking connection for first time on page load.
*
* While not connected, we check again whenever the mouse
* enters the popover component, to pick up on the user
* leaving to turn on Tailscale then returning to the view.
* See `onMouseEnter` on the div below.
*/
useEffect(() => checkTSConnection(), [])
const handleSignInClick = useCallback(() => { const handleSignInClick = useCallback(() => {
if (auth.viewerIdentity) { if (auth.viewerIdentity) {
newSession() newSession()
@ -93,7 +136,7 @@ function LoginPopoverContent({
}, [node.IP, auth.viewerIdentity, newSession]) }, [node.IP, auth.viewerIdentity, newSession])
return ( return (
<> <div onMouseEnter={!canConnectOverTS ? checkTSConnection : undefined}>
<div className="text-black text-sm font-medium leading-tight"> <div className="text-black text-sm font-medium leading-tight">
{!auth.canManageNode ? "Viewing" : "Managing"} {!auth.canManageNode ? "Viewing" : "Managing"}
{auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`} {auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`}
@ -117,9 +160,15 @@ function LoginPopoverContent({
<button <button
className={cx( className={cx(
"w-full px-3 py-2 bg-indigo-500 rounded shadow text-center text-white text-sm font-medium mt-2", "w-full px-3 py-2 bg-indigo-500 rounded shadow text-center text-white text-sm font-medium mt-2",
{ "mb-2": auth.viewerIdentity } {
"mb-2": auth.viewerIdentity,
"cursor-not-allowed": !canConnectOverTS,
}
)} )}
onClick={handleSignInClick} onClick={handleSignInClick}
// TODO: add some helper info when disabled
// due to needing to connect to TS
disabled={!canConnectOverTS}
> >
{auth.viewerIdentity ? "Sign in to confirm identity" : "Sign in"} {auth.viewerIdentity ? "Sign in to confirm identity" : "Sign in"}
</button> </button>
@ -144,6 +193,6 @@ function LoginPopoverContent({
</div> </div>
</> </>
)} )}
</> </div>
) )
} }