// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause import { useCallback, useEffect, useState } from "react" import { apiFetch, setSynoToken } from "src/api" import useSWR from "swr" export type AuthResponse = { serverMode: AuthServerMode authorized: boolean viewerIdentity?: { loginName: string nodeName: string nodeIP: string profilePicUrl?: string capabilities: { [key in PeerCapability]: boolean } } needsSynoAuth?: boolean } export type AuthServerMode = "login" | "readonly" | "manage" export type PeerCapability = "*" | "ssh" | "subnets" | "exitnodes" | "account" /** * canEdit reports whether the given auth response specifies that the viewer * has the ability to edit the given capability. */ export function canEdit(cap: PeerCapability, auth: AuthResponse): boolean { if (!auth.authorized || !auth.viewerIdentity) { return false } if (auth.viewerIdentity.capabilities["*"] === true) { return true // can edit all features } return auth.viewerIdentity.capabilities[cap] === true } /** * hasAnyEditCapabilities reports whether the given auth response specifies * that the viewer has at least one edit capability. If this is true, the * user is able to go through the auth flow to authenticate a management * session. */ export function hasAnyEditCapabilities(auth: AuthResponse): boolean { return Object.values(auth.viewerIdentity?.capabilities || {}).includes(true) } /** * useAuth reports and refreshes Tailscale auth status for the web client. */ export default function useAuth() { const { data, error, mutate } = useSWR("/auth") const [ranSynoAuth, setRanSynoAuth] = useState(false) const loading = !data && !error // Start Synology auth flow if needed. useEffect(() => { if (data?.needsSynoAuth && !ranSynoAuth) { fetch("/webman/login.cgi") .then((r) => r.json()) .then((a) => { setSynoToken(a.SynoToken) setRanSynoAuth(true) mutate() }) .catch((error) => { console.error("Synology auth error:", error) }) } }, [data?.needsSynoAuth, ranSynoAuth, mutate]) const newSession = useCallback(() => { return apiFetch<{ authUrl?: string }>("/auth/session/new", "GET") .then((d) => { if (d.authUrl) { window.open(d.authUrl, "_blank") return apiFetch("/auth/session/wait", "GET") } }) .then(() => { mutate() }) .catch((error) => { console.error(error) }) }, [mutate]) // Start regular auth flow. useEffect(() => { const needsAuth = data && !loading && !data.authorized && hasAnyEditCapabilities(data) && new URLSearchParams(window.location.search).get("check") === "now" if (needsAuth) { newSession() } }, [data, loading, newSession]) return { data, loading, newSession, } }