client/web: move auth session creation out of /api/auth

Splits auth session creation into two new endpoints:

/api/auth/session/new - to request a new auth session

/api/auth/session/wait - to block until user has completed auth url

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-11-03 13:38:01 -04:00
committed by Sonia Appasamy
parent 658971d7c0
commit e5dcf7bdde
5 changed files with 176 additions and 97 deletions

View File

@@ -3,19 +3,19 @@ import React from "react"
import LegacyClientView from "src/components/views/legacy-client-view"
import LoginClientView from "src/components/views/login-client-view"
import ReadonlyClientView from "src/components/views/readonly-client-view"
import useAuth, { AuthResponse } from "src/hooks/auth"
import useAuth, { AuthResponse, SessionsCallbacks } from "src/hooks/auth"
import useNodeData from "src/hooks/node-data"
import ManagementClientView from "./views/management-client-view"
export default function App() {
const { data: auth, loading: loadingAuth, waitOnAuth } = useAuth()
const { data: auth, loading: loadingAuth, sessions } = useAuth()
return (
<div className="flex flex-col items-center min-w-sm max-w-lg mx-auto py-14">
{loadingAuth ? (
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
) : (
<WebClient auth={auth} waitOnAuth={waitOnAuth} />
<WebClient auth={auth} sessions={sessions} />
)}
</div>
)
@@ -23,10 +23,10 @@ export default function App() {
function WebClient({
auth,
waitOnAuth,
sessions,
}: {
auth?: AuthResponse
waitOnAuth: () => Promise<void>
sessions: SessionsCallbacks
}) {
const { data, refreshData, updateNode } = useNodeData()
@@ -45,7 +45,7 @@ function WebClient({
<ManagementClientView {...data} />
) : data.DebugMode === "login" || data.DebugMode === "full" ? (
// Render new client interface in readonly mode.
<ReadonlyClientView data={data} auth={auth} waitOnAuth={waitOnAuth} />
<ReadonlyClientView data={data} auth={auth} sessions={sessions} />
) : (
// Render legacy client interface.
<LegacyClientView

View File

@@ -1,5 +1,5 @@
import React from "react"
import { AuthResponse } from "src/hooks/auth"
import { AuthResponse, AuthType, SessionsCallbacks } from "src/hooks/auth"
import { NodeData } from "src/hooks/node-data"
import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg"
@@ -17,11 +17,11 @@ import ProfilePic from "src/ui/profile-pic"
export default function ReadonlyClientView({
data,
auth,
waitOnAuth,
sessions,
}: {
data: NodeData
auth?: AuthResponse
waitOnAuth: () => Promise<void>
sessions: SessionsCallbacks
}) {
return (
<>
@@ -51,12 +51,14 @@ export default function ReadonlyClientView({
<div className="text-sm leading-tight">{data.IP}</div>
</div>
</div>
{data.DebugMode === "full" && (
{auth?.authNeeded == AuthType.tailscale && (
<button
className="button button-blue ml-6"
onClick={() => {
window.open(auth?.authUrl, "_blank")
waitOnAuth()
sessions
.new()
.then((url) => window.open(url, "_blank"))
.then(() => sessions.wait())
}}
>
Access

View File

@@ -8,20 +8,23 @@ export enum AuthType {
export type AuthResponse = {
ok: boolean
authUrl?: string
authNeeded?: AuthType
}
export type SessionsCallbacks = {
new: () => Promise<string> // creates new auth session and returns authURL
wait: () => Promise<void> // blocks until auth is completed
}
// useAuth reports and refreshes Tailscale auth status
// for the web client.
export default function useAuth() {
const [data, setData] = useState<AuthResponse>()
const [loading, setLoading] = useState<boolean>(true)
const loadAuth = useCallback((wait?: boolean) => {
const url = wait ? "/auth?wait=true" : "/auth"
const loadAuth = useCallback(() => {
setLoading(true)
return apiFetch(url, "GET")
return apiFetch("/auth", "GET")
.then((r) => r.json())
.then((d) => {
setData(d)
@@ -44,11 +47,33 @@ export default function useAuth() {
})
}, [])
const newSession = useCallback(() => {
return apiFetch("/auth/session/new", "GET")
.then((r) => r.json())
.then((d) => d.authUrl)
.catch((error) => {
console.error(error)
})
}, [])
const waitForSessionCompletion = useCallback(() => {
return apiFetch("/auth/session/wait", "GET")
.then(() => loadAuth()) // refresh auth data
.catch((error) => {
console.error(error)
})
}, [])
useEffect(() => {
loadAuth()
}, [])
const waitOnAuth = useCallback(() => loadAuth(true), [])
return { data, loading, waitOnAuth }
return {
data,
loading,
sessions: {
new: newSession,
wait: waitForSessionCompletion,
},
}
}