mirror of
https://github.com/gurucomputing/headscale-ui.git
synced 2026-01-23 02:34:43 +00:00
removed archive from current source, fixed stage build
This commit is contained in:
parent
051368f010
commit
d29374e07b
56 changed files with 6 additions and 6330 deletions
|
|
@ -1 +0,0 @@
|
|||
engine-strict=true
|
||||
|
|
@ -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
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 400
|
||||
}
|
||||
|
|
@ -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
3499
archive/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
10
archive/src/app.d.ts
vendored
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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('');
|
||||
|
|
@ -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 <your device key></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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
// vite.config.js
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit()],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
:8080 {
|
||||
redir / /web
|
||||
uri strip_prefix /web
|
||||
uri strip_prefix /web/
|
||||
file_server {
|
||||
root ./build
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@
|
|||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(async () => {
|
||||
goto(`${base}/users.html`);
|
||||
// goto(`${base}/users.html`);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue