Browse Source

auto update script executed

Toby Chui 1 year ago
parent
commit
ae61127f23
11 changed files with 619 additions and 21 deletions
  1. 4 1
      api.go
  2. 2 1
      main.go
  3. 54 0
      mod/ganserv/authkey.go
  4. 4 0
      mod/ganserv/ganserv.go
  5. 91 2
      mod/ganserv/handlers.go
  6. 119 0
      mod/ganserv/zerotier.go
  7. 5 1
      start.go
  8. BIN
      tmp/GeoLite2-Country.mmdb
  9. 1 1
      web/components/gan.html
  10. 319 14
      web/components/gandetails.html
  11. 20 1
      web/main.css

+ 4 - 1
api.go

@@ -84,8 +84,11 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
 	authRouter.HandleFunc("/api/gan/network/list", ganManager.HandleListNetwork)
 	authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
-	authRouter.HandleFunc("/api/gan/network/test", ganManager.HandleTest)
 	authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
+	authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
+	authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
+
+	authRouter.HandleFunc("/api/gan/network/test", ganManager.HandleTest)
 
 	//mDNS APIs
 	authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)

+ 2 - 1
main.go

@@ -30,10 +30,11 @@ import (
 var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
 var showver = flag.Bool("version", false, "Show version of this server")
 var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
+var ztAuthToken = flag.String("ztauth", "", "Zerotier authtoken for the local node")
 
 var (
 	name        = "Zoraxy"
-	version     = "2.11"
+	version     = "2.2"
 	nodeUUID    = "generic"
 	development = true //Set this to false to use embedded web fs
 

+ 54 - 0
mod/ganserv/authkey.go

@@ -0,0 +1,54 @@
+package ganserv
+
+import (
+	"bufio"
+	"fmt"
+	"log"
+	"os"
+	"runtime"
+	"strings"
+)
+
+func TryLoadorAskUserForAuthkey() string {
+	//Check for zt auth token
+	value, exists := os.LookupEnv("ZT_AUTH")
+	if !exists {
+		log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.")
+	} else {
+		return value
+	}
+
+	authKey := ""
+	if runtime.GOOS == "windows" {
+		b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret")
+		if err == nil {
+			authKey = string(b)
+		} else {
+			log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
+		}
+	} else if runtime.GOOS == "linux" {
+		b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret")
+		if err == nil {
+			authKey = string(b)
+		} else {
+			log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
+		}
+	} else if runtime.GOOS == "darwin" {
+		b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret")
+		if err == nil {
+			authKey = string(b)
+		} else {
+			log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error())
+		}
+	}
+
+	if authKey == "" {
+		//Ask user for input
+		reader := bufio.NewReader(os.Stdin)
+		fmt.Println("Please enter the Zerotier authtoken manually: ")
+		input, _ := reader.ReadString('\n')
+		return strings.TrimSpace(input)
+	}
+
+	return ""
+}

+ 4 - 0
mod/ganserv/ganserv.go

@@ -58,6 +58,10 @@ type NetworkMetaData struct {
 	Desc string
 }
 
+type MemberMetaData struct {
+	Name string
+}
+
 type NetworkManager struct {
 	authToken        string
 	apiPort          int

+ 91 - 2
mod/ganserv/handlers.go

@@ -147,6 +147,7 @@ func (m *NetworkManager) HandleTest(w http.ResponseWriter, r *http.Request) {
 	utils.SendOK(w)
 }
 
+//Handle listing of network members. Set details=true for listing all details
 func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
 	netid, err := utils.GetPara(r, "netid")
 	if err != nil {
@@ -155,15 +156,103 @@ func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request
 	}
 
 	details, _ := utils.GetPara(r, "detail")
+
+	memberIds, err := m.getNetworkMembers(netid)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
 	if details == "" {
 		//Only show client ids
-		memberIds, err := m.getNetworkMembers(netid)
+		js, _ := json.Marshal(memberIds)
+		utils.SendJSONResponse(w, string(js))
+	} else {
+		//Show detail members info
+		detailMemberInfo := []*MemberInfo{}
+		for _, thisMemberId := range memberIds {
+			memInfo, err := m.getNetworkMemberInfo(netid, thisMemberId)
+			if err == nil {
+				detailMemberInfo = append(detailMemberInfo, memInfo)
+			}
+		}
+
+		js, _ := json.Marshal(detailMemberInfo)
+		utils.SendJSONResponse(w, string(js))
+	}
+}
+
+//Handle Authorization of members
+func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
+	netid, err := utils.PostPara(r, "netid")
+	if err != nil {
+		utils.SendErrorResponse(w, "net id not set")
+		return
+	}
+
+	memberid, err := utils.PostPara(r, "memid")
+	if err != nil {
+		utils.SendErrorResponse(w, "memid not set")
+		return
+	}
+
+	//Check if the target memeber exists
+	if !m.memberExistsInNetwork(netid, memberid) {
+		utils.SendErrorResponse(w, "member not exists in given network")
+		return
+	}
+
+	setAuthorized, err := utils.PostPara(r, "auth")
+	if err != nil || setAuthorized == "" {
+		//Get the member authorization state
+		memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
 		if err != nil {
 			utils.SendErrorResponse(w, err.Error())
 			return
 		}
 
-		js, _ := json.Marshal(memberIds)
+		js, _ := json.Marshal(memberInfo.Authorized)
 		utils.SendJSONResponse(w, string(js))
+	} else if setAuthorized == "true" {
+		m.AuthorizeMember(netid, memberid, true)
+	} else if setAuthorized == "false" {
+		m.AuthorizeMember(netid, memberid, false)
+	} else {
+		utils.SendErrorResponse(w, "unknown operation state: "+setAuthorized)
+	}
+}
+
+//Handle delete of a given memver
+func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
+	netid, err := utils.PostPara(r, "netid")
+	if err != nil {
+		utils.SendErrorResponse(w, "net id not set")
+		return
+	}
+
+	memberid, err := utils.PostPara(r, "memid")
+	if err != nil {
+		utils.SendErrorResponse(w, "memid not set")
+		return
 	}
+
+	//Check if that member is authorized.
+	memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
+	if err != nil {
+		utils.SendErrorResponse(w, "member not exists in given GANet")
+		return
+	}
+
+	if memberInfo.Authorized {
+		//Deauthorized this member before deleting
+		m.AuthorizeMember(netid, memberid, false)
+	}
+
+	//Remove the memeber
+	err = m.deleteMember(netid, memberid)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	utils.SendOK(w)
 }

+ 119 - 0
mod/ganserv/zerotier.go

@@ -89,6 +89,34 @@ type NetworkInfo struct {
 	} `json:"v6AssignMode"`
 }
 
+type MemberInfo struct {
+	ActiveBridge                 bool          `json:"activeBridge"`
+	Address                      string        `json:"address"`
+	AuthenticationExpiryTime     int           `json:"authenticationExpiryTime"`
+	Authorized                   bool          `json:"authorized"`
+	Capabilities                 []interface{} `json:"capabilities"`
+	CreationTime                 int64         `json:"creationTime"`
+	ID                           string        `json:"id"`
+	Identity                     string        `json:"identity"`
+	IPAssignments                []interface{} `json:"ipAssignments"`
+	LastAuthorizedCredential     interface{}   `json:"lastAuthorizedCredential"`
+	LastAuthorizedCredentialType interface{}   `json:"lastAuthorizedCredentialType"`
+	LastAuthorizedTime           int           `json:"lastAuthorizedTime"`
+	LastDeauthorizedTime         int           `json:"lastDeauthorizedTime"`
+	NoAutoAssignIps              bool          `json:"noAutoAssignIps"`
+	Nwid                         string        `json:"nwid"`
+	Objtype                      string        `json:"objtype"`
+	RemoteTraceLevel             int           `json:"remoteTraceLevel"`
+	RemoteTraceTarget            interface{}   `json:"remoteTraceTarget"`
+	Revision                     int           `json:"revision"`
+	SsoExempt                    bool          `json:"ssoExempt"`
+	Tags                         []interface{} `json:"tags"`
+	VMajor                       int           `json:"vMajor"`
+	VMinor                       int           `json:"vMinor"`
+	VProto                       int           `json:"vProto"`
+	VRev                         int           `json:"vRev"`
+}
+
 //Get the zerotier node info from local service
 func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
 	url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
@@ -467,3 +495,94 @@ func (m *NetworkManager) getNetworkMembers(networkId string) ([]string, error) {
 
 	return members, nil
 }
+
+func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool {
+	//Get a list of member
+	memberids, err := m.getNetworkMembers(netid)
+	if err != nil {
+		return false
+	}
+	for _, thisMemberId := range memberids {
+		if thisMemberId == memid {
+			return true
+		}
+	}
+
+	return false
+}
+
+//Get a network memeber info by netid and memberid
+func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
+	req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("X-Zt1-Auth", m.authToken)
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	thisMemeberInfo := &MemberInfo{}
+	payload, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	err = json.Unmarshal(payload, &thisMemeberInfo)
+	if err != nil {
+		return nil, err
+	}
+
+	return thisMemeberInfo, nil
+}
+
+//Set the authorization state of a member
+func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
+	url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
+	payload := []byte(`{"authorized": true}`)
+	if !setAuthorized {
+		payload = []byte(`{"authorized": false}`)
+	}
+
+	req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Set("X-ZT1-AUTH", m.authToken)
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
+	}
+
+	return nil
+}
+
+//Delete a member from the network
+func (m *NetworkManager) deleteMember(netid string, memid string) error {
+	req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
+	if err != nil {
+		return err
+	}
+	req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
+	}
+
+	return nil
+}

+ 5 - 1
start.go

@@ -130,8 +130,12 @@ func startupSequence() {
 	mdnsTickerStop = stopChan
 
 	//Create GAN Manager
+	usingZtAuthToken := *ztAuthToken
+	if usingZtAuthToken == "" {
+		usingZtAuthToken = ganserv.TryLoadorAskUserForAuthkey()
+	}
 	ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
-		AuthToken: "hgaode9ptnpuaoi1ilbdw9i4",
+		AuthToken: usingZtAuthToken,
 		ApiPort:   9993,
 		Database:  sysdb,
 	})

BIN
tmp/GeoLite2-Country.mmdb


+ 1 - 1
web/components/gan.html

@@ -114,7 +114,7 @@
                 data.forEach(function(gan){
                     $("#GANetList").append(`<tr>
                         <td>${gan.nwid}</td>
-                        <td>${""}</td>
+                        <td>${gan.name}</td>
                         <td>${""}</td>
                         <td>${""}</td>
                         <td>${""}</td>

+ 319 - 14
web/components/gandetails.html

@@ -1,7 +1,7 @@
 <div class="standardContainer">
     <button onclick="exitToGanList();" class="ui large circular black icon button"><i class="angle left icon"></i></button>
     <div style="max-width: 300px; margin-top: 1em;">
-        <button onclick='$("#gannetDetailEdit").toggle();' class="ui mini basic right floated circular icon button" style="display: inline-block; margin-top: 2.5em;"><i class="ui edit icon"></i></button>
+        <button onclick='$("#gannetDetailEdit").slideToggle("fast");' class="ui mini basic right floated circular icon button" style="display: inline-block; margin-top: 2.5em;"><i class="ui edit icon"></i></button>
         <h1 class="ui header">
             <span class="ganetID"></span>
             <div class="sub header ganetName"></div>
@@ -21,28 +21,152 @@
           <label>Network Description</label>
           <textarea id="gaNetDescInput" style="resize: none;"></textarea>
           <button onclick="saveNameAndDesc(this);" class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui save icon"></i> Save</button>
+          <button onclick='$("#gannetDetailEdit").slideUp("fast");' class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui red remove icon"></i> Cancel</button>
         </div>
-        <div class="field">
-            <table class="ui very basic collapsing celled table">
-                <tbody>
-                  <tr>
-                    <td>
-                     
-                    </td>
-                    <td>
-                      
-                    </td>
-                  </tr>
-                </tbody>
-            </table>
+    </div>
+    <div class="ui divider"></div>
+    <h2>Settings</h2>
+    <div class="" style="overflow-x: auto;">
+        <table class="ui basic celled unstackable table" style="min-width: 560px;">
+            <thead>
+                <tr>
+                    <th colspan="4">IPv4 Auto-Assign</th>
+                </tr>
+            </thead>
+            <tbody id="ganetRangeTable">
+                
+            </tbody>
+        </table>
+    </div>
+    <div class="ui basic segment form">
+        <div class="unstackable fields">
+            <div class="ten wide field">
+                <label>Multicast Recipient Limit</label>
+                <input type="number" id="" placeholder="32" value="32">
+            </div>
+            <div class="six wide field">
+                <div class="ui toggle checkbox" style="margin-top: 2.3em; padding-left: 0.6em;">
+                    <label>Enable Multicast</label>
+                    <input type="checkbox" tabindex="0" class="hidden">
+                </div>
+            </div>
         </div>
     </div>
+
     <div class="ui divider"></div>
+    <h2>Members</h2>
+    <div class="ui checkbox" style="margin-bottom: 1em;">
+        <input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);">
+        <label>Show Unauthorized Members</label>
+    </div>
+    <div class="" style="overflow-x: auto;">
+        <table class="ui celled unstackable table">
+            <thead>
+                <tr>
+                    <th>Auth</th>
+                    <th>Address</th>
+                    <th>Name/Description</th>
+                    <th>Managed IP</th>
+                    <th>Last Seen</th>
+                    <th>Version</th>
+                    <th>Remove</th>
+                </tr>
+            </thead>
+            <tbody id="networkMemeberTable">
+                <tr>
+                    
+                </tr>
+            </tbody>
+        </table>
+    </div>
     <br><br>
 </div>
 <script>
+    $(".checkbox").checkbox();
     var currentGANetID = "";
+    var currentGANNetMemeberListener = undefined;
     var currentGaNetDetails = {};
+    var currentGANMemberList = [];
+    var netRanges = {
+            "10.147.17.*": "10.147.17.0/24",
+            "10.147.18.*": "10.147.18.0/24",
+            "10.147.19.*": "10.147.19.0/24",
+            "10.147.20.*": "10.147.20.0/24",
+            "10.144.*.*": "10.144.0.0/16",
+            "10.241.*.*": "10.241.0.0/16",
+            "10.242.*.*": "10.242.0.0/16",
+            "10.243.*.*": "10.243.0.0/16",
+            "10.244.*.*": "10.244.0.0/16",
+            "172.22.*.*": "172.22.0.0/15",
+            "172.23.*.*": "172.23.0.0/16",
+            "172.24.*.*": "172.24.0.0/14",
+            "172.25.*.*": "172.25.0.0/16",
+            "172.26.*.*": "172.26.0.0/15",
+            "172.27.*.*": "172.27.0.0/16",
+            "172.28.*.*": "172.28.0.0/15",
+            "172.29.*.*": "172.29.0.0/16",
+            "172.30.*.*": "172.30.0.0/15",
+            "192.168.191.*": "192.168.191.0/24",
+            "192.168.192.*": "192.168.192.0/24",
+            "192.168.193.*": "192.168.193.0/24",
+            "192.168.194.*": "192.168.194.0/24",
+            "192.168.195.*": "192.168.195.0/24",
+            "192.168.196.*": "192.168.196.0/24"
+        }
+
+    function generateIPRangeTable(netRanges) {
+        $("#ganetRangeTable").empty();
+        const tableBody = document.getElementById('ganetRangeTable');
+        const cidrs = Object.values(netRanges);
+        
+        // Set the number of rows and columns to display in the table
+        const numRows = 6;
+        const numCols = 4;
+
+        let row = document.createElement('tr');
+        let col = 0;
+        for (let i = 0; i < cidrs.length; i++) {
+            if (col >= numCols) {
+            tableBody.appendChild(row);
+            row = document.createElement('tr');
+            col = 0;
+            }
+
+            const td = document.createElement('td');
+            td.setAttribute('class', `clickable iprange`);
+            td.setAttribute('CIDR', cidrs[i]);
+            td.innerHTML = cidrs[i];
+            let thisCidr = cidrs[i];
+            td.onclick = function(){
+                selectNetworkRange(thisCidr);
+            };
+            
+            row.appendChild(td);
+            col++;
+        }
+
+        // Add any remaining cells to the table
+        if (col > 0) {
+            for (let i = col; i < numCols; i++) {
+                row.appendChild(document.createElement('td'));
+            }
+            tableBody.appendChild(row);
+        }
+    }
+
+    function highlightCurrentGANetCIDR(){
+        var currentCIDR = currentGaNetDetails.routes[0].target;
+        $(".iprange").each(function(){
+            if ($(this).attr("CIDR") == currentCIDR){
+                $(this).addClass("active");
+            }
+        })
+    }
+
+    function selectNetworkRange(cidr){
+        alert(cidr);
+    }
+
 
     function saveNameAndDesc(object=undefined){
         var name = $("#gaNetNameInput").val();
@@ -82,11 +206,192 @@
         });
     }
 
+    function initNetDetails(){
+         //Get the details of the net
+         $.get("/api/gan/network/list?netid=" + currentGANetID, function(data){
+            if (data.error !== undefined){
+                msgbox(data.error, false, 6000);
+            }else{
+                currentGaNetDetails = data;
+                highlightCurrentGANetCIDR();
+            }
+        });
+    }
+
+    //Member table populate
+    function renderMemeberTable(forceUpdate = false) {
+        $.ajax({
+            url: '/api/gan/members/list?netid=e7dd1ce7bfd3b1f9&detail=true',
+            type: 'GET',
+            success: function(data) {
+                const tableBody = $('#networkMemeberTable');
+                data.sort((a, b) => a.address.localeCompare(b.address));
+                //Check if the new object equal to the old one
+                if (objectEqual(currentGANMemberList, data) && !forceUpdate){
+                    //Do not need to update it
+                    return;
+                }
+                tableBody.empty();
+                currentGANMemberList = data;
+
+                data.forEach((member) => {
+                    let lastAuthTime = new Date(member.lastAuthorizedTime).toLocaleString();
+                    if (member.lastAuthorizedTime == 0){
+                        lastAuthTime = "Never";
+                    }
+
+                    let version = `${member.vMajor}.${member.vMinor}.${member.vProto}.${member.vRev}`;
+                    if (member.vMajor == -1){
+                        version = "Unknown";
+                    }
+
+                    let authorizedCheckbox = `<div class="ui fitted checkbox">
+                        <input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);">
+                        <label></label>
+                    </div>`;
+                    if (member.authorized){
+                        authorizedCheckbox = `<div class="ui fitted checkbox">
+                            <input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);" checked="">
+                            <label></label>
+                        </div>`
+                    }
+
+                    let rowClass = "authorized";
+                    let unauthorizedStyle = "";
+                    if (!$("#showUnauthorizedMembers")[0].checked && !member.authorized){
+                        unauthorizedStyle = "display:none;";
+                    }
+                    if (!member.authorized){
+                        rowClass = "unauthorized"
+                    }
+                    const row = $(`<tr class="GANetMemberEntity ${rowClass}" style="${unauthorizedStyle}">`);
+                    row.append($(`<td class="GANetMember ${rowClass}" style="text-align: center;">`).html(authorizedCheckbox));
+                    row.append($('<td>').text(member.address));
+                    row.append($('<td>').text(""));
+                    row.append($('<td>').text(member.ipAssignments || ''));
+                    row.append($('<td>').text(lastAuthTime));
+                    row.append($('<td>').text(version));
+                    row.append($(`<td style="text-align: center;" onclick="handleMemberDelete('${member.address}');">`).html(`<button class="ui basic mini icon button"><i class="red remove icon"></i></button>`));
+
+                    tableBody.append(row);
+                });
+            },
+            error: function(xhr, status, error) {
+                console.log('Error:', error);
+            }
+        });
+    }
+
+    function objectEqual(obj1, obj2) {
+        // compare types
+        if (typeof obj1 !== typeof obj2) {
+            return false;
+        }
+
+        // compare values
+        if (typeof obj1 !== 'object' || obj1 === null) {
+            return obj1 === obj2;
+        }
+
+        const keys1 = Object.keys(obj1);
+        const keys2 = Object.keys(obj2);
+
+        // compare keys
+        if (keys1.length !== keys2.length) {
+            return false;
+        }
+
+        for (const key of keys1) {
+            if (!keys2.includes(key)) {
+            return false;
+            }
+
+            // recursively compare values
+            if (!objectEqual(obj1[key], obj2[key])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+
+    function changeUnauthorizedVisibility(visable){
+        if(visable){
+            $(".GANetMemberEntity.unauthorized").show();
+        }else{
+            $(".GANetMemberEntity.unauthorized").hide();
+        }
+    }
+
+    function handleMemberAuth(object){
+        let targetMemberAddr = $(object).attr("addr");
+        let isAuthed = object.checked;
+        $.ajax({
+            url: "/api/gan/members/authorize",
+            method: "POST",
+            data: {
+                netid:currentGANetID, 
+                memid: targetMemberAddr, 
+                auth: isAuthed
+            },
+            success: function(data){
+                if (data.error != undefined){
+                    msgbox(data.error, false, 6000);
+                }else{
+                    if (isAuthed){
+                        msgbox("Member Authorized");
+                    }else{
+                        msgbox("Member Deauthorized");
+                    }
+                    
+                }
+
+                renderMemeberTable(true);
+            }
+        })
+    }
+
+    function handleMemberDelete(addr){
+        if (confirm("Confirm delete member " + addr + " ?")){
+            $.ajax({
+                url: "/api/gan/members/delete",
+                method: "POST",
+                data: {
+                    netid:currentGANetID, 
+                    memid: addr, 
+                },
+                success: function(data){
+                    if (data.error != undefined){
+                        msgbox(data.error, false, 6000);
+                    }else{
+                        msgbox("Member Deleted");
+                    }
+                    renderMemeberTable(true);
+                }
+            });
+        }
+       
+    }
+
     //Entry points
     function initGanetDetails(ganetId){
         currentGANetID = ganetId;
         $(".ganetID").text(ganetId);
         initNetNameAndDesc(ganetId);
+        generateIPRangeTable(netRanges);
+        initNetDetails();
+        renderMemeberTable(true);
+
+        //Setup a listener to listen for member list change
+        if (currentGANNetMemeberListener == undefined){
+            currentGANNetMemeberListener = setInterval(function(){
+                if ($('#networkMemeberTable').length > 0 && currentGANetID){
+                    renderMemeberTable();
+                }
+            }, 3000);
+        }
+        
     }
 
     //Exit point

+ 20 - 1
web/main.css

@@ -435,4 +435,23 @@ body{
 /*
     Global Area Network
     gan.html
-*/
+*/
+.clickable{
+    cursor: pointer;
+}
+
+.clickable:hover{
+    opacity: 0.6;
+}
+
+#ganetRangeTable td{
+    text-align: center;
+}
+
+.GANetMember.authorized{
+    border-left: 6px solid #3c9c63 !important;
+}
+
+.GANetMember.unauthorized{
+    border-left: 6px solid #9c3c3c !important;
+}