mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-22 08:51:41 +00:00
client/web: hook up data fetching to fill --dev React UI
Updates tailscale/corp#13775 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
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…
x
Reference in New Issue
Block a user