// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause import cx from "classnames" import React, { useCallback, useMemo, useState } from "react" import ChevronDown from "src/assets/icons/chevron-down.svg?react" import Eye from "src/assets/icons/eye.svg?react" import User from "src/assets/icons/user.svg?react" import { AuthResponse, hasAnyEditCapabilities } from "src/hooks/auth" import { useTSWebConnected } from "src/hooks/ts-web-connected" import { NodeData } from "src/types" import Button from "src/ui/button" import Popover from "src/ui/popover" import ProfilePic from "src/ui/profile-pic" import { assertNever, isHTTPS } from "src/utils/util" export default function LoginToggle({ node, auth, newSession, }: { node: NodeData auth: AuthResponse newSession: () => Promise }) { const [open, setOpen] = useState(false) const { tsWebConnected, checkTSWebConnection } = useTSWebConnected( auth.serverMode, node.IPv4 ) return ( ) : auth.serverMode === "login" ? ( ) : auth.serverMode === "manage" ? ( ) : ( assertNever(auth.serverMode) ) } side="bottom" align="end" open={open} onOpenChange={setOpen} asChild >
{auth.authorized ? ( ) : ( )}
) } /** * TriggerWhenManaging is displayed as the trigger for the login popover * when the user has an active authorized managment session. */ function TriggerWhenManaging({ auth, open, setOpen, }: { auth: AuthResponse open: boolean setOpen: (next: boolean) => void }) { return (
) } /** * TriggerWhenReading is displayed as the trigger for the login popover * when the user is currently in read mode (doesn't have an authorized * management session). */ function TriggerWhenReading({ auth, open, setOpen, }: { auth: AuthResponse open: boolean setOpen: (next: boolean) => void }) { return ( ) } /** * PopoverContentHeader is the header for the login popover. */ function PopoverContentHeader({ auth }: { auth: AuthResponse }) { return (
{auth.authorized ? "Managing" : "Viewing"} {auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`}
) } /** * PopoverContentFooter is the footer for the login popover. */ function PopoverContentFooter({ auth }: { auth: AuthResponse }) { return auth.viewerIdentity ? ( <>

We recognize you because you are accessing this page from{" "} {auth.viewerIdentity.nodeName || auth.viewerIdentity.nodeIP}

) : null } /** * ReadonlyModeContent is the body of the login popover when the web * client is being run in "readonly" server mode. */ function ReadonlyModeContent({ auth }: { auth: AuthResponse }) { return ( <>

This web interface is running in read-only mode.{" "} Learn more →

) } /** * LoginModeContent is the body of the login popover when the web * client is being run in "login" server mode. */ function LoginModeContent({ node, auth, tsWebConnected, checkTSWebConnection, }: { node: NodeData auth: AuthResponse tsWebConnected: boolean checkTSWebConnection: () => void }) { const https = isHTTPS() // We can't run the ts web connection test when the webpage is loaded // over HTTPS. So in this case, we default to presenting a login button // with some helper text reminding the user to check their connection // themselves. const hasACLAccess = https || tsWebConnected const hasEditCaps = useMemo(() => { if (!auth.viewerIdentity) { // If not connected to login client over tailscale, we won't know the viewer's // identity. So we must assume they may be able to edit something and have the // management client handle permissions once the user gets there. return true } return hasAnyEditCapabilities(auth) }, [auth]) const handleLogin = useCallback(() => { // Must be connected over Tailscale to log in. // Send user to Tailscale IP and start check mode const manageURL = `http://${node.IPv4}:5252/?check=now` if (window.self !== window.top) { // If we're inside an iframe, open management client in new window. window.open(manageURL, "_blank") } else { window.location.href = manageURL } }, [node.IPv4]) return (
{!hasACLAccess || !hasEditCaps ? ( <>

{!hasEditCaps ? ( // ACLs allow access, but user isn't allowed to edit any features, // restricted to readonly. No point in sending them over to the // tailscaleIP:5252 address. <> You don’t have permission to make changes to this device, but you can view most of its details. ) : !node.ACLAllowsAnyIncomingTraffic ? ( // Tailnet ACLs don't allow access to anyone. <> The current tailnet policy file does not allow connecting to this device. ) : ( // ACLs don't allow access to this user specifically. <> Cannot access this device’s Tailscale IP. Make sure you are connected to your tailnet, and that your policy file allows access. )}{" "} Learn more →

) : ( // User can connect to Tailcale IP; sign in when ready. <>

You can see most of this device’s details. To make changes, you need to sign in.

{https && ( // we don't know if the user can connect over TS, so // provide extra tips in case they have trouble.

Make sure you are connected to your tailnet, and that your policy file allows access.

)} )}
) } /** * ManageModeContent is the body of the login popover when the web * client is being run in "manage" server mode. */ function ManageModeContent({ auth, newSession, }: { node: NodeData auth: AuthResponse newSession: () => void }) { const handleLogin = useCallback(() => { if (window.self !== window.top) { // If we're inside an iframe, start session in new window. let url = new URL(window.location.href) url.searchParams.set("check", "now") window.open(url, "_blank") } else { newSession() } }, [newSession]) const hasAnyPermissions = useMemo(() => hasAnyEditCapabilities(auth), [auth]) return ( <> {!auth.authorized && (hasAnyPermissions ? ( // User is connected over Tailscale, but needs to complete check mode. <>

To make changes, sign in to confirm your identity. This extra step helps us keep your device secure.

) : ( // User is connected over tailscale, but doesn't have permission to manage.

You don’t have permission to make changes to this device, but you can view most of its details.{" "} Learn more →

))} ) } function SignInButton({ auth, onClick, }: { auth: AuthResponse onClick: () => void }) { return ( ) }