client/web: add localapi proxy

Adds proxy to the localapi from /api/local/ web client endpoint.
The localapi proxy is restricted to an allowlist of those actually
used by the web client frontend.

Updates tailscale/corp#13775

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-08-28 16:44:48 -04:00
committed by Sonia Appasamy
parent c919ff540f
commit da6eb076aa
5 changed files with 152 additions and 10 deletions

View File

@@ -5,7 +5,7 @@ import useNodeData from "src/hooks/node-data"
export default function App() {
// TODO(sonia): use isPosting value from useNodeData
// to fill loading states.
const { data, updateNode } = useNodeData()
const { data, refreshData, updateNode } = useNodeData()
return (
<div className="py-14">
@@ -15,7 +15,11 @@ export default function App() {
) : (
<>
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<Header data={data} updateNode={updateNode} />
<Header
data={data}
refreshData={refreshData}
updateNode={updateNode}
/>
<IP data={data} />
<State data={data} updateNode={updateNode} />
</main>

View File

@@ -1,5 +1,6 @@
import cx from "classnames"
import React from "react"
import { apiFetch } from "src/api"
import { NodeData, NodeUpdate } from "src/hooks/node-data"
// TODO(tailscale/corp#13775): legacy.tsx contains a set of components
@@ -9,9 +10,11 @@ import { NodeData, NodeUpdate } from "src/hooks/node-data"
export function Header({
data,
refreshData,
updateNode,
}: {
data: NodeData
refreshData: () => void
updateNode: (update: NodeUpdate) => void
}) {
return (
@@ -89,7 +92,11 @@ export function Header({
</button>{" "}
|{" "}
<button
onClick={() => updateNode({ ForceLogout: true })}
onClick={() =>
apiFetch("/local/v0/logout", { method: "POST" })
.then(refreshData)
.catch((err) => alert("Logout failed: " + err.message))
}
className="hover:text-gray-700"
>
Logout

View File

@@ -35,7 +35,7 @@ export default function useNodeData() {
const [data, setData] = useState<NodeData>()
const [isPosting, setIsPosting] = useState<boolean>(false)
const fetchNodeData = useCallback(
const refreshData = useCallback(
() =>
apiFetch("/data")
.then((r) => r.json())
@@ -102,7 +102,7 @@ export default function useNodeData() {
if (url) {
window.open(url, "_blank")
}
fetchNodeData()
refreshData()
})
.catch((err) => alert("Failed operation: " + err.message))
},
@@ -112,11 +112,11 @@ export default function useNodeData() {
useEffect(
() => {
// Initial data load.
fetchNodeData()
refreshData()
// Refresh on browser tab focus.
const onVisibilityChange = () => {
document.visibilityState === "visible" && fetchNodeData()
document.visibilityState === "visible" && refreshData()
}
window.addEventListener("visibilitychange", onVisibilityChange)
return () => {
@@ -128,5 +128,5 @@ export default function useNodeData() {
[]
)
return { data, updateNode, isPosting }
return { data, refreshData, updateNode, isPosting }
}