diff --git a/api.md b/api.md index 51b75719d8a55..e40d427553bea 100644 --- a/api.md +++ b/api.md @@ -209,10 +209,6 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the "192.68.0.21:59128" ], - // derp (string) is the IP:port of the DERP server currently being used. - // Learn about DERP servers at https://tailscale.com/kb/1232/. - "derp":"", - // mappingVariesByDestIP (boolean) is 'true' if the host's NAT mappings // vary based on the destination IP. "mappingVariesByDestIP":false, diff --git a/client/web/qnap.go b/client/web/qnap.go index d3b1d8dd7a4c8..5ccd7679836a5 100644 --- a/client/web/qnap.go +++ b/client/web/qnap.go @@ -16,21 +16,23 @@ import ( "net/url" ) -// authorizeQNAP authenticates the logged-in QNAP user and verifies -// that they are authorized to use the web client. It returns true if the -// request was handled and no further processing is required. -func authorizeQNAP(w http.ResponseWriter, r *http.Request) (handled bool) { +// authorizeQNAP authenticates the logged-in QNAP user and verifies that they +// are authorized to use the web client. +// It reports true if the request is authorized to continue, and false otherwise. +// authorizeQNAP manages writing out any relevant authorization errors to the +// ResponseWriter itself. +func authorizeQNAP(w http.ResponseWriter, r *http.Request) (ok bool) { _, resp, err := qnapAuthn(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) - return true + return false } if resp.IsAdmin == 0 { http.Error(w, "user is not an admin", http.StatusForbidden) - return true + return false } - return false + return true } type qnapAuthResponse struct { diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx index 28350e52e5bc5..eb403a5e73d90 100644 --- a/client/web/src/components/app.tsx +++ b/client/web/src/components/app.tsx @@ -1,30 +1,123 @@ import React from "react" import { Footer, Header, IP, State } from "src/components/legacy" -import useNodeData from "src/hooks/node-data" +import useNodeData, { NodeData } from "src/hooks/node-data" +import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg" +import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg" +import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg" export default function App() { // TODO(sonia): use isPosting value from useNodeData // to fill loading states. const { data, refreshData, updateNode } = useNodeData() - return ( + if (!data) { + // TODO(sonia): add a loading view + return
Loading...
+ } + + const needsLogin = data?.Status === "NeedsLogin" || data?.Status === "NoState" + + return !needsLogin && + (data.DebugMode === "login" || data.DebugMode === "full") ? ( +
+ {data.DebugMode === "login" ? ( + + ) : ( + + )} +
+ ) : ( + // Legacy client UI
- {!data ? ( - // TODO(sonia): add a loading view -
Loading...
+
+
+ + +
+
+ ) +} + +function LoginView(props: NodeData) { + return ( + <> +
+ +
+
+
+ +
+
+ Owned by +
+
+ {/* TODO(sonia): support tagged node profile view more eloquently */} + {props.Profile.LoginName} +
+
+
+
+
+ +
+
+ {props.DeviceName} +
+
{props.IP}
+
+
+ +
+
+ + ) +} + +function ManageView(props: NodeData) { + return ( +
+
+ +
+

{props.Profile.LoginName}

+ {/* TODO(sonia): support tagged node profile view more eloquently */} + +
+
+

This device

+
+
+
+ +

{props.DeviceName}

+
+

{props.IP}

+
+
+

+ Tailscale is up and running. You can connect to this device from devices + in your tailnet by using its name or IP address. +

+
+ ) +} + +function ProfilePic({ url }: { url: string }) { + return ( +
+ {url ? ( +
) : ( - <> -
-
- - -
-