Convert auth to TS (#1976)

* 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
This commit is contained in:
Paulus Schoutsen 2018-11-04 10:01:33 +01:00 committed by GitHub
parent bcbf0ba75a
commit 1ca242405b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 182 additions and 69 deletions

View File

@ -73,7 +73,7 @@
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.1", "eslint-import-resolver-webpack": "^0.10.1",
"fecha": "^2.3.3", "fecha": "^2.3.3",
"home-assistant-js-websocket": "^3.1.5", "home-assistant-js-websocket": "^3.2.4",
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"js-yaml": "^3.12.0", "js-yaml": "^3.12.0",

View File

@ -6,6 +6,34 @@ import { Auth } from "home-assistant-js-websocket";
const CALLBACK_SET_TOKEN = "externalAuthSetToken"; const CALLBACK_SET_TOKEN = "externalAuthSetToken";
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken"; 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) { if (!window.externalApp && !window.webkit) {
throw new Error( throw new Error(
"External auth requires either externalApp or webkit defined on Window object." "External auth requires either externalApp or webkit defined on Window object."
@ -14,21 +42,24 @@ if (!window.externalApp && !window.webkit) {
export default class ExternalAuth extends Auth { export default class ExternalAuth extends Auth {
constructor(hassUrl) { constructor(hassUrl) {
super(); super({
this.data = {
hassUrl, hassUrl,
clientId: "",
refresh_token: "",
access_token: "", access_token: "",
expires_in: 0,
// This will trigger connection to do a refresh right away // This will trigger connection to do a refresh right away
expires: 0, expires: 0,
}; });
} }
async refreshAccessToken() { public async refreshAccessToken() {
const responseProm = new Promise((resolve, reject) => { const responseProm = new Promise<RefreshTokenResponse>(
(resolve, reject) => {
window[CALLBACK_SET_TOKEN] = (success, data) => window[CALLBACK_SET_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data); success ? resolve(data) : reject(data);
}); }
);
// Allow promise to set resolve on window object. // Allow promise to set resolve on window object.
await 0; await 0;
@ -38,23 +69,18 @@ export default class ExternalAuth extends Auth {
if (window.externalApp) { if (window.externalApp) {
window.externalApp.getExternalAuth(callbackPayload); window.externalApp.getExternalAuth(callbackPayload);
} else { } else {
window.webkit.messageHandlers.getExternalAuth.postMessage( window.webkit!.messageHandlers.getExternalAuth.postMessage(
callbackPayload callbackPayload
); );
} }
// Response we expect back:
// {
// "access_token": "qwere",
// "expires_in": 1800
// }
const tokens = await responseProm; const tokens = await responseProm;
this.data.access_token = tokens.access_token; this.data.access_token = tokens.access_token;
this.data.expires = tokens.expires_in * 1000 + Date.now(); this.data.expires = tokens.expires_in * 1000 + Date.now();
} }
async revoke() { public async revoke() {
const responseProm = new Promise((resolve, reject) => { const responseProm = new Promise((resolve, reject) => {
window[CALLBACK_REVOKE_TOKEN] = (success, data) => window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data); success ? resolve(data) : reject(data);
@ -68,7 +94,7 @@ export default class ExternalAuth extends Auth {
if (window.externalApp) { if (window.externalApp) {
window.externalApp.revokeExternalAuth(callbackPayload); window.externalApp.revokeExternalAuth(callbackPayload);
} else { } else {
window.webkit.messageHandlers.revokeExternalAuth.postMessage( window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
callbackPayload callbackPayload
); );
} }

View File

@ -1,5 +1,18 @@
import { AuthData } from "home-assistant-js-websocket";
const storage = window.localStorage || {}; 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. // So that core.js and main app hit same shared object.
let tokenCache = window.__tokenCache; let tokenCache = window.__tokenCache;
if (!tokenCache) { if (!tokenCache) {
@ -15,19 +28,23 @@ export function askWrite() {
); );
} }
export function saveTokens(tokens) { export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens; tokenCache.tokens = tokens;
if (tokenCache.writeEnabled) { if (tokenCache.writeEnabled) {
try { try {
storage.hassTokens = JSON.stringify(tokens); 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() { export function enableWrite() {
tokenCache.writeEnabled = true; tokenCache.writeEnabled = true;
if (tokenCache.tokens) {
saveTokens(tokenCache.tokens); saveTokens(tokenCache.tokens);
} }
}
export function loadTokens() { export function loadTokens() {
if (tokenCache.tokens === undefined) { if (tokenCache.tokens === undefined) {

View File

@ -1,4 +1,4 @@
import { createCollection } from "home-assistant-js-websocket"; import { createCollection, Connection } from "home-assistant-js-websocket";
const fetchNotifications = (conn) => const fetchNotifications = (conn) =>
conn.sendMessagePromise({ conn.sendMessagePromise({
@ -11,8 +11,11 @@ const subscribeUpdates = (conn, store) =>
"persistent_notifications_updated" "persistent_notifications_updated"
); );
export const subscribeNotifications = (conn, onChange) => export const subscribeNotifications = (
createCollection( conn: Connection,
onChange: (notifications: Notification[]) => void
) =>
createCollection<Notification[]>(
"_ntf", "_ntf",
fetchNotifications, fetchNotifications,
subscribeUpdates, subscribeUpdates,

View File

@ -1,10 +0,0 @@
import { createCollection } from "home-assistant-js-websocket";
export const subscribePanels = (conn, onChange) =>
createCollection(
"_pnl",
(conn_) => conn_.sendMessagePromise({ type: "get_panels" }),
null,
conn,
onChange
);

14
src/data/ws-panels.ts Normal file
View File

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

View File

@ -1,15 +0,0 @@
import { createCollection } from "home-assistant-js-websocket";
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, onChange) =>
createCollection("_thm", fetchThemes, subscribeUpdates, conn, onChange);

25
src/data/ws-themes.ts Normal file
View File

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

View File

@ -1,4 +0,0 @@
import { createCollection, getUser } from "home-assistant-js-websocket";
export const subscribeUser = (conn, onChange) =>
createCollection("_usr", (conn_) => getUser(conn_), null, conn, onChange);

20
src/data/ws-user.ts Normal file
View File

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

View File

@ -5,12 +5,21 @@ import {
subscribeEntities, subscribeEntities,
subscribeServices, subscribeServices,
ERR_INVALID_AUTH, ERR_INVALID_AUTH,
Auth,
Connection,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { loadTokens, saveTokens } from "../common/auth/token_storage"; import { loadTokens, saveTokens } from "../common/auth/token_storage";
import { subscribePanels } from "../data/ws-panels"; import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes"; import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user"; 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 hassUrl = `${location.protocol}//${location.host}`;
const isExternal = location.search.includes("external_auth=1"); const isExternal = location.search.includes("external_auth=1");
@ -33,7 +42,7 @@ const connProm = async (auth) => {
// Clear url if we have been able to establish a connection // Clear url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) { if (location.search.includes("auth_callback=1")) {
history.replaceState(null, null, location.pathname); history.replaceState(null, "", location.pathname);
} }
return { auth, conn }; return { auth, conn };
@ -43,7 +52,9 @@ const connProm = async (auth) => {
} }
// We can get invalid auth if auth tokens were stored that are no longer valid // We can get invalid auth if auth tokens were stored that are no longer valid
// Clear stored tokens. // Clear stored tokens.
if (!isExternal) saveTokens(null); if (!isExternal) {
saveTokens(null);
}
auth = await authProm(); auth = await authProm();
const conn = await createConnection({ auth }); const conn = await createConnection({ auth });
return { auth, conn }; return { auth, conn };
@ -54,7 +65,9 @@ window.hassConnection = authProm().then(connProm);
// Start fetching some of the data that we will need. // Start fetching some of the data that we will need.
window.hassConnection.then(({ conn }) => { window.hassConnection.then(({ conn }) => {
const noop = () => {}; const noop = () => {
// do nothing
};
subscribeEntities(conn, noop); subscribeEntities(conn, noop);
subscribeConfig(conn, noop); subscribeConfig(conn, noop);
subscribeServices(conn, noop); subscribeServices(conn, noop);
@ -64,8 +77,12 @@ window.hassConnection.then(({ conn }) => {
}); });
window.addEventListener("error", (e) => { window.addEventListener("error", (e) => {
const homeAssistant = document.querySelector("home-assistant"); const homeAssistant = document.querySelector("home-assistant") as any;
if (homeAssistant && homeAssistant.hass && homeAssistant.hass.callService) { if (
homeAssistant &&
homeAssistant.hass &&
(homeAssistant.hass as HomeAssistant).callService
) {
homeAssistant.hass.callService("system_log", "write", { homeAssistant.hass.callService("system_log", "write", {
logger: `frontend.${ logger: `frontend.${
__DEV__ ? "js_dev" : "js" __DEV__ ? "js_dev" : "js"

View File

@ -8,6 +8,12 @@ import {
HassEntityAttributeBase, HassEntityAttributeBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
declare global {
var __DEV__: boolean;
var __BUILD__: "latest" | "es5";
var __VERSION__: string;
}
export interface Credential { export interface Credential {
auth_provider_type: string; auth_provider_type: string;
auth_provider_id: string; auth_provider_id: string;
@ -34,6 +40,11 @@ export interface Theme {
"accent-color": string; "accent-color": string;
} }
export interface Themes {
default_theme: string;
themes: { [key: string]: Theme };
}
export interface Panel { export interface Panel {
component_name: string; component_name: string;
config?: { [key: string]: any }; config?: { [key: string]: any };
@ -42,22 +53,31 @@ export interface Panel {
url_path: string; url_path: string;
} }
export interface Panels {
[name: string]: Panel;
}
export interface Translation { export interface Translation {
nativeName: string; nativeName: string;
fingerprints: { [fragment: string]: string }; fingerprints: { [fragment: string]: string };
} }
export interface Notification {
notification_id: string;
message: string;
title: string;
status: "read" | "unread";
created_at: string;
}
export interface HomeAssistant { export interface HomeAssistant {
auth: Auth; auth: Auth;
connection: Connection; connection: Connection;
connected: boolean; connected: boolean;
states: HassEntities; states: HassEntities;
config: HassConfig; config: HassConfig;
themes: { themes: Themes;
default_theme: string; panels: Panels;
themes: { [key: string]: Theme };
};
panels: { [key: string]: Panel };
panelUrl: string; panelUrl: string;
language: string; language: string;
resources: { [key: string]: any }; resources: { [key: string]: any };

View File

@ -41,7 +41,7 @@ function createConfig(isProdBuild, latestBuild) {
app: "./src/entrypoints/app.js", app: "./src/entrypoints/app.js",
authorize: "./src/entrypoints/authorize.js", authorize: "./src/entrypoints/authorize.js",
onboarding: "./src/entrypoints/onboarding.js", onboarding: "./src/entrypoints/onboarding.js",
core: "./src/entrypoints/core.js", core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.js", compatibility: "./src/entrypoints/compatibility.js",
"custom-panel": "./src/entrypoints/custom-panel.js", "custom-panel": "./src/entrypoints/custom-panel.js",
"hass-icons": "./src/entrypoints/hass-icons.js", "hass-icons": "./src/entrypoints/hass-icons.js",
@ -173,7 +173,7 @@ function createConfig(isProdBuild, latestBuild) {
swDest: "service_worker.js", swDest: "service_worker.js",
importWorkboxFrom: "local", importWorkboxFrom: "local",
include: [ include: [
/core.js$/, /core.ts$/,
/app.js$/, /app.js$/,
/custom-panel.js$/, /custom-panel.js$/,
/hass-icons.js$/, /hass-icons.js$/,

View File

@ -7426,10 +7426,10 @@ hoek@4.x.x:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
home-assistant-js-websocket@^3.1.5: home-assistant-js-websocket@^3.2.4:
version "3.1.5" version "3.2.4"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.1.5.tgz#56358660bbccb5f24e16f9197bbad12739f9b756" resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.2.4.tgz#0c4212e6ac57b60ed939aa420253994e4f9f0bef"
integrity sha512-BlANSkA9ob6wlUJCYS26n1tfMla+aJzB2rhhXSFi0iiTtWWfuOlcSOw8pxjeWhh1I9oAcbQ4qxhB7Np1EXG+Og== integrity sha512-DaHpWIjJFLwTWNbHeGSCEUsbeyLUWAyWUgsYkiVWxzbfm+vqC5YaLNRu+Ma64SQYh5yGSYr7h25p2hip1GvyhQ==
home-or-tmp@^2.0.0: home-or-tmp@^2.0.0:
version "2.0.0" version "2.0.0"