client/web: hook up data fetching to fill --dev React UI

Updates tailscale/corp#13775

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
pull/8916/head
Sonia Appasamy 1 year ago committed by Sonia Appasamy
parent 623d72c83b
commit 18280ebf7d

@ -7,12 +7,19 @@ export default function App() {
return ( return (
<div className="py-14"> <div className="py-14">
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl"> {!data ? (
<Header data={data} /> // TODO(sonia): add a loading view
<IP data={data} /> <div className="text-center">Loading...</div>
<State data={data} /> ) : (
</main> <>
<Footer data={data} /> <main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<Header data={data} />
<IP data={data} />
<State data={data} />
</main>
<Footer data={data} />
</>
)}
</div> </div>
) )
} }

@ -1,8 +1,4 @@
export type UserProfile = { import { useEffect, useState } from "react"
LoginName: string
DisplayName: string
ProfilePicURL: string
}
export type NodeData = { export type NodeData = {
Profile: UserProfile Profile: UserProfile
@ -20,29 +16,22 @@ export type NodeData = {
IPNVersion: string IPNVersion: string
} }
// testData is static set of nodedata used during development. export type UserProfile = {
// This can be removed once we have a real node data API. LoginName: string
const testData: NodeData = { DisplayName: string
Profile: { ProfilePicURL: string
LoginName: "amelie",
DisplayName: "Amelie Pangolin",
ProfilePicURL: "https://login.tailscale.com/logo192.png",
},
Status: "Running",
DeviceName: "amelies-laptop",
IP: "100.1.2.3",
AdvertiseExitNode: false,
AdvertiseRoutes: "",
LicensesURL: "https://tailscale.com/licenses/tailscale",
TUNMode: false,
IsSynology: true,
DSMVersion: 7,
IsUnraid: false,
UnraidToken: "",
IPNVersion: "0.1.0",
} }
// useNodeData returns basic data about the current node. // useNodeData returns basic data about the current node.
export default function useNodeData() { export default function useNodeData() {
return testData const [data, setData] = useState<NodeData>()
useEffect(() => {
fetch("/api/data")
.then((response) => response.json())
.then((json) => setData(json))
.catch((error) => console.error(error))
}, [])
return data
} }

@ -31,6 +31,7 @@ import (
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/groupmember" "tailscale.com/util/groupmember"
"tailscale.com/util/httpm"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
@ -78,30 +79,6 @@ func init() {
template.Must(tmpl.New("web.css").Parse(webCSS)) template.Must(tmpl.New("web.css").Parse(webCSS))
} }
type tmplData struct {
Profile tailcfg.UserProfile
SynologyUser string
Status string
DeviceName string
IP string
AdvertiseExitNode bool
AdvertiseRoutes string
LicensesURL string
TUNMode bool
IsSynology bool
DSMVersion int // 6 or 7, if IsSynology=true
IsUnraid bool
UnraidToken string
IPNVersion string
}
type postedData struct {
AdvertiseRoutes string
AdvertiseExitNode bool
Reauthenticate bool
ForceLogout bool
}
// authorize returns the name of the user accessing the web UI after verifying // authorize returns the name of the user accessing the web UI after verifying
// whether the user has access to the web UI. The function will write the // whether the user has access to the web UI. The function will write the
// error to the provided http.ResponseWriter. // error to the provided http.ResponseWriter.
@ -294,12 +271,26 @@ req.send(null);
// ServeHTTP processes all requests for the Tailscale web client. // ServeHTTP processes all requests for the Tailscale web client.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.devMode { if s.devMode {
if r.URL.Path == "/api/data" {
user, err := authorize(w, r)
if err != nil {
return
}
switch r.Method {
case httpm.GET:
s.serveGetNodeDataJSON(w, r, user)
case httpm.POST:
s.servePostNodeUpdate(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
return
}
// When in dev mode, proxy to the Vite dev server. // When in dev mode, proxy to the Vite dev server.
s.devProxy.ServeHTTP(w, r) s.devProxy.ServeHTTP(w, r)
return return
} }
ctx := r.Context()
if authRedirect(w, r) { if authRedirect(w, r) {
return return
} }
@ -309,80 +300,49 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" { switch {
case r.URL.Path == "/redirect" || r.URL.Path == "/redirect/":
io.WriteString(w, authenticationRedirectHTML) io.WriteString(w, authenticationRedirectHTML)
return return
case r.Method == "POST":
s.servePostNodeUpdate(w, r)
return
default:
s.serveGetNodeData(w, r, user)
return
} }
}
type nodeData struct {
Profile tailcfg.UserProfile
SynologyUser string
Status string
DeviceName string
IP string
AdvertiseExitNode bool
AdvertiseRoutes string
LicensesURL string
TUNMode bool
IsSynology bool
DSMVersion int // 6 or 7, if IsSynology=true
IsUnraid bool
UnraidToken string
IPNVersion string
}
func (s *Server) getNodeData(ctx context.Context, user string) (*nodeData, error) {
st, err := s.lc.Status(ctx) st, err := s.lc.Status(ctx)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
prefs, err := s.lc.GetPrefs(ctx) prefs, err := s.lc.GetPrefs(ctx)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return nil, err
return
}
if r.Method == "POST" {
defer r.Body.Close()
var postData postedData
type mi map[string]any
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
mp := &ipn.MaskedPrefs{
AdvertiseRoutesSet: true,
WantRunningSet: true,
}
mp.Prefs.WantRunning = true
mp.Prefs.AdvertiseRoutes = routes
log.Printf("Doing edit: %v", mp.Pretty())
if _, err := s.lc.EditPrefs(ctx, mp); err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
var reauth, logout bool
if postData.Reauthenticate {
reauth = true
}
if postData.ForceLogout {
logout = true
}
log.Printf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
url, err := s.tailscaleUp(r.Context(), st, postData)
log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
if url != "" {
json.NewEncoder(w).Encode(mi{"url": url})
} else {
io.WriteString(w, "{}")
}
return
} }
profile := st.User[st.Self.UserID] profile := st.User[st.Self.UserID]
deviceName := strings.Split(st.Self.DNSName, ".")[0] deviceName := strings.Split(st.Self.DNSName, ".")[0]
versionShort := strings.Split(st.Version, "-")[0] versionShort := strings.Split(st.Version, "-")[0]
data := tmplData{ data := &nodeData{
SynologyUser: user, SynologyUser: user,
Profile: profile, Profile: profile,
Status: st.BackendState, Status: st.BackendState,
@ -410,16 +370,106 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if len(st.TailscaleIPs) != 0 { if len(st.TailscaleIPs) != 0 {
data.IP = st.TailscaleIPs[0].String() data.IP = st.TailscaleIPs[0].String()
} }
return data, nil
}
func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request, user string) {
data, err := s.getNodeData(r.Context(), user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
if err := tmpl.Execute(buf, data); err != nil { if err := tmpl.Execute(buf, *data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Write(buf.Bytes()) w.Write(buf.Bytes())
} }
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData postedData) (authURL string, retErr error) { func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request, user string) {
data, err := s.getNodeData(r.Context(), user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(*data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
return
}
type nodeUpdate struct {
AdvertiseRoutes string
AdvertiseExitNode bool
Reauthenticate bool
ForceLogout bool
}
func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
st, err := s.lc.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var postData nodeUpdate
type mi map[string]any
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
mp := &ipn.MaskedPrefs{
AdvertiseRoutesSet: true,
WantRunningSet: true,
}
mp.Prefs.WantRunning = true
mp.Prefs.AdvertiseRoutes = routes
log.Printf("Doing edit: %v", mp.Pretty())
if _, err := s.lc.EditPrefs(r.Context(), mp); err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
var reauth, logout bool
if postData.Reauthenticate {
reauth = true
}
if postData.ForceLogout {
logout = true
}
log.Printf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
url, err := s.tailscaleUp(r.Context(), st, postData)
log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
if url != "" {
json.NewEncoder(w).Encode(mi{"url": url})
} else {
io.WriteString(w, "{}")
}
return
}
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData nodeUpdate) (authURL string, retErr error) {
if postData.ForceLogout { if postData.ForceLogout {
if err := s.lc.Logout(ctx); err != nil { if err := s.lc.Logout(ctx); err != nil {
return "", fmt.Errorf("Logout error: %w", err) return "", fmt.Errorf("Logout error: %w", err)

@ -138,7 +138,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+ tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/client/web tailscale.com/util/groupmember from tailscale.com/client/web
tailscale.com/util/httpm from tailscale.com/client/tailscale tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/net/interfaces+ tailscale.com/util/lineread from tailscale.com/net/interfaces+
L tailscale.com/util/linuxfw from tailscale.com/net/netns L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/net/netcheck+ tailscale.com/util/mak from tailscale.com/net/netcheck+

Loading…
Cancel
Save