diff --git a/Dockerfile b/Dockerfile index 1b4ab3a..28c1cb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ ENV TZ="UTC" ENV COLOR="blue-grey" ENV HS_SERVER=http://localhost/ ENV KEY="" -ENV SCRIPT_NAME=/ +# ENV SCRIPT_NAME=/ ENV DOMAIN_NAME=http://localhost ENV AUTH_TYPE="" ENV LOG_LEVEL="Info" diff --git a/Jenkinsfile b/Jenkinsfile index 1c4189c..c587df6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,10 +3,11 @@ pipeline { label 'linux-x64' } environment { - APP_VERSION = 'v0.5.6' - HS_VERSION = "v0.20.0" // Version of Headscale this is compatible with + APP_VERSION = 'v0.6.0' + HS_VERSION = "v0.21.0" // Version of Headscale this is compatible with BUILD_DATE = '' - BUILDER_NAME = "multiarch-${env.BRANCH_NAME}" + BUILDER_NAME = "multiarch-${env.BUILD_TAG}" + DOCKERHUB_CRED = credentials('dockerhub-ifargle-pat') @@ -21,14 +22,11 @@ pipeline { timestamps() } stages { - stage ('Jenkins ENV') { + stage('Build ENV') { steps { + sh 'printenv' script { BUILD_DATE = java.time.LocalDate.now() } - } - } - stage('Create Build ENV') { - steps { sh """ # Create the builder: docker buildx create --name $BUILDER_NAME --driver-opt=image=moby/buildkit @@ -85,6 +83,30 @@ pipeline { } } } + stage('Pull Test') { + steps { + script { + if (env.BRANCH_NAME == 'main') { + sh """ + docker pull git.sysctl.io/albert/headscale-webui:latest + docker pull registry-1.docker.io/ifargle/headscale-webui:latest + docker pull ghcr.io/ifargle/headscale-webui:latest + docker pull git.sysctl.io/albert/headscale-webui:${APP_VERSION} + docker pull registry-1.docker.io/ifargle/headscale-webui:${APP_VERSION} + docker pull ghcr.io/ifargle/headscale-webui:${APP_VERSION} + """ + } + else { + sh """ + docker pull git.sysctl.io/albert/headscale-webui:testing + docker pull ghcr.io/ifargle/headscale-webui:testing + docker pull git.sysctl.io/albert/headscale-webui:${env.BRANCH_NAME} + docker pull ghcr.io/ifargle/headscale-webui:${env.BRANCH_NAME} + """ + } + } + } + } } post { always { diff --git a/README.md b/README.md index 1085e11..b7c8420 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ --- # Features 1. Enable/Disable routes and exit nodes + * Manage failover routes as well 2. Add, move, rename, and remove machines 3. Add and remove users/namespaces 4. Add and expire PreAuth keys @@ -32,8 +33,13 @@ * Enable / disable routes and exit nodes * Add and delete machine tags 7. Basic and OIDC Authentication - * OIDC Authentication tested with Authelia + * OIDC Authentication tested with Authelia and Keycloak 8. Change your color theme! See MaterializeCSS Documentation for Colors for examples. +9. Search your machines and users. + * Machines have tags you can use to filter search: + * `tag:tagname` Searches only for specific tags + * `machine:machine-name` Searches only for specific machines + * `user:user-name` Searches only for specific users --- @@ -42,16 +48,11 @@ --- # Screenshots: -Overview Page: -![Overview](screenshots/oidc_overview.png) -Users Page: +![Overview](screenshots/overview.png) +![Routes](screenshots/routes.png) +![Machines](screenshots/machines.png) ![Users](screenshots/users.png) -Machine Information: -![Add a new machine](screenshots/machines_expanded.png) -Machines Page: -![Machine Details](screenshots/machines.png) -Settings Page showing an API Key Test: -![API Key Test](screenshots/settings.png) +![Settings](screenshots/settings.png) --- # Tech used: diff --git a/SETUP.md b/SETUP.md index db87cbe..72a0558 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,6 +1,4 @@ *PR's to help expand and improve documentation are always welcome!* -* Thanks to @FredericIV for assistance with Keycloak and Caddy -* Thanks to @qiangyt for assistance with general reverse proxy fixes and nginx # Installation and Setup * Use [docker-compose.yml](docker-compose.yml) as an example @@ -17,7 +15,7 @@ * `TZ` - Set this to your current timezone. Example: `Asia/Tokyo` * `COLOR` Set this to your preferred color scheme. See the [MaterializeCSS docs](https://materializecss.github.io/materialize/color.html#palette) for examples. Only set the "base" color -- ie, instead of `blue-gray darken-1`, just use `blue-gray`. * `HS_SERVER` is the URL for your Headscale control server. - * `SCRIPT_NAME` is your "Base Path" for hosting. For example, if you want to host on http://localhost/admin, set this to `/admin` + * `SCRIPT_NAME` is your "Base Path" for hosting. For example, if you want to host on http://localhost/admin, set this to `/admin`, otherwise remove this variable entirely. * `KEY` is your encryption key. Set this to a random value generated from `openssl rand -base64 32` * `AUTH_TYPE` can be set to `Basic` or `OIDC`. See the [Authentication](#Authentication) section below for more information. * `LOG_LEVEL` can be one of `Debug`, `Info`, `Warning`, `Error`, or `Critical` for decreasing verbosity. Default is `Info` if removed from your Environment. diff --git a/docker-compose.yml b/docker-compose.yml index f1d5880..d6b74da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: - COLOR=red # Use the base colors (ie, no darken-3, etc) - - HS_SERVER=https://headscale.$DOMAIN # Reachable endpoint for your Headscale server - DOMAIN_NAME=https://headscale.$DOMAIN # The base domain name for this container. - - SCRIPT_NAME=/admin # This is your applications base path (wsgi requires the name "SCRIPT_NAME") + - SCRIPT_NAME=/admin # This is your applications base path (wsgi requires the name "SCRIPT_NAME"). Remove if you are hosing at the root / - KEY="YourKeyBetweenQuotes" # Generate with "openssl rand -base64 32" - used to encrypt your key on disk. - AUTH_TYPE=oidc # AUTH_TYPE is either Basic or OIDC. Empty for no authentication - LOG_LEVEL=info # Log level. "DEBUG", "ERROR", "WARNING", or "INFO". Default "INFO" diff --git a/headscale.py b/headscale.py index adf1bb3..6805512 100644 --- a/headscale.py +++ b/headscale.py @@ -1,6 +1,6 @@ # pylint: disable=wrong-import-order -import requests, json, os, logging +import requests, json, os, logging, yaml from cryptography.fernet import Fernet from datetime import timedelta, date from dateutil import parser @@ -19,8 +19,21 @@ ################################################################## # Functions related to HEADSCALE and API KEYS ################################################################## - -def get_url(): return os.environ['HS_SERVER'] +def get_url(inpage=False): + if not inpage: + return os.environ['HS_SERVER'] + config_file = "" + try: + config_file = open("/etc/headscale/config.yml", "r") + app.logger.info("Opening /etc/headscale/config.yml") + except: + config_file = open("/etc/headscale/config.yaml", "r") + app.logger.info("Opening /etc/headscale/config.yaml") + config_yaml = yaml.safe_load(config_file) + if "server_url" in config_yaml: + return str(config_yaml["server_url"]) + app.logge.warning("Failed to find server_url in the config. Falling back to ENV variable") + return os.environ['HS_SERVER'] def set_api_key(api_key): # User-set encryption key @@ -194,9 +207,8 @@ def move_user(url, api_key, machine_id, new_user): return response.json() def update_route(url, api_key, route_id, current_state): - action = "" - if current_state == "True": action = "disable" - if current_state == "False": action = "enable" + action = "disable" if current_state == "True" else "enable" + app.logger.info("Updating Route %s: Action: %s", str(route_id), str(action)) # Debug @@ -299,9 +311,8 @@ def get_routes(url, api_key): } ) return response.json() - ################################################################## -# Functions related to NAMESPACES +# Functions related to USERS ################################################################## # Get all users in use @@ -370,7 +381,7 @@ def add_user(url, api_key, data): return {"status": status, "body": response.json()} ################################################################## -# Functions related to PREAUTH KEYS in NAMESPACES +# Functions related to PREAUTH KEYS in USERS ################################################################## # Get all PreAuth keys associated with a user "user_name" diff --git a/helper.py b/helper.py index 7ef771c..100a623 100644 --- a/helper.py +++ b/helper.py @@ -73,6 +73,21 @@ def key_check(): def get_color(import_id, item_type = ""): """ Sets colors for users/namespaces """ # Define the colors... Seems like a good number to start with + if item_type == "failover": + colors = [ + "teal lighten-1", + "blue lighten-1", + "blue-grey lighten-1", + "indigo lighten-2", + "brown lighten-1", + "grey lighten-1", + "indigo lighten-2", + "deep-orange lighten-1", + "yellow lighten-2", + "purple lighten-2", + ] + index = import_id % len(colors) + return colors[index] if item_type == "text": colors = [ "red-text text-lighten-1", diff --git a/pyproject.toml b/pyproject.toml index bc862d4..ceda49f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "headscale-webui" -version = "v0.5.6" +version = "v0.6.0" description = "A simple web UI for small-scale Headscale deployments." authors = ["Albert Copeland "] license = "AGPL" diff --git a/renderer.py b/renderer.py index 5d56c49..882a8d6 100644 --- a/renderer.py +++ b/renderer.py @@ -1,6 +1,6 @@ # pylint: disable=line-too-long, wrong-import-order -import headscale, helper, pytz, os, yaml, logging +import headscale, helper, pytz, os, yaml, logging, json from flask import Flask, Markup, render_template from datetime import datetime from dateutil import parser @@ -203,8 +203,7 @@ def render_overview(): # Remove OIDC if it isn't available: if "oidc" not in config_yaml: oidc_content = "" # Remove DERP if it isn't available or isn't enabled - if "derp" not in config_yaml: - derp_content = "" + if "derp" not in config_yaml: derp_content = "" if "derp" in config_yaml: if "server" in config_yaml["derp"]: if str(config_yaml["derp"]["server"]["enabled"]) == "False": @@ -231,12 +230,14 @@ def render_overview(): content = "
" + overview_content + general_content + derp_content + oidc_content + dns_content + "" return Markup(content) -def thread_machine_content(machine, machine_content, idx): +def thread_machine_content(machine, machine_content, idx, all_routes, failover_pair_prefixes): # machine = passed in machine information # content = place to write the content - app.logger.debug("Machine Information") - app.logger.debug(str(machine)) + # app.logger.debug("Machine Information") + # app.logger.debug(str(machine)) + app.logger.debug("Machine Information =================") + app.logger.debug("Name: %s, ID: %s, User: %s, givenName: %s, ", str(machine["name"]), str(machine["id"]), str(machine["user"]["name"]), str(machine["givenName"])) url = headscale.get_url() api_key = headscale.get_api_key() @@ -250,48 +251,124 @@ def thread_machine_content(machine, machine_content, idx): routes = "" # Test if the machine is an exit node: - exit_node = False - # If the LENGTH of "routes" is NULL/0, there are no routes, enabled or disabled: + exit_route_found = False + exit_route_enabled = False + # If the device has enabled Failover routes (High Availability routes) + ha_enabled = False + + # If the length of "routes" is NULL/0, there are no routes, enabled or disabled: if len(pulled_routes["routes"]) > 0: - advertised_and_enabled = False - advertised_route = False + advertised_routes = False + # First, check if there are any routes that are both enabled and advertised + # If that is true, we will output the collection-item for routes. Otherwise, it will not be displayed. for route in pulled_routes["routes"]: - if route ["advertised"] and route["enabled"]: - advertised_and_enabled = True - if route["advertised"]: - advertised_route = True - if advertised_and_enabled or advertised_route: + if route["advertised"]: + advertised_routes = True + if advertised_routes: routes = """
  • directions Routes -

    +

    """ + # app.logger.debug("Pulled Routes Dump: "+str(pulled_routes)) + # app.logger.debug("All Routes Dump: "+str(all_routes)) + + # Find all exits and put their ID's into the exit_routes array + exit_routes = [] + exit_enabled_color = "red" + exit_tooltip = "enable" + exit_route_enabled = False + for route in pulled_routes["routes"]: - app.logger.debug("Route: ["+str(route['machine']['name'])+"] id: "+str(route['id'])+" / prefix: "+str(route['prefix'])+" enabled?: "+str(route['enabled'])) - # Check if the route is enabled: - route_enabled = "red" - route_tooltip = 'enable' - if route["enabled"]: - route_enabled = "green" - route_tooltip = 'disable' - if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/0" and str(route["enabled"]) == "True": - exit_node = True - routes = routes+""" -

    - """+route['prefix']+""" + if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/0": + exit_routes.append(route["id"]) + exit_route_found = True + # Test if it is enabled: + if route["enabled"]: + exit_enabled_color = "green" + exit_tooltip = 'disable' + exit_route_enabled = True + app.logger.debug("Found exit route ID's: "+str(exit_routes)) + app.logger.debug("Exit Route Information: ID: %s | Enabled: %s | exit_route_enabled: %s / Found: %s", str(route["id"]), str(route["enabled"]), str(exit_route_enabled), str(exit_route_found)) + + # Print the button for the Exit routes: + if exit_route_found: + routes = routes+"""

    + Exit Route

    """ - routes = routes+"

  • " + + # Check if the route has another enabled identical route. + # Check all routes from the current machine... + for route in pulled_routes["routes"]: + # ... against all routes from all machines .... + for route_info in all_routes["routes"]: + app.logger.debug("Comparing routes %s and %s", str(route["prefix"]), str(route_info["prefix"])) + # ... If the route prefixes match and are not exit nodes ... + if str(route_info["prefix"]) == str(route["prefix"]) and (route["prefix"] != "0.0.0.0/0" and route["prefix"] != "::/0"): + # Check if the route ID's match. If they don't ... + app.logger.debug("Found a match: %s and %s", str(route["prefix"]), str(route_info["prefix"])) + if route_info["id"] != route["id"]: + app.logger.debug("Route ID's don't match. They're on different nodes.") + # ... Check if the routes prefix is already in the array... + if route["prefix"] not in failover_pair_prefixes: + # IF it isn't, add it. + app.logger.info("New HA pair found: %s", str(route["prefix"])) + failover_pair_prefixes.append(str(route["prefix"])) + if route["enabled"] and route_info["enabled"]: + # If it is already in the array. . . + # Show as HA only if both routes are enabled: + app.logger.debug("Both routes are enabled. Setting as HA [%s] (%s) ", str(machine["name"]), str(route["prefix"])) + ha_enabled = True + # If the route is an exit node and already counted as a failover route, it IS a failover route, so display it. + if route["prefix"] != "0.0.0.0/0" and route["prefix"] != "::/0" and route["prefix"] in failover_pair_prefixes: + route_enabled = "red" + route_tooltip = 'enable' + color_index = failover_pair_prefixes.index(str(route["prefix"])) + route_enabled_color = helper.get_color(color_index, "failover") + if route["enabled"]: + color_index = failover_pair_prefixes.index(str(route["prefix"])) + route_enabled = helper.get_color(color_index, "failover") + route_tooltip = 'disable' + routes = routes+"""

    + """+route['prefix']+""" +

    + """ + + # Get the remaining routes: + for route in pulled_routes["routes"]: + # Get the remaining routes - No exits or failover pairs + if route["prefix"] != "0.0.0.0/0" and route["prefix"] != "::/0" and route["prefix"] not in failover_pair_prefixes: + app.logger.debug("Route: ["+str(route['machine']['name'])+"] id: "+str(route['id'])+" / prefix: "+str(route['prefix'])+" enabled?: "+str(route['enabled'])) + route_enabled = "red" + route_tooltip = 'enable' + if route["enabled"]: + route_enabled = "green" + route_tooltip = 'disable' + routes = routes+"""

    + """+route['prefix']+""" +

    + """ + routes = routes+"

    " # Get machine tags tag_array = "" - for tag in machine["forcedTags"]: tag_array = tag_array+"{tag: '"+tag[4:]+"'}, " + for tag in machine["forcedTags"]: + tag_array = tag_array+"{tag: '"+tag[4:]+"'}, " tags = """
  • label @@ -364,16 +441,16 @@ def thread_machine_content(machine, machine_content, idx): preauth_key = str(machine["preAuthKey"]["key"])[0:10] else: preauth_key = "None" - # Set the status badge color: + # Set the status and user badge color: text_color = helper.text_color_duration(last_seen_delta) - # Set the user badge color: user_color = helper.get_color(int(machine["user"]["id"])) # Generate the various badges: - status_badge = "fiber_manual_record" + status_badge = "fiber_manual_record" user_badge = ""+machine["user"]["name"]+"" - exit_node_badge = "" if not exit_node else "Exit Node" - expiration_badge = "" if not expiring_soon else "Expiring!" + exit_node_badge = "" if not exit_route_enabled else "Exit" + ha_route_badge = "" if not ha_enabled else "HA" + expiration_badge = "" if not expiring_soon else "Expiring!" machine_content[idx] = (str(render_template( 'machines_card.html', @@ -388,6 +465,7 @@ def thread_machine_content(machine, machine_content, idx): machine_ips = Markup(machine_ips), advertised_routes = Markup(routes), exit_node_badge = Markup(exit_node_badge), + ha_route_badge = Markup(ha_route_badge), status_badge = Markup(status_badge), user_badge = Markup(user_badge), last_update_time = str(last_update_time), @@ -397,10 +475,10 @@ def thread_machine_content(machine, machine_content, idx): preauth_key = str(preauth_key), expiration_badge = Markup(expiration_badge), machine_tags = Markup(tags), + taglist = machine["forcedTags"] ))) app.logger.info("Finished thread for machine "+machine["givenName"]+" index "+str(idx)) -# Render the cards for the machines page: def render_machines_cards(): app.logger.info("Rendering machine cards") url = headscale.get_url() @@ -412,16 +490,23 @@ def render_machines_cards(): num_threads = len(machines_list["machines"]) iterable = [] machine_content = {} + failover_pair_prefixes = [] for i in range (0, num_threads): app.logger.debug("Appending iterable: "+str(i)) iterable.append(i) # Flask-Executor Method: + + # Get all routes + all_routes = headscale.get_routes(url, api_key) + # app.logger.debug("All found routes") + # app.logger.debug(str(all_routes)) + if LOG_LEVEL == "DEBUG": # DEBUG: Do in a forloop: - for idx in iterable: thread_machine_content(machines_list["machines"][idx], machine_content, idx) + for idx in iterable: thread_machine_content(machines_list["machines"][idx], machine_content, idx, all_routes, failover_pair_prefixes) else: app.logger.info("Starting futures") - futures = [executor.submit(thread_machine_content, machines_list["machines"][idx], machine_content, idx) for idx in iterable] + futures = [executor.submit(thread_machine_content, machines_list["machines"][idx], machine_content, idx, all_routes, failover_pair_prefixes) for idx in iterable] # Wait for the executor to finish all jobs: wait(futures, return_when=ALL_COMPLETED) app.logger.info("Finished futures") @@ -429,24 +514,23 @@ def render_machines_cards(): # Sort the content by machine_id: sorted_machines = {key: val for key, val in sorted(machine_content.items(), key = lambda ele: ele[0])} - content = "
    " + content = "
    " + content = content+"" return Markup(content) -# Render the cards for the Users page: def render_users_cards(): app.logger.info("Rendering Users cards") url = headscale.get_url() api_key = headscale.get_api_key() user_list = headscale.get_users(url, api_key) - content = "
    " + content = "
    " + content = content+"" return Markup(content) -# Builds the preauth key table for the User page def build_preauth_key_table(user_name): app.logger.info("Building the PreAuth key table for User: %s", str(user_name)) url = headscale.get_url() @@ -549,7 +632,7 @@ def build_preauth_key_table(user_name): def oidc_nav_dropdown(user_name, email_address, name): app.logger.info("OIDC is enabled. Building the OIDC nav dropdown") html_payload = """ - + '),n.$el.find(".carousel-item").each(function(t,e){if(n.images.push(t),n.showIndicators){var i=b('
  • ');0===e&&i[0].classList.add("active"),n.$indicators.append(i)}}),n.showIndicators&&n.$el.append(n.$indicators),n.count=n.images.length,n.options.numVisible=Math.min(n.count,n.options.numVisible),n.xform="transform",["webkit","Moz","O","ms"].every(function(t){var e=t+"Transform";return void 0===document.body.style[e]||(n.xform=e,!1)}),n._setupEventHandlers(),n._scroll(n.offset),n}return _inherits(i,Component),_createClass(i,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Carousel=void 0}},{key:"_setupEventHandlers",value:function(){var i=this;this._handleCarouselTapBound=this._handleCarouselTap.bind(this),this._handleCarouselDragBound=this._handleCarouselDrag.bind(this),this._handleCarouselReleaseBound=this._handleCarouselRelease.bind(this),this._handleCarouselClickBound=this._handleCarouselClick.bind(this),void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTapBound),this.el.addEventListener("touchmove",this._handleCarouselDragBound),this.el.addEventListener("touchend",this._handleCarouselReleaseBound)),this.el.addEventListener("mousedown",this._handleCarouselTapBound),this.el.addEventListener("mousemove",this._handleCarouselDragBound),this.el.addEventListener("mouseup",this._handleCarouselReleaseBound),this.el.addEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.addEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&(this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.$indicators.find(".indicator-item").each(function(t,e){t.addEventListener("click",i._handleIndicatorClickBound)}));var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){var i=this;void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTapBound),this.el.removeEventListener("touchmove",this._handleCarouselDragBound),this.el.removeEventListener("touchend",this._handleCarouselReleaseBound)),this.el.removeEventListener("mousedown",this._handleCarouselTapBound),this.el.removeEventListener("mousemove",this._handleCarouselDragBound),this.el.removeEventListener("mouseup",this._handleCarouselReleaseBound),this.el.removeEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.removeEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&this.$indicators.find(".indicator-item").each(function(t,e){t.removeEventListener("click",i._handleIndicatorClickBound)}),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleCarouselTap",value:function(t){"mousedown"===t.type&&b(t.target).is("img")&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._trackBound,100)}},{key:"_handleCarouselDrag",value:function(t){var e=void 0,i=void 0,n=void 0;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),n=this.reference-e,Math.abs(this.referenceY-i)<30&&!this.verticalDragged)(2=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1}},{key:"_handleCarouselClick",value:function(t){if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){var e=b(t.target).closest(".carousel-item").index();0!==this._wrap(this.center)-e&&(t.preventDefault(),t.stopPropagation()),this._cycleTo(e)}}},{key:"_handleIndicatorClick",value:function(t){t.stopPropagation();var e=b(t.target).closest(".indicator-item");e.length&&this._cycleTo(e.index())}},{key:"_handleResize",value:function(t){this.options.fullWidth?(this.itemWidth=this.$el.find(".carousel-item").first().innerWidth(),this.imageHeight=this.$el.find(".carousel-item.active").height(),this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()}},{key:"_setCarouselHeight",value:function(t){var i=this,e=this.$el.find(".carousel-item.active").length?this.$el.find(".carousel-item.active").first():this.$el.find(".carousel-item").first(),n=e.find("img").first();if(n.length)if(n[0].complete){var s=n.height();if(0=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}},{key:"_track",value:function(){var t,e,i,n;e=(t=Date.now())-this.timestamp,this.timestamp=t,i=this.offset-this.frame,this.frame=this.offset,n=1e3*i/(1+e),this.velocity=.8*n+.2*this.velocity}},{key:"_autoScroll",value:function(){var t=void 0,e=void 0;this.amplitude&&(t=Date.now()-this.timestamp,2<(e=this.amplitude*Math.exp(-t/this.options.duration))||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScrollBound)):this._scroll(this.target))}},{key:"_scroll",value:function(t){var e=this;this.$el.hasClass("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&window.clearTimeout(this.scrollingTimeout),this.scrollingTimeout=window.setTimeout(function(){e.$el.removeClass("scrolling")},this.options.duration);var i,n,s,o,a=void 0,r=void 0,l=void 0,h=void 0,d=void 0,u=void 0,c=this.center,p=1/this.options.numVisible;if(this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim),o=-(s=(n=this.offset-this.center*this.dim)<0?1:-1)*n*2/this.dim,i=this.count>>1,this.options.fullWidth?(l="translateX(0)",u=1):(l="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",l+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",u=1-p*o),this.showIndicators){var v=this.center%this.count,f=this.$indicators.find(".indicator-item.active");f.index()!==v&&(f.removeClass("active"),this.$indicators.find(".indicator-item").eq(v)[0].classList.add("active"))}if(!this.noWrap||0<=this.center&&this.center=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"prev",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"set",value:function(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}],[{key:"init",value:function(t,e){return _get(i.__proto__||Object.getPrototypeOf(i),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Carousel}},{key:"defaults",get:function(){return e}}]),i}();M.Carousel=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"carousel","M_Carousel")}(cash),function(S){"use strict";var e={onOpen:void 0,onClose:void 0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_TapTarget=i).options=S.extend({},n.defaults,e),i.isOpen=!1,i.$origin=S("#"+i.$el.attr("data-target")),i._setup(),i._calculatePositioning(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.TapTarget=void 0}},{key:"_setupEventHandlers",value:function(){this._handleDocumentClickBound=this._handleDocumentClick.bind(this),this._handleTargetClickBound=this._handleTargetClick.bind(this),this._handleOriginClickBound=this._handleOriginClick.bind(this),this.el.addEventListener("click",this._handleTargetClickBound),this.originEl.addEventListener("click",this._handleOriginClickBound);var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleTargetClickBound),this.originEl.removeEventListener("click",this._handleOriginClickBound),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleTargetClick",value:function(t){this.open()}},{key:"_handleOriginClick",value:function(t){this.close()}},{key:"_handleResize",value:function(t){this._calculatePositioning()}},{key:"_handleDocumentClick",value:function(t){S(t.target).closest(".tap-target-wrapper").length||(this.close(),t.preventDefault(),t.stopPropagation())}},{key:"_setup",value:function(){this.wrapper=this.$el.parent()[0],this.waveEl=S(this.wrapper).find(".tap-target-wave")[0],this.originEl=S(this.wrapper).find(".tap-target-origin")[0],this.contentEl=this.$el.find(".tap-target-content")[0],S(this.wrapper).hasClass(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.$el.before(S(this.wrapper)),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.$el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.originEl||(this.originEl=this.$origin.clone(!0,!0),this.originEl.addClass("tap-target-origin"),this.originEl.removeAttr("id"),this.originEl.removeAttr("style"),this.originEl=this.originEl[0],this.waveEl.append(this.originEl)),this.wrapper.append(this.waveEl))}},{key:"_calculatePositioning",value:function(){var t="fixed"===this.$origin.css("position");if(!t)for(var e=this.$origin.parents(),i=0;i'+t.getAttribute("label")+"")[0]),i.each(function(t){var e=n._appendOptionWithIcon(n.$el,t,"optgroup-option");n._addOptionToValueDict(t,e)})}}),this.$el.after(this.dropdownOptions),this.input=document.createElement("input"),d(this.input).addClass("select-dropdown dropdown-trigger"),this.input.setAttribute("type","text"),this.input.setAttribute("readonly","true"),this.input.setAttribute("data-target",this.dropdownOptions.id),this.el.disabled&&d(this.input).prop("disabled","true"),this.$el.before(this.input),this._setValueToInput();var t=d('');if(this.$el.before(t[0]),!this.el.disabled){var e=d.extend({},this.options.dropdownOptions);e.onOpenEnd=function(t){var e=d(n.dropdownOptions).find(".selected").first();if(e.length&&(M.keyDown=!0,n.dropdown.focusedIndex=e.index(),n.dropdown._focusFocusedItem(),M.keyDown=!1,n.dropdown.isScrollable)){var i=e[0].getBoundingClientRect().top-n.dropdownOptions.getBoundingClientRect().top;i-=n.dropdownOptions.clientHeight/2,n.dropdownOptions.scrollTop=i}},this.isMultiple&&(e.closeOnClick=!1),this.dropdown=M.Dropdown.init(this.input,e)}this._setSelectedStates()}},{key:"_addOptionToValueDict",value:function(t,e){var i=Object.keys(this._valueDict).length,n=this.dropdownOptions.id+i,s={};e.id=n,s.el=t,s.optionEl=e,this._valueDict[n]=s}},{key:"_removeDropdown",value:function(){d(this.wrapper).find(".caret").remove(),d(this.input).remove(),d(this.dropdownOptions).remove(),d(this.wrapper).before(this.$el),d(this.wrapper).remove()}},{key:"_appendOptionWithIcon",value:function(t,e,i){var n=e.disabled?"disabled ":"",s="optgroup-option"===i?"optgroup-option ":"",o=this.isMultiple?'":e.innerHTML,a=d("
  • "),r=d("");r.html(o),a.addClass(n+" "+s),a.append(r);var l=e.getAttribute("data-icon");if(l){var h=d('');a.prepend(h)}return d(this.dropdownOptions).append(a[0]),a[0]}},{key:"_toggleEntryFromArray",value:function(t){var e=!this._keysSelected.hasOwnProperty(t),i=d(this._valueDict[t].optionEl);return e?this._keysSelected[t]=!0:delete this._keysSelected[t],i.toggleClass("selected",e),i.find('input[type="checkbox"]').prop("checked",e),i.prop("selected",e),e}},{key:"_setValueToInput",value:function(){var i=[];if(this.$el.find("option").each(function(t){if(d(t).prop("selected")){var e=d(t).text();i.push(e)}}),!i.length){var t=this.$el.find("option:disabled").eq(0);t.length&&""===t[0].value&&i.push(t.text())}this.input.value=i.join(", ")}},{key:"_setSelectedStates",value:function(){for(var t in this._keysSelected={},this._valueDict){var e=this._valueDict[t],i=d(e.el).prop("selected");d(e.optionEl).find('input[type="checkbox"]').prop("checked",i),i?(this._activateOption(d(this.dropdownOptions),d(e.optionEl)),this._keysSelected[t]=!0):d(e.optionEl).removeClass("selected")}}},{key:"_activateOption",value:function(t,e){e&&(this.isMultiple||t.find("li.selected").removeClass("selected"),d(e).addClass("selected"))}},{key:"getSelectedValues",value:function(){var t=[];for(var e in this._keysSelected)t.push(this._valueDict[e].el.value);return t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FormSelect}},{key:"defaults",get:function(){return e}}]),n}();M.FormSelect=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"formSelect","M_FormSelect")}(cash),function(s,e){"use strict";var i={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Range=i).options=s.extend({},n.defaults,e),i._mousedown=!1,i._setupThumb(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}},{key:"_setupEventHandlers",value:function(){this._handleRangeChangeBound=this._handleRangeChange.bind(this),this._handleRangeMousedownTouchstartBound=this._handleRangeMousedownTouchstart.bind(this),this._handleRangeInputMousemoveTouchmoveBound=this._handleRangeInputMousemoveTouchmove.bind(this),this._handleRangeMouseupTouchendBound=this._handleRangeMouseupTouchend.bind(this),this._handleRangeBlurMouseoutTouchleaveBound=this._handleRangeBlurMouseoutTouchleave.bind(this),this.el.addEventListener("change",this._handleRangeChangeBound),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.addEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("change",this._handleRangeChangeBound),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_handleRangeChange",value:function(){s(this.value).html(this.$el.val()),s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px")}},{key:"_handleRangeMousedownTouchstart",value:function(t){if(s(this.value).html(this.$el.val()),this._mousedown=!0,this.$el.addClass("active"),s(this.thumb).hasClass("active")||this._showRangeBubble(),"input"!==t.type){var e=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",e+"px")}}},{key:"_handleRangeInputMousemoveTouchmove",value:function(){if(this._mousedown){s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px"),s(this.value).html(this.$el.val())}}},{key:"_handleRangeMouseupTouchend",value:function(){this._mousedown=!1,this.$el.removeClass("active")}},{key:"_handleRangeBlurMouseoutTouchleave",value:function(){if(!this._mousedown){var t=7+parseInt(this.$el.css("padding-left"))+"px";s(this.thumb).hasClass("active")&&(e.remove(this.thumb),e({targets:this.thumb,height:0,width:0,top:10,easing:"easeOutQuad",marginLeft:t,duration:100})),s(this.thumb).removeClass("active")}}},{key:"_setupThumb",value:function(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),s(this.thumb).addClass("thumb"),s(this.value).addClass("value"),s(this.thumb).append(this.value),this.$el.after(this.thumb)}},{key:"_removeThumb",value:function(){s(this.thumb).remove()}},{key:"_showRangeBubble",value:function(){var t=-7+parseInt(s(this.thumb).parent().css("padding-left"))+"px";e.remove(this.thumb),e({targets:this.thumb,height:30,width:30,top:-30,marginLeft:t,duration:300,easing:"easeOutQuint"})}},{key:"_calcRangeOffset",value:function(){var t=this.$el.width()-15,e=parseFloat(this.$el.attr("max"))||100,i=parseFloat(this.$el.attr("min"))||0;return(parseFloat(this.$el.val())-i)/(e-i)*t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Range}},{key:"defaults",get:function(){return i}}]),n}();M.Range=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"range","M_Range"),t.init(s("input[type=range]"))}(cash,M.anime); \ No newline at end of file diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index c48887c..0000000 --- a/templates/login.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - ●::[●●] Error - - - - - - - - - - - - - - - - - - - - - - - - - - -



    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    - - - - - - - - \ No newline at end of file diff --git a/templates/machines.html b/templates/machines.html index eb351b8..27a52c7 100644 --- a/templates/machines.html +++ b/templates/machines.html @@ -7,10 +7,14 @@ {% block OIDC_NAV_DROPDOWN %} {{ OIDC_NAV_DROPDOWN}} {% endblock %} {% block OIDC_NAV_MOBILE %} {{ OIDC_NAV_MOBILE }} {% endblock %} +{% block INPAGE_SEARCH%} {{ INPAGE_SEARCH }} {% endblock %} + {% block content %}

    - {{ cards }} +
    + {{ cards }} +
    diff --git a/templates/machines_card.html b/templates/machines_card.html index f7eab90..fb43e77 100644 --- a/templates/machines_card.html +++ b/templates/machines_card.html @@ -1,80 +1,84 @@ -
      -
    • -
      -
      - {{ status_badge }} - - {{ machine_id }}. {{ given_name }} - -
      - -
      - {{ user_badge }} - {{ exit_node_badge }} - {{ expiration_badge }} -
      -
      -
      -
        -
      • - settings - Machine Actions -

        - Rename - Move - Delete -

        -

        -

        -

        -
      • -
      • - domain - Hostname -

        {{ hostname }}

        -
      • -
      • - language - User -

        {{ ns_name }}

        -
      • -
      • - network_wifi - IP Addresses -

        {{ machine_ips}}

        -
      • -
      • - access_time - Last Seen -

        {{ last_seen_time}}

        -
      • -
      • - update - Last Update -

        {{ last_update_time }}

        -
      • -
      • - history - Created At -

        {{ created_time }}

        -
      • -
      • - hourglass_empty - Expiration -

        {{ expiry_time }}

        -
      • -
      • - key - PreAuth Key Prefix -

        {{ preauth_key }}

        -
      • - {{ advertised_routes }} - {{ machine_tags }} -
      -
      -
    • -
    \ No newline at end of file +
  • +
    + machine:{{given_name}} + user:{{ns_name}} + {{taglist}} +
    +
    +
    + {{ status_badge }} + + {{ machine_id }}. {{ given_name }} + +
    + +
    + {{ user_badge }} + {{ exit_node_badge }} + {{ expiration_badge }} + {{ ha_route_badge }} +
    +
    +
    +
      +
    • + settings + Machine Actions +

      + Rename + Move + Delete +

      +

      +

      +

      +
    • +
    • + domain + Hostname +

      {{ hostname }}

      +
    • +
    • + language + User +

      {{ ns_name }}

      +
    • +
    • + network_wifi + IP Addresses +

      {{ machine_ips}}

      +
    • +
    • + access_time + Last Seen +

      {{ last_seen_time}}

      +
    • +
    • + update + Last Update +

      {{ last_update_time }}

      +
    • +
    • + history + Created At +

      {{ created_time }}

      +
    • +
    • + hourglass_empty + Expiration +

      {{ expiry_time }}

      +
    • +
    • + key + PreAuth Key Prefix +

      {{ preauth_key }}

      +
    • + {{ advertised_routes }} + {{ machine_tags }} +
    +
    +
  • \ No newline at end of file diff --git a/templates/routes.html b/templates/routes.html new file mode 100644 index 0000000..02b77bd --- /dev/null +++ b/templates/routes.html @@ -0,0 +1,30 @@ +{% extends 'template.html' %} +{% set page = "Routes" %} +{% set routes_active = "active" %} + +{% block title %} {{ page }} {% endblock %} +{% block header %} {{ page }} {% endblock %} + +{% block OIDC_NAV_DROPDOWN %} {{ OIDC_NAV_DROPDOWN}} {% endblock %} +{% block OIDC_NAV_MOBILE %} {{ OIDC_NAV_MOBILE }} {% endblock %} + +{% block content %} +

    +
    + {{ render_page }} +
    +
    + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html index 5d1c522..3198fb9 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -36,7 +36,7 @@ About - + diff --git a/templates/template.html b/templates/template.html index 486a1be..e569e8a 100644 --- a/templates/template.html +++ b/templates/template.html @@ -23,19 +23,14 @@ - - - -
    CompatibilityHeadscale {{ HS_VERSION }}
    CompatibilityHeadscale {{ HS_VERSION }}
    App Version{{ APP_VERSION }}
    Build Date {{ BUILD_DATE }}
    Git Commit{{ GIT_COMMIT }}