removed archive from current source, fixed stage build

This commit is contained in:
Chris Bisset 2025-02-10 00:51:39 +00:00
parent 051368f010
commit d29374e07b
56 changed files with 6 additions and 6330 deletions

View file

@ -1 +0,0 @@
engine-strict=true

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,7 +0,0 @@
{
"useTabs": true,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 400
}

View file

@ -1,13 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
}

3499
archive/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,35 +0,0 @@
{
"name": "headscale-ui",
"version": "2024.10.10",
"scripts": {
"dev": "vite dev --port 8080 --host 0.0.0.0",
"build": "vite build",
"package": "vite package",
"preview": "vite preview --https --port 443 --host 0.0.0.0",
"stage": "/usr/bin/caddy run --adapter caddyfile --config ./Caddyfile",
"check": "svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-check --tsconfig ./jsconfig.json --watch",
"lint": "prettier --check --plugin-search-dir=. .",
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3",
"@sveltejs/adapter-static": "^3",
"@sveltejs/kit": "^2",
"@tailwindcss/typography": "^0",
"@vitejs/plugin-basic-ssl": "^1",
"autoprefixer": "^10",
"daisyui": "^4",
"fuse.js": "^7",
"postcss": "^8",
"postcss-load-config": "^5",
"prettier": "^3",
"prettier-plugin-svelte": "^3",
"svelte": "^4",
"svelte-check": "^3",
"svelte-preprocess": "^5",
"tailwindcss": "^3",
"typescript": "^5"
},
"type": "module"
}

View file

@ -1,13 +0,0 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
};
module.exports = config;

View file

@ -1,24 +0,0 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;
.form-input {
@apply shadow appearance-none border rounded w-full bg-base-100 py-2 px-3 text-base-content text-sm mb-3 leading-tight focus:outline-none;
}
.card-primary {
@apply grid grid-cols-1 divide-y p-2 max-w-screen-lg mx-4 border-base-content rounded-md text-sm text-base-content shadow
}
.card-pending {
@apply flex justify-between p-1 mb-4 max-w-screen-lg border border-dashed mx-4 border-base-content rounded-md text-sm text-base-content shadow
}
.card-input {
@apply shadow appearance-none border rounded w-64 py-1 px-3 bg-base-100 text-base-content text-sm leading-tight focus:outline-none;
}
.card-select {
@apply shadow border rounded w-64 py-1 px-3 bg-base-100 text-base-content text-sm leading-tight focus:outline-2;
}

10
archive/src/app.d.ts vendored
View file

@ -1,10 +0,0 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
// interface Locals {}
// interface Platform {}
// interface Session {}
// interface Stuff {}
}

View file

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View file

@ -1,43 +0,0 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { slide } from 'svelte/transition';
import { alertStore } from '$lib/common/stores.js';
export let ms = 3000;
let visible = false;
let timeout: number;
onMount(async () => {
window.addEventListener('unhandledrejection', function (promiseRejectionEvent) {
$alertStore = promiseRejectionEvent.reason;
});
});
const onMessageChange = (message: string, ms: number) => {
clearTimeout(timeout);
if (!message) {
// hide Alert if message is empty
visible = false;
} else {
visible = true;
if (ms > 0)
timeout = window.setTimeout(() => {
$alertStore = '';
}, ms); // and hide it after ms milliseconds
}
};
$: onMessageChange($alertStore, ms); // whenever the alert store or the ms props changes run onMessageChange
onDestroy(() => clearTimeout(timeout)); // make sure we clean-up the timeout
</script>
{#if visible}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
transition:slide|global
class="absolute alert text-lg left-1/2 transform -translate-x-1/2 justify-center shadow-lg max-w-lg"
on:keypress on:click={() => {
$alertStore = '';
}}
>
<p>{$alertStore}</p>
</div>
{/if}

View file

@ -1,41 +0,0 @@
<script>
import { onMount } from 'svelte';
import { deviceSortStore, deviceSortDirectionStore, userSortStore, sortDirectionStore, themeStore, showACLPagesStore} from '$lib/common/stores.js';
import { URLStore } from '$lib/common/stores.js';
import { APIKeyStore } from '$lib/common/stores.js';
import { preAuthHideStore } from '$lib/common/stores.js';
onMount(async () => {
// stores headscale theme
themeStore.set(localStorage.getItem('headscaleTheme') || 'hsui');
themeStore.subscribe((val) => localStorage.setItem('headscaleTheme', val));
// stores device sort preferences
deviceSortStore.set(localStorage.getItem('headscaleDeviceSort') || 'id');
deviceSortStore.subscribe((val) => localStorage.setItem('headscaleDeviceSort', val));
deviceSortDirectionStore.set(localStorage.getItem('headscaleDeviceSortDirection') || 'ascending');
deviceSortDirectionStore.subscribe((val) => localStorage.setItem('headscaleDeviceSortDirection', val));
// stores user sort preferences
userSortStore.set(localStorage.getItem('headscaleUserSort') || 'id');
userSortStore.subscribe((val) => localStorage.setItem('headscaleUserSort', val));
sortDirectionStore.set(localStorage.getItem('headscaleUserSortDirection') || 'ascending');
sortDirectionStore.subscribe((val) => localStorage.setItem('headscaleUserSortDirection', val));
// stores URL and API key
URLStore.set(localStorage.getItem('headscaleURL') || '');
// remove trailing slashes when storing the URL
URLStore.subscribe((val) => localStorage.setItem('headscaleURL', val.replace(/\/+$/, '')));
APIKeyStore.set(localStorage.getItem('headscaleAPIKey') || '');
APIKeyStore.subscribe((val) => localStorage.setItem('headscaleAPIKey', val));
// stores whether preauthkeys get hidden when expired/used
preAuthHideStore.set((localStorage.getItem('headscalePreAuthHide') || 'false') == 'true');
preAuthHideStore.subscribe((val) => localStorage.setItem('headscalePreAuthHide', val ? 'true' : 'false'));
// dev setting stores
showACLPagesStore.set((localStorage.getItem('showACLPages') || 'false') == 'true');
showACLPagesStore.subscribe((val) => localStorage.setItem('showACLPages', val ? 'true' : 'false'));
});
</script>

View file

@ -1,556 +0,0 @@
<script context="module" lang="ts">
import { APIKey, Device, PreAuthKey, User } from '$lib/common/classes';
import { deviceStore, userStore, apiTestStore} from '$lib/common/stores.js';
import { sortDevices, sortUsers } from '$lib/common/sorting.svelte';
import { filterDevices, filterUsers } from './searching.svelte';
export async function getUsers() {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for getting users
let endpointURL = '/api/v1/user';
//returning variables
let headscaleUsers = [new User()];
let headscaleUsersResponse: Response = new Response();
await fetch(headscaleURL + endpointURL, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
// return the api data
headscaleUsersResponse = response;
} else {
return response.text().then((text) => {
apiTestStore.set('failed');
throw text;
});
}
})
.catch((error) => {
apiTestStore.set('failed');
throw error;
});
await headscaleUsersResponse.json().then((data) => {
headscaleUsers = data.users;
// sort the users
headscaleUsers = sortUsers(headscaleUsers);
});
// Set the store
apiTestStore.set('succeeded');
userStore.set(headscaleUsers);
// Filter the store
filterUsers();
}
export async function editUser(currentUsername: string, newUsername: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/user/' + currentUsername + '/rename/' + newUsername;
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function newAPIKey(APIKeyExpiration: string): Promise<string> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/apikey';
let APIKeyResponse = new Response();
let APIKeyString = '';
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
},
body: JSON.stringify({
expiration: APIKeyExpiration
})
})
.then((response) => {
if (response.ok) {
APIKeyResponse = response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
await APIKeyResponse.json().then((data) => {
APIKeyString = data.apiKey;
});
return APIKeyString;
}
export async function expireAPIKey(APIKeyPrefix: string) {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/apikey/expire';
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
},
body: JSON.stringify({
prefix: APIKeyPrefix
})
})
.then((response) => {
if (response.ok) {
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function updateTags(deviceID: string, tags: string[]): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = `/api/v1/node/${deviceID}/tags`;
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
},
body: JSON.stringify({
tags: tags
})
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function removeUser(currentUsername: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/user/' + currentUsername;
await fetch(headscaleURL + endpointURL, {
method: 'DELETE',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function newUser(newUsername: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/user';
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
},
body: JSON.stringify({
name: newUsername.toLowerCase()
})
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function getDevices(): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for getting devices
let endpointURL = `/api/v1/node`;
//returning variables
let headscaleDevices = [new Device()];
let headscaleDeviceResponse: Response = new Response();
// attempt to get the user data
await fetch(headscaleURL + endpointURL, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
// return the api data
headscaleDeviceResponse = response;
} else {
return response.text().then((text) => {
apiTestStore.set('failed');
throw text;
});
}
})
.catch((error) => {
apiTestStore.set('failed');
throw error;
});
await headscaleDeviceResponse.json().then((data) => {
headscaleDevices = data[`nodes`];
headscaleDevices = sortDevices(headscaleDevices);
});
// set the stores
apiTestStore.set('succeeded');
deviceStore.set(headscaleDevices);
// filter the store
filterDevices();
}
export async function getAPIKeys(): Promise<APIKey[]> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/apikey';
let apiKeysResponse = new Response();
let apiKeys = [new APIKey()];
await fetch(headscaleURL + endpointURL, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
apiKeysResponse = response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
await apiKeysResponse.json().then((data) => {
apiKeys = data.apiKeys;
});
return apiKeys;
}
export async function getPreauthKeys(userName: string): Promise<PreAuthKey[]> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/preauthkey';
//returning variables
let headscalePreAuthKey = [new PreAuthKey()];
let headscalePreAuthKeyResponse: Response = new Response();
await fetch(headscaleURL + endpointURL + '?user=' + userName, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
headscalePreAuthKeyResponse = response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
await headscalePreAuthKeyResponse.json().then((data) => {
headscalePreAuthKey = data.preAuthKeys;
});
return headscalePreAuthKey;
}
export async function newPreAuthKey(userName: string, expiry: string, reusable: boolean, ephemeral: boolean): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = '/api/v1/preauthkey';
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
},
body: JSON.stringify({
user: userName,
expiration: expiry,
reusable: reusable,
ephemeral: ephemeral
})
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function removePreAuthKey(userName: string, preAuthKey: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for removing devices
let endpointURL = '/api/v1/preauthkey/expire';
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
},
body: JSON.stringify({
user: userName,
key: preAuthKey
})
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function newDevice(key: string, userName: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = `/api/v1/node/register`;
await fetch(headscaleURL + endpointURL + '?user=' + userName + '&key=' + key, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function moveDevice(deviceID: string, user: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = `/api/v1/node/${deviceID}/user?user=${user}`;
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function renameDevice(deviceID: string, name: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for editing users
let endpointURL = `/api/v1/node/${deviceID}/rename/${name}`;
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
export async function removeDevice(deviceID: string): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for removing devices
let endpointURL = `/api/v1/node/${deviceID}`;
await fetch(headscaleURL + endpointURL, {
method: 'DELETE',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
return response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
</script>

View file

@ -1,72 +0,0 @@
export class Device {
public id: string = '';
public name: string = '';
public givenName: string = '';
public lastSeen: string = '';
public ipAddresses: string[] = [];
public forcedTags: string[] = [];
public validTags: string[] = [];
public invalidTags: string[] = [];
public user: { name: string } = { name: '' };
public online?: boolean;
public constructor(init?: Partial<Device>) {
Object.assign(this, init);
}
}
export class ACL {
public groups: { [key: string]: [string] } = {};
public constructor(init?: Partial<Route>) {
Object.assign(this, init);
}
}
export class Route {
// current (hs 18+) method of handling a route
advertised: boolean = true;
prefix: string = '';
enabled: boolean = false;
id: number = 0;
public constructor(init?: Partial<Route>) {
Object.assign(this, init);
}
}
export class APIKey {
id: string = '';
prefix: string = '';
expiration: string = '';
createdAt: string = '';
lastSeen: string = '';
public constructor(init?: Partial<Route>) {
Object.assign(this, init);
}
}
export class PreAuthKey {
public user: string = '';
public id: string = '';
public key: string = '';
public createdAt: string = '';
public expiration: string = '';
public reusable: boolean = false;
public ephemeral: boolean = false;
public used: boolean = false;
public constructor(init?: Partial<PreAuthKey>) {
Object.assign(this, init);
}
}
export class User {
public id: string = '';
public name: string = '';
public createdAt: string = '';
public constructor(init?: Partial<User>) {
Object.assign(this, init);
}
}

View file

@ -1,97 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { writable } from 'svelte/store';
import { base } from '$app/paths';
import { showACLPagesStore } from './stores';
// navigation bar variables
let navExpanded = writable('');
let componentLoaded = false;
onMount(async () => {
// get the navbar state from the local store
navExpanded = writable(localStorage.getItem('navExpanded') || '');
// subscribe to the navbar state and update the local storage where needed
navExpanded.subscribe((val) => localStorage.setItem('navExpanded', val));
// if there is no initial stored navbar state, try to determine a state based on screen size
if ($navExpanded == '') {
// assuming a mobile unless larger than 640px
if (window.outerWidth >= 640) {
$navExpanded = 'expanded';
} else {
$navExpanded = 'collapsed';
}
}
componentLoaded = true;
});
</script>
<!-- let the page initialize before showing the nav bar -->
{#if componentLoaded}
<nav class="bg-base-200 flex shadow-xl w-14 h-screen sticky top-0" class:navCollapsed={$navExpanded == 'collapsed'} class:navExpanded={$navExpanded == 'expanded'} transition:fade|global>
<!-- links on top of sidebar -->
<div class="absolute top-0 w-full">
<button class="w-full nav-item" on:click={() => ($navExpanded == 'collapsed' ? ($navExpanded = 'expanded') : ($navExpanded = 'collapsed'))}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
</svg>
<span class="indent-4 text-primary font-extrabold">Headscale</span>
</button>
<div />
<a href="{base}/users.html" class="nav-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="indent-4">User View</span>
</a>
{#if $showACLPagesStore}
<a href="{base}/groups.html" class="nav-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"
/>
</svg>
<span class="indent-4">Group View</span>
</a>
{/if}
<a href="{base}/devices.html" class="nav-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="indent-4">Device View</span>
</a>
<a href="{base}/settings.html" class="nav-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="indent-4">Settings</span>
</a>
</div>
</nav>
{/if}
<style lang="postcss">
.navExpanded {
transition: ease-out 200ms;
width: 180px;
}
.navCollapsed {
transition: ease-out 200ms;
width: 55px;
}
.nav-item {
@apply flex items-center text-sm py-4 px-4 h-12 overflow-hidden text-base-content stroke-base-content text-ellipsis cursor-default whitespace-nowrap rounded hover:bg-base-300 transition duration-300 ease-in-out;
}
</style>

View file

@ -1,36 +0,0 @@
<script context="module" lang="ts">
import { userFilterStore, userStore, userSearchStore, deviceFilterStore, deviceSearchStore, deviceStore } from './stores';
import { get } from 'svelte/store';
import Fuse from 'fuse.js';
import type { Device, User } from './classes';
export function filterUsers() {
// only run if we have search contents set
if (get(userSearchStore)) {
let searcher = new Fuse(get(userStore), {
keys: ['id', 'name']
});
// search using the searchstore term, and take the resultant array contents and set it to userFilterStore
userFilterStore.set(searcher.search(get(userSearchStore)).map((a) => a.item));
} else {
// if we have no search parameters, just copy across the whole object
userFilterStore.set(get(userStore));
}
}
export function filterDevices() {
// only run if we have search contents set
if (get(deviceSearchStore)) {
let searcher = new Fuse(get(deviceStore), {
keys: ['id', 'givenName', 'name', 'forcedTags', 'validTags', 'user.name']
});
// search using the searchstore term, and take the resultant array contents and set it to userFilterStore
deviceFilterStore.set(searcher.search(get(deviceSearchStore)).map((a) => a.item));
} else {
// if we have no search parameters, just copy across the whole object
deviceFilterStore.set(get(deviceStore));
}
}
</script>

View file

@ -1,75 +0,0 @@
<script context="module" lang="ts">
import type { Device, User } from './classes';
export function sortUsers(users: User[]): User[] {
let sortKey = localStorage.getItem('headscaleUserSort') || '';
let sortDirection = localStorage.getItem('headscaleUserSortDirection') || '';
let sortedUsers = users;
let collator = new Intl.Collator([], { numeric: true });
if (sortDirection == 'ascending') {
switch (sortKey) {
case 'id':
sortedUsers = users.sort((a: User, b: User) => collator.compare(a.id, b.id));
break;
case 'createdAt':
sortedUsers = users.sort((a: User, b: User) => -collator.compare(a.createdAt, b.createdAt));
break;
case 'name':
sortedUsers = users.sort((a: User, b: User) => collator.compare(a.name, b.name));
break;
}
}
if (sortDirection == 'descending') {
switch (sortKey) {
case 'id':
sortedUsers = users.sort((a: User, b: User) => -collator.compare(a.id, b.id));
break;
case 'createdAt':
sortedUsers = users.sort((a: User, b: User) => collator.compare(a.createdAt, b.createdAt));
break;
case 'name':
sortedUsers = users.sort((a: User, b: User) => -collator.compare(a.name, b.name));
break;
}
}
return sortedUsers;
}
export function sortDevices(devices: Device[]): Device[] {
let sortKey = localStorage.getItem('headscaleDeviceSort') || '';
let sortDirection = localStorage.getItem('headscaleDeviceSortDirection') || '';
let sortedDevices = devices;
let collator = new Intl.Collator([], { numeric: true });
if (sortDirection == 'ascending') {
switch (sortKey) {
case 'id':
sortedDevices = devices.sort((a: Device, b: Device) => collator.compare(a.id, b.id));
break;
case 'lastSeen':
sortedDevices = devices.sort((a: Device, b: Device) => -collator.compare(a.lastSeen, b.lastSeen));
break;
case 'givenName':
sortedDevices = devices.sort((a: Device, b: Device) => collator.compare(a.givenName, b.givenName));
break;
}
}
if (sortDirection == 'descending') {
switch (sortKey) {
case 'id':
sortedDevices = devices.sort((a: Device, b: Device) => -collator.compare(a.id, b.id));
break;
case 'lastSeen':
sortedDevices = devices.sort((a: Device, b: Device) => collator.compare(a.lastSeen, b.lastSeen));
break;
case 'givenName':
sortedDevices = devices.sort((a: Device, b: Device) => -collator.compare(a.givenName, b.givenName));
break;
}
}
return sortedDevices;
}
</script>

View file

@ -1,41 +0,0 @@
import { writable } from 'svelte/store';
import { Device, User, ACL } from '$lib/common/classes';
//
// localStorage Stores (global scope, saves to the browser)
//
// stores the theme
export const themeStore = writable('');
// stores URL and API Key
export const URLStore = writable('');
export const APIKeyStore = writable('');
// stores sorting preferences
export const deviceSortStore = writable('id');
export const deviceSortDirectionStore = writable('ascending');
export const userSortStore = writable('id');
export const sortDirectionStore = writable('ascending');
// stores preauth key preference
export const preAuthHideStore = writable(false);
// Dev Setting Stores
// Shows or Hides ACL Settings
export const showACLPagesStore = writable(false);
//
// Normal Stores (global scope, saves until refresh)
//
// stores user and device data
export const userStore = writable([new User()]);
export const userFilterStore = writable([new User()]);
export const deviceStore = writable([new Device()]);
export const deviceFilterStore = writable([new Device()]);
// stores ACL object
export const aclStore = writable(new ACL());
// used to store the value of an alert across all components
export const alertStore = writable('');
// used to determine if the API is functioning
export const apiTestStore = writable('');
// stores search state
export const userSearchStore = writable('');
export const deviceSearchStore = writable('');

View file

@ -1,114 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { userStore } from '$lib/common/stores';
import { getDevices, newDevice } from '$lib/common/apiFunctions.svelte';
import { alertStore } from '$lib/common/stores.js';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { goto } from "$app/navigation";
// whether the new card html element is visible
export let newDeviceCardVisible = false;
export let newDeviceKey = '';
let newDeviceForm: HTMLFormElement;
let selectedUser = '';
let tabs = ['Default Configuration', 'With Preauth Keys', 'With OIDC'];
let activeTab = 0;
function newDeviceAction() {
if (newDeviceForm.reportValidity()) {
newDevice(newDeviceKey, selectedUser)
.then((response) => {
newDeviceCardVisible = false;
newDeviceKey = '';
// refresh devices after editing
getDevices();
// Clear device key in url
if ($page.url.searchParams.get('nodekey')) {
$page.url.searchParams.delete('nodekey');
goto(`?${$page.url.searchParams.toString()}`);
}
})
.catch((error) => {
$alertStore = error;
});
} else {
$alertStore = 'provide a valid key';
}
}
</script>
<!-- html -->
{#if newDeviceCardVisible == true}
<div in:fade|global out:fade|global={{ duration: newDeviceCardVisible ? 0 : 500 }} class="p-2 max-w-screen-lg border border-dashed border-base-content mx-4 rounded-md text-sm text-base-content shadow mb-10">
<div class="tabs">
{#each tabs as tab, index}
<button class="tab tab-bordered h-fit w-1/3" class:tab-active={activeTab == index} on:click={() => (activeTab = index)}>{tab}</button>
{/each}
</div>
<!-- Default Configuration -->
{#if activeTab == 0}
<div in:fade|global class="m-2">
<p>Install Tailscale with the client pointing to your domain (see <a target="_blank" rel="noreferrer" class="link link-primary" href="https://github.com/juanfont/headscale/tree/main/docs">headscale client documentation</a>). Log in using the tray icon, and your browser should give you instructions with a key.</p>
<div class="m-2"><code>headscale -u USER nodes register --key &lt;your device key&gt;</code></div>
<div class="my-2"><p>Copy the key below:</p></div>
<form class="flex flex-wrap" bind:this={newDeviceForm} on:submit|preventDefault={newDeviceAction}>
<div class="flex-none mr-4">
<label class="block text-secondary text-sm font-bold mb-2" for="text">Device Key</label>
<input bind:value={newDeviceKey} minlength="54" class="card-input" type="text" required placeholder="******************" />
</div>
<div class="flex-none">
<label class="block text-secondary text-sm font-bold mb-2" for="select">Select User</label>
<select class="card-select mr-3" required bind:value={selectedUser}>
{#each $userStore as user}
<option>{user.name}</option>
{/each}
</select>
</div>
<div class="flex-none pt-6">
<button
><svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-1 inline rounded-full hover:bg-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<button
on:click={() => {
newDeviceCardVisible = false;
newDeviceKey = '';
}}
type="button"
><svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-1 inline rounded-full hover:bg-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
</div>
</form>
</div>
{/if}
<!-- With Preauth Keys -->
{#if activeTab == 1}
<div in:fade|global class="m-2">
<p>Preauth Keys provide the capability to install tailscale using a pre-registered key (see the <code class="bg-base-200 px-2 rounded">--authkey</code> flag in the <a target="_blank" rel="noreferrer" class="link link-primary" href="https://tailscale.com/kb/1080/cli/">tailscale command line documentation</a>)</p>
<p>Preauth Keys are especially useful for deploying headscale as an always-on VPN (see the <code class="bg-base-200 px-2 rounded">TS_UNATTENDEDMODE</code> install option in the <a target="_blank" rel="noreferrer" class="link link-primary" href="https://tailscale.com/kb/1189/install-windows-msi/">tailscale documentation</a>) or router-level VPN.</p>
<div class="bg-base-200 p-4 m-2 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="pl-2">Preauth Keys can be managed in the <a href="{base}/" class="link link-primary">User Section</a> of the UI</span>
</div>
</div>
{/if}
<!-- With OIDC -->
{#if activeTab == 2}
<div in:fade|global class="m-2">
<p>OIDC provides the ability to register an external authentication provider (such as <a target="_blank" rel="noreferrer" class="link link-primary" href="https://www.keycloak.org/">keycloak</a>) to authenticate devices to headscale.</p>
<br />
<p>Configure Headscale to register with an authentication provider (see <a target="_blank" rel="noreferrer" class="link link-primary" href="https://github.com/juanfont/headscale/blob/main/config-example.yaml">headscale configuration documentation</a>). Once configured, successfully authenticated devices will automatically self-register</p>
</div>
{/if}
</div>
{/if}

View file

@ -1,142 +0,0 @@
<script lang="ts">
import { Device } from '$lib/common/classes';
import { slide } from 'svelte/transition';
import DeviceRoutes from './DeviceCard/DeviceRoutes.svelte';
import DeviceTags from './DeviceCard/DeviceTags.svelte';
import MoveDevice from './DeviceCard/MoveDevice.svelte';
import RemoveDevice from './DeviceCard/RemoveDevice.svelte';
import RenameDevice from './DeviceCard/RenameDevice.svelte';
export let device = new Device();
let cardExpanded = false;
let cardEditing = false;
// returns button colour based on time difference
function timeDifference(date: Date) {
let currentTime = new Date();
let timeDifference = Math.round((currentTime.getTime() - date.getTime()) / 1000);
if (timeDifference < 3600) {
return 'bg-success';
} else if (timeDifference < 86400) {
return 'bg-warning';
}
return 'bg-error';
}
// return button colour based on online status
function onlineBackground(online: boolean) {
return online ? 'bg-success' : 'bg-error';
}
function getBadgeColour(date: Date, online?: boolean) {
if (online !== undefined) {
return onlineBackground(online);
}
return timeDifference(date);
}
// returns time last seen in human readable format
function timeSince(date: Date) {
let currentTime = new Date();
// gets time difference in seconds
let timeDifference = Math.round((currentTime.getTime() - date.getTime()) / 1000);
let timeUnit = '';
if (timeDifference < 60) {
timeUnit = 'seconds';
} else if (timeDifference < 3600) {
timeDifference = Math.floor(timeDifference / 60);
if (timeDifference == 1) {
timeUnit = 'minute';
} else {
timeUnit = 'minutes';
}
} else if (timeDifference < 86400) {
timeDifference = Math.floor(timeDifference / (60 * 60));
if (timeDifference == 1) {
timeUnit = 'hour';
} else {
timeUnit = 'hours';
}
} else {
timeDifference = Math.floor(timeDifference / (60 * 60 * 24));
if (timeDifference == 1) {
timeUnit = 'day';
} else {
timeUnit = 'days';
}
}
return `Last seen ${timeDifference} ${timeUnit} ago`;
}
</script>
<div class="card-primary bg-base-200">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:keypress on:click={() => (cardExpanded = !cardExpanded)} class="flex items-center">
<span class="min-w-64 w-1/2 font-bold">
{#if cardEditing == false}
{#if device.online}
<span class="badge badge-xs tooltip {getBadgeColour(new Date(device.lastSeen), device.online)}" data-tip=online /> {device.id}: {device.givenName}
{:else}
<span class="badge badge-xs tooltip {getBadgeColour(new Date(device.lastSeen), device.online)}" data-tip={timeSince(new Date(device.lastSeen))} /> {device.id}: {device.givenName}
{/if}
{/if}
<RenameDevice bind:cardEditing {device} />
</span>
<div class="grow w-full"><DeviceTags {device} /></div>
<div class="grow min-w-fit">
<RemoveDevice {device} />
<button type="button">
{#if !cardExpanded}
<!-- Icon: chevron down -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
{:else}
<!-- Icon: chevron up -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
</svg>
{/if}
</button>
</div>
</div>
{#if cardExpanded}
<!-- we put a conditional on the outro transition so page changes do not trigger the animation -->
<div in:slide|global out:slide|global={{ duration: cardExpanded ? 0 : 500 }} class="mt-2 pt-2 pl-2">
<div class="overflow-x-auto">
<table class="table table-compact w-full">
<tbody>
<tr>
<th>Device Last Seen</th>
<td>{new Date(device.lastSeen)}</td>
</tr>
<tr>
<th>IP Addresses</th>
<td>
<ul class="list-disc list-inside">
{#each device.ipAddresses as address}
<li>{address}</li>
{/each}
</ul>
</td>
</tr>
<tr>
<th>Assigned User</th>
<MoveDevice {device} />
</tr>
<tr>
<th>Device Name</th>
<td>{device.name}</td>
</tr>
<tr>
<DeviceRoutes {device} />
</tr>
</tbody>
</table>
</div>
</div>
{/if}
</div>

View file

@ -1,68 +0,0 @@
<script lang="ts">
import { getDeviceRoutes, modifyDeviceRoutes } from './DeviceRoutesAPI.svelte';
import { Device, Route } from '$lib/common/classes';
import { onMount } from 'svelte';
import { alertStore } from '$lib/common/stores';
export let device = new Device();
let routesList: Route[] = [];
let routeID = 0;
onMount(async () => {
getDeviceRoutesAction();
});
function getDeviceRoutesAction() {
getDeviceRoutes(device.id)
.then((routes) => {
routesList = routes;
})
.catch((error) => {
$alertStore = error;
});
}
function modifyDeviceRoutesAction() {
modifyDeviceRoutes(device.id, routesList, routeID)
.then((response) => {
getDeviceRoutesAction();
})
.catch((error) => {
$alertStore = error;
});
}
</script>
<th>Device Routes</th>
<td
><ul class="list-disc list-inside">
{#each routesList as route, index}
<li>
{route.prefix}
{#if route.enabled}
<button
on:click={() => {
routesList[index].enabled = false;
routeID = route.id;
modifyDeviceRoutesAction();
}}
type="button"
class="btn btn-xs tooltip capitalize bg-success text-success-content mx-1"
data-tip="press to disable route">active</button
>
{:else}
<button
on:click={() => {
routesList[index].enabled = true;
routeID = route.id
modifyDeviceRoutesAction();
}}
type="button"
class="btn btn-xs tooltip capitalize bg-secondary text-secondary-content mx-1"
data-tip="press to enable route">pending</button
>
{/if}
</li>
{/each}
</ul></td
>

View file

@ -1,84 +0,0 @@
<script context="module" lang="ts">
import type { Route } from '$lib/common/classes';
export async function getDeviceRoutes(deviceID: string): Promise<Route[]> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
// endpoint url for getting users
let endpointURL = `/api/v1/node/${deviceID}/routes`;
//returning variables
let headscaleRouteList: Route[] = [];
let headscaleDeviceResponse: Response = new Response();
await fetch(headscaleURL + endpointURL, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
// return the api data
headscaleDeviceResponse = response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
await headscaleDeviceResponse.json().then((data) => {
headscaleRouteList = data.routes;
});
return headscaleRouteList;
}
export async function modifyDeviceRoutes(deviceID: string, routeList: Route[], routeID: number): Promise<any> {
// variables in local storage
let headscaleURL = localStorage.getItem('headscaleURL') || '';
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
let endpointURL = '';
routeList.forEach((route) => {
if (route.id == routeID) {
endpointURL = `/api/v1/routes/${routeID}/`;
if (route.enabled) {
endpointURL += 'enable';
} else {
endpointURL += 'disable';
}
}
});
//returning variables
let headscaleDeviceResponse: Response = new Response();
await fetch(headscaleURL + endpointURL, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${headscaleAPIKey}`
}
})
.then((response) => {
if (response.ok) {
// return the api data
headscaleDeviceResponse = response;
} else {
return response.text().then((text) => {
throw JSON.parse(text).message;
});
}
})
.catch((error) => {
throw error;
});
}
</script>

View file

@ -1,42 +0,0 @@
<script lang='ts'>
import NewDeviceTag from './DeviceTags/NewDeviceTag.svelte';
import { Device } from '$lib/common/classes';
import { updateTags, getDevices } from '$lib/common/apiFunctions.svelte';
import { alertStore } from '$lib/common/stores.js';
export let device = new Device();
function updateTagsAction(tag: String) {
let tagList = device.forcedTags;
// remove tag we're trying to remove
tagList = tagList.filter(element => element !== tag);
updateTags(device.id, tagList).then((response) => {
// refresh devices after editing
getDevices();
})
.catch((error) => {
$alertStore = error;
});
}
</script>
<div class="flex gap-1">
<span><NewDeviceTag {device}/></span>
{#each device.forcedTags as tag}
<span class="btn btn-xs btn-primary normal-case">{tag.replace("tag:","")}
<!-- Cancel symbol -->
<button on:click|stopPropagation={() => {updateTagsAction(tag)}}
class="ml-1"
><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
</span>
{/each}
{#each device.validTags as tag}
<span class="mb-1 mr-1 btn btn-xs btn-secondary normal-case">{tag.replace("tag:","")}</span>
{/each}
</div>

View file

@ -1,65 +0,0 @@
<script>
import { updateTags, getDevices } from '$lib/common/apiFunctions.svelte';
import { Device } from '$lib/common/classes';
import { fade } from 'svelte/transition';
import { alertStore } from '$lib/common/stores.js';
let editingTag = false;
let newTag = '';
export let device = new Device();
function updateTagsAction() {
let tagList = device.forcedTags;
tagList.push(`tag:${newTag}`);
// remove duplicates
tagList = [...new Set(tagList)];
// force lowercase
tagList = tagList.map(str => str.toLowerCase());
updateTags(device.id, tagList)
.then((response) => {
editingTag = false;
newTag = '';
// refresh devices after editing
getDevices();
})
.catch((error) => {
$alertStore = error;
});
}
</script>
<button
on:click|stopPropagation={() => {
editingTag = true;
}}
class="btn btn-xs border-dotted border-2 btn-primary opacity-60 normal-case"
>
{#if !editingTag}
<span>+ tag</span>
{:else}
<!-- svelte-ignore a11y-autofocus -->
<form on:submit|preventDefault={updateTagsAction}>
<input bind:value={newTag} autofocus required class="bg-primary w-16" />
<button in:fade|global class="ml-1">
<!-- checkmark symbol -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<!-- Delete cancel symbol -->
<button
type="button"
in:fade|global
on:click|stopPropagation={() => {
editingTag = false;
newTag = '';
}}
class="ml-1"
><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
</form>
{/if}
</button>

View file

@ -1,59 +0,0 @@
<script>
import { fade } from 'svelte/transition';
import { Device } from '$lib/common/classes';
import { userStore, alertStore } from '$lib/common/stores';
import { moveDevice, getDevices } from '$lib/common/apiFunctions.svelte';
export let device = new Device();
let deviceMoving = false;
let selectedUser = device.user.name;
function moveDeviceAction() {
moveDevice(device.id, selectedUser)
.then((response) => {
deviceMoving = false;
// refresh devices after editing
getDevices();
})
.catch((error) => {
$alertStore = error;
});
}
</script>
<td>
{#if !deviceMoving}
{device.user.name}
<!-- edit symbol -->
<button
on:click={() => {
deviceMoving = true;
}}
type="button"
class="ml-2"
><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg></button
>
{:else}
<form on:submit|preventDefault={moveDeviceAction}>
<select class="card-select mr-3" required bind:value={selectedUser}>
{#each $userStore as user}
<option>{user.name}</option>
{/each}
</select>
<!-- edit accept symbol -->
<button in:fade|global class=""
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<!-- edit cancel symbol -->
<button type="button" in:fade|global on:click|stopPropagation={() => (deviceMoving = false)}
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
</form>
{/if}
</td>

View file

@ -1,46 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { getDevices, removeDevice } from '$lib/common/apiFunctions.svelte';
import { deviceStore, alertStore } from '$lib/common/stores.js';
import { Device } from '$lib/common/classes';
export let device = new Device();
let cardDeleting = false;
function removeDeviceAction() {
removeDevice(device.id)
.then((response) => {
cardDeleting = false;
// refresh Devices after editing
getDevices();
})
.catch((error) => {
$alertStore = error;
});
}
</script>
{#if !cardDeleting}
<!-- Delete trash symbol -->
<button on:click|stopPropagation={() => (cardDeleting = true)} class="mr-4"
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg></button
>
{:else}
<!-- Delete Warning -->
<span in:fade|global class="font-bold text-red-400">Deleting {device.name}. Confirm </span>
<!-- Delete confirm symbol -->
<button in:fade|global on:click|stopPropagation={() => removeDeviceAction()}
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<span in:fade|global class="font-bold text-red-400">or Cancel </span>
<!-- Delete cancel symbol -->
<button in:fade|global on:click|stopPropagation={() => (cardDeleting = false)} class="mr-4"
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
{/if}

View file

@ -1,58 +0,0 @@
<script lang="ts">
import { fade, slide } from 'svelte/transition';
import { getDevices, renameDevice } from '$lib/common/apiFunctions.svelte';
import { alertStore} from '$lib/common/stores.js';
import { Device } from '$lib/common/classes';
let editUserForm: HTMLFormElement;
let newDeviceName = '';
export let cardEditing = false;
export let device = new Device();
function editingDevice() {
cardEditing = true;
newDeviceName = device.givenName;
}
function renameDeviceAction() {
if (editUserForm.reportValidity()) {
renameDevice(device.id, newDeviceName.toLowerCase())
.then((response) => {
cardEditing = false;
// refresh users after editing
getDevices();
})
.catch((error) => {
$alertStore = error;
});
} else {
$alertStore = 'Use lower case letters, periods, or dashes only';
}
}
</script>
{#if !cardEditing}
<!-- edit symbol -->
<button type="button" on:click|stopPropagation={() => editingDevice()} class="ml-2"
><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg></button
>
{:else}
<form bind:this={editUserForm} on:submit|preventDefault={renameDeviceAction}>
<!-- Input has to be lower case, but we will force lower case on submit -->
<input in:slide|global on:click|stopPropagation bind:value={newDeviceName} class="card-input mb-1 lowercase" required pattern="[a-zA-Z0-9\-\.]+" placeholder="name" />
<!-- edit accept symbol -->
<button in:fade|global on:click|stopPropagation class=""
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<!-- edit cancel symbol -->
<button type="button" in:fade|global on:click|stopPropagation={() => (cardEditing = false)}
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
</form>
{/if}

View file

@ -1,23 +0,0 @@
<script lang="ts">
import { filterDevices } from '$lib/common/searching.svelte';
import { deviceSearchStore } from '$lib/common/stores';
export let overrideSort = false;
// called when search changes
deviceSearchStore.subscribe((value) => {
if (value != '') {
overrideSort = true;
} else {
overrideSort = false;
}
filterDevices();
});
</script>
<!-- search icon -->
<div class="flex">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-3 mr-1 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> <input type="text" placeholder="search" bind:value={$deviceSearchStore} class="input input-bordered input-xs w-full max-w-xs" />
</div>

View file

@ -1,41 +0,0 @@
<script lang="ts">
import { getDevices } from '$lib/common/apiFunctions.svelte';
import { deviceSortDirectionStore, deviceSortStore } from '$lib/common/stores.js';
function sortAction() {
if ($deviceSortDirectionStore == 'ascending') {
$deviceSortDirectionStore = 'descending';
} else {
$deviceSortDirectionStore = 'ascending';
}
getDevices();
}
</script>
<span class="flex">
<button
on:keypress on:click={() => {
sortAction();
}}
class="mx-1"
>
{#if $deviceSortDirectionStore == 'ascending'}
<!-- ascending sort icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
</svg>
{:else}
<!-- descending sort icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4" />
</svg>
{/if}
</button>
<span class="btn-group">
<button class:btn-active="{$deviceSortStore == 'id'}" on:click="{() => {$deviceSortStore = 'id'; getDevices()}}" class="btn btn-xs">ID</button>
<button class:btn-active="{$deviceSortStore == 'givenName'}" on:click="{() => {$deviceSortStore = 'givenName'; getDevices()}}" class="btn btn-xs capitalize">Device Name</button>
<button class:btn-active="{$deviceSortStore == 'lastSeen'}" on:click="{() => {$deviceSortStore = 'lastSeen'; getDevices()}}" class="btn btn-xs capitalize">Last Seen</button>
</span>
</span>

View file

@ -1,13 +0,0 @@
<script lang="ts">
import { showACLPagesStore } from '$lib/common/stores';
import { fade } from 'svelte/transition';
let showDevSettings = false;
</script>
<div class="inline-block"><h1 class="text-2xl bold text-primary mb-4">Developer Flags<input type="checkbox" class="toggle toggle-sm tooltip ml-2 align-middle" data-tip="To enable development features. Only check this if you're a developer or like being confused" bind:checked={showDevSettings} /></h1></div>
{#if showDevSettings}
<div in:fade|global>
<h2 class="text-xl bold text-secondary mb-2 ml-2">ACL Pages <input bind:checked={$showACLPagesStore} type="checkbox" class="toggle toggle-sm ml-2 align-middle" /></h2>
</div>
{/if}

View file

@ -1,91 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { URLStore } from '$lib/common/stores.js';
import { APIKeyStore } from '$lib/common/stores.js';
import { getAPIKeys } from '$lib/common/apiFunctions.svelte';
import { onMount } from 'svelte';
import ApiKeyTimeLeft from './ServerSettings/APIKeyTimeLeft.svelte';
import RolloverApi from './ServerSettings/RolloverAPI.svelte';
// Server Settings
let apiStatus = 'untested';
let apiKeyInputState = 'password';
function TestServerSettings() {
getAPIKeys()
.then(() => {
apiStatus = 'succeeded';
})
.catch(() => {
apiStatus = 'failed';
});
}
function ClearServerSettings() {
$URLStore = '';
$APIKeyStore = '';
apiStatus = 'untested';
}
onMount(() => {
// test api settings on page load
TestServerSettings();
});
</script>
<form>
<h1 class="text-2xl bold text-primary mb-4">Server Settings</h1>
<label class="block text-secondary text-sm font-bold mb-2" for="url"> Headscale URL </label>
<input bind:value={$URLStore} class="form-input" type="url" pattern={String.raw`https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`} placeholder="https://hs.yourdomain.com.au" />
<p class="text-xs text-base-content text-italics mb-8">URL for your headscale server instance</p>
<label class="block text-secondary text-sm font-bold mb-2" for="password">
Headscale API Key
{#if apiStatus == 'succeeded'}
{#key $APIKeyStore}
<ApiKeyTimeLeft />
{/key}
{/if}
</label>
<div class="flex relative">
<input bind:value={$APIKeyStore} {...{ type: apiKeyInputState }} minlength="54" maxlength="54" class="form-input" disabled='{apiStatus == 'succeeded'}' required placeholder="******************" />
<button
type="button"
class="absolute right-40"
on:click={() => {
apiKeyInputState == 'text' ? (apiKeyInputState = 'password') : (apiKeyInputState = 'text');
}}
>
{#if apiKeyInputState == 'password'}
<!-- eye off -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 my-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
><path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /></svg
>
{:else}
<!-- eye on -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 my-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
{/if}
</button>
<RolloverApi {apiStatus} />
</div>
<p class="text-xs text-base-content text-italics mb-8">Generate an API key for your headscale instance and place it here.</p>
{#if apiStatus != 'succeeded'}
<button on:click={() => {TestServerSettings()}} class="btn btn-sm btn-secondary capitalize" type="button">Save API Key</button>
{:else}
<button on:click={() => {apiStatus = 'untested'}} class="btn btn-sm btn-primary capitalize" type="button">Edit API Key</button>
{/if}
<button on:click={() => ClearServerSettings()} class="btn btn-sm btn-primary capitalize" type="button">Clear Server Settings</button>
<button on:click={() => TestServerSettings()} class="btn btn-sm btn-secondary capitalize" type="button">Test Server Settings</button>
{#if apiStatus === 'succeeded'}
<svg in:fly|global={{ x: 10, duration: 600 }} xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline" fill="none" viewBox="0 0 24 24" stroke="green" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{/if}
{#if apiStatus === 'failed'}
<svg in:fly|global={{ x: 10, duration: 600 }} xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline" fill="none" viewBox="0 0 24 24" stroke="red" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
{/if}
</form>

View file

@ -1,46 +0,0 @@
<script lang="ts">
import { getAPIKeys } from '$lib/common/apiFunctions.svelte';
import { APIKey } from '$lib/common/classes';
import { alertStore, APIKeyStore } from '$lib/common/stores';
import { onMount } from 'svelte';
let keyList = [new APIKey()];
let timeLeftWarning = false;
let timeLeftTip = '';
function getAPIKeysAction(): void {
getAPIKeys()
.then((keys) => {
keyList = keys;
// match up the current apikey to the keylist
keyList.forEach(key => {
if($APIKeyStore.includes(key.prefix)) {
timeLeft(new Date(key.expiration));
}
});
})
.catch((error) => {
$alertStore = error;
});
}
// sets time expiry in human readable format
function timeLeft(date: Date): void {
let currentTime = new Date();
// gets time difference in seconds
let timeDifferenceDays = Math.round((date.getTime() - currentTime.getTime()) / 1000 / 60 / 60 / 24);
if(timeDifferenceDays < 30) {
$alertStore = `${timeDifferenceDays} days left before API Key expiry, consider rolling your key`
timeLeftWarning = true;
}
timeLeftTip = `${timeDifferenceDays} days left before expiry`;
}
onMount(() => {
getAPIKeysAction();
});
</script>
<button type="button" class="tooltip" data-tip={timeLeftTip}>
<!-- clock -->
<svg xmlns="http://www.w3.org/2000/svg" class:stroke-error="{timeLeftWarning}" class="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</button>

View file

@ -1,51 +0,0 @@
<script lang="ts">
import { expireAPIKey, getAPIKeys, newAPIKey } from '$lib/common/apiFunctions.svelte';
import { APIKey } from '$lib/common/classes';
import { alertStore, APIKeyStore } from '$lib/common/stores';
let keyList = [new APIKey()];
let currentKey = new APIKey();
export let apiStatus = '';
// get current API keys
// Match to current key
function getAPIKeysAction(): void {
getAPIKeys()
.then((keys) => {
keyList = keys;
// match up the current apikey to the keylist
keyList.forEach((key) => {
if ($APIKeyStore.includes(key.prefix)) {
currentKey = key;
// create the new key
newAPIKeyAction()
.then((data) => {
$APIKeyStore = data;
// expire the old key
expireAPIKey(currentKey.prefix);
})
.catch((error) => {
$alertStore = error;
});
}
});
})
.catch((error) => {
$alertStore = error;
});
}
// create new API key
function newAPIKeyAction() {
let event = new Date();
event.setDate(event.getDate() + 90);
return newAPIKey(event.toISOString());
}
</script>
<button
on:click={() => {
getAPIKeysAction();
}}
class="btn btn-sm btn-secondary capitalize ml-4"
type="button" disabled="{apiStatus != 'succeeded'}">Rollover API Key</button
>

View file

@ -1,11 +0,0 @@
<script lang="ts">
import { themeStore } from '$lib/common/stores.js'
let themes = ['hsui', 'light', 'dark', 'cupcake', 'bumblebee', 'emerald', 'corporate', 'synthwave', 'retro', 'cyberpunk', 'valentine', 'halloween', 'garden', 'forest', 'aqua', 'lofi', 'pastel', 'fantasy', 'wireframe', 'black', 'luxury', 'dracula', 'cmyk', 'autumn', 'business', 'acid', 'lemonade', 'night', 'coffee', 'winter'];
</script>
<h1 class="text-2xl bold text-primary mb-4">Theme Settings</h1>
<select bind:value={$themeStore} class="select select-bordered w-full select-sm max-w-xs">
{#each themes as localTheme}
<option value={localTheme}>{localTheme}</option>
{/each}
</select>

View file

@ -1,55 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { alertStore } from '$lib/common/stores.js';
import { getUsers, newUser } from '$lib/common/apiFunctions.svelte';
// name for user creation
let newUserName = '';
// whether the new card html element is visible
export let newUserCardVisible = false;
// The Form used for validating input
let newUserForm: HTMLFormElement;
function newUserAction(): void {
if (newUserForm.reportValidity()) {
newUser(newUserName)
.then((response) => {
newUserCardVisible = false;
newUserName = '';
// refresh users after editing
getUsers();
})
.catch((error) => {
$alertStore = error;
});
} else {
$alertStore = 'Use lower case letters, periods, or dashes only';
}
}
</script>
<!-- html -->
{#if newUserCardVisible}
<div in:fade|global out:fade|global={{ duration: newUserCardVisible ? 0 : 500 }} class="card-pending">
<form on:submit|preventDefault={newUserAction} class="relative" bind:this={newUserForm}>
<!-- Input has to be lower case, but we will force lower case on submit -->
<input bind:value={newUserName} class="card-input lowercase" required pattern="[a-zA-Z0-9\-\.]+" placeholder="name" />
</form>
<div>
<button on:click={() => newUserAction()}
><svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 inline rounded-full hover:bg-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<button
on:click={() => {
newUserCardVisible = false;
newUserName = '';
}}
><svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 inline rounded-full hover:bg-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
</div>
</div>
{/if}

View file

@ -1,23 +0,0 @@
<script lang="ts">
import { filterUsers } from '$lib/common/searching.svelte';
import { userSearchStore } from '$lib/common/stores';
export let overrideSort = false;
// called when search changes
userSearchStore.subscribe((value) => {
if (value != '') {
overrideSort = true;
} else {
overrideSort = false;
}
filterUsers();
});
</script>
<!-- search icon -->
<div class="flex">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-3 mr-1 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> <input type="text" placeholder="search" bind:value={$userSearchStore} class="input input-bordered input-xs w-full max-w-xs" />
</div>

View file

@ -1,41 +0,0 @@
<script lang="ts">
import { getUsers } from '$lib/common/apiFunctions.svelte';
import { sortDirectionStore, userSortStore } from '$lib/common/stores.js';
function sortAction() {
if ($sortDirectionStore == 'ascending') {
$sortDirectionStore = 'descending';
} else {
$sortDirectionStore = 'ascending';
}
getUsers();
}
</script>
<span class="flex">
<button
on:click={() => {
sortAction();
}}
class="mx-1"
>
{#if $sortDirectionStore == 'ascending'}
<!-- ascending sort icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
</svg>
{:else}
<!-- descending sort icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4" />
</svg>
{/if}
</button>
<span class="btn-group">
<button class:btn-active="{$userSortStore == 'id'}" on:click="{() => {$userSortStore = 'id'; getUsers()}}" class="btn btn-xs">ID</button>
<button class:btn-active="{$userSortStore == 'name'}" on:click="{() => {$userSortStore = 'name'; getUsers()}}" class="btn btn-xs capitalize">User Name</button>
<button class:btn-active="{$userSortStore == 'createdAt'}" on:click="{() => {$userSortStore = 'createdAt'; getUsers()}}" class="btn btn-xs capitalize">Creation Date</button>
</span>
</span>

View file

@ -1,54 +0,0 @@
<script lang="ts">
import { fade, slide } from 'svelte/transition';
import EditUser from '$lib/users/UserCard/RenameUser.svelte';
import RemoveUser from '$lib/users/UserCard/RemoveUser.svelte';
import PreauthKeys from '$lib/users/UserCard/PreAuthKeys.svelte';
import { User } from '$lib/common/classes';
import { userStore } from '$lib/common/stores';
// function for refreshing users from parent
export let user = new User();
let cardExpanded = false;
</script>
<div in:fade|global class="card-primary bg-base-200">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:keypress on:click={() => (cardExpanded = !cardExpanded)} class="flex justify-between">
<div>
<EditUser {user} />
</div>
<div>
<RemoveUser {user} />
<!-- chevron for expanding -->
<button>
{#if !cardExpanded}
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
</svg>
{/if}
</button>
</div>
</div>
{#if cardExpanded}
<!-- we put a conditional on the outro transition so page changes do not trigger the animation -->
<div in:slide|global out:slide|global={{ duration: cardExpanded ? 0 : 500 }} class="mt-2 pt-2 pl-2">
<div class="overflow-x-auto">
<table class="table table-compact w-full">
<tbody>
<tr>
<th>User Creation Date</th>
<td>{new Date(user.createdAt)}</td>
</tr>
{#key $userStore}
<PreauthKeys {user} />
{/key}
</tbody>
</table>
</div>
</div>
{/if}
</div>

View file

@ -1,122 +0,0 @@
<script lang="ts">
import { getPreauthKeys, removePreAuthKey } from '$lib/common/apiFunctions.svelte';
import { PreAuthKey, User } from '$lib/common/classes';
import { alertStore, preAuthHideStore } from '$lib/common/stores';
import NewPreAuthKey from './PreAuthKeys/NewPreAuthKey.svelte';
import { onMount } from 'svelte';
// function for refreshing users from parent
export let user = new User();
export let keyList = [new PreAuthKey];
let newPreAuthKeyShow = false;
function expirePreAuthKeyAction(userName: string, preAuthKey: string) {
removePreAuthKey(userName, preAuthKey)
.then(() => {
getPreauthKeysAction();
})
.catch((error) => {
$alertStore = error;
});
}
function getPreauthKeysAction() {
getPreauthKeys(user.name)
.then((keys) => {
keyList = keys;
})
.catch((error) => {
$alertStore = error;
});
}
onMount(async () => {
getPreauthKeysAction();
})
</script>
<tr>
<th>
<div>Preauth Keys
<button
on:keypress on:click={() => {
newPreAuthKeyShow = !newPreAuthKeyShow;
}}
>
{#if !newPreAuthKeyShow}
<!-- plus icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
{:else}
<!-- minus icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" />
</svg>
{/if}
</button>
</div>
<div class="border rounded p-1 -mx-2 mt-2 w-fit">
<input
type="checkbox"
bind:checked={($preAuthHideStore)}
class="checkbox checkbox-xs text-base-content"
/>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
on:keypress on:click={() => {
$preAuthHideStore = !$preAuthHideStore
}}
class="font-normal ml-2">Hide Expired/Used Keys</span
>
</div>
</th>
<td>
{#if newPreAuthKeyShow}
<NewPreAuthKey bind:newPreAuthKeyShow {user} bind:keyList />
{/if}
<table class="table table-compact w-full">
<tbody>
{#each keyList as key}
<!-- hide if key is expired or used (and not reusable) and checkbox is checked -->
<tr class:hidden={$preAuthHideStore && ((key.used && !key.reusable) || new Date(key.expiration).getTime() < new Date().getTime())}>
<th>{key.id}</th>
<td
><code class="border p-1 rounded">{key.key}</code>
<div class="tooltip" data-tip={new Date(key.expiration).toLocaleString()}>
{#if new Date(key.expiration).getTime() > new Date().getTime()}
<div class="btn btn-xs capitalize bg-success text-success-content mx-1">active</div>
{:else if key.id != ''}
<div class="btn btn-xs capitalize bg-error text-error-content mx-1">expired</div>
{/if}
</div>
{#if !key.used && key.id != ''}
<div class="btn btn-xs capitalize bg-primary text-primary-content mx-1">unused</div>
{:else if key.id != ''}
<div class="btn btn-xs capitalize bg-warning text-warning-content mx-1">used</div>
{/if}
{#if key.reusable && key.id != ''}
<div class="btn btn-xs capitalize bg-secondary text-secondary-content mx-1">reusable</div>
{/if}
{#if key.ephemeral && key.id != ''}
<div class="btn btn-xs capitalize bg-accent text-accent-content mx-1">ephemeral</div>
{/if}
</td>
<td>
<!-- Allow ability to expire if not expired -->
{#if new Date(key.expiration).getTime() > new Date().getTime() && (!key.used || key.reusable)}
<!-- trash symbol -->
<button class="mr-2" on:click={() => {expirePreAuthKeyAction(user.name, key.key)}}
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg></button
>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</td>
</tr>

View file

@ -1,70 +0,0 @@
<script>
import { newPreAuthKey, getPreauthKeys } from '$lib/common/apiFunctions.svelte';
import { PreAuthKey, User } from '$lib/common/classes';
import { fade } from 'svelte/transition';
import { alertStore } from '$lib/common/stores';
let currentTime = new Date();
let minDate = new Date(currentTime.setMinutes(currentTime.getMinutes() + 60 - currentTime.getTimezoneOffset())).toISOString().slice(0, 16);
export let newPreAuthKeyShow = false;
export let user = new User();
export let keyList = [new PreAuthKey];
let expiry = minDate;
let reusable = false;
let ephemeral = false;
function NewPreAuthKeyAction() {
let formattedDate = new Date(expiry).toISOString();
newPreAuthKey(user.name, formattedDate, reusable, ephemeral)
.then(() => {
newPreAuthKeyShow = false;
getPreauthKeysAction();
})
.catch((error) => {
$alertStore = error;
});
}
function getPreauthKeysAction() {
getPreauthKeys(user.name)
.then((keys) => {
keyList = keys;
})
.catch((error) => {
$alertStore = error;
});
}
</script>
<div in:fade|global class="card-pending">
<form on:submit|preventDefault={NewPreAuthKeyAction}>
<table class="table table-compact w-full">
<tbody>
<tr>
<th>Expiry:</th>
<td><input bind:value={expiry} class="border rounded px-2" type="datetime-local" required min={minDate} /><br /></td>
</tr>
<tr>
<th>Reusable:</th>
<td>
<input type="checkbox" bind:checked={reusable} class="checkbox checkbox-sm text-base-content" />
</td>
</tr>
<tr>
<th>Ephemeral:</th>
<td>
<input type="checkbox" bind:checked={ephemeral} class="checkbox checkbox-sm text-base-content" />
</td>
</tr>
</tbody>
</table>
<button class="btn btn-sm m-3 btn-primary capitalize">Create Preauth Key</button>
<button
on:click={() => {
newPreAuthKeyShow = false;
}}
type="button"
class="btn btn-sm m-1 btn-secondary capitalize">Cancel</button
>
</form>
</div>

View file

@ -1,46 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { getUsers, removeUser } from '$lib/common/apiFunctions.svelte';
import { alertStore } from '$lib/common/stores.js';
import { User } from '$lib/common/classes';
export let user = new User();
let cardDeleting = false;
function removeUserAction() {
removeUser(user.name)
.then((response) => {
cardDeleting = false;
// refresh users after editing
getUsers();
})
.catch((error) => {
$alertStore = error;
});
}
</script>
{#if !cardDeleting}
<!-- Delete trash symbol -->
<button on:click|stopPropagation={() => (cardDeleting = true)} class="mr-4"
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg></button
>
{:else}
<!-- Delete Warning -->
<span in:fade|global class="font-bold text-red-400">Deleting {user.name}. Confirm </span>
<!-- Delete confirm symbol -->
<button in:fade|global on:click|stopPropagation={() => removeUserAction()}
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<span in:fade|global class="font-bold text-red-400">or Cancel </span>
<!-- Delete cancel symbol -->
<button in:fade|global on:click|stopPropagation={() => (cardDeleting = false)} class="mr-4"
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
{/if}

View file

@ -1,59 +0,0 @@
<script lang="ts">
import { fade, slide } from 'svelte/transition';
import { getUsers, editUser } from '$lib/common/apiFunctions.svelte';
import { alertStore } from '$lib/common/stores.js';
import { User } from '$lib/common/classes';
let cardEditing = false;
let editUserForm: HTMLFormElement;
let newUserName = '';
export let user = new User();
function editingUser() {
cardEditing = true;
newUserName = user.name;
}
function renameUserAction() {
if (editUserForm.reportValidity()) {
editUser(user.name, newUserName)
.then((response) => {
cardEditing = false;
// refresh users after editing
getUsers();
})
.catch((error) => {
$alertStore = error;
});
} else {
$alertStore = 'Use lower case letters, periods, or dashes only';
}
}
</script>
{#if !cardEditing}
<span class="font-bold">{user.id}: {user.name}</span>
<!-- edit symbol -->
<button type="button" on:click|stopPropagation={() => editingUser()} class="ml-2"
><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg></button
>
{:else}
<form bind:this={editUserForm} on:submit|preventDefault={renameUserAction}>
<!-- Input has to be lower case, but we will force lower case on submit -->
<input in:slide|global on:click|stopPropagation bind:value={newUserName} class="card-input mb-1 lowercase" required pattern="[a-zA-Z0-9\-\.]+" placeholder="name" />
<!-- edit accept symbol -->
<button in:fade|global on:click|stopPropagation class=""
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
<!-- edit cancel symbol -->
<button type="button" in:fade|global on:click|stopPropagation={() => (cardEditing = false)}
><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg></button
>
</form>
{/if}

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,30 +0,0 @@
<script lang="ts">
import '../app.css';
import Nav from '$lib/common/nav.svelte';
import Alert from '$lib/common/Alert.svelte';
import Stores from '$lib/common/Stores.svelte';
import { themeStore } from '$lib/common/stores.js'
// NOTE: the element that is using one of the theme attributes must be in the DOM on mount
</script>
<main data-theme={$themeStore} class="flex flex-col">
<!-- initialize localStorage -->
<Stores></Stores>
<div class="flex">
<!-- sidebar -->
<Nav />
<!-- main window -->
<div class="flex flex-1 min-w-0 flex-col bg-base-100">
<Alert />
<!-- header -->
<!-- <div class="flex bg-gray-100 h-12 p-4">Header</div> -->
<!-- content -->
<div>
<slot />
</div>
</div>
</div>
<!-- <div class="flex">Footer</div> -->
</main>

View file

@ -1,9 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { onMount } from 'svelte';
onMount(async () => {
goto(`${base}/users.html`);
});
</script>

View file

@ -1,81 +0,0 @@
<!-- typescript -->
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { getDevices, getUsers } from '$lib/common/apiFunctions.svelte';
import { apiTestStore, deviceFilterStore, deviceStore } from '$lib/common/stores.js';
import CreateDevice from '$lib/devices/CreateDevice.svelte';
import DeviceCard from '$lib/devices/DeviceCard.svelte';
import SearchDevices from '$lib/devices/SearchDevices.svelte';
import SortDevices from '$lib/devices/SortDevices.svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
let newDeviceKey = '';
let newDeviceCardVisible = false;
//
// Component Variables
//
// let's the page know if it's ready to load
let componentLoaded = false;
// We define the meat of our script in onMount as doing so forces client side rendering.
// Doing so also does not perform any actions until components are initialized
onMount(async () => {
// Handle nodekey
newDeviceKey = $page.url.searchParams.get('nodekey') ?? ''
newDeviceCardVisible = newDeviceKey.length > 0 ? true : false
// update user list
getUsers();
// attempt to pull list of devices
getDevices();
// load the page
componentLoaded = true;
});
</script>
<!-- html -->
{#if componentLoaded}
<div in:fade|global>
<div in:fade|global class="px-4 pt-4">
<h1 class="text-2xl bold text-primary">Device View</h1>
</div>
{#if $apiTestStore === 'succeeded'}
<!-- instantiate device based components -->
<table>
<tr
><td
><!-- device creation visibility button -->
<div class="p-4">
{#if newDeviceCardVisible == false}
<button on:click={() => (newDeviceCardVisible = true)} class="btn btn-primary btn-xs capitalize" type="button">+ New Device</button>
{:else}
<button on:click={() => (newDeviceCardVisible = false)} class="btn btn-secondary btn-xs capitalize" type="button">- Hide New Device</button>
{/if}
</div></td
><td><SortDevices /></td><td><SearchDevices /></td></tr
>
</table>
<CreateDevice bind:newDeviceCardVisible bind:newDeviceKey />
<div class="flex flex-col gap-2">
{#each $deviceStore as device}
{#if $deviceFilterStore.includes(device)}
<DeviceCard {device} />
{/if}
{/each}
</div>
{/if}
{#if $apiTestStore === 'failed'}
<div in:fade|global class="max-w-lg mx-auto p-4 border-4 text-sm text-base-content shadow-lg text-center">
<p>API test did not succeed.<br />Headscale might be down or API settings may need to be set<br />change server settings in the <a href="{base}/settings.html" class="link link-primary">settings</a> page</p>
</div>
{/if}
</div>
{/if}

View file

@ -1,23 +0,0 @@
<script lang="ts">
//
// Imports
//
import { showACLPagesStore } from '$lib/common/stores';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
// Set to true once component is initialized
let componentLoaded = false;
onMount(async () => {
componentLoaded = true;
});
</script>
<body>
{#if showACLPagesStore}
<div hidden={!componentLoaded} in:fade|global class="px-4 py-4 w-4/5 max-w-screen-lg">
<h1 class="text-2xl bold text-primary">Group View</h1>
</div>
{/if}
</body>

View file

@ -1,33 +0,0 @@
<script lang="ts">
//
// Imports
//
import DevSettings from '$lib/settings/DevSettings.svelte';
import ServerSettings from '$lib/settings/ServerSettings.svelte';
import ThemeSettings from '$lib/settings/ThemeSettings.svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
// Set to true once component is initialized
let componentLoaded = false;
onMount(async () => {
// Display component frontend
await new Promise(r => setTimeout(r, 200));
componentLoaded = true;
});
</script>
<!-- html -->
<body>
<div hidden={!componentLoaded} in:fade|global class="px-4 py-4 w-4/5 max-w-screen-lg">
<ServerSettings />
<div class="p-4" />
<ThemeSettings />
<div class="p-4" />
<h1 class="text-2xl bold text-primary mb-4">Version</h1>
<b>insert-version</b>
<div class ="p-4"></div>
<DevSettings></DevSettings>
</div>
</body>

View file

@ -1,70 +0,0 @@
<!-- typescript -->
<script lang="ts">
import { base } from '$app/paths';
import { getUsers } from '$lib/common/apiFunctions.svelte';
import { apiTestStore, userFilterStore, userStore } from '$lib/common/stores';
import CreateUser from '$lib/users/CreateUser.svelte';
import SearchUsers from '$lib/users/SearchUsers.svelte';
import SortUsers from '$lib/users/SortUsers.svelte';
import UserCard from '$lib/users/UserCard.svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
//
// Component Variables
//
// whether the new card html element is visible
let newUserCardVisible = false;
// let's the page know if it's ready to load
let componentLoaded = false;
// We define the meat of our script in onMount as doing so forces client side rendering.
// Doing so also does not perform any actions until components are initialized
onMount(async () => {
// update user list
getUsers();
// load the page
componentLoaded = true;
});
</script>
<!-- html -->
{#if componentLoaded}
<div in:fade|global>
<div class="px-4 pt-4">
<h1 class="text-2xl bold text-primary">User View</h1>
</div>
{#if $apiTestStore === 'succeeded'}
<!-- instantiate user based components -->
<table>
<tr
><td
><!-- device creation visibility button -->
<div class="p-4">
{#if newUserCardVisible == false}
<button on:click={() => (newUserCardVisible = true)} class="btn btn-primary btn-xs capitalize" type="button">+ New User</button>
{:else}
<button on:click={() => (newUserCardVisible = false)} class="btn btn-secondary btn-xs capitalize" type="button">- Hide New User</button>
{/if}
</div></td
><td><SortUsers /></td><td><SearchUsers /></td></tr
>
</table>
<CreateUser bind:newUserCardVisible />
<div class="flex flex-col gap-2">
{#each $userStore as user}
{#if $userFilterStore.includes(user)}
<UserCard {user} />
{/if}
{/each}
</div>
{/if}
{#if $apiTestStore === 'failed'}
<div in:fade|global class="max-w-lg mx-auto p-4 border-4 text-sm text-base-content shadow-lg text-center">
<p>API test did not succeed.<br />Headscale might be down or API settings may need to be set<br />change server settings in the <a href="{base}/settings.html" class="link link-primary">settings</a> page</p>
</div>
{/if}
</div>
{/if}

View file

@ -1,26 +0,0 @@
import adapter from '@sveltejs/adapter-static';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
fallback: 'index.html',
precompress: false
}),
paths: {
base: "/web"
},
csp: {
mode: "hash",
directives: { "script-src": ["self"] },
}
},
preprocess: [
preprocess({
postcss: true
})
]
};
export default config;

View file

@ -1,30 +0,0 @@
const config = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
daisyui: {
themes: [
{
hsui: {
"primary": "#065f46",
"secondary": "#0369a1",
"accent": "#6b21a8",
"neutral": "#78716c",
"base-100": "#FFFFFF",
"info": "#3ABFF8",
"success": "#36D399",
"warning": "#FBBD23",
"error": "#F87272",
},
},
"light", "dark", "cupcake", "bumblebee", "emerald", "corporate", "synthwave", "retro", "cyberpunk", "valentine", "halloween", "garden", "forest", "aqua", "lofi", "pastel", "fantasy", "wireframe", "black", "luxury", "dracula", "cmyk", "autumn", "business", "acid", "lemonade", "night", "coffee", "winter"
],
},
plugins: [require("@tailwindcss/typography"), require("daisyui")]
};
module.exports = config;

View file

@ -1,10 +0,0 @@
// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
};
export default config;

View file

@ -1,6 +1,6 @@
:8080 {
redir / /web
uri strip_prefix /web
uri strip_prefix /web/
file_server {
root ./build
}

View file

@ -5,11 +5,12 @@
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 8080",
"build": "vite build",
"preview": "vite preview",
"preview": "vite preview --host 0.0.0.0 --port 8080",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check ."
"lint": "prettier --check .",
"stage": "/usr/bin/caddy run --adapter caddyfile --config ./caddyfile"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",

View file

@ -4,6 +4,6 @@
import { onMount } from 'svelte';
onMount(async () => {
goto(`${base}/users.html`);
// goto(`${base}/users.html`);
});
</script>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import ServerSettings from '$lib/components/settings/server-settings.svelte';
import ThemeSettings from '$lib/components/settings/theme-settings.svelte';
import { appSettings } from '$lib/state.svelte';
import { appSettings } from '$lib/state.svelte';
appSettings.navbarTitle = 'Settings';
appSettings.sidebarDrawerOpen = false;