├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── publish-dev-image.yaml │ └── publish-release.yaml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── Caddyfile ├── LICENSE.md ├── README.md ├── SECURITY.md ├── docker ├── development │ ├── dockerfile │ └── scripts │ │ ├── 1-image-build.sh │ │ ├── 2-initialise.sh │ │ ├── install-container-dependencies.sh │ │ └── install-openvscode-server.sh └── production │ ├── Caddyfile │ ├── dockerfile │ └── scripts │ ├── 1-image-build.sh │ └── 2-initialise.sh ├── documentation ├── architecture.md ├── assets │ ├── README_ports.gif │ └── headscale-ui-demo.gif ├── configuration.md ├── development.md ├── route_queries.md ├── style.md └── testing.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── src ├── app.css ├── app.d.ts ├── app.html ├── lib │ ├── common │ │ ├── Alert.svelte │ │ ├── Stores.svelte │ │ ├── apiFunctions.svelte │ │ ├── classes.ts │ │ ├── nav.svelte │ │ ├── searching.svelte │ │ ├── sorting.svelte │ │ └── stores.js │ ├── devices │ │ ├── CreateDevice.svelte │ │ ├── DeviceCard.svelte │ │ ├── DeviceCard │ │ │ ├── DeviceRoutes.svelte │ │ │ ├── DeviceRoutes │ │ │ │ ├── DeviceRoute.svelte │ │ │ │ └── DeviceRouteAPI.svelte │ │ │ ├── DeviceTags.svelte │ │ │ ├── DeviceTags │ │ │ │ └── NewDeviceTag.svelte │ │ │ ├── MoveDevice.svelte │ │ │ ├── RemoveDevice.svelte │ │ │ └── RenameDevice.svelte │ │ ├── SearchDevices.svelte │ │ └── SortDevices.svelte │ ├── settings │ │ ├── DevSettings.svelte │ │ ├── ServerSettings.svelte │ │ ├── ServerSettings │ │ │ ├── APIKeyTimeLeft.svelte │ │ │ └── RolloverAPI.svelte │ │ └── ThemeSettings.svelte │ └── users │ │ ├── CreateUser.svelte │ │ ├── SearchUsers.svelte │ │ ├── SortUsers.svelte │ │ ├── UserCard.svelte │ │ └── UserCard │ │ ├── PreAuthKeys.svelte │ │ ├── PreAuthKeys │ │ └── NewPreAuthKey.svelte │ │ ├── RemoveUser.svelte │ │ └── RenameUser.svelte └── routes │ ├── +layout.js │ ├── +layout.svelte │ ├── +page.svelte │ ├── devices.html │ └── +page.svelte │ ├── groups.html │ └── +page.svelte │ ├── settings.html │ └── +page.svelte │ └── users.html │ └── +page.svelte ├── static └── favicon.png ├── svelte.config.js ├── tailwind.config.cjs └── vite.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [gurucomputing] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ** Supporting Details ** 11 | Provide the following: 12 | * Browser Version: 13 | * Headscale Version: 14 | * Any Browser Errors (`control+shift+i` in chrome to see) 15 | 16 | ** Note ** 17 | No bug reports are currently being accepted against the alpha version of headscale. Test against the production/stable version. 18 | 19 | **Describe the bug** 20 | A clear and concise description of what the bug is. Screenshots if applicable 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-dev-image.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Dev Image 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | steps: 15 | - name: Variable Gathering 16 | id: gathervars 17 | run: | 18 | # get a current BUILD_DATE 19 | echo "BUILD_DATE=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV 20 | # set version based on BUILD_DATE 21 | echo "VERSION=$(date +%Y.%m.%d)-development" >> $GITHUB_ENV 22 | 23 | - name: Checkout Repository 24 | uses: actions/checkout@v3 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Log in to the Container registry 30 | uses: docker/login-action@v1 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Build and push Docker Image 37 | uses: docker/build-push-action@v4 38 | with: 39 | build-args: | 40 | BUILD_DATE=${{ env.BUILD_DATE }} 41 | VERSION=${{ env.VERSION }} 42 | context: ./docker/development 43 | tags: | 44 | ghcr.io/${{ github.repository }}-dev:latest 45 | ghcr.io/${{ github.repository }}-dev:${{ env.VERSION }} 46 | push: true 47 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | permissions: write-all 10 | 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Variable Gathering 16 | id: gathervars 17 | run: | 18 | NOT_PREVIOUSLY_PUBLISHED=0 19 | # get a current BUILD_DATE 20 | VERSION=$(jq -r '.version' ./package.json) 21 | echo "BUILD_DATE=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV 22 | echo "VERSION=$VERSION" >> $GITHUB_ENV 23 | 24 | # setting tags 25 | if echo "$VERSION" | grep -q "beta"; then 26 | echo "TAGS=ghcr.io/${{ github.repository }}:beta, ghcr.io/${{ github.repository }}:$VERSION, ghcr.io/${{ github.repository }}:latest" >> $GITHUB_ENV 27 | else 28 | echo "TAGS=ghcr.io/${{ github.repository }}:release, ghcr.io/${{ github.repository }}:latest, ghcr.io/${{ github.repository }}:$VERSION" >> $GITHUB_ENV 29 | fi 30 | echo "PRIMARY_TAG=latest" >> $GITHUB_ENV 31 | # check if version has already been published 32 | $(docker manifest inspect ghcr.io/${{ github.repository }}:$VERSION > /dev/null) || NOT_PREVIOUSLY_PUBLISHED=1 33 | echo "NOT_PREVIOUSLY_PUBLISHED=$NOT_PREVIOUSLY_PUBLISHED" >> $GITHUB_ENV 34 | 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3 37 | 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v3 40 | 41 | - name: Log in to the Container registry 42 | uses: docker/login-action@v3 43 | if: ${{ env.NOT_PREVIOUSLY_PUBLISHED != 0 }} 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.actor }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Build and push Docker Image 50 | uses: docker/build-push-action@v6 51 | if: ${{ env.NOT_PREVIOUSLY_PUBLISHED != 0 }} 52 | with: 53 | build-args: | 54 | BUILD_DATE=${{ env.BUILD_DATE }} 55 | VERSION=${{ env.VERSION }} 56 | context: ./docker/production 57 | tags: | 58 | ${{ env.TAGS }} 59 | platforms: linux/amd64,linux/arm/v7,linux/arm64/v8 60 | push: true 61 | 62 | - name: Extract build out of docker image 63 | uses: shrink/actions-docker-extract@v3 64 | if: ${{ env.NOT_PREVIOUSLY_PUBLISHED != 0 }} 65 | id: extract 66 | with: 67 | image: ghcr.io/${{ github.repository }}:${{ env.PRIMARY_TAG }} 68 | path: web 69 | 70 | - name: create release asset 71 | if: ${{ env.NOT_PREVIOUSLY_PUBLISHED != 0 }} 72 | run: | 73 | cd "${{ steps.extract.outputs.destination }}" 74 | 7z a headscale-ui.zip web 75 | 76 | - name: Create Release 77 | uses: softprops/action-gh-release@v2 78 | if: ${{ env.NOT_PREVIOUSLY_PUBLISHED != 0 }} 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | with: 82 | tag_name: ${{ env.VERSION }} 83 | name: headscale-ui 84 | files: ${{ steps.extract.outputs.destination }}/headscale-ui.zip 85 | generate_release_notes: true 86 | make_latest: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /web 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | *.tar.gz -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "printWidth": 400 7 | } 8 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | :8080 { 2 | redir / /web 3 | uri strip_prefix /web 4 | file_server { 5 | root ./build 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, gurucomputing 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Headscale-UI 2 | A web frontend for the [headscale](https://github.com/juanfont/headscale) Tailscale-compatible coordination server. 3 | 4 | ![](documentation/assets/headscale-ui-demo.gif) 5 | 6 | ## Installation 7 | > [!WARNING] 8 | > The latest major release of headscale ui change the default container ports from `80` and `443` to `8080` and `8443` respectively. If you are using the `HTTP_PORT` or `HTTPS_PORT` environment variables this does not affect you, otherwise you need to change your ports in your docker-compose or kubernetes manifests. 9 | 10 | Headscale-UI is currently released as a static site: just take the release and host with your favorite web server. Headscale-UI expects to be served from the `/web` path to avoid overlap with headscale on the same domain. Note that due to CORS (see https://github.com/juanfont/headscale/issues/623), headscale UI *must* be served on the same subdomain, or CORS headers injected via reverse proxy. 11 | 12 | ### Docker Installation 13 | If you are using docker, you can install `headscale` alongside `headscale-ui`, like so: 14 | 15 | ```yaml 16 | version: '3.5' 17 | services: 18 | headscale: 19 | image: headscale/headscale:stable 20 | container_name: headscale 21 | volumes: 22 | - ./container-config:/etc/headscale 23 | - ./container-data/data:/var/lib/headscale 24 | # ports: 25 | # - 27896:8080 26 | command: serve 27 | restart: unless-stopped 28 | headscale-ui: 29 | image: ghcr.io/gurucomputing/headscale-ui:latest 30 | restart: unless-stopped 31 | container_name: headscale-ui 32 | # ports: 33 | # - 8443:8443 34 | # - 8080:8080 35 | ``` 36 | 37 | Headscale UI serves on port 8080/8443 and uses a self signed cert by default. You will need to add a `config.yaml` file under your `container-config` folder so that `headscale` has all of the required settings declared. An example from the official `headscale` repo is [here](https://github.com/juanfont/headscale/blob/main/config-example.yaml). 38 | 39 | ### Additional Docker Settings 40 | The docker container lets you set the following settings: 41 | | Variable | Description | Example | 42 | |----|----|----| 43 | | HTTP_PORT | Sets the HTTP port to an alternate value | `8080` | 44 | | HTTPS_PORT | Sets the HTTPS port to an alternate value | `8443` | 45 | 46 | ### Proxy Settings 47 | You will need a reverse proxy to install `headscale-ui` on your domain. Here is an example [Caddy Config](https://caddyserver.com/) to achieve this: 48 | ``` 49 | https://hs.yourdomain.com.au { 50 | reverse_proxy /web* http://headscale-ui:8080 51 | reverse_proxy * http://headscale:8080 52 | } 53 | 54 | 55 | ``` 56 | 57 | ### Cross Domain Installation 58 | If you do not want to configure headscale-ui on the same subdomain as headscale, you must intercept headscale traffic via your reverse proxy to fix CORS (see https://github.com/juanfont/headscale/issues/623). Here is an example fix with Caddy, replacing your headscale UI domain with `hs-ui.yourdomain.com.au`: 59 | ``` 60 | https://hs.yourdomain.com.au { 61 | @hs-options { 62 | host hs.yourdomain.com.au 63 | method OPTIONS 64 | } 65 | @hs-other { 66 | host hs.yourdomain.com.au 67 | } 68 | handle @hs-options { 69 | header { 70 | Access-Control-Allow-Origin https://hs-ui.yourdomain.au 71 | Access-Control-Allow-Headers * 72 | Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE" 73 | } 74 | respond 204 75 | } 76 | handle @hs-other { 77 | reverse_proxy http://headscale:8080 { 78 | header_down Access-Control-Allow-Origin https://hs-ui.yourdomain.com.au 79 | header_down Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE" 80 | header_down Access-Control-Allow-Headers * 81 | } 82 | } 83 | } 84 | 85 | ``` 86 | 87 | ### Other Configurations 88 | See [Other Configurations](/documentation/configuration.md) for further proxy examples, such as Traefik 89 | 90 | ## Versioning 91 | The following versions correspond to the appropriate headscale version 92 | | Headscale Version | HS-UI Version | 93 | |-------------------|---------------| 94 | | 26+ | 2025-05-22+ | 95 | | 25+ | 2025-03-14+ | 96 | | 24+ | 2025-01-20+ | 97 | | 23+ | 2024-10-01+ | 98 | | 19+ | 2023-01-30+ | 99 | | <19 | <2023-01-30 | 100 | 101 | ## Troubleshooting 102 | Make sure you are using the latest version of headscale. Headscale-UI is only tested against: 103 | 104 | * The current stable version of headscale 105 | * Chrome/Chrome Mobile 106 | * Firefox/Firefox Mobile 107 | 108 | Note that while mobile is checked for functionality, the web experience is not mobile optimised. 109 | 110 | If you are getting errors about preflight checks, it's probably CORS related. Make sure your UI sits on the same subdomain as headscale or inject CORS headers. 111 | 112 | ### Errors related to "Missing Bearer Prefix" 113 | Your API key is either not saved or you haven't configured your reverse proxy. Create an API key in `headscale` (via command line) with `headscale apikeys create` or `docker exec headscale apikeys create` and save it in `settings`. 114 | 115 | HS-UI *has* to be ran on the same subdomain as headscale or you need to configure CORS. Yes you need to use a reverse proxy to do this. Use a reverse proxy. If you are trying to use raw IPs and ports, it *will* not work. 116 | 117 | ## Security 118 | see [security](/SECURITY.md) for details 119 | 120 | ## Development 121 | see [development](/documentation/development.md) for details 122 | 123 | ## Style Guide 124 | see [style](/documentation/style.md) for details 125 | 126 | ## Architecture 127 | See [architecture](/documentation/architecture.md) for details 128 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ### Authentication and Authorization 2 | In the current client-only format, the headscale API secret is stored within the browser's `localStorage` area. While `localStorage` is not an ideal location for secrets storage, it is currently the *only* possible method of securing data to a browser without some sort of backend facilitation. 3 | 4 | What this means to *you* is that your API credentials are tied to your browser profile. If you open an incognito window or another browser profile, your API key will *not* carry across. 5 | 6 | `localStorage` secrets have the possibility of being exploited by XSS. This exploitation avenue is mitigated by the static nature of the site: all pages are protected by a hashsum CSP (content security protection) that prevent modifying or adding javascript from other sources. 7 | 8 | The future state for `heascale-ui` is not to rely on `localStorage` at all, but due to the architecture, any other methods require tighter integration with the core `headscale` product. For now this is not on the headscale roadmap. 9 | 10 | ## Vulnerability Disclosure 11 | 12 | If any method of bypassing or leaking the `localStorage` secrets is found, please contact myself directly at `chris@gurucomputing.com.au` rather than opening an issue. -------------------------------------------------------------------------------- /docker/development/dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | 3 | # Volumes 4 | VOLUME /data 5 | 6 | # Ports 7 | # openvscode server port. Note: Runs HTTP by default 8 | EXPOSE 3000 9 | 10 | # System Environment Variables 11 | ENV PATH="/opt/vscode:${PATH}" 12 | ENV HOME="/data/home" 13 | 14 | # User Set Environment Variables 15 | # Set to false if you do not want to attempt to pull a repository on first load 16 | ENV AUTOINITIALIZE=false 17 | # sets a connection token for VSCode Server. https://github.com/gitpod-io/openvscode-server#securing-access-to-your-ide 18 | ENV USE_CONNECTION_TOKEN=true 19 | #Set to a secret to have some measure of protection for vscode. Randomized if left blank 20 | ENV CONNECTION_TOKEN= 21 | # Project name. Typically the same as the project in the URL 22 | ENV PROJECT_NAME="headscale-ui" 23 | # URL for the github/git location 24 | ENV PROJECT_URL="https://github.com/gurucomputing/headscale-ui" 25 | # autostart the dev command on boot? 26 | ENV AUTOSTART="false" 27 | # command to run in the background on startup 28 | ENV DEV_COMMAND="npm run dev" 29 | 30 | # Set the staging environment 31 | WORKDIR /staging/scripts 32 | WORKDIR /staging 33 | RUN chown 1000:1000 /staging 34 | 35 | # Copy across the scripts folder 36 | COPY scripts/* ./scripts/ 37 | 38 | # Set permissions for all scripts. We do not want normal users to have write 39 | # access to the scripts 40 | RUN chown -R 0:0 scripts 41 | RUN chmod -R 755 scripts 42 | 43 | # Build the image. This build runs as root 44 | RUN /staging/scripts/1-image-build.sh 45 | 46 | # set to the non-root user 47 | USER 1000:1000 48 | 49 | WORKDIR /data 50 | 51 | ENTRYPOINT /bin/sh /staging/scripts/2-initialise.sh 52 | -------------------------------------------------------------------------------- /docker/development/scripts/1-image-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script environment 4 | # turn on bash logging, exit on error 5 | set -ex 6 | 7 | # # create a non-root user. Not needed for node image 8 | # useradd -m -d /data/home dev-user 9 | 10 | # set new home directory 11 | mkdir -p /data/home 12 | usermod -d /data/home node 13 | 14 | # Add the ability to set file permissions on /data to the non-privileged user 15 | echo "ALL ALL=NOPASSWD: /bin/chown -R 1000\:1000 /data" >> /etc/sudoers 16 | 17 | # install dependencies 18 | /staging/scripts/install-container-dependencies.sh 19 | /staging/scripts/install-openvscode-server.sh 20 | 21 | # set tmux to use mouse scroll 22 | echo "set -g mouse on" > /data/home/.tmux.conf 23 | 24 | chown -R 1000:1000 /data -------------------------------------------------------------------------------- /docker/development/scripts/2-initialise.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #----# 4 | # placeholder for testing 5 | # while true; do sleep 1; done 6 | #----# 7 | 8 | # set file permissions if required 9 | if [ $(id -u) -ne $(stat -c %u /data) ] 10 | then 11 | if [ $(id -u) -eq 1000 ] 12 | then 13 | echo "---- Detected File Permission Mismatch ----" 14 | echo "---- Forcing File Permissions to the node user ----" 15 | sudo /bin/chown -R 1000:1000 /data 16 | else 17 | echo "---- You are not running as the default non-root user AND your file permissions don't match your user ---\n" 18 | echo "---- You may need to manually fix your file permissions ----" 19 | fi 20 | fi 21 | 22 | # create the home directory if it doesn't exist 23 | cd /data 24 | if ! [ -d /data/home ] 25 | then 26 | mkdir /data/home 27 | # set tmux to use mouse scroll 28 | echo "set -g mouse on" > /data/home/.tmux.conf 29 | fi 30 | 31 | #attempt to initialize and run a repository 32 | if [ "$AUTOINITIALIZE" = "true" ] 33 | then 34 | # check if there is a copy of ${PROJECT_NAME}, if not assume this is a fresh install 35 | if ! [ -d /data/${PROJECT_NAME} ] 36 | then 37 | echo "-- Fresh Install detected, setting up your dev environment --" 38 | echo "-- Installing Source --" 39 | # clone the latest version of ${PROJECT_NAME} 40 | cd /data 41 | git clone ${PROJECT_URL} 42 | cd ${PROJECT_NAME} 43 | else 44 | cd /data/${PROJECT_NAME} 45 | fi 46 | 47 | if [ "$AUTOSTART" = "true" ] 48 | then 49 | # run the sub process 50 | tmux new-session -d "${DEV_COMMAND}; bash -i" 51 | fi 52 | fi 53 | 54 | # run the main process. 55 | if [ "$USE_CONNECTION_TOKEN" = "false" ] 56 | then 57 | /opt/openvscode-server/bin/openvscode-server --host 0.0.0.0 --without-connection-token 58 | else 59 | /opt/openvscode-server/bin/openvscode-server --host 0.0.0.0 --connection-token=${CONNECTION_TOKEN} 60 | fi -------------------------------------------------------------------------------- /docker/development/scripts/install-container-dependencies.sh: -------------------------------------------------------------------------------- 1 | # install dependencies 2 | # tmux used for monitoring secondary processes 3 | # sudo for running specific commands as root 4 | # ncdu file navigation 5 | # caddy web server 6 | apt-get update 7 | apt-get install -y --no-install-recommends tmux sudo git ncdu caddy 8 | apt-get clean -------------------------------------------------------------------------------- /docker/development/scripts/install-openvscode-server.sh: -------------------------------------------------------------------------------- 1 | # script variables 2 | OPENVSCODE_VERSION="1.98.0" 3 | OPENVSCODE_URL="https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v$OPENVSCODE_VERSION/openvscode-server-v$OPENVSCODE_VERSION-linux-x64.tar.gz" 4 | OPENVSCODE_RELEASE="openvscode-server-v$OPENVSCODE_VERSION-linux-x64" 5 | 6 | # install openVSCode 7 | cd /opt 8 | 9 | ### Download Open VSCode 10 | curl -LJO "$OPENVSCODE_URL" 11 | 12 | ### Extract and move into directory 13 | tar -xzf "$OPENVSCODE_RELEASE.tar.gz" 14 | mv $OPENVSCODE_RELEASE openvscode-server 15 | rm -f "$OPENVSCODE_RELEASE.tar.gz" 16 | -------------------------------------------------------------------------------- /docker/production/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | skip_install_trust 3 | auto_https disable_redirects 4 | http_port {$HTTP_PORT} 5 | https_port {$HTTPS_PORT} 6 | } 7 | 8 | :{$HTTP_PORT} { 9 | redir / /web 10 | uri strip_prefix /web 11 | file_server { 12 | root /web 13 | } 14 | } 15 | 16 | :{$HTTPS_PORT} { 17 | redir / /web 18 | uri strip_prefix /web 19 | tls internal { 20 | on_demand 21 | } 22 | file_server { 23 | root /web 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docker/production/dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts AS build 2 | 3 | # arguments 4 | ARG VERSION="master" 5 | # Branch to check out 6 | ARG CHECKOUT_BRANCH="master" 7 | 8 | #environment variables 9 | ENV PROJECT_NAME="headscale-ui" 10 | # URL for the github/git location 11 | ENV PROJECT_URL="https://github.com/gurucomputing/headscale-ui" 12 | 13 | # Set the staging environment 14 | WORKDIR /staging/scripts 15 | WORKDIR /staging 16 | RUN chown 1000:1000 /staging 17 | 18 | # Copy across the scripts folder 19 | COPY scripts/* ./scripts/ 20 | 21 | # Set permissions for all scripts. We do not want normal users to have write 22 | # access to the scripts 23 | RUN chown -R 0:0 scripts 24 | RUN chmod -R 755 scripts 25 | 26 | # Build the image. This build runs as root 27 | RUN /staging/scripts/1-image-build.sh 28 | 29 | ##### 30 | ## Second Image 31 | ##### 32 | 33 | FROM alpine:latest 34 | 35 | #environment variables 36 | ENV PROJECT_NAME="headscale-ui" 37 | # URL for the github/git location 38 | ENV PROJECT_URL="https://github.com/gurucomputing/headscale-ui" 39 | # Ports that caddy will run on 40 | ENV HTTP_PORT="8080" 41 | ENV HTTPS_PORT="8443" 42 | 43 | # Production Web Server port. Runs a self signed SSL certificate 44 | EXPOSE 443 45 | 46 | # Set the staging environment 47 | WORKDIR /data 48 | WORKDIR /web 49 | WORKDIR /staging/scripts 50 | WORKDIR /staging 51 | 52 | # Copy across the scripts folder 53 | COPY scripts/* ./scripts/ 54 | # Copy default caddy config from project root 55 | COPY ./Caddyfile /staging/Caddyfile 56 | COPY --from=build /staging/${PROJECT_NAME}/build /web 57 | 58 | RUN apk add --no-cache caddy 59 | 60 | # Create a group and user 61 | RUN addgroup -S appgroup && adduser -D appuser -G appgroup 62 | 63 | # Set permissions for all scripts. We do not want normal users to have write 64 | # access to the scripts 65 | RUN chown -R 0:0 scripts 66 | RUN chmod -R 755 scripts 67 | 68 | RUN chown -R appuser:appgroup /web 69 | RUN chown -R appuser:appgroup /data 70 | 71 | # Tell docker that all future commands should run as the appuser user 72 | USER appuser 73 | 74 | WORKDIR /data 75 | 76 | ENTRYPOINT /bin/sh /staging/scripts/2-initialise.sh -------------------------------------------------------------------------------- /docker/production/scripts/1-image-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | 4 | # add dependencies 5 | # git for cloning the repository 6 | apk add --no-cache git 7 | 8 | #clone the project 9 | git clone ${PROJECT_URL} ${PROJECT_NAME} 10 | cd ${PROJECT_NAME} 11 | git checkout ${CHECKOUT_BRANCH} 12 | 13 | # install the project 14 | npm install 15 | 16 | # inject the version number 17 | sed -i "s/insert-version/${VERSION}/g" ./src/routes/settings.html/+page.svelte 18 | 19 | # build the project 20 | npm run build -------------------------------------------------------------------------------- /docker/production/scripts/2-initialise.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #----# 4 | # placeholder for testing 5 | # while true; do sleep 1; done 6 | #----# 7 | 8 | # check if /data/Caddyfile exists, copy across if not 9 | if [ ! -f /data/Caddyfile ]; 10 | then 11 | echo "no Caddyfile detected, copying across default config" 12 | cp /staging/Caddyfile /data/Caddyfile 13 | fi 14 | 15 | echo "Starting Caddy" 16 | /usr/sbin/caddy run --adapter caddyfile --config /data/Caddyfile 17 | -------------------------------------------------------------------------------- /documentation/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | Headscale-UI is based on the [svelte-kit](https://kit.svelte.dev/) framework and designed to compile to static HTML/JS/CSS. As such, once built (with `npm run build` or by downloading the packages), Headscale-UI can be hosted on any static file server (including headscale's static file server, once support has been added) 3 | 4 | ## App Design 5 | Headscale-UI uses the `static` adapter built into svelte-kit, meaning that several svelte-kit functions are not feasible in a static deploymnet. Backend services (such as any route ending in `.js` or `.ts`) cannot be used, and most if not all script functions should be defined within the `onMount` function of svelte. 6 | 7 | ### Client Side Design 8 | All Headscale-UI features and functions should be client side only. *Any* backend features should be considered to be implemented in a separate backend. This can be the [Headscale](https://github.com/juanfont/headscale) application itself (preferred), or potentially implementing a Backend-as-a-Service API such as [Supabase](https://supabase.com/). 9 | 10 | ## Dependencies 11 | Dependencies are kept to a minimum and kept to large, actively maintained repositories. Great care should be taken before suggesting or adding any additional dependencies: headscale is a sensitive tool and attack surfaces must be kept minimal. 12 | 13 | ### Dev Dependencies 14 | * [SvelteKit](https://kit.svelte.dev/) - The HTML/JS Framework and Toolkit 15 | * [Tailwind CSS](https://tailwindcss.com/) - CSS Framework 16 | * [DaisyUI](https://daisyui.com/) - CSS Theme and Components 17 | * [Typescript](https://www.typescriptlang.org/) - for static type checking 18 | * [Prettier](https://prettier.io/) - for Code Formatting 19 | * [Fuse.js](https://fusejs.io/) - for intelligent searching 20 | -------------------------------------------------------------------------------- /documentation/assets/README_ports.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gurucomputing/headscale-ui/7dd92ab4ad317264f2fe5c5d2e4fb54a1b07607a/documentation/assets/README_ports.gif -------------------------------------------------------------------------------- /documentation/assets/headscale-ui-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gurucomputing/headscale-ui/7dd92ab4ad317264f2fe5c5d2e4fb54a1b07607a/documentation/assets/headscale-ui-demo.gif -------------------------------------------------------------------------------- /documentation/configuration.md: -------------------------------------------------------------------------------- 1 | # Traefik Configuration 2 | 3 | (Thanks [DennisGaida](https://github.com/DennisGaida) and [Niek](https://github.com/Niek)) 4 | 5 | Below is a complete docker-compose example for bringing up Traefik + headscale + headscale-ui. Run with: `docker-compose up -d` and headscale-ui will be accessible at . 6 | 7 | ```yaml 8 | version: '3.9' 9 | 10 | services: 11 | headscale: 12 | image: headscale/headscale:latest 13 | pull_policy: always 14 | container_name: headscale 15 | restart: unless-stopped 16 | command: serve 17 | volumes: 18 | - ./headscale/config:/etc/headscale 19 | - ./headscale/data:/var/lib/headscale 20 | labels: 21 | - traefik.enable=true 22 | - traefik.http.routers.headscale-rtr.rule=PathPrefix(`/`) # you might want to add: && Host(`your.domain.name`)" 23 | - traefik.http.services.headscale-svc.loadbalancer.server.port=8080 24 | 25 | headscale-ui: 26 | image: ghcr.io/gurucomputing/headscale-ui:latest 27 | pull_policy: always 28 | container_name: headscale-ui 29 | restart: unless-stopped 30 | labels: 31 | - traefik.enable=true 32 | - traefik.http.routers.headscale-ui-rtr.rule=PathPrefix(`/web`) # you might want to add: && Host(`your.domain.name`)" 33 | - traefik.http.services.headscale-ui-svc.loadbalancer.server.port=8080 34 | 35 | traefik: 36 | image: traefik:latest 37 | pull_policy: always 38 | restart: unless-stopped 39 | container_name: traefik 40 | command: 41 | - --api.insecure=true # remove in production 42 | - --providers.docker 43 | - --entrypoints.web.address=:80 44 | - --entrypoints.websecure.address=:443 45 | - --global.sendAnonymousUsage=false 46 | ports: 47 | - 80:80 48 | - 443:443 49 | - 8080:8080 # web UI (enabled with api.insecure) 50 | volumes: 51 | - /var/run/docker.sock:/var/run/docker.sock:ro 52 | - ./traefik/certificates:/certificates 53 | ``` 54 | 55 | # NGINX Proxy Manager Configuration 56 | 57 | If running Headscale and Headscale UI outside of a consolidated docker-compose file (as above), NGINX Proxy Manager is another easy way to run all three. NGINX Proxy Manager is an easy way to run Headscale and Headscale UI behind a reverse proxy that can manager SSL certs automatically. This assumes the following: 58 | 59 | 1. Headscale is set up on your Docker host (or another location you can route to) per the instructions [here](https://github.com/juanfont/headscale). 60 | 2. NGINX Proxy Manager is running and you can use it to generate SSL certificates. More information on NGINX Proxy Manager are [here](https://github.com/NginxProxyManager/nginx-proxy-manager). 61 | 62 | Use this simplified docker-compose file to run headscale-ui: 63 | 64 | ```yaml 65 | version: '3.5' 66 | services: 67 | headscale-ui: 68 | image: ghcr.io/gurucomputing/headscale-ui:latest 69 | restart: unless-stopped 70 | container_name: headscale-ui 71 | ports: 72 | - 8443:443 # Use the port of your choice, but map it to 443 on the container 73 | ``` 74 | 75 | Once all three services are running, set up Headscale and Headscale UI _by creating a proxy host_: 76 | 77 | 1. Details: Enter the FQDN you will be using for Headscale and Headscale UI, and enable Websockets Support and Block Common Exploits. 78 | 2. SSL: Select or create the SSL certificate you'll be using for the entire FQDN where both will run. Make sure to enable Force SSL, HTTP/2 Support, HSTS and HSTS Subdomains. 79 | 3. Advanced: In the text box, add the following to manage the Headscale UI path properly: 80 | ```json 81 | location /web/ { 82 | proxy_pass https://XXX.XXX.XXX.XXXX:port/web/; 83 | } 84 | ``` 85 | 86 | # Nginx Example Configuration 87 | From https://github.com/gurucomputing/headscale-ui/issues/71 88 | 89 | ``` 90 | map $http_upgrade $connection_upgrade { 91 | default keep-alive; 92 | 'websocket' upgrade; 93 | '' close; 94 | } 95 | 96 | server { 97 | server_name headscale-01.example.com; 98 | 99 | location /web { 100 | alias /usr/local/www/headscale-ui; 101 | index index.html; 102 | } 103 | 104 | location / { 105 | proxy_pass http://localhost:8080; 106 | proxy_http_version 1.1; 107 | proxy_set_header Upgrade $http_upgrade; 108 | proxy_set_header Connection $connection_upgrade; 109 | proxy_set_header Host $server_name; 110 | proxy_redirect http:// https://; 111 | proxy_buffering off; 112 | proxy_set_header X-Real-IP $remote_addr; 113 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 114 | proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; 115 | add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; 116 | } 117 | 118 | listen 443 ssl; 119 | ssl_certificate fullchain.pem; 120 | ssl_certificate_key privkey.pem; 121 | [...] 122 | } 123 | 124 | server { 125 | if ($host = headscale-01.example.com) { 126 | return 301 https://$host$request_uri; 127 | } 128 | 129 | server_name headscale-01.example.com; 130 | listen 80; 131 | return 404; 132 | } 133 | ``` 134 | -------------------------------------------------------------------------------- /documentation/development.md: -------------------------------------------------------------------------------- 1 | ## Development Documentation 2 | 3 | Development can be done either by using the official development docker image, or via a normal nodejs installation. 4 | 5 | ## Testing 6 | 7 | All branches should undergo manual testing as specified in the [Testing](./testing.md) document. If someone is well versed in unit automation tests for browser front ends, please educate me! For now do it manually before making a pull request. 8 | 9 | ### Quick Start (Docker) 10 | * `docker run -p 443:443 -p 3000:3000 -v "$(pwd)"/data:/data ghcr.io/gurucomputing/headscale-ui-dev:latest` 11 | 12 | A full browser based vscode development environment will be found at `http://:3000/?tkn=`. The authentication token will be printed in your docker logs, and must be included in the URL. 13 | 14 | > You can set a custom authentication token using the $CONNECTION_TOKEN environment variable 15 | 16 | Once started, the development environment can be found at `/data/headscale-ui` inside vscode. The development server (including hot reloading) will be found at port 443. The running `npm run dev` process can be accessed within tmux, accessed with `tmux a` in the vscode terminal. 17 | 18 | ### Remapping port 443 19 | You may wish to remap port 443 to something else (like 9443). You *cannot* remap via docker directly (IE: `docker run -p 9443:443`): doing so breaks the hot-reload mechanism. Instead, you must change the port the server runs on, and map it correctly (IE: `docker run -p 9443:9443`). You can change the server port under `package.json` once the container is loaded (see below gif for details): 20 | 21 | ![](/documentation/assets/README_ports.gif) 22 | 23 | If you wish to do the same with the `npm run stage` mechanism, you can edit the included `Caddyfile` to run on the correct port, changing `:443` to the appropriate port. 24 | 25 | ### Additional Docker Commands 26 | 27 | | Variable | Description | Example | 28 | |----|----|----| 29 | | AUTOINITIALZE | On first load, will automatically initialise and clone the repository | `true` | 30 | | USE_CONNECTION_TOKEN | sets a connection token for VSCode Server. | `true` | 31 | | CONNECTION_TOKEN | Set to a secret to have some measure of protection for vscode. Randomized if left blank | `my_secret_token` | 32 | | PROJECT_NAME | name of the project you are cloning in | `headscale-ui` | 33 | | PROJECT_URL | url of the project you are cloning in | `https://github.com/gurucomputing/headscale-ui` | 34 | | AUTOSTART | will automatically run the headscale server on container load within `tmux` | `true` | 35 | | DEV_COMMAND | sets the autostart command | `npm run dev` | 36 | 37 | ### Quick Start (npm) 38 | Clone the repository with `git clone https://github.com/gurucomputing/headscale-ui`, navigate to the project directory, and install with `npm install`. 39 | 40 | Development environment can be ran with `npm run dev`. Static site can be generated with `npm run build`. Testing (and potentially even production) can be ran with `npm run stage` *if* caddy is installed in your distribution (red hat/fedora can install with `sudo dnf install caddy`). -------------------------------------------------------------------------------- /documentation/route_queries.md: -------------------------------------------------------------------------------- 1 | # Route Queries 2 | 3 | Some routes offer additional behavior to a route when a `?` exists in the URL. These are called query string parameters or route queries. Route queries are used to modify the behavior of a route. Below are the available route queries. 4 | 5 | ## Devices 6 | 7 | /devices.html 8 | 9 | ### Parameters 10 | 11 | #### `?nodekey={nodekey of a pending device}` 12 | 13 | When this parameter exists, it will automatically open the New Device form and pre-fill the Device Key input automatically. Everything right of the `=` is used as the value of the input. 14 | 15 | Below is an example of how to set up a redirect in NGINX from the default headscale /register/{nodekey} URL to utilize this parameter: 16 | 17 | ```nginx 18 | ... 19 | 20 | location /register/nodekey { 21 | rewrite ^/register/(.*)$ /web/devices.html?nodekey=$1 redirect; 22 | } 23 | 24 | location /web { 25 | 26 | ... 27 | ``` 28 | -------------------------------------------------------------------------------- /documentation/style.md: -------------------------------------------------------------------------------- 1 | # Style 2 | Rough style guide for development in Headscale-UI. Documentation will be improved as style gets more defined 3 | 4 | ## CSS 5 | all ` 98 | -------------------------------------------------------------------------------- /src/lib/common/searching.svelte: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /src/lib/common/sorting.svelte: -------------------------------------------------------------------------------- 1 | 76 | -------------------------------------------------------------------------------- /src/lib/common/stores.js: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import { Device, User, ACL } from '$lib/common/classes'; 3 | 4 | // 5 | // localStorage Stores (global scope, saves to the browser) 6 | // 7 | 8 | // stores the theme 9 | export const themeStore = writable(''); 10 | // stores URL and API Key 11 | export const URLStore = writable(''); 12 | export const APIKeyStore = writable(''); 13 | // stores sorting preferences 14 | export const deviceSortStore = writable('id'); 15 | export const deviceSortDirectionStore = writable('ascending'); 16 | export const userSortStore = writable('id'); 17 | export const sortDirectionStore = writable('ascending'); 18 | // stores preauth key preference 19 | export const preAuthHideStore = writable(false); 20 | 21 | // Dev Setting Stores 22 | // Shows or Hides ACL Settings 23 | export const showACLPagesStore = writable(false); 24 | 25 | // 26 | // Normal Stores (global scope, saves until refresh) 27 | // 28 | // stores user and device data 29 | export const userStore = writable([new User()]); 30 | export const userFilterStore = writable([new User()]); 31 | export const deviceStore = writable([new Device()]); 32 | export const deviceFilterStore = writable([new Device()]); 33 | // stores ACL object 34 | export const aclStore = writable(new ACL()); 35 | // used to store the value of an alert across all components 36 | export const alertStore = writable(''); 37 | // used to determine if the API is functioning 38 | export const apiTestStore = writable(''); 39 | // stores search state 40 | export const userSearchStore = writable(''); 41 | export const deviceSearchStore = writable(''); -------------------------------------------------------------------------------- /src/lib/devices/CreateDevice.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | 46 | {#if newDeviceCardVisible == true} 47 |
48 |
49 | {#each tabs as tab, index} 50 | 51 | {/each} 52 |
53 | 54 | {#if activeTab == 0} 55 |
56 |

Install Tailscale with the client pointing to your domain (see headscale client documentation). Log in using the tray icon, and your browser should give you instructions with a key.

57 |
headscale -u USER nodes register --key <your device key>
58 |

Copy the key below:

59 |
60 |
61 | 62 | 63 |
64 |
65 | 66 | 71 |
72 |
73 | 78 | 88 |
89 |
90 |
91 | {/if} 92 | 93 | {#if activeTab == 1} 94 |
95 |

Preauth Keys provide the capability to install tailscale using a pre-registered key (see the --authkey flag in the tailscale command line documentation)

96 |

Preauth Keys are especially useful for deploying headscale as an always-on VPN (see the TS_UNATTENDEDMODE install option in the tailscale documentation) or router-level VPN.

97 |
98 | 99 | 100 | 101 | Preauth Keys can be managed in the User Section of the UI 102 |
103 |
104 | {/if} 105 | 106 | {#if activeTab == 2} 107 |
108 |

OIDC provides the ability to register an external authentication provider (such as keycloak) to authenticate devices to headscale.

109 |
110 |

Configure Headscale to register with an authentication provider (see headscale configuration documentation). Once configured, successfully authenticated devices will automatically self-register

111 |
112 | {/if} 113 |
114 | {/if} 115 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard.svelte: -------------------------------------------------------------------------------- 1 | 74 | 75 |
76 | 77 |
(cardExpanded = !cardExpanded)} class="flex items-center"> 78 | 79 | {#if cardEditing == false} 80 | {#if device.online} 81 | {device.id}: {device.givenName} 82 | {:else} 83 | {device.id}: {device.givenName} 84 | {/if} 85 | {/if} 86 | 87 | 88 |
89 |
90 | 91 | 104 |
105 |
106 | {#if cardExpanded} 107 | 108 |
109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 |
Device Last Seen{new Date(device.lastSeen)}
IP Addresses 119 |
    120 | {#each device.ipAddresses as address} 121 |
  • {address}
  • 122 | {/each} 123 |
124 |
Assigned User
Device Name{device.name}
139 |
140 |
141 | {/if} 142 |
143 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/DeviceRoutes.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | Device Routes 11 |
    13 | {#each device.availableRoutes as route} 14 |
  • 15 | 16 |
  • 17 | {/each} 18 |
20 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/DeviceRoutes/DeviceRoute.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | {route} 25 | {#if device.approvedRoutes.includes(route)} 26 | 27 | {:else} 28 | 38 | {/if} 39 | {#if device.subnetRoutes.includes(route)} 40 | 41 | {/if} 42 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/DeviceRoutes/DeviceRouteAPI.svelte: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/DeviceTags.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | 25 | 26 | {#each device.forcedTags as tag} 27 | {tag.replace("tag:","")} 28 | 29 | 35 | 36 | {/each} 37 |
38 | 39 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/DeviceTags/NewDeviceTag.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 50 | 51 | 63 | 64 | {/if} 65 | 66 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/MoveDevice.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | {#if !deviceMoving} 26 | {device.user.name} 27 | 28 | 38 | {:else} 39 |
40 | 45 | 46 | 51 | 52 | 57 |
58 | {/if} 59 | 60 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/RemoveDevice.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#if !cardDeleting} 24 | 25 | 30 | {:else} 31 | 32 | Deleting {device.name}. Confirm 33 | 34 | 39 | or Cancel 40 | 41 | 46 | {/if} 47 | -------------------------------------------------------------------------------- /src/lib/devices/DeviceCard/RenameDevice.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | {#if !cardEditing} 35 | 36 | 41 | {:else} 42 |
43 | 44 | 45 | 46 | 51 | 52 | 57 |
58 | {/if} -------------------------------------------------------------------------------- /src/lib/devices/SearchDevices.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/lib/devices/SortDevices.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/lib/settings/DevSettings.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

Developer Flags

9 | {#if showDevSettings} 10 |
11 |

ACL Pages

12 |
13 | {/if} 14 | -------------------------------------------------------------------------------- /src/lib/settings/ServerSettings.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 |

Server Settings

38 | 39 | 40 |

URL for your headscale server instance

41 | 49 |
50 | 51 | 71 | 72 |
73 |

Generate an API key for your headscale instance and place it here.

74 | {#if apiStatus != 'succeeded'} 75 | 76 | {:else} 77 | 78 | {/if} 79 | 80 | 81 | {#if apiStatus === 'succeeded'} 82 | 83 | 84 | 85 | {/if} 86 | {#if apiStatus === 'failed'} 87 | 88 | 89 | 90 | {/if} 91 |
92 | -------------------------------------------------------------------------------- /src/lib/settings/ServerSettings/APIKeyTimeLeft.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 | 47 | -------------------------------------------------------------------------------- /src/lib/settings/ServerSettings/RolloverAPI.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | 52 | -------------------------------------------------------------------------------- /src/lib/settings/ThemeSettings.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |

Theme Settings

7 | 12 | -------------------------------------------------------------------------------- /src/lib/users/CreateUser.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | {#if newUserCardVisible} 33 |
34 |
35 | 36 | 37 |
38 |
39 | 44 | 53 |
54 |
55 | {/if} 56 | -------------------------------------------------------------------------------- /src/lib/users/SearchUsers.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/lib/users/SortUsers.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/lib/users/UserCard.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 |
(cardExpanded = !cardExpanded)} class="flex justify-between"> 17 |
18 | 19 |
20 |
21 | 22 | 23 | 34 |
35 |
36 | {#if cardExpanded} 37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | {#key $userStore} 47 | 48 | {/key} 49 | 50 |
User Creation Date{new Date(user.createdAt)}
51 |
52 |
53 | {/if} 54 |
55 | -------------------------------------------------------------------------------- /src/lib/users/UserCard/PreAuthKeys.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | 41 |
Preauth Keys 42 | 59 |
60 |
61 | 66 | 67 | { 69 | $preAuthHideStore = !$preAuthHideStore 70 | }} 71 | class="font-normal ml-2">Hide Expired/Used Keys 73 |
74 | 75 | 76 | {#if newPreAuthKeyShow} 77 | 78 | {/if} 79 | 80 | 81 | {#each keyList as key} 82 | 83 | 84 | 85 | 106 | 117 | 118 | {/each} 119 | 120 |
{key.id}{key.key} 87 |
88 | {#if new Date(key.expiration).getTime() > new Date().getTime()} 89 |
active
90 | {:else if key.id != ''} 91 |
expired
92 | {/if} 93 |
94 | {#if !key.used && key.id != ''} 95 |
unused
96 | {:else if key.id != ''} 97 |
used
98 | {/if} 99 | {#if key.reusable && key.id != ''} 100 |
reusable
101 | {/if} 102 | {#if key.ephemeral && key.id != ''} 103 |
ephemeral
104 | {/if} 105 |
107 | 108 | {#if new Date(key.expiration).getTime() > new Date().getTime() && (!key.used || key.reusable)} 109 | 110 | 115 | {/if} 116 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /src/lib/users/UserCard/PreAuthKeys/NewPreAuthKey.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 58 | 59 | 60 |
Expiry:
Reusable: 50 | 51 |
Ephemeral: 56 | 57 |
61 | 62 | 69 |
70 |
71 | -------------------------------------------------------------------------------- /src/lib/users/UserCard/RemoveUser.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#if !cardDeleting} 24 | 25 | 30 | {:else} 31 | 32 | Deleting {user.name}. Confirm 33 | 34 | 39 | or Cancel 40 | 41 | 46 | {/if} 47 | -------------------------------------------------------------------------------- /src/lib/users/UserCard/RenameUser.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | {#if !cardEditing} 35 | {user.id}: {user.name} 36 | 37 | 42 | {:else} 43 |
44 | 45 | 46 | 47 | 52 | 53 | 58 |
59 | {/if} -------------------------------------------------------------------------------- /src/routes/+layout.js: -------------------------------------------------------------------------------- 1 | export const prerender = true; -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 14 | 15 |
16 | 17 |
29 | 30 |
31 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/routes/devices.html/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | 41 | 42 | 43 | {#if componentLoaded} 44 |
45 |
46 |

Device View

47 |
48 | {#if $apiTestStore === 'succeeded'} 49 | 50 | 51 | 63 |
54 |
55 | {#if newDeviceCardVisible == false} 56 | 57 | {:else} 58 | 59 | {/if} 60 |
64 | 65 | 66 | 67 |
68 | {#each $deviceStore as device} 69 | {#if $deviceFilterStore.includes(device)} 70 | 71 | {/if} 72 | {/each} 73 |
74 | {/if} 75 | {#if $apiTestStore === 'failed'} 76 |
77 |

API test did not succeed.
Headscale might be down or API settings may need to be set
change server settings in the settings page

78 |
79 | {/if} 80 |
81 | {/if} 82 | -------------------------------------------------------------------------------- /src/routes/groups.html/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {#if showACLPagesStore} 19 | 22 | {/if} 23 | 24 | -------------------------------------------------------------------------------- /src/routes/settings.html/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 |