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",
"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",

View File

@ -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."
@ -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;
@ -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);
@ -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
);
}

View File

@ -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) {
@ -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() {

View File

@ -1,4 +1,4 @@
import { createCollection } from "home-assistant-js-websocket";
import { createCollection, Connection } from "home-assistant-js-websocket";
const fetchNotifications = (conn) =>
conn.sendMessagePromise({
@ -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,

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,
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");
@ -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 };
@ -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 };
@ -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);
@ -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"

View File

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

View File

@ -41,7 +41,7 @@ function createConfig(isProdBuild, latestBuild) {
app: "./src/entrypoints/app.js",
authorize: "./src/entrypoints/authorize.js",
onboarding: "./src/entrypoints/onboarding.js",
core: "./src/entrypoints/core.js",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.js",
"custom-panel": "./src/entrypoints/custom-panel.js",
"hass-icons": "./src/entrypoints/hass-icons.js",
@ -173,7 +173,7 @@ function createConfig(isProdBuild, latestBuild) {
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [
/core.js$/,
/core.ts$/,
/app.js$/,
/custom-panel.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"
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
home-assistant-js-websocket@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.1.5.tgz#56358660bbccb5f24e16f9197bbad12739f9b756"
integrity sha512-BlANSkA9ob6wlUJCYS26n1tfMla+aJzB2rhhXSFi0iiTtWWfuOlcSOw8pxjeWhh1I9oAcbQ4qxhB7Np1EXG+Og==
home-assistant-js-websocket@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.2.4.tgz#0c4212e6ac57b60ed939aa420253994e4f9f0bef"
integrity sha512-DaHpWIjJFLwTWNbHeGSCEUsbeyLUWAyWUgsYkiVWxzbfm+vqC5YaLNRu+Ma64SQYh5yGSYr7h25p2hip1GvyhQ==
home-or-tmp@^2.0.0:
version "2.0.0"