diff --git a/src/lib/components/common/classes.svelte.ts b/src/lib/components/common/classes.svelte.ts index 7a80d8d..f57467e 100644 --- a/src/lib/components/common/classes.svelte.ts +++ b/src/lib/components/common/classes.svelte.ts @@ -17,7 +17,9 @@ export class AppSettingsObject { sidebarDrawerOpen = false // for determining if the sidebar is open when on a small screen toastAlerts = new SvelteMap(); // 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) { Object.assign(this, init); } @@ -32,4 +34,17 @@ export class toastAlert { public constructor(init?: Partial) { 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) { + Object.assign(this, init); + } } \ No newline at end of file diff --git a/src/lib/components/settings/server-settings-functions.svelte.ts b/src/lib/components/settings/server-settings-functions.svelte.ts index 7e46b1f..481de09 100644 --- a/src/lib/components/settings/server-settings-functions.svelte.ts +++ b/src/lib/components/settings/server-settings-functions.svelte.ts @@ -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}`) diff --git a/src/lib/components/settings/server-settings.svelte b/src/lib/components/settings/server-settings.svelte index 1c43a6f..fe3e099 100644 --- a/src/lib/components/settings/server-settings.svelte +++ b/src/lib/components/settings/server-settings.svelte @@ -1,25 +1,42 @@

Server Settings

-
+ - +
+ + {#if appSettings.apiKeyExpiration != undefined} + + {/if} +
-