Skip to content

Commit

Permalink
Convert auth to TS (#1976)
Browse files Browse the repository at this point in the history
* Convert auth to TS

* Lint

* Update HA-JS-WS to 3.2.0

* Migrate ws collections to TS

* Upgrade to latest HAWS

* Bump HAWS

* Lint

* Add types to WS calls
  • Loading branch information
balloob committed Nov 4, 2018
1 parent bcbf0ba commit 1ca2424
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 69 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -73,7 +73,7 @@
"es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.1",
"fecha": "^2.3.3",
"home-assistant-js-websocket": "^3.1.5",
"home-assistant-js-websocket": "^3.2.4",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.12.0",
Expand Down
Expand Up @@ -6,6 +6,34 @@ import { Auth } from "home-assistant-js-websocket";
const CALLBACK_SET_TOKEN = "externalAuthSetToken";
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";

interface BasePayload {
callback: string;
}

interface RefreshTokenResponse {
access_token: string;
expires_in: number;
}

declare global {
interface Window {
externalApp?: {
getExternalAuth(payload: BasePayload);
revokeExternalAuth(payload: BasePayload);
};
webkit?: {
messageHandlers: {
getExternalAuth: {
postMessage(payload: BasePayload);
};
revokeExternalAuth: {
postMessage(payload: BasePayload);
};
};
};
}
}

if (!window.externalApp && !window.webkit) {
throw new Error(
"External auth requires either externalApp or webkit defined on Window object."
Expand All @@ -14,21 +42,24 @@ if (!window.externalApp && !window.webkit) {

export default class ExternalAuth extends Auth {
constructor(hassUrl) {
super();

this.data = {
super({
hassUrl,
clientId: "",
refresh_token: "",
access_token: "",
expires_in: 0,
// This will trigger connection to do a refresh right away
expires: 0,
};
});
}

async refreshAccessToken() {
const responseProm = new Promise((resolve, reject) => {
window[CALLBACK_SET_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data);
});
public async refreshAccessToken() {
const responseProm = new Promise<RefreshTokenResponse>(
(resolve, reject) => {
window[CALLBACK_SET_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data);
}
);

// Allow promise to set resolve on window object.
await 0;
Expand All @@ -38,23 +69,18 @@ export default class ExternalAuth extends Auth {
if (window.externalApp) {
window.externalApp.getExternalAuth(callbackPayload);
} else {
window.webkit.messageHandlers.getExternalAuth.postMessage(
window.webkit!.messageHandlers.getExternalAuth.postMessage(
callbackPayload
);
}

// Response we expect back:
// {
// "access_token": "qwere",
// "expires_in": 1800
// }
const tokens = await responseProm;

this.data.access_token = tokens.access_token;
this.data.expires = tokens.expires_in * 1000 + Date.now();
}

async revoke() {
public async revoke() {
const responseProm = new Promise((resolve, reject) => {
window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data);
Expand All @@ -68,7 +94,7 @@ export default class ExternalAuth extends Auth {
if (window.externalApp) {
window.externalApp.revokeExternalAuth(callbackPayload);
} else {
window.webkit.messageHandlers.revokeExternalAuth.postMessage(
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
callbackPayload
);
}
Expand Down
@@ -1,5 +1,18 @@
import { AuthData } from "home-assistant-js-websocket";

const storage = window.localStorage || {};

declare global {
interface Window {
__tokenCache: {
// undefined: we haven't loaded yet
// null: none stored
tokens?: AuthData | null;
writeEnabled?: boolean;
};
}
}

// So that core.js and main app hit same shared object.
let tokenCache = window.__tokenCache;
if (!tokenCache) {
Expand All @@ -15,18 +28,22 @@ export function askWrite() {
);
}

export function saveTokens(tokens) {
export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens;
if (tokenCache.writeEnabled) {
try {
storage.hassTokens = JSON.stringify(tokens);
} catch (err) {} // eslint-disable-line
} catch (err) {
// write failed, ignore it. Happens if storage is full or private mode.
}
}
}

export function enableWrite() {
tokenCache.writeEnabled = true;
saveTokens(tokenCache.tokens);
if (tokenCache.tokens) {
saveTokens(tokenCache.tokens);
}
}

export function loadTokens() {
Expand Down
9 changes: 6 additions & 3 deletions src/data/ws-notifications.js → src/data/ws-notifications.ts
@@ -1,4 +1,4 @@
import { createCollection } from "home-assistant-js-websocket";
import { createCollection, Connection } from "home-assistant-js-websocket";

const fetchNotifications = (conn) =>
conn.sendMessagePromise({
Expand All @@ -11,8 +11,11 @@ const subscribeUpdates = (conn, store) =>
"persistent_notifications_updated"
);

export const subscribeNotifications = (conn, onChange) =>
createCollection(
export const subscribeNotifications = (
conn: Connection,
onChange: (notifications: Notification[]) => void
) =>
createCollection<Notification[]>(
"_ntf",
fetchNotifications,
subscribeUpdates,
Expand Down
10 changes: 0 additions & 10 deletions src/data/ws-panels.js

This file was deleted.

14 changes: 14 additions & 0 deletions src/data/ws-panels.ts
@@ -0,0 +1,14 @@
import { createCollection, Connection } from "home-assistant-js-websocket";
import { Panels } from "../types";

export const subscribePanels = (
conn: Connection,
onChange: (panels: Panels) => void
) =>
createCollection<Panels>(
"_pnl",
() => conn.sendMessagePromise({ type: "get_panels" }),
undefined,
conn,
onChange
);
15 changes: 0 additions & 15 deletions src/data/ws-themes.js

This file was deleted.

25 changes: 25 additions & 0 deletions src/data/ws-themes.ts
@@ -0,0 +1,25 @@
import { createCollection, Connection } from "home-assistant-js-websocket";
import { Themes } from "../types";

const fetchThemes = (conn) =>
conn.sendMessagePromise({
type: "frontend/get_themes",
});

const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
(event) => store.setState(event.data, true),
"themes_updated"
);

export const subscribeThemes = (
conn: Connection,
onChange: (themes: Themes) => void
) =>
createCollection<Themes>(
"_thm",
fetchThemes,
subscribeUpdates,
conn,
onChange
);
4 changes: 0 additions & 4 deletions src/data/ws-user.js

This file was deleted.

20 changes: 20 additions & 0 deletions src/data/ws-user.ts
@@ -0,0 +1,20 @@
import {
createCollection,
getUser,
Connection,
} from "home-assistant-js-websocket";
import { User } from "../types";

export const subscribeUser = (
conn: Connection,
onChange: (user: User) => void
) =>
createCollection<User>(
"_usr",
// the getUser command is mistyped in current verrsion of HAWS.
// Fixed in 3.2.5
() => (getUser(conn) as unknown) as Promise<User>,
undefined,
conn,
onChange
);
27 changes: 22 additions & 5 deletions src/entrypoints/core.js → src/entrypoints/core.ts
Expand Up @@ -5,12 +5,21 @@ import {
subscribeEntities,
subscribeServices,
ERR_INVALID_AUTH,
Auth,
Connection,
} from "home-assistant-js-websocket";

import { loadTokens, saveTokens } from "../common/auth/token_storage";
import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user";
import { HomeAssistant } from "../types";

declare global {
interface Window {
hassConnection: Promise<{ auth: Auth; conn: Connection }>;
}
}

const hassUrl = `${location.protocol}//${location.host}`;
const isExternal = location.search.includes("external_auth=1");
Expand All @@ -33,7 +42,7 @@ const connProm = async (auth) => {

// Clear url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
history.replaceState(null, null, location.pathname);
history.replaceState(null, "", location.pathname);
}

return { auth, conn };
Expand All @@ -43,7 +52,9 @@ const connProm = async (auth) => {
}
// We can get invalid auth if auth tokens were stored that are no longer valid
// Clear stored tokens.
if (!isExternal) saveTokens(null);
if (!isExternal) {
saveTokens(null);
}
auth = await authProm();
const conn = await createConnection({ auth });
return { auth, conn };
Expand All @@ -54,7 +65,9 @@ window.hassConnection = authProm().then(connProm);

// Start fetching some of the data that we will need.
window.hassConnection.then(({ conn }) => {
const noop = () => {};
const noop = () => {
// do nothing
};
subscribeEntities(conn, noop);
subscribeConfig(conn, noop);
subscribeServices(conn, noop);
Expand All @@ -64,8 +77,12 @@ window.hassConnection.then(({ conn }) => {
});

window.addEventListener("error", (e) => {
const homeAssistant = document.querySelector("home-assistant");
if (homeAssistant && homeAssistant.hass && homeAssistant.hass.callService) {
const homeAssistant = document.querySelector("home-assistant") as any;
if (
homeAssistant &&
homeAssistant.hass &&
(homeAssistant.hass as HomeAssistant).callService
) {
homeAssistant.hass.callService("system_log", "write", {
logger: `frontend.${
__DEV__ ? "js_dev" : "js"
Expand Down

0 comments on commit 1ca2424

Please sign in to comment.