Add an authorize page for authentication (#1147)
* Use authorize page if auth provider * Add webcomponent polyfill * More fixes * ES5 fix * Lint * Use redirect_uri * upgrade uglify to fix tests? * Update browsers used for testing
This commit is contained in:
parent
912969111f
commit
3b7a206cec
|
@ -28,3 +28,4 @@ dist
|
|||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
const gulp = require('gulp');
|
||||
const replace = require('gulp-batch-replace');
|
||||
const rename = require('gulp-rename');
|
||||
|
||||
const config = require('../config');
|
||||
const minifyStream = require('../common/transform').minifyStream;
|
||||
const {
|
||||
bundledStreamFromHTML,
|
||||
} = require('../common/html');
|
||||
|
||||
const es5Extra = "<script src='/frontend_es5/custom-elements-es5-adapter.js'></script>";
|
||||
|
||||
async function buildAuth(es6) {
|
||||
let stream = await bundledStreamFromHTML('src/authorize.html');
|
||||
stream = stream.pipe(replace([['<!--EXTRA_SCRIPTS-->', es6 ? '' : es5Extra]]));
|
||||
|
||||
return minifyStream(stream, /* es6= */ es6)
|
||||
.pipe(rename('authorize.html'))
|
||||
.pipe(gulp.dest(es6 ? config.output : config.output_es5));
|
||||
}
|
||||
|
||||
gulp.task('authorize-es5', () => buildAuth(/* es6= */ false));
|
||||
gulp.task('authorize', () => buildAuth(/* es6= */ true));
|
|
@ -61,6 +61,7 @@
|
|||
document.getElementById('ha-init-skeleton').classList.add('error');
|
||||
};
|
||||
window.noAuth = '{{ no_auth }}';
|
||||
window.clientId = '{{ client_id }}'
|
||||
window.Polymer = {
|
||||
lazyRegister: true,
|
||||
useNativeCSSProperties: true,
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
export default function fetchToken(clientId, code) {
|
||||
const data = new FormData();
|
||||
data.append('grant_type', 'authorization_code');
|
||||
data.append('code', code);
|
||||
return fetch('/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Basic ${btoa(clientId)}`
|
||||
},
|
||||
body: data,
|
||||
}).then((resp) => {
|
||||
if (!resp.ok) throw new Error('Unable to fetch tokens');
|
||||
return resp.json();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export default function refreshAccessToken(clientId, refreshToken) {
|
||||
const data = new FormData();
|
||||
data.append('grant_type', 'refresh_token');
|
||||
data.append('refresh_token', refreshToken);
|
||||
return fetch('/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Basic ${btoa(clientId)}`
|
||||
},
|
||||
body: data,
|
||||
}).then((resp) => {
|
||||
if (!resp.ok) throw new Error('Unable to fetch tokens');
|
||||
return resp.json();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export default function parseQuery(queryString) {
|
||||
const query = {};
|
||||
const items = queryString.split('&');
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i].split('=');
|
||||
const key = decodeURIComponent(item[0]);
|
||||
const value = item.length > 1 ? decodeURIComponent(item[1]) : undefined;
|
||||
query[key] = value;
|
||||
}
|
||||
return query;
|
||||
}
|
74
js/core.js
74
js/core.js
|
@ -1,21 +1,26 @@
|
|||
import * as HAWS from 'home-assistant-js-websocket';
|
||||
|
||||
import fetchToken from './common/auth/fetch_token.js';
|
||||
import refreshToken_ from './common/auth/refresh_token.js';
|
||||
import parseQuery from './common/util/parse_query.js';
|
||||
|
||||
window.HAWS = HAWS;
|
||||
window.HASS_DEMO = __DEMO__;
|
||||
window.HASS_DEV = __DEV__;
|
||||
window.HASS_BUILD = __BUILD__;
|
||||
window.HASS_VERSION = __VERSION__;
|
||||
|
||||
const init = window.createHassConnection = function (password) {
|
||||
const init = window.createHassConnection = function (password, accessToken) {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const url = `${proto}://${window.location.host}/api/websocket?${window.HASS_BUILD}`;
|
||||
const options = {
|
||||
setupRetry: 10,
|
||||
};
|
||||
if (password !== undefined) {
|
||||
if (password) {
|
||||
options.authToken = password;
|
||||
} else if (accessToken) {
|
||||
options.accessToken = accessToken;
|
||||
}
|
||||
|
||||
return HAWS.createConnection(url, options)
|
||||
.then(function (conn) {
|
||||
HAWS.subscribeEntities(conn);
|
||||
|
@ -24,12 +29,65 @@ const init = window.createHassConnection = function (password) {
|
|||
});
|
||||
};
|
||||
|
||||
if (window.noAuth === '1') {
|
||||
window.hassConnection = init();
|
||||
} else if (window.localStorage.authToken) {
|
||||
window.hassConnection = init(window.localStorage.authToken);
|
||||
function redirectLogin() {
|
||||
const urlBase = __DEV__ ? '/home-assistant-polymer/src' : `/frontend_${__BUILD__}`;
|
||||
document.location = `${urlBase}/authorize.html?response_type=code&client_id=${window.clientId}&redirect_uri=/`;
|
||||
}
|
||||
|
||||
window.refreshToken = () =>
|
||||
refreshToken_(window.clientId, window.tokens.refresh_token).then((accessTokenResp) => {
|
||||
window.tokens.access_token = accessTokenResp.access_token;
|
||||
localStorage.tokens = JSON.stringify(window.tokens);
|
||||
return accessTokenResp.access_token;
|
||||
}, () => redirectLogin());
|
||||
|
||||
function resolveCode(code) {
|
||||
fetchToken(window.clientId, code).then((tokens) => {
|
||||
localStorage.tokens = JSON.stringify(tokens);
|
||||
// Refresh the page and have tokens in place.
|
||||
document.location = location.pathname;
|
||||
}, (err) => {
|
||||
// eslint-disable-next-line
|
||||
console.error('Resolve token failed', err);
|
||||
alert('Unable to fetch tokens');
|
||||
redirectLogin();
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (location.search) {
|
||||
const query = parseQuery(location.search.substr(1));
|
||||
if (query.code) {
|
||||
resolveCode(query.code);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (localStorage.tokens) {
|
||||
window.tokens = JSON.parse(localStorage.tokens);
|
||||
window.hassConnection = init(null, window.tokens.access_token).catch((err) => {
|
||||
if (err !== HAWS.ERR_INVALID_AUTH) throw err;
|
||||
|
||||
return window.refreshToken().then(accessToken => init(null, accessToken));
|
||||
});
|
||||
return;
|
||||
}
|
||||
redirectLogin();
|
||||
}
|
||||
|
||||
function mainLegacy() {
|
||||
if (window.noAuth === '1') {
|
||||
window.hassConnection = init();
|
||||
} else if (window.localStorage.authToken) {
|
||||
window.hassConnection = init(window.localStorage.authToken);
|
||||
} else {
|
||||
window.hassConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.clientId) {
|
||||
main();
|
||||
} else {
|
||||
window.hassConnection = null;
|
||||
mainLegacy();
|
||||
}
|
||||
|
||||
window.addEventListener('error', (e) => {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"dependencies": {
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"fecha": "^2.3.3",
|
||||
"home-assistant-js-websocket": "^1.1.2",
|
||||
"home-assistant-js-websocket": "1.2.0",
|
||||
"mdn-polyfills": "^5.5.0",
|
||||
"preact": "^8.2.6",
|
||||
"unfetch": "^3.0.0"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
||||
<link rel="import" href='../../../src/components/ha-markdown.html'>
|
||||
<link rel='import' href='../../../src/resources/ha-style.html'>
|
||||
<link rel="import" href='./ha-form.html'>
|
||||
<link rel="import" href='../../../src/components/ha-form.html'>
|
||||
|
||||
<dom-module id="ha-config-flow">
|
||||
<template>
|
||||
|
@ -57,10 +57,6 @@
|
|||
<ha-markdown content='[[_computeStepDescription(localize, step)]]'></ha-markdown>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[step.errors.base]]'>
|
||||
<div class='error'>[[_computeBaseError(localize, step)]]</div>
|
||||
</template>
|
||||
|
||||
<ha-form
|
||||
data='{{stepData}}'
|
||||
schema='[[step.data_schema]]'
|
||||
|
@ -196,10 +192,6 @@ class HaConfigFlow extends
|
|||
return localize(`component.${step.handler}.config.step.${step.step_id}.description`);
|
||||
}
|
||||
|
||||
_computeBaseError(localize, step) {
|
||||
return localize(`component.${step.handler}.config.error.${step.errors.base}`);
|
||||
}
|
||||
|
||||
_computeLabelCallback(localize, step) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return schema => localize(`component.${step.handler}.config.step.${step.step_id}.data.${schema.name}`);
|
||||
|
|
|
@ -16,6 +16,7 @@ cp -r public/__init__.py $OUTPUT_DIR_ES5/
|
|||
|
||||
# Build frontend
|
||||
BUILD_DEV=0 ./node_modules/.bin/gulp
|
||||
BUILD_DEV=0 ./node_modules/.bin/gulp authorize authorize-es5
|
||||
|
||||
# Entry points
|
||||
cp build/*.js build/*.html $OUTPUT_DIR
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
<link rel='import' href='../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel="import" href='../../bower_components/paper-button/paper-button.html'>
|
||||
|
||||
<link rel='import' href='../components/ha-form.html'>
|
||||
<link rel='import' href='../util/hass-mixins.html'>
|
||||
|
||||
<dom-module id='ha-auth-flow'>
|
||||
<template>
|
||||
<template is='dom-if' if='[[_equals(_state, "loading")]]'>
|
||||
Please wait
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equals(_state, "error")]]'>
|
||||
Something went wrong
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equals(_state, "step")]]'>
|
||||
<template is='dom-if' if='[[_equals(_step.type, "abort")]]'>
|
||||
Aborted
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equals(_step.type, "create_entry")]]'>
|
||||
Success!
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equals(_step.type, "form")]]'>
|
||||
<ha-form
|
||||
data='{{_stepData}}'
|
||||
schema='[[_step.data_schema]]'
|
||||
error='[[_step.errors]]'
|
||||
></ha-form>
|
||||
</template>
|
||||
<paper-button on-click='_handleSubmit'>[[_computeSubmitCaption(_step.type)]]</paper-button>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
/*
|
||||
* @appliesMixin window.hassMixins.EventsMixin
|
||||
*/
|
||||
class HaAuthFlow extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||
static get is() { return 'ha-auth-flow'; }
|
||||
static get properties() {
|
||||
return {
|
||||
authProvider: Object,
|
||||
clientId: String,
|
||||
clientSecret: String,
|
||||
redirectUri: String,
|
||||
oauth2State: String,
|
||||
_state: {
|
||||
type: String,
|
||||
value: 'loading'
|
||||
},
|
||||
_stepData: {
|
||||
type: Object,
|
||||
value: () => ({}),
|
||||
},
|
||||
_step: Object,
|
||||
};
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
fetch('/auth/login_flow', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
handler: [this.authProvider.type, this.authProvider.id],
|
||||
redirect_uri: this.redirectUri,
|
||||
})
|
||||
}).then((response) => {
|
||||
if (!response.ok) throw new Error();
|
||||
return response.json();
|
||||
}).then(step => this.setProperties({
|
||||
_step: step,
|
||||
_state: 'step',
|
||||
})).catch((err) => {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error starting auth flow', err);
|
||||
this._state = 'error';
|
||||
});
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_computeSubmitCaption(stepType) {
|
||||
return stepType === 'form' ? 'Submit' : 'Start over';
|
||||
}
|
||||
|
||||
_handleSubmit() {
|
||||
if (this._step.type !== 'form') {
|
||||
this.fire('reset');
|
||||
return;
|
||||
}
|
||||
this._state = 'loading';
|
||||
|
||||
fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`
|
||||
},
|
||||
body: JSON.stringify(this._stepData)
|
||||
}).then((response) => {
|
||||
if (!response.ok) throw new Error();
|
||||
return response.json();
|
||||
}).then((newStep) => {
|
||||
if (newStep.type === 'create_entry') {
|
||||
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
|
||||
let url = this.redirectUri;
|
||||
if (!url.includes('?')) {
|
||||
url += '?';
|
||||
} else if (!url.endsWith('&')) {
|
||||
url += '&';
|
||||
}
|
||||
|
||||
url += `code=${encodeURIComponent(newStep.result)}`;
|
||||
|
||||
if (this.oauth2State) {
|
||||
url += `&state=${encodeURIComponent(this.oauth2State)}`;
|
||||
}
|
||||
|
||||
document.location = url;
|
||||
return;
|
||||
}
|
||||
|
||||
const props = {
|
||||
_step: newStep,
|
||||
_state: 'step',
|
||||
};
|
||||
if (newStep.step_id !== this._step.step_id) {
|
||||
props._stepData = {};
|
||||
}
|
||||
this.setProperties(props);
|
||||
}).catch((err) => {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error loading auth providers', err);
|
||||
this._state = 'error-loading';
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(HaAuthFlow.is, HaAuthFlow);
|
||||
</script>
|
|
@ -0,0 +1,81 @@
|
|||
<link rel='import' href='../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel='import' href='../../bower_components/polymer/lib/elements/dom-if.html'>
|
||||
<link rel='import' href='../../bower_components/polymer/lib/elements/dom-repeat.html'>
|
||||
<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout-classes.html">
|
||||
|
||||
<link rel='import' href='./ha-pick-auth-provider.html'>
|
||||
<link rel='import' href='./ha-auth-flow.html'>
|
||||
|
||||
<dom-module id='ha-authorize'>
|
||||
<template>
|
||||
<style is="custom-style" include="iron-flex iron-positioning"></style>
|
||||
<style>
|
||||
.layout {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
<div class="layout vertical center fit">
|
||||
<img src="/static/icons/favicon-192x192.png" height="192" />
|
||||
|
||||
<template is='dom-if' if='[[_authProvider]]'>
|
||||
<ha-auth-flow
|
||||
client-id='[[clientId]]'
|
||||
client-secret='[[clientSecret]]'
|
||||
redirect-uri='[[redirectUri]]'
|
||||
oauth2-state='[[oauth2State]]'
|
||||
auth-provider='[[_authProvider]]'
|
||||
on-reset='_handleReset'
|
||||
/>
|
||||
</template>
|
||||
<template is='dom-if' if='[[!_authProvider]]'>
|
||||
<ha-pick-auth-provider
|
||||
client-id='[[clientId]]'
|
||||
client-secret='[[clientSecret]]'
|
||||
on-pick='_handleAuthProviderPick'
|
||||
></ha-pick-auth-provider>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
class HaAuthorize extends Polymer.Element {
|
||||
static get is() { return 'ha-authorize'; }
|
||||
static get properties() {
|
||||
return {
|
||||
_authProvider: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
clientId: String,
|
||||
clientSecret: String,
|
||||
redirectUri: String,
|
||||
oauth2State: String,
|
||||
};
|
||||
}
|
||||
ready() {
|
||||
super.ready();
|
||||
const query = {};
|
||||
const values = location.search.substr(1).split('&');
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i].split('=');
|
||||
if (value.length > 1) {
|
||||
query[decodeURIComponent(value[0])] = decodeURIComponent(value[1]);
|
||||
}
|
||||
}
|
||||
const props = {};
|
||||
if (query.client_id) props.clientId = query.client_id;
|
||||
if (query.client_secret) props.clientSecret = query.client_secret;
|
||||
if (query.redirect_uri) props.redirectUri = query.redirect_uri;
|
||||
if (query.state) props.oauth2State = query.state;
|
||||
this.setProperties(props);
|
||||
}
|
||||
_handleAuthProviderPick(ev) {
|
||||
this._authProvider = ev.detail;
|
||||
}
|
||||
_handleReset() {
|
||||
this._authProvider = null;
|
||||
}
|
||||
}
|
||||
customElements.define(HaAuthorize.is, HaAuthorize);
|
||||
</script>
|
|
@ -0,0 +1,86 @@
|
|||
<link rel='import' href='../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel='import' href='../../bower_components/paper-item/paper-item.html'>
|
||||
|
||||
<link rel='import' href='../util/hass-mixins.html'>
|
||||
|
||||
<dom-module id='ha-pick-auth-provider'>
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
text-align: center;
|
||||
font-family: Roboto;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<template is='dom-if' if='[[_equal(_state, "loading")]]'>
|
||||
Loading auth providers.
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equal(_state, "no-results")]]'>
|
||||
No auth providers found.
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equal(_state, "error-loading")]]'>
|
||||
Error loading
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equal(_state, "pick")]]'>
|
||||
<p>Log in with</p>
|
||||
<template is='dom-repeat' items='[[authProviders]]'>
|
||||
<paper-item on-click='_handlePick'>[[item.name]]</paper-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
/*
|
||||
* @appliesMixin window.hassMixins.EventsMixin
|
||||
*/
|
||||
class HaPickAuthProvider extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||
static get is() { return 'ha-pick-auth-provider'; }
|
||||
static get properties() {
|
||||
return {
|
||||
_state: {
|
||||
type: String,
|
||||
value: 'loading'
|
||||
},
|
||||
authProviders: Array,
|
||||
clientId: String,
|
||||
clientSecret: String,
|
||||
};
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
fetch('/auth/providers', {
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`
|
||||
}
|
||||
}).then((response) => {
|
||||
if (!response.ok) throw new Error();
|
||||
return response.json();
|
||||
}).then((authProviders) => {
|
||||
this.setProperties({
|
||||
authProviders,
|
||||
_state: 'pick',
|
||||
});
|
||||
if (authProviders.length === 1) {
|
||||
this.fire('pick', authProviders[0]);
|
||||
}
|
||||
}).catch((err) => {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error loading auth providers', err);
|
||||
this._state = 'error-loading';
|
||||
});
|
||||
}
|
||||
|
||||
_handlePick(ev) {
|
||||
this.fire('pick', ev.model.item);
|
||||
}
|
||||
|
||||
_equal(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
}
|
||||
customElements.define(HaPickAuthProvider.is, HaPickAuthProvider);
|
||||
</script>
|
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home Assistant</title>
|
||||
<!--EXTRA_SCRIPTS-->
|
||||
</head>
|
||||
<body>
|
||||
<ha-authorize>Loading</ha-authorize>
|
||||
<script>
|
||||
function addScript(src) {
|
||||
var e = document.createElement('script');
|
||||
e.src = src;
|
||||
document.head.appendChild(e);
|
||||
}
|
||||
var webComponentsSupported = (
|
||||
'customElements' in window &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
addScript('/static/webcomponents-lite.js');
|
||||
}
|
||||
</script>
|
||||
<link rel='import' href='./auth/ha-authorize.html'>
|
||||
</body>
|
||||
</html>
|
|
@ -1,13 +1,13 @@
|
|||
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
|
||||
<link rel="import" href='../../bower_components/polymer/polymer-element.html'>
|
||||
|
||||
<link rel="import" href='../../../bower_components/paper-input/paper-input.html'>
|
||||
<link rel="import" href='../../../bower_components/paper-checkbox/paper-checkbox.html'>
|
||||
<link rel='import' href='../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html'>
|
||||
<link rel='import' href='../../../bower_components/paper-listbox/paper-listbox.html'>
|
||||
<link rel='import' href='../../../bower_components/paper-item/paper-item.html'>
|
||||
<link rel="import" href='../../bower_components/paper-input/paper-input.html'>
|
||||
<link rel="import" href='../../bower_components/paper-checkbox/paper-checkbox.html'>
|
||||
<link rel='import' href='../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html'>
|
||||
<link rel='import' href='../../bower_components/paper-listbox/paper-listbox.html'>
|
||||
<link rel='import' href='../../bower_components/paper-item/paper-item.html'>
|
||||
|
||||
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
||||
<link rel="import" href='../../../src/components/ha-paper-slider.html'>
|
||||
<link rel='import' href='../util/hass-mixins.html'>
|
||||
<link rel="import" href='./ha-paper-slider.html'>
|
||||
|
||||
|
||||
<dom-module id="ha-form">
|
||||
|
@ -18,6 +18,10 @@
|
|||
}
|
||||
</style>
|
||||
<template is='dom-if' if='[[_isArray(schema)]]' restamp>
|
||||
<template is='dom-if' if='[[error.base]]'>
|
||||
[[computeError(error.base, schema)]]
|
||||
</template>
|
||||
|
||||
<template is='dom-repeat' items='[[schema]]'>
|
||||
<ha-form
|
||||
data='[[_getValue(data, item)]]'
|
||||
|
@ -112,14 +116,14 @@ class HaForm extends window.hassMixins.EventsMixin(Polymer.Element) {
|
|||
// schema object.
|
||||
computeLabel: {
|
||||
type: Function,
|
||||
value: schema => schema && schema.name,
|
||||
value: () => schema => schema && schema.name,
|
||||
},
|
||||
|
||||
// A function that will computes an error message to be displayed for a
|
||||
// given error ID, and relevant schema object
|
||||
computeError: {
|
||||
type: Function,
|
||||
value: (error, schema) => error, // eslint-disable-line no-unused-vars
|
||||
value: () => (error, schema) => error, // eslint-disable-line no-unused-vars
|
||||
},
|
||||
};
|
||||
}
|
|
@ -205,9 +205,17 @@ class HomeAssistant extends Polymer.Element {
|
|||
}
|
||||
),
|
||||
callApi: (method, path, parameters) => {
|
||||
var host = window.location.protocol + '//' + window.location.host;
|
||||
var auth = conn.options.authToken ? conn.options : {};
|
||||
return window.hassCallApi(host, auth, method, path, parameters);
|
||||
const host = window.location.protocol + '//' + window.location.host;
|
||||
const auth = conn.options;
|
||||
return window.hassCallApi(host, auth, method, path, parameters).catch((err) => {
|
||||
if (err.status_code !== 401 || !auth.accessToken) throw err;
|
||||
|
||||
// If we connect with access token and get 401, refresh token and try again
|
||||
return window.refreshToken().then((accessToken) => {
|
||||
conn.options.accessToken = accessToken;
|
||||
return window.hassCallApi(host, auth, method, path, parameters);
|
||||
});
|
||||
});
|
||||
},
|
||||
}, this.$.storage.getStoredState());
|
||||
|
||||
|
@ -216,12 +224,21 @@ class HomeAssistant extends Polymer.Element {
|
|||
this.loadBackendTranslations();
|
||||
};
|
||||
|
||||
conn.addEventListener('ready', reconnected);
|
||||
|
||||
var disconnected = () => {
|
||||
const disconnected = () => {
|
||||
this._updateHass({ connected: false });
|
||||
};
|
||||
|
||||
conn.addEventListener('ready', reconnected);
|
||||
|
||||
// If we reconnect after losing connection and access token is no longer
|
||||
// valid.
|
||||
conn.addEventListener('reconnect-error', (_conn, err) => {
|
||||
if (err !== window.HAWS.ERR_INVALID_AUTH) return;
|
||||
disconnected();
|
||||
this.unsubConnection();
|
||||
window.refreshToken().then(accessToken =>
|
||||
this.handleConnectionPromise(window.createHassConnection(null, accessToken)));
|
||||
});
|
||||
conn.addEventListener('disconnected', disconnected);
|
||||
|
||||
var unsubEntities;
|
||||
|
@ -310,16 +327,9 @@ class HomeAssistant extends Polymer.Element {
|
|||
}
|
||||
|
||||
handleLogout() {
|
||||
delete localStorage.authToken;
|
||||
var conn = this.connection;
|
||||
this.connectionPromise = null;
|
||||
try {
|
||||
this.connection = null;
|
||||
} catch (err) {
|
||||
// home-assistant-main crashes when hass is set to null.
|
||||
// However, after it is done, home-assistant-main is removed from the DOM by this element.
|
||||
}
|
||||
conn.close();
|
||||
this.connection.close();
|
||||
localStorage.clear();
|
||||
document.location = '/';
|
||||
}
|
||||
|
||||
setTheme(event) {
|
||||
|
|
|
@ -33,6 +33,8 @@ window.hassCallApi = function (host, auth, method, path, parameters) {
|
|||
|
||||
if (auth.authToken) {
|
||||
req.setRequestHeader('X-HA-access', auth.authToken);
|
||||
} else if (auth.accessToken) {
|
||||
req.setRequestHeader('authorization', `Bearer ${auth.accessToken}`);
|
||||
}
|
||||
|
||||
req.onload = function () {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
import parseQuery from '../../../js/common/util/parse_query.js';
|
||||
|
||||
describe('parseQuery', () => {
|
||||
it('works', () => {
|
||||
assert.deepEqual(parseQuery('hello=world'), { hello: 'world' });
|
||||
assert.deepEqual(parseQuery('hello=world&drink=soda'), {
|
||||
hello: 'world',
|
||||
drink: 'soda',
|
||||
});
|
||||
assert.deepEqual(parseQuery('hello=world&no_value&drink=soda'), {
|
||||
hello: 'world',
|
||||
no_value: undefined,
|
||||
drink: 'soda',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,7 +9,7 @@
|
|||
}, {
|
||||
"browserName": "safari",
|
||||
"platform": "macOS 10.12",
|
||||
"version": "10"
|
||||
"version": "latest"
|
||||
}, {
|
||||
"browserName": "firefox",
|
||||
"platform": "Windows 10",
|
||||
|
@ -17,7 +17,7 @@
|
|||
}, {
|
||||
"browserName": "MicrosoftEdge",
|
||||
"platform": "Windows 10",
|
||||
"version": "14.14393"
|
||||
"version": "latest"
|
||||
}, {
|
||||
"deviceName": "Android GoogleAPI Emulator",
|
||||
"platformName": "Android",
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -2163,6 +2163,14 @@ commander@^2.8.1:
|
|||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
|
||||
|
||||
commander@~2.13.0:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
|
||||
|
||||
commander@~2.15.0:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
|
@ -4234,9 +4242,9 @@ hoek@4.x.x:
|
|||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
|
||||
|
||||
home-assistant-js-websocket@^1.1.2:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-1.1.4.tgz#36da056be18210ada76abfa2bc1f247ecbebce11"
|
||||
home-assistant-js-websocket@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-1.2.0.tgz#aa965a7ae47606ea82b919ce74a310ef18412cd7"
|
||||
|
||||
home-or-tmp@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -8197,10 +8205,10 @@ ua-parser-js@^0.7.9:
|
|||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
|
||||
|
||||
uglify-es@^3.1.9:
|
||||
version "3.1.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.1.9.tgz#6c82df628ac9eb7af9c61fd70c744a084abe6161"
|
||||
version "3.3.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
|
||||
dependencies:
|
||||
commander "~2.11.0"
|
||||
commander "~2.13.0"
|
||||
source-map "~0.6.1"
|
||||
|
||||
uglify-js@2.6.x:
|
||||
|
@ -8227,10 +8235,10 @@ uglify-js@^3.0.5:
|
|||
source-map "~0.5.1"
|
||||
|
||||
uglify-js@^3.1.9:
|
||||
version "3.1.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.1.9.tgz#dffca799308cf327ec3ac77eeacb8e196ce3b452"
|
||||
version "3.3.24"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.24.tgz#abeae7690c602ebd006f4567387a0c0c333bdc0d"
|
||||
dependencies:
|
||||
commander "~2.11.0"
|
||||
commander "~2.15.0"
|
||||
source-map "~0.6.1"
|
||||
|
||||
uglify-to-browserify@~1.0.0:
|
||||
|
|
Loading…
Reference in New Issue