2023-11-28 13:15:19 -05:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
2023-11-27 20:23:41 -05:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
2023-08-29 18:02:01 -04:00
|
|
|
import { apiFetch, setUnraidCsrfToken } from "src/api"
|
2023-11-27 20:23:41 -05:00
|
|
|
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
|
2023-11-15 16:04:44 -05:00
|
|
|
import { VersionInfo } from "src/hooks/self-update"
|
2023-08-15 08:24:32 -07:00
|
|
|
|
|
|
|
export type NodeData = {
|
|
|
|
Profile: UserProfile
|
2023-11-08 17:33:27 -05:00
|
|
|
Status: NodeState
|
2023-08-15 08:24:32 -07:00
|
|
|
DeviceName: string
|
2023-11-08 17:33:27 -05:00
|
|
|
OS: string
|
2023-08-15 08:24:32 -07:00
|
|
|
IP: string
|
2023-11-08 17:33:27 -05:00
|
|
|
IPv6: string
|
|
|
|
ID: string
|
|
|
|
KeyExpiry: string
|
|
|
|
KeyExpired: boolean
|
2023-11-27 20:23:41 -05:00
|
|
|
UsingExitNode?: ExitNode
|
|
|
|
AdvertisingExitNode: boolean
|
|
|
|
AdvertisedRoutes?: SubnetRoute[]
|
2023-08-15 08:24:32 -07:00
|
|
|
TUNMode: boolean
|
|
|
|
IsSynology: boolean
|
|
|
|
DSMVersion: number
|
|
|
|
IsUnraid: boolean
|
|
|
|
UnraidToken: string
|
|
|
|
IPNVersion: string
|
2023-11-15 16:04:44 -05:00
|
|
|
ClientVersion?: VersionInfo
|
2023-11-07 11:11:33 -08:00
|
|
|
URLPrefix: string
|
2023-11-09 16:19:22 -05:00
|
|
|
DomainName: string
|
2023-11-08 17:33:27 -05:00
|
|
|
TailnetName: string
|
|
|
|
IsTagged: boolean
|
|
|
|
Tags: string[]
|
2023-11-13 14:54:24 -05:00
|
|
|
RunningSSHServer: boolean
|
2023-11-29 15:58:56 -05:00
|
|
|
ControlAdminURL: string
|
|
|
|
LicensesURL: string
|
2023-08-15 08:24:32 -07:00
|
|
|
}
|
|
|
|
|
2023-11-08 17:33:27 -05:00
|
|
|
type NodeState =
|
|
|
|
| "NoState"
|
|
|
|
| "NeedsLogin"
|
|
|
|
| "NeedsMachineAuth"
|
|
|
|
| "Stopped"
|
|
|
|
| "Starting"
|
|
|
|
| "Running"
|
|
|
|
|
2023-08-15 11:38:13 -04:00
|
|
|
export type UserProfile = {
|
|
|
|
LoginName: string
|
|
|
|
DisplayName: string
|
|
|
|
ProfilePicURL: string
|
2023-08-15 08:24:32 -07:00
|
|
|
}
|
|
|
|
|
2023-11-27 20:23:41 -05:00
|
|
|
export type SubnetRoute = {
|
|
|
|
Route: string
|
|
|
|
Approved: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* NodeUpdaters provides a set of mutation functions for a node.
|
|
|
|
*
|
|
|
|
* These functions handle both making the requested change, as well as
|
|
|
|
* refreshing the app's node data state upon completion to reflect any
|
|
|
|
* relevant changes in the UI.
|
|
|
|
*/
|
|
|
|
export type NodeUpdaters = {
|
|
|
|
/**
|
|
|
|
* patchPrefs updates node preferences.
|
|
|
|
* Only provided preferences will be updated.
|
|
|
|
* Similar to running the tailscale set command in the CLI.
|
|
|
|
*/
|
|
|
|
patchPrefs: (d: PrefsPATCHData) => Promise<void>
|
|
|
|
/**
|
|
|
|
* postExitNode updates the node's status as either using or
|
|
|
|
* running as an exit node.
|
|
|
|
*/
|
|
|
|
postExitNode: (d: ExitNode) => Promise<void>
|
|
|
|
/**
|
|
|
|
* postSubnetRoutes updates the node's advertised subnet routes.
|
|
|
|
*/
|
|
|
|
postSubnetRoutes: (d: string[]) => Promise<void>
|
2023-08-16 18:52:31 -04:00
|
|
|
}
|
|
|
|
|
2023-11-27 20:23:41 -05:00
|
|
|
type PrefsPATCHData = {
|
2023-11-13 14:54:24 -05:00
|
|
|
RunSSHSet?: boolean
|
|
|
|
RunSSH?: boolean
|
2023-11-27 20:23:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type RoutesPOSTData = {
|
|
|
|
UseExitNode?: string
|
|
|
|
AdvertiseExitNode?: boolean
|
|
|
|
AdvertiseRoutes?: string[]
|
2023-11-13 14:54:24 -05:00
|
|
|
}
|
|
|
|
|
2023-08-15 08:24:32 -07:00
|
|
|
// useNodeData returns basic data about the current node.
|
|
|
|
export default function useNodeData() {
|
2023-08-15 11:38:13 -04:00
|
|
|
const [data, setData] = useState<NodeData>()
|
2023-08-16 18:52:31 -04:00
|
|
|
const [isPosting, setIsPosting] = useState<boolean>(false)
|
2023-08-15 11:38:13 -04:00
|
|
|
|
2023-08-28 16:44:48 -04:00
|
|
|
const refreshData = useCallback(
|
2023-08-29 13:20:25 -04:00
|
|
|
() =>
|
2023-08-29 18:02:01 -04:00
|
|
|
apiFetch("/data", "GET")
|
2023-08-29 13:20:25 -04:00
|
|
|
.then((r) => r.json())
|
2023-08-29 18:02:01 -04:00
|
|
|
.then((d: NodeData) => {
|
|
|
|
setData(d)
|
|
|
|
setUnraidCsrfToken(d.IsUnraid ? d.UnraidToken : undefined)
|
|
|
|
})
|
2023-08-29 13:20:25 -04:00
|
|
|
.catch((error) => console.error(error)),
|
|
|
|
[setData]
|
|
|
|
)
|
2023-08-16 18:52:31 -04:00
|
|
|
|
2023-11-27 20:23:41 -05:00
|
|
|
const prefsPATCH = useCallback(
|
|
|
|
(d: PrefsPATCHData) => {
|
2023-11-13 14:54:24 -05:00
|
|
|
setIsPosting(true)
|
|
|
|
if (data) {
|
|
|
|
const optimisticUpdates = data
|
2023-11-27 20:23:41 -05:00
|
|
|
if (d.RunSSHSet) {
|
|
|
|
optimisticUpdates.RunningSSHServer = Boolean(d.RunSSH)
|
2023-11-13 14:54:24 -05:00
|
|
|
}
|
|
|
|
// Reflect the pref change immediatley on the frontend,
|
|
|
|
// then make the prefs PATCH. If the request fails,
|
|
|
|
// data will be updated to it's previous value in
|
|
|
|
// onComplete below.
|
|
|
|
setData(optimisticUpdates)
|
|
|
|
}
|
|
|
|
|
|
|
|
const onComplete = () => {
|
|
|
|
setIsPosting(false)
|
|
|
|
refreshData() // refresh data after PATCH finishes
|
|
|
|
}
|
|
|
|
|
2023-11-27 20:23:41 -05:00
|
|
|
return apiFetch("/local/v0/prefs", "PATCH", d)
|
2023-11-13 14:54:24 -05:00
|
|
|
.then(onComplete)
|
2023-11-27 20:23:41 -05:00
|
|
|
.catch((err) => {
|
2023-11-13 14:54:24 -05:00
|
|
|
onComplete()
|
|
|
|
alert("Failed to update prefs")
|
2023-11-27 20:23:41 -05:00
|
|
|
throw err
|
2023-11-13 14:54:24 -05:00
|
|
|
})
|
|
|
|
},
|
|
|
|
[setIsPosting, refreshData, setData, data]
|
|
|
|
)
|
|
|
|
|
2023-11-27 20:23:41 -05:00
|
|
|
const routesPOST = useCallback(
|
|
|
|
(d: RoutesPOSTData) => {
|
|
|
|
setIsPosting(true)
|
|
|
|
const onComplete = () => {
|
|
|
|
setIsPosting(false)
|
|
|
|
refreshData() // refresh data after POST finishes
|
|
|
|
}
|
|
|
|
|
|
|
|
return apiFetch("/routes", "POST", d)
|
|
|
|
.then(onComplete)
|
|
|
|
.catch((err) => {
|
|
|
|
onComplete()
|
|
|
|
alert("Failed to update routes")
|
|
|
|
throw err
|
|
|
|
})
|
|
|
|
},
|
|
|
|
[setIsPosting, refreshData]
|
|
|
|
)
|
|
|
|
|
2023-08-16 18:52:31 -04:00
|
|
|
useEffect(
|
2023-08-24 11:18:38 -04:00
|
|
|
() => {
|
|
|
|
// Initial data load.
|
2023-08-28 16:44:48 -04:00
|
|
|
refreshData()
|
2023-08-24 11:18:38 -04:00
|
|
|
|
|
|
|
// Refresh on browser tab focus.
|
|
|
|
const onVisibilityChange = () => {
|
2023-08-28 16:44:48 -04:00
|
|
|
document.visibilityState === "visible" && refreshData()
|
2023-08-24 11:18:38 -04:00
|
|
|
}
|
|
|
|
window.addEventListener("visibilitychange", onVisibilityChange)
|
|
|
|
return () => {
|
|
|
|
// Cleanup browser tab listener.
|
|
|
|
window.removeEventListener("visibilitychange", onVisibilityChange)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Run once.
|
2023-11-28 16:31:56 -05:00
|
|
|
[refreshData]
|
2023-08-16 18:52:31 -04:00
|
|
|
)
|
2023-08-15 11:38:13 -04:00
|
|
|
|
2023-11-27 20:23:41 -05:00
|
|
|
const nodeUpdaters: NodeUpdaters = useMemo(
|
|
|
|
() => ({
|
|
|
|
patchPrefs: prefsPATCH,
|
|
|
|
postExitNode: (node) =>
|
|
|
|
routesPOST({
|
|
|
|
AdvertiseExitNode: node.ID === runAsExitNode.ID,
|
|
|
|
UseExitNode:
|
|
|
|
node.ID === noExitNode.ID || node.ID === runAsExitNode.ID
|
|
|
|
? undefined
|
|
|
|
: node.ID,
|
|
|
|
AdvertiseRoutes: data?.AdvertisedRoutes?.map((r) => r.Route), // unchanged
|
|
|
|
}),
|
|
|
|
postSubnetRoutes: (routes) =>
|
|
|
|
routesPOST({
|
|
|
|
AdvertiseRoutes: routes,
|
|
|
|
AdvertiseExitNode: data?.AdvertisingExitNode, // unchanged
|
|
|
|
UseExitNode: data?.UsingExitNode?.ID, // unchanged
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
[
|
|
|
|
data?.AdvertisingExitNode,
|
|
|
|
data?.AdvertisedRoutes,
|
|
|
|
data?.UsingExitNode?.ID,
|
|
|
|
prefsPATCH,
|
|
|
|
routesPOST,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
2023-11-29 18:24:29 -05:00
|
|
|
return { data, refreshData, nodeUpdaters, isPosting }
|
2023-08-15 08:24:32 -07:00
|
|
|
}
|