`
break;
}
title_element.innerHTML = title_html
content_element.innerHTML = content_html
var instance = M.Modal.getInstance(element);
instance.open()
}
// Enables the Floating Action Button (FAB) for the Machines and Users page
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.fixed-action-btn');
var instances = M.FloatingActionButton.init(elems, {hoverEnabled:false});
});
// Init the date picker when adding PreAuth keys
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.datepicker');
var instances = M.Datepicker.init(elems);
});
//-----------------------------------------------------------
// Settings Page Actions
//-----------------------------------------------------------
function test_key() {
document.getElementById('test_modal_results').innerHTML = loading()
var data = $.ajax({
type:"GET",
url: "api/test_key",
success: function(response) {
if(response == "Unauthenticated") {
html = `
warningError
Key authentication failed. Check your key.
`
document.getElementById('test_modal_results').innerHTML = html
} else {
json = JSON.parse(response)
var html = `
checkSuccess
Key authenticated with the Headscale server.
Key Information
Key ID
${json['id']}
Prefix
${json['prefix']}
Expiration Date
${json['expiration']}
Creation Date
${json['createdAt']}
`
document.getElementById('test_modal_results').innerHTML = html
}
}
})
}
function save_key() {
var api_key = document.getElementById('api_key').value;
if (!api_key) {
html = `
`
for (let i=0; i < response.users.length; i++) {
var name = response["users"][i]["name"]
select_html = select_html+`
`
}
select_html = select_html+`
`
select_html = select_html+`
`
modal_body.innerHTML = select_html
// Initialize the form and the machine tabs
M.FormSelect.init(document.querySelectorAll('select'))
M.Tabs.init(document.getElementById('new_machine_tabs'));
}
})
}
//-----------------------------------------------------------
// Machine Page Actions
//-----------------------------------------------------------
function delete_chip(machine_id, chipsData) {
// We need to get ALL the current tags -- We don't care about what's deleted, just what's remaining
// chipsData is an array generated from from the creation of the array.
chips = JSON.stringify(chipsData)
var formattedData = [];
for (let tag in chipsData) {
formattedData[tag] = '"tag:'+chipsData[tag].tag+'"'
}
var tags_list = '{"tags": [' + formattedData + ']}'
var data = {"id": machine_id, "tags_list": tags_list}
$.ajax({
type:"POST",
url: "api/set_machine_tags",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
M.toast({html: 'Tag removed.'});
}
})
}
function add_chip(machine_id, chipsData) {
chips = JSON.stringify(chipsData).toLowerCase()
chipsData[chipsData.length - 1].tag = chipsData[chipsData.length - 1].tag.trim().replace(/\s+/g, '-')
last_chip_fixed = chipsData[chipsData.length - 1].tag
var formattedData = [];
for (let tag in chipsData) {
formattedData[tag] = '"tag:'+chipsData[tag].tag+'"'
}
var tags_list = '{"tags": [' + formattedData + ']}'
var data = {"id": machine_id, "tags_list": tags_list}
$.ajax({
type:"POST",
url: "api/set_machine_tags",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
M.toast({html: 'Tag "' + last_chip_fixed + '" added.'});
}
})
}
function add_machine() {
var key = document.getElementById('add_machine_key_field').value
var user = document.getElementById('add_machine_user_select').value
var data = {"key": key, "user": user}
if (user == "") {
load_modal_generic("error", "User is empty", "Select a user before submitting")
return
}
if (key == "") {
load_modal_generic("error", "Key is empty", "Input the key generated by your tailscale login command")
return
}
$.ajax({
type: "POST",
url: "api/register_machine",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
alert("Response: "+response)
}
})
}
function rename_machine(machine_id) {
var new_name = document.getElementById('new_name_form').value;
var data = {"id": machine_id, "new_name": new_name};
// String to test against
var regexIT= /[`!@#$%^&*()_+\=\[\]{};':"\\|,.<>\/?~]/;
if (regexIT.test(new_name)) { load_modal_generic("error", "Invalid Name", "Name cannot contain special characters ('"+regexIT+"')") ;return }
// If there are characters other than - and alphanumeric, throw an error
if (new_name.includes(' ')) { load_modal_generic("error", "Name cannot have spaces", "Allowed characters are dashes (-) and alphanumeric characters"); return }
// If it is longer than 32 characters, throw an error
if (new_name.length > 32) { load_modal_generic("error", "Name is too long", "The name name is too long. Maximum length is 32 characters"); return }
// If the new_name is empty, throw an error
if (!new_name) { load_modal_generic("error", "Name can't be empty", "Please enter a machine name before submitting."); return}
$.ajax({
type:"POST",
url: "api/rename_machine",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
if (response.status == "True") {
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
document.getElementById(machine_id+'-name-container').innerHTML = machine_id+". "+new_name
M.toast({html: 'Machine '+machine_id+' renamed to '+new_name});
} else {
load_modal_generic("error", "Error setting the machine name", "Headscale response: "+JSON.stringify(response.body.message))
}
}
})
}
function move_machine(machine_id) {
new_user = document.getElementById('move-select').value
var data = {"id": machine_id, "new_user": new_user};
$.ajax({
type:"POST",
url: "api/move_user",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
document.getElementById(machine_id+'-user-container').innerHTML = response.machine.user.name
document.getElementById(machine_id+'-ns-badge').innerHTML = response.machine.user.name
// Get the color and set it
var user_color = get_color(response.machine.user.id)
document.getElementById(machine_id+'-ns-badge').className = "badge ipinfo " + user_color + " white-text hide-on-small-only"
M.toast({html: "'"+response.machine.givenName+"' moved to user "+response.machine.user.name});
}
})
}
function delete_machine(machine_id) {
var data = {"id": machine_id};
$.ajax({
type: "POST",
url: "api/delete_machine",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
// When the machine is deleted, hide its collapsible:
document.getElementById(machine_id+'-main-collapsible').className = "collapsible popout hide";
M.toast({html: 'Machine deleted.'});
}
})
}
function toggle_route(route_id, current_state) {
var data = {"route_id": route_id, "current_state": current_state}
$.ajax({
type:"POST",
url: "api/update_route",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
// Response is a JSON object containing the Headscale API response of /v1/api/machines//route
var element = document.getElementById(route_id);
var disabledClass = "waves-effect waves-light btn-small red lighten-2 tooltipped";
var enabledClass = "waves-effect waves-light btn-small green lighten-2 tooltipped";
var disabledTooltip = "Click to enable"
var enabledTooltip = "Click to disable"
var disableState = "False"
var enableState = "True"
var action_taken = "unchanged.";
if (element.className == disabledClass) {
// 1. Change the class to change the color of the icon
// 2. Change the "action taken" for the M.toast popup
// 3. Change the tooltip to say "Click to enable/disable"
element.className = enabledClass
var action_taken = "enabled."
element.setAttribute('data-tooltip', enabledTooltip)
element.setAttribute('onclick', 'toggle_route('+route_id+', "'+enableState+'")')
} else if (element.className == enabledClass) {
element.className = disabledClass
var action_taken = "disabled."
element.setAttribute('data-tooltip', disabledTooltip)
element.setAttribute('onclick', 'toggle_route('+route_id+', "'+disableState+'")')
}
M.toast({html: 'Route '+action_taken});
}
})
}
//-----------------------------------------------------------
// Machine Page Helpers
//-----------------------------------------------------------
function btn_toggle(state) {
if (state == "show" ) { document.getElementById('new_machine_modal_confirm').className = 'green btn-flat white-text' }
else { document.getElementById('new_machine_modal_confirm').className = 'green btn-flat white-text hide' }
}
//-----------------------------------------------------------
// User Page Actions
//-----------------------------------------------------------
function rename_user(user_id, old_name) {
var new_name = document.getElementById('new_user_name_form').value;
var data = {"old_name": old_name, "new_name": new_name}
// String to test against
var regexIT= /[`!@#$%^&*()_+\=\[\]{};':"\\|,.<>\/?~]/;
if (regexIT.test(new_name)) { load_modal_generic("error", "Invalid Name", "Name cannot contain special characters ('"+regexIT+"')") ;return }
// If there are characters other than - and alphanumeric, throw an error
if (new_name.includes(' ')) { load_modal_generic("error", "Name cannot have spaces", "Allowed characters are dashes (-) and alphanumeric characters"); return }
// If it is longer than 32 characters, throw an error
if (new_name.length > 32) { load_modal_generic("error", "Name is too long", "The user name is too long. Maximum length is 32 characters"); return }
// If the new_name is empty, throw an error
if (!new_name) { load_modal_generic("error", "Name can't be empty", "The user name cannot be empty."); return}
$.ajax({
type: "POST",
url: "api/rename_user",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
if (response.status == "True") {
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
// Rename the user on the page:
document.getElementById(user_id+'-name-span').innerHTML = new_name
// Set the button to use the NEW name as the OLD name for both buttons
var rename_button_sm = document.getElementById(user_id+'-rename-user-sm')
rename_button_sm.setAttribute('onclick', 'load_modal_rename_user('+user_id+', "'+new_name+'")')
var rename_button_lg = document.getElementById(user_id+'-rename-user-lg')
rename_button_lg.setAttribute('onclick', 'load_modal_rename_user('+user_id+', "'+new_name+'")')
// Send the completion toast
M.toast({html: "User '"+old_name+"' renamed to '"+new_name+"'."})
} else {
load_modal_generic("error", "Error setting user name", "Headscale response: "+JSON.stringify(response.body.message))
}
}
})
}
function delete_user(user_id, user_name) {
var data = {"name": user_name};
$.ajax({
type: "POST",
url: "api/delete_user",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
if (response.status == "True") {
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
// When the machine is deleted, hide its collapsible:
document.getElementById(user_id+'-main-collapsible').className = "collapsible popout hide";
M.toast({html: 'User deleted.'});
} else {
// We errored. Decipher the error Headscale sent us and display it:
load_modal_generic("error", "Error deleting user", "Headscale response: "+JSON.stringify(response.body.message))
}
}
})
}
function add_user() {
var user_name = document.getElementById('add_user_name_field').value
var data = {"name": user_name}
$.ajax({
type: "POST",
url: "api/add_user",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
if (response.status == "True") {
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
// Send the completion toast
M.toast({html: "User '"+user_name+"' added to Headscale. Refreshing..."})
window.location.reload()
} else {
// We errored. Decipher the error Headscale sent us and display it:
load_modal_generic("error", "Error adding user", "Headscale response: "+JSON.stringify(response.body.message))
}
}
})
}
function add_preauth_key(user_name) {
var date = document.getElementById('preauth_key_expiration_date').value
var ephemeral = document.getElementById('checkbox-ephemeral').checked
var reusable = document.getElementById('checkbox-reusable').checked
var expiration = date+"T00:00:00.000Z" // Headscale format.
// If there is no date, error:
if (!date) {load_modal_generic("error", "Invalid Date", "Please enter a valid date"); return}
var data = {"user": user_name, "reusable": reusable, "ephemeral": ephemeral, "expiration": expiration}
$.ajax({
type: "POST",
url: "api/add_preauth_key",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
if (response.status == "True") {
// Send the completion toast
M.toast({html: 'PreAuth key created in user '+user_name})
// If this is successfull, we should reload the table and close the modal:
var user_data = {"name": user_name}
$.ajax({
type: "POST",
url: "api/build_preauthkey_table",
data: JSON.stringify(user_data),
contentType: "application/json",
success: function(table_data) {
table = document.getElementById(user_name+'-preauth-keys-collection')
table.innerHTML = table_data
// The tooltips need to be re-initialized afterwards:
M.Tooltip.init(document.querySelectorAll('.tooltipped'))
}
})
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
// The tooltips need to be re-initialized afterwards:
M.Tooltip.init(document.querySelectorAll('.tooltipped'))
} else {
load_modal_generic("error", "Error adding a pre-auth key", "Headscale response: "+JSON.stringify(response.body.message))
}
}
})
}
function expire_preauth_key(user_name, key) {
var data = {"user": user_name, "key": key}
$.ajax({
type: "POST",
url: "api/expire_preauth_key",
data: JSON.stringify(data),
contentType: "application/json",
success: function(response) {
if (response.status == "True") {
// Send the completion toast
M.toast({html: 'PreAuth expired in '+user_name})
// If this is successfull, we should reload the table and close the modal:
var user_data = {"name": user_name}
$.ajax({
type: "POST",
url: "api/build_preauthkey_table",
data: JSON.stringify(user_data),
contentType: "application/json",
success: function(table_data) {
table = document.getElementById(user_name+'-preauth-keys-collection')
table.innerHTML = table_data
// The tooltips need to be re-initialized afterwards:
M.Tooltip.init(document.querySelectorAll('.tooltipped'))
}
})
// Get the modal element and close it
modal_element = document.getElementById('card_modal')
M.Modal.getInstance(modal_element).close()
// The tooltips need to be re-initialized afterwards:
M.Tooltip.init(document.querySelectorAll('.tooltipped'))
} else {
load_modal_generic("error", "Error expiring a pre-auth key", "Headscale response: "+JSON.stringify(response.body.message))
}
}
})
}
//-----------------------------------------------------------
// User Page Helpers
//-----------------------------------------------------------
// Toggle expired items on the Users PreAuth section:
function toggle_expired() {
var toggle_hide = document.getElementsByClassName('expired-row');
var hidden = document.getElementsByClassName('expired-row hide');
if (hidden.length == 0) {
for (var i = 0; i < toggle_hide.length; i++) {
toggle_hide[i].className = "expired-row hide";
}
} else if (hidden.length > 0) {
for (var i = 0; i < toggle_hide.length; i++) {
toggle_hide[i].className = "expired-row";
}
}
}
// Copy a PreAuth Key to the clipboard. Show only the Prefix by default
function copy_preauth_key(key) {
navigator.clipboard.writeText(key);
M.toast({html: 'PreAuth key copied to clipboard.'})
}