Skip to content

Commit

Permalink
Add long-lived access token support
Browse files Browse the repository at this point in the history
  • Loading branch information
matt8707 committed Apr 18, 2024
1 parent 0548470 commit d8ec18b
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 21 deletions.
99 changes: 99 additions & 0 deletions src/lib/Components/TokenModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts">
import { configuration, lang, motion } from '$lib/Stores';
import Modal from '$lib/Modal/Index.svelte';
import { base } from '$app/paths';
import { closeModal } from 'svelte-modals';
export let isOpen: boolean;
let token = '';
async function handleClick() {
if (!token) return;
$configuration.token = token;
try {
const response = await fetch(`${base}/_api/save_config`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify($configuration)
});
if (response.ok) {
// ok
closeModal();
} else {
console.log('Failed to save configuration.', response);
}
} catch (error) {
console.error('Failed to save configuration.', error);
}
}
</script>

{#if isOpen}
<Modal>
<h1 slot="title">{$lang('login')}</h1>

<div data-exclude-drag-modal>
<p>
A <b>long-lived access token</b> is required for authentication when using both Ingress and the
Home Assistant Companion app simultaneously.
</p>
<p>
Open your user profile, <a
href="{$configuration?.hassUrl}/profile/security"
target="_blank"
>
{$configuration?.hassUrl}/profile/security
</a>
to create and copy a new <b>long-lived access token</b>.
</p>
</div>

<form on:submit|preventDefault={handleClick}>
<h2>{$lang('token')}</h2>

<input class="input" type="password" bind:value={token} />

<button
style:transition="opacity {$motion}ms ease"
class="done action"
type="submit"
disabled={token === ''}
>
{$lang('save')}
</button>
</form>
</Modal>
{/if}

<style>
div {
display: block;
margin-top: 1rem;
border-radius: 0.6rem;
padding: 0.1rem 1.2rem 0.3rem 1.2rem;
background-color: rgba(255, 255, 255, 0.1);
user-select: text;
}
a {
color: #00dbff;
}
button {
opacity: 1;
margin-top: 2rem;
background-color: rgb(255, 255, 255, 0.1) !important;
font-weight: 400;
}
button:disabled {
opacity: 0.4;
pointer-events: none;
}
</style>
11 changes: 10 additions & 1 deletion src/lib/Settings/Index.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script lang="ts">
import { base } from '$app/paths';
import { editMode, lang, motion, ripple, selectedLanguage } from '$lib/Stores';
import { configuration, editMode, lang, motion, ripple, selectedLanguage } from '$lib/Stores';
import { fade } from 'svelte/transition';
import { modals, closeModal } from 'svelte-modals';
import Modal from '$lib/Modal/Index.svelte';
import Language from '$lib/Settings/Language.svelte';
import Addons from '$lib/Settings/Addons.svelte';
import Motion from '$lib/Settings/Motion.svelte';
import Token from '$lib/Settings/Token.svelte';
import CustomJs from '$lib/Settings/CustomJs.svelte';
import Logout from '$lib/Settings/Logout.svelte';
import Ripple from 'svelte-ripple';
Expand Down Expand Up @@ -47,13 +48,19 @@
...(form.maptiler && { maptiler: { apikey: form.maptiler } })
};
const token = form.token || undefined;
const custom_js = form.custom_js ? Boolean(form.custom_js === 'true') : undefined;
const formMotion = form.motion ? Boolean(form.motion === 'true') : undefined;
const json: any = {
locale: $selectedLanguage
};
if (token) {
json.token = token;
$configuration.token = token as string;
}
if (Object.keys(addons).length > 0) json.addons = addons;
if (custom_js) json.custom_js = custom_js;
Expand Down Expand Up @@ -108,6 +115,8 @@
<form id="settings" name="settings" bind:this={formElement}>
<Language {languages} />

<Token />

<Addons {data} />

<CustomJs />
Expand Down
45 changes: 45 additions & 0 deletions src/lib/Settings/Token.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import { configuration, lang } from '$lib/Stores';
let token = $configuration?.token;
function handleFocus(event: FocusEvent) {
const target = event.target as HTMLInputElement;
target.type = event.type === 'focus' ? 'text' : 'password';
}
const href = 'https://www.home-assistant.io/docs/authentication/#your-account-profile';
</script>

<h2>{$lang('token')}</h2>

<p class="overflow">
{$lang('docs')} -
<a {href} target="blank">{href}</a>
</p>

<input
class="input"
type="password"
name="token"
placeholder={$lang('token')}
bind:value={token}
on:focus={handleFocus}
on:blur={handleFocus}
/>

<style>
a {
color: #fa8f92;
}
p {
margin-block-end: 0.6rem;
font-size: 0.9rem;
opacity: 0.75;
}
p:hover {
cursor: default;
}
</style>
41 changes: 27 additions & 14 deletions src/lib/Socket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
getAuth,
createLongLivedTokenAuth,
createConnection,
subscribeConfig,
subscribeEntities,
Expand All @@ -9,12 +10,12 @@ import {
ERR_HASS_HOST_REQUIRED,
ERR_INVALID_HTTPS_TO_HTTP
} from 'home-assistant-js-websocket';
import type { SaveTokensFunc } from 'home-assistant-js-websocket';
import type { Auth, AuthData } from 'home-assistant-js-websocket';
import { states, connection, config, connected, event, persistentNotifications } from '$lib/Stores';
import { closeModal } from 'svelte-modals';
import type { PersistentNotification } from '$lib/Types';
import { openModal, closeModal } from 'svelte-modals';
import type { Configuration, PersistentNotification } from '$lib/Types';

export const options = {
const options = {
hassUrl: undefined as string | undefined,
async loadTokens() {
try {
Expand All @@ -23,27 +24,39 @@ export const options = {
return undefined;
}
},
saveTokens(tokens: SaveTokensFunc) {
saveTokens(tokens: AuthData | null) {
localStorage.hassTokens = JSON.stringify(tokens);
},
clearTokens() {
localStorage.hassTokens = null;
}
};

export async function authentication(options: { hassUrl?: string }) {
let auth;
export async function authentication(configuration: Configuration) {
if (!configuration?.hassUrl) {
console.error('hassUrl is undefined...');
return;
}

let auth: Auth | undefined;

try {
auth = await getAuth(options);
if (auth.expired) {
auth.refreshAccessToken();
// long lived access token
if (configuration?.token) {
auth = createLongLivedTokenAuth(configuration?.hassUrl, configuration?.token);

// companion app and ingress causes issues with auth redirect
// open special modal to enter long lived access token
} else if (navigator.userAgent.includes('Home Assistant')) {
openModal(() => import('$lib/Components/TokenModal.svelte'));
return;

// default auth flow
} else {
auth = await getAuth({ ...options, hassUrl: configuration?.hassUrl });
if (auth.expired) auth.refreshAccessToken();
}
} catch (_error) {
handleError(_error);
}

try {
// connection
const conn = await createConnection({ auth });
connection.set(conn);
Expand Down
1 change: 1 addition & 0 deletions src/lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Configuration {
custom_js?: boolean;
motion?: boolean;
addons?: Addons;
token?: string;
}

export interface Addons {
Expand Down
21 changes: 15 additions & 6 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
clickOriginatedFromMenu,
connection
} from '$lib/Stores';
import { authentication, options } from '$lib/Socket';
import { authentication } from '$lib/Socket';
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import { modals } from 'svelte-modals';
Expand Down Expand Up @@ -68,12 +68,8 @@
console.debug('authenticating...');
if ($configuration?.hassUrl) {
options.hassUrl = $configuration?.hassUrl;
}
try {
await authentication(options);
await authentication($configuration);
console.debug('authenticated.');
clearInterval(retryInterval);
} catch (err) {
Expand All @@ -83,6 +79,19 @@
}
}
/**
* Reconnect if long-lived access token changes
*/
$: if ($configuration?.token) updateConnection();
function updateConnection() {
if (isConnecting || !browser) return;
clearInterval(retryInterval);
connect();
retryInterval = setInterval(connect, 3000);
}
onDestroy(() => clearInterval(retryInterval));
onMount(async () => {
Expand Down

0 comments on commit d8ec18b

Please sign in to comment.