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:
routerino 2022-08-13 19:47:54 +10:00 committed by GitHub
commit e50bcb77f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 287 additions and 35 deletions

View file

@ -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') || '';

View file

@ -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 = '';

View file

@ -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'}

View 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>

View 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
>

View file

@ -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>