mirror of
https://github.com/gurucomputing/headscale-ui.git
synced 2026-07-27 17:54:07 +00:00
Merge pull request #39 from gurucomputing/10-automatic-warning-for-api-key-expiry
10 automatic warning for api key expiry
This commit is contained in:
commit
e50bcb77f6
6 changed files with 287 additions and 35 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script context="module" lang="ts">
|
||||
import { Device, PreAuthKey, Route, User } from '$lib/common/classes';
|
||||
import { APIKey, Device, PreAuthKey, Route, User } from '$lib/common/classes';
|
||||
import { deviceStore, userStore, apiTestStore } from '$lib/common/stores.js';
|
||||
import { filterDevices, filterUsers } from './searching.svelte';
|
||||
|
||||
|
|
@ -83,6 +83,77 @@
|
|||
});
|
||||
}
|
||||
|
||||
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') || '';
|
||||
|
|
@ -273,7 +344,14 @@
|
|||
let headscaleAPIKey = localStorage.getItem('headscaleAPIKey') || '';
|
||||
|
||||
// endpoint url for getting users
|
||||
let endpointURL = '/api/v1/machine/' + deviceID + '/routes?' + routes.map(encodeURIComponent).map(route=>`routes=${route}`).join('&');
|
||||
let endpointURL =
|
||||
'/api/v1/machine/' +
|
||||
deviceID +
|
||||
'/routes?' +
|
||||
routes
|
||||
.map(encodeURIComponent)
|
||||
.map((route) => `routes=${route}`)
|
||||
.join('&');
|
||||
|
||||
//returning variables
|
||||
let headscaleDeviceResponse: Response = new Response();
|
||||
|
|
@ -300,6 +378,42 @@
|
|||
});
|
||||
}
|
||||
|
||||
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') || '';
|
||||
|
|
|
|||
|
|
@ -23,6 +23,18 @@ export class Route {
|
|||
}
|
||||
}
|
||||
|
||||
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 namespace: string = '';
|
||||
public id: string = '';
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@
|
|||
import { fly } from 'svelte/transition';
|
||||
import { URLStore } from '$lib/common/stores.js';
|
||||
import { APIKeyStore } from '$lib/common/stores.js';
|
||||
import { getUsers } from '$lib/common/apiFunctions.svelte';
|
||||
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 headscaleURL = $URLStore;
|
||||
let headscaleAPIKey = $APIKeyStore;
|
||||
let serverSettingsForm: HTMLFormElement;
|
||||
let apiStatus = 'untested';
|
||||
let apiKeyInputState = 'password';
|
||||
|
||||
function TestServerSettings() {
|
||||
getUsers()
|
||||
getAPIKeys()
|
||||
.then(() => {
|
||||
apiStatus = 'succeeded';
|
||||
})
|
||||
|
|
@ -20,33 +21,61 @@
|
|||
});
|
||||
}
|
||||
|
||||
function SaveServerSettings(): void {
|
||||
if (serverSettingsForm.reportValidity()) {
|
||||
$URLStore = headscaleURL;
|
||||
$APIKeyStore = headscaleAPIKey;
|
||||
}
|
||||
function ClearServerSettings() {
|
||||
$URLStore = '';
|
||||
$APIKeyStore = '';
|
||||
apiStatus = 'untested';
|
||||
}
|
||||
|
||||
function ClearServerSettings() {
|
||||
headscaleURL = '';
|
||||
headscaleAPIKey = '';
|
||||
$URLStore = headscaleURL;
|
||||
$APIKeyStore = headscaleAPIKey;
|
||||
}
|
||||
onMount(() => {
|
||||
// test api settings on page load
|
||||
TestServerSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<form bind:this={serverSettingsForm}>
|
||||
<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={headscaleURL} 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" />
|
||||
<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 </label>
|
||||
<input bind:value={headscaleAPIKey} minlength="54" maxlength="54" class="form-input" type="password" required placeholder="******************" />
|
||||
<p class="text-xs text-base-content text-italics mb-8">Generate an API key for your headscale instance and place it here.</p>
|
||||
<!-- disable the SaveServerSettings button if nothing has changed from stored values, or the dependent inputs do not validate -->
|
||||
<div class="tooltip z-10" data-tip="Note: API Key and URL currently save to localStorage (IE: Your Browser) Make sure you are using a trusted computer">
|
||||
<button disabled={headscaleAPIKey === $APIKeyStore && headscaleURL === $URLStore} on:click={() => SaveServerSettings()} class="btn btn-sm btn-accent capitalize" type="button">Save Server Settings</button>
|
||||
<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'}
|
||||
|
|
|
|||
46
src/lib/settings/ServerSettings/APIKeyTimeLeft.svelte
Normal file
46
src/lib/settings/ServerSettings/APIKeyTimeLeft.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<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>
|
||||
51
src/lib/settings/ServerSettings/RolloverAPI.svelte
Normal file
51
src/lib/settings/ServerSettings/RolloverAPI.svelte
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<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
|
||||
>
|
||||
|
|
@ -12,19 +12,19 @@
|
|||
|
||||
onMount(async () => {
|
||||
// Display component frontend
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
componentLoaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- html -->
|
||||
<body>
|
||||
{#if componentLoaded}
|
||||
<div in:fade class="px-4 py-4 w-4/5">
|
||||
<ServerSettings />
|
||||
<div class="p-4"></div>
|
||||
<ThemeSettings />
|
||||
<div class="p-4"></div>
|
||||
<h1 class="text-2xl bold text-primary mb-4">Version</h1><b>insert-version</b>
|
||||
</div>
|
||||
{/if}
|
||||
<div hidden={!componentLoaded} in:fade 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>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue