mirror of
https://github.com/gurucomputing/headscale-ui.git
synced 2026-01-23 02:34:43 +00:00
rotate api keys on demand
This commit is contained in:
parent
58cb788f22
commit
0636ea9419
4 changed files with 144 additions and 15 deletions
|
|
@ -17,7 +17,9 @@ export class AppSettingsObject {
|
|||
sidebarDrawerOpen = false // for determining if the sidebar is open when on a small screen
|
||||
toastAlerts = new SvelteMap<string, toastAlert>(); // for adding or removing alerts
|
||||
apiTested = true // used to hide the app if the api tests are failing
|
||||
|
||||
apiKeyList: APIKey[] = [] //list of apikeys retrieved from headscale API
|
||||
apiKeyExpiration?: number = undefined // number of days left until the key in use expires
|
||||
|
||||
public constructor(init?: Partial<AppSettingsObject>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
|
@ -32,4 +34,17 @@ export class toastAlert {
|
|||
public constructor(init?: Partial<toastAlert>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
// retrieved as an array from headscale
|
||||
export class APIKey {
|
||||
id = '' // unique identifier for headscale
|
||||
prefix = '' // beginning of key to match full string
|
||||
expiration = '' // when key expires, formatting as datetime
|
||||
createdAt = '' // date of creation
|
||||
lastSeen = '' // date last seen, seems to be always null?
|
||||
|
||||
public constructor(init?: Partial<APIKey>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { newToastAlert } from "../layout/toast.svelte.ts";
|
||||
import { appSettings, persistentAppSettings } from "../common/state.svelte";
|
||||
|
||||
export async function testAPIConnectivity() {
|
||||
export async function getAPIKeys() {
|
||||
try {
|
||||
const response = await fetch(`${persistentAppSettings.headscaleURL}/api/v1/apikey`, {
|
||||
method: 'GET',
|
||||
|
|
@ -14,16 +14,113 @@ export async function testAPIConnectivity() {
|
|||
if (!response.ok) {
|
||||
newToastAlert(`API test failed (check your server settings): ${response.status}`);
|
||||
appSettings.apiTested = false;
|
||||
} else {
|
||||
appSettings.apiKeyList = (await response.json()).apiKeys;
|
||||
appSettings.apiTested = true;
|
||||
|
||||
// determine the remaining time for the key we are currently using
|
||||
appSettings.apiKeyList.forEach(key => {
|
||||
if (persistentAppSettings.headscaleAPIKey.startsWith(key.prefix)) {
|
||||
getKeyRemainingTime(new Date(key.expiration));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
appSettings.apiTested = true;
|
||||
|
||||
if (persistentAppSettings.debugLogging) {
|
||||
newToastAlert(`API Response: ${JSON.stringify(data)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
let message
|
||||
let message: string;
|
||||
if (error instanceof Error) { message = error.message }
|
||||
else { message = String(error) }
|
||||
newToastAlert(`API test failed (check your server settings): ${message}`)
|
||||
appSettings.apiTested = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getKeyRemainingTime(expiration: Date) {
|
||||
let currentTime = new Date();
|
||||
// gets time difference in seconds
|
||||
appSettings.apiKeyExpiration = Math.round((expiration.getTime() - currentTime.getTime()) / 1000 / 60 / 60 / 24);
|
||||
if (appSettings.apiKeyExpiration < 30) {
|
||||
newToastAlert(`${appSettings.apiKeyExpiration} days left before API Key expiry, consider rolling your key`);
|
||||
}
|
||||
}
|
||||
|
||||
export function rotateAPIKey() {
|
||||
appSettings.apiKeyList.forEach(key => {
|
||||
// select the current key being used
|
||||
if (persistentAppSettings.headscaleAPIKey.startsWith(key.prefix)) {
|
||||
let currentKey = key;
|
||||
let newExpiration = new Date();
|
||||
newExpiration.setDate(newExpiration.getDate() + 90);
|
||||
|
||||
// create a new API key with the new new expiration, set it as the current API key,
|
||||
// and then expire the previous API key
|
||||
|
||||
createNewAPIKey(newExpiration).then((apiKey) => {
|
||||
if (apiKey == undefined) {
|
||||
throw new Error("expecting API key string, string was undefined");
|
||||
}
|
||||
persistentAppSettings.headscaleAPIKey = apiKey;
|
||||
expireAPIKey(currentKey.prefix).then(() => {
|
||||
getAPIKeys().then(() => {
|
||||
// console.log(appSettings.apiKeyList);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function createNewAPIKey(expireDate: Date) {
|
||||
try {
|
||||
const response = await fetch(`${persistentAppSettings.headscaleURL}/api/v1/apikey`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${persistentAppSettings.headscaleAPIKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"expiration": expireDate.toISOString()
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
newToastAlert(`Creating new API Key Failed (check your server settings): ${response.status}`);
|
||||
appSettings.apiTested = false;
|
||||
} else {
|
||||
let apiKey = '';
|
||||
apiKey = (await response.json()).apiKey;
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
let message: string;
|
||||
if (error instanceof Error) { message = error.message }
|
||||
else { message = String(error) }
|
||||
newToastAlert(`API Call Failed (check your server settings): ${message}`)
|
||||
appSettings.apiTested = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function expireAPIKey(apiPrefix: string) {
|
||||
try {
|
||||
const response = await fetch(`${persistentAppSettings.headscaleURL}/api/v1/apikey/expire`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${persistentAppSettings.headscaleAPIKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"prefix": apiPrefix
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
newToastAlert(`API test failed (check your server settings): ${response.status}`);
|
||||
appSettings.apiTested = false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
let message: string;
|
||||
if (error instanceof Error) { message = error.message }
|
||||
else { message = String(error) }
|
||||
newToastAlert(`API test failed (check your server settings): ${message}`)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
<script lang="ts">
|
||||
import { persistentAppSettings } from '$lib/components/common/state.svelte';
|
||||
import { testAPIConnectivity } from './server-settings-functions.svelte.ts';
|
||||
import { getAPIKeys, rotateAPIKey } from './server-settings-functions.svelte.ts';
|
||||
import { appSettings } from '$lib/components/common/state.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
let apiSecretHidden = $state(true); // for hiding or showing the API key
|
||||
let rotateButtonDisabled = $state(false);
|
||||
|
||||
function rotateAPIKeyClick() {
|
||||
rotateButtonDisabled = true;
|
||||
rotateAPIKey();
|
||||
rotateButtonDisabled = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-control">
|
||||
<h1 class="bold mb-4 text-xl text-primary">Server Settings</h1>
|
||||
|
||||
<form id="server-settings" onsubmit={testAPIConnectivity}>
|
||||
<form id="server-settings" onsubmit={getAPIKeys}>
|
||||
<label class="mb-2 block font-bold text-secondary" for="headscaleURL"> Headscale URL </label>
|
||||
<input id="headscaleURL" bind:value={persistentAppSettings.headscaleURL} class="input input-sm input-bordered w-full" type="url" placeholder="https://hs.yourdomain.com.au" />
|
||||
<label for="headscaleURL" class="label">
|
||||
<span class="label-text-alt">URL for your headscale server instance (does not need populating if it's on the same subdomain)</span>
|
||||
</label>
|
||||
<label class="mb-2 block font-bold text-secondary" for="headscaleKey"> Headscale API Key </label>
|
||||
<div class="relative flex">
|
||||
<label class="mb-2 block font-bold text-secondary" for="headscaleKey"> Headscale API Key </label>
|
||||
{#if appSettings.apiKeyExpiration != undefined}
|
||||
<button type="button" disabled={rotateButtonDisabled} onclick={rotateAPIKeyClick} class="tooltip" data-tip="{appSettings.apiKeyExpiration} days left. Click to rotate key" aria-label="check time left"
|
||||
><svg data-slot="icon" fill="none" class="-my-3 ml-2 h-5 w-5 stroke-success" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"></path>
|
||||
</svg></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="relative flex">
|
||||
<input id="headscaleKey" bind:value={persistentAppSettings.headscaleAPIKey} class="input input-sm input-bordered w-full" minlength="40" maxlength="40" type={apiSecretHidden ? 'password' : 'text'} required placeholder="******************" />
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2"
|
||||
onclick={() => {
|
||||
apiSecretHidden = !apiSecretHidden;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import '../app.css';
|
||||
import { testAPIConnectivity } from '$lib/components/settings/server-settings-functions.svelte';
|
||||
import { getAPIKeys } from '$lib/components/settings/server-settings-functions.svelte';
|
||||
let { children } = $props();
|
||||
|
||||
onMount(async () => {
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
}
|
||||
|
||||
// perform an initial API test
|
||||
testAPIConnectivity();
|
||||
getAPIKeys();
|
||||
|
||||
// delay load until page is hydrated
|
||||
appSettings.appLoaded = true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue