<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").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> </h1> <div class="ui divider"></div> <p><span class="ganetDesc"></span></p> </div> <div id="gannetDetailEdit" class="ui form" style="margin-top: 1em; display:none;"> <div class="ui divider"></div> <p>You can change the network name and description below. <br>The name and description is only for easy management purpose and will not effect the network operation.</p> <div class="field"> <label>Network Name</label> <input type="text" id="gaNetNameInput" placeholder=""> </div> <div class="field"> <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> <br><br> </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> <br> <div class="ui form"> <h3>Custom IP Range</h3> <p>Manual IP Range Configuration. The IP range must be within the selected CIDR range. <br>Use <code>Utilities > IP to CIDR tool</code> if you are not too familiar with CIDR notations.</p> <div class="two fields"> <div class="field"> <label>IP Start</label> <input type="text" class="ganIpStart" placeholder=""> </div> <div class="field"> <label>IP End</label> <input type="text" class="ganIpEnd" placeholder=""> </div> </div> </div> <button onclick="setNetworkRange();" class="ui basic button"><i class="ui blue save icon"></i> Save Settings</button> <div class="ui divider"></div> <h2>Members</h2> <p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p> <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</th> <th>Managed IP</th> <th>Authorized Since</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, td); }; 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"); populateStartEndIpByCidr(currentCIDR); } }) } function populateStartEndIpByCidr(cidr){ function cidrToRange(cidr) { var range = [2]; cidr = cidr.split('/'); var start = ip2long(cidr[0]); range[0] = long2ip(start); range[1] = long2ip(Math.pow(2, 32 - cidr[1]) + start - 1); return range; } var cidrRange = cidrToRange(cidr); $(".ganIpStart").val(cidrRange[0]); $(".ganIpEnd").val(cidrRange[1]); } function selectNetworkRange(cidr, object){ populateStartEndIpByCidr(cidr); $(".iprange.active").removeClass("active"); $(object).addClass("active"); } function setNetworkRange(){ var ipstart = $(".ganIpStart").val().trim(); var ipend = $(".ganIpEnd").val().trim(); if (ipstart == ""){ $(".ganIpStart").parent().addClass("error"); }else{ $(".ganIpStart").parent().removeClass("error"); } if (ipend == ""){ $(".ganIpEnd").parent().addClass("error"); }else{ $(".ganIpEnd").parent().removeClass("error"); } //Get CIDR from selected range group var cidr = $(".iprange.active").attr("cidr"); $.ajax({ url: "/api/gan/network/setRange", metohd: "POST", data:{ netid: currentGANetID, cidr: cidr, ipstart: ipstart, ipend: ipend }, success: function(data){ if (data.error != undefined){ msgbox(data.error, false, 5000) }else{ msgbox("Network Range Updated") } } }) } function saveNameAndDesc(object=undefined){ var name = $("#gaNetNameInput").val(); var desc = $("#gaNetDescInput").val(); if (object != undefined){ $(object).addClass("loading"); } $.ajax({ url: "/api/gan/network/name", method: "POST", data: { netid: currentGANetID, name: name, desc: desc, }, success: function(data){ initNetNameAndDesc(); if (object != undefined){ $(object).removeClass("loading"); msgbox("Network Metadata Updated"); } $("#gannetDetailEdit").slideUp("fast"); } }); } function initNetNameAndDesc(){ //Get the details of the net $.get("/api/gan/network/name?netid=" + currentGANetID, function(data){ if (data.error !== undefined){ msgbox(data.error, false, 6000); }else{ $("#gaNetNameInput").val(data[0]); $(".ganetName").html(data[0]); $("#gaNetDescInput").val(data[1]); $(".ganetDesc").text(data[1]); } }); } 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(); } }); } //Handle delete IP from memeber function deleteIpFromMemeber(memberid, ip){ $.ajax({ url: "/api/gan/members/ip", metohd: "POST", data: { netid: currentGANetID, memid: memberid, opr: "del", ip: ip, }, success: function(data){ if (data.error != undefined){ msgbox(data.error, false, 5000); }else{ msgbox("IP removed from member " + memberid) } renderMemeberTable(); } }); } function addIpToMemeberFromInput(memberid, newip){ function isValidIPv4Address(address) { // Split the address into its 4 components const parts = address.split('.'); // Check that there are 4 components if (parts.length !== 4) { return false; } // Check that each component is a number between 0 and 255 for (let i = 0; i < 4; i++) { const part = parseInt(parts[i], 10); if (isNaN(part) || part < 0 || part > 255) { return false; } } // The address is valid return true; } if (!isValidIPv4Address(newip)){ msgbox(newip + " is not a valid IPv4 address", false, 5000) return } $.ajax({ url: "/api/gan/members/ip", metohd: "POST", data: { netid: currentGANetID, memid: memberid, opr: "add", ip: newip, }, success: function(data){ if (data.error != undefined){ msgbox(data.error, false, 5000); }else{ msgbox("IP added to member " + memberid) } renderMemeberTable(); } }) } //Member table populate function renderMemeberTable(forceUpdate = false) { $.ajax({ url: '/api/gan/members/list?netid=' + currentGANetID + '&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; var authroziedCount = 0; 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"; }else{ authroziedCount++; } let assignedIp = ""; if (member.ipAssignments.length == 0){ assignedIp = "Not assigned" }else{ assignedIp = `<div class="ui list">` member.ipAssignments.forEach(function(thisIp){ assignedIp += `<div class="item" style="width: 100%;">${thisIp} <a style="cursor:pointer; float: right;" title="Remove IP" onclick="deleteIpFromMemeber('${member.address}','${thisIp}');"><i class="red remove icon"></i></a></div>`; }) assignedIp += `</div>` } 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>').html(`<span class="memberName" addr="${member.address}"></span> <a style="cursor:pointer; float: right;" title="Edit Memeber Name" onclick="renameMember('${member.address}');"><i class="grey edit icon"></i></a>`)); row.append($('<td>').html(`${assignedIp} <div class="ui action mini fluid input" style="min-width: 200px;"> <input type="text" placeholder="IPv4" onchange="$(this).val($(this).val().trim());"> <button onclick="addIpToMemeberFromInput('${member.address}',$(this).parent().find('input').val());" class="ui basic icon button"> <i class="add icon"></i> </button> </div>`)); row.append($('<td>').text(lastAuthTime)); row.append($('<td>').text(version)); row.append($(`<td title="Deauthorize & Delete Memeber" 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); }); if (data.length == 0){ tableBody.append(`<tr> <td colspan="7"><i class="green check circle icon"></i> No member has joined this network yet.</td> </tr>`); } if (data.length > 0 && authroziedCount == 0 && !$("#showUnauthorizedMembers")[0].checked){ //All nodes are unauthorized. Show tips to enable unauthorize display tableBody.append(`<tr> <td colspan="7"><i class="yellow exclamation circle icon"></i> Unauthorized nodes detected. Enable "Show Unauthorized Member" to change member access permission.</td> </tr>`); } initNameForMembers(); }, error: function(xhr, status, error) { console.log('Error:', error); } }); } function initNameForMembers(){ $(".memberName").each(function(){ let addr = $(this).attr("addr"); let targetDOM = $(this); $.ajax({ url: "/api/gan/members/name", method: "POST", data: { netid: currentGANetID, memid: addr, }, success: function(data){ if (data.error != undefined){ $(targetDOM).text("N/A"); }else{ $(targetDOM).text(data.Name); } } }); }) } function renameMember(targetMemberAddr){ if (targetMemberAddr == ""){ msgbox("Member address cannot be empty", false, 5000) return } let newname = prompt("Enter a easy manageable name for " + targetMemberAddr, ""); if (newname != null && newname.trim() != "") { $.ajax({ url: "/api/gan/members/name", method: "POST", data: { netid: currentGANetID, memid: targetMemberAddr, name: newname }, success: function(data){ if (data.error != undefined){ msgbox(data.error, false, 6000); }else{ msgbox("Member Name Updated"); } renderMemeberTable(true); } }) } } //Helper function to check if two objects are equal recursively 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 function exitToGanList(){ $("#gan").load("./components/gan.html", function(){ if (tabSwitchEventBind["gan"]){ tabSwitchEventBind["gan"](); } }); } </script>