mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
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
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,3 +28,4 @@ dist
|
|||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
.lokalise_token
|
.lokalise_token
|
||||||
|
yarn-error.log
|
||||||
|
23
gulp/tasks/auth.js
Normal file
23
gulp/tasks/auth.js
Normal file
@ -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');
|
document.getElementById('ha-init-skeleton').classList.add('error');
|
||||||
};
|
};
|
||||||
window.noAuth = '{{ no_auth }}';
|
window.noAuth = '{{ no_auth }}';
|
||||||
|
window.clientId = '{{ client_id }}'
|
||||||
window.Polymer = {
|
window.Polymer = {
|
||||||
lazyRegister: true,
|
lazyRegister: true,
|
||||||
useNativeCSSProperties: true,
|
useNativeCSSProperties: true,
|
||||||
|
15
js/common/auth/fetch_token.js
Normal file
15
js/common/auth/fetch_token.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
}
|
15
js/common/auth/refresh_token.js
Normal file
15
js/common/auth/refresh_token.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
}
|
11
js/common/util/parse_query.js
Normal file
11
js/common/util/parse_query.js
Normal file
@ -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;
|
||||||
|
}
|
70
js/core.js
70
js/core.js
@ -1,21 +1,26 @@
|
|||||||
import * as HAWS from 'home-assistant-js-websocket';
|
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.HAWS = HAWS;
|
||||||
window.HASS_DEMO = __DEMO__;
|
window.HASS_DEMO = __DEMO__;
|
||||||
window.HASS_DEV = __DEV__;
|
window.HASS_DEV = __DEV__;
|
||||||
window.HASS_BUILD = __BUILD__;
|
window.HASS_BUILD = __BUILD__;
|
||||||
window.HASS_VERSION = __VERSION__;
|
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 proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
const url = `${proto}://${window.location.host}/api/websocket?${window.HASS_BUILD}`;
|
const url = `${proto}://${window.location.host}/api/websocket?${window.HASS_BUILD}`;
|
||||||
const options = {
|
const options = {
|
||||||
setupRetry: 10,
|
setupRetry: 10,
|
||||||
};
|
};
|
||||||
if (password !== undefined) {
|
if (password) {
|
||||||
options.authToken = password;
|
options.authToken = password;
|
||||||
|
} else if (accessToken) {
|
||||||
|
options.accessToken = accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HAWS.createConnection(url, options)
|
return HAWS.createConnection(url, options)
|
||||||
.then(function (conn) {
|
.then(function (conn) {
|
||||||
HAWS.subscribeEntities(conn);
|
HAWS.subscribeEntities(conn);
|
||||||
@ -24,12 +29,65 @@ const init = window.createHassConnection = function (password) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.noAuth === '1') {
|
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();
|
window.hassConnection = init();
|
||||||
} else if (window.localStorage.authToken) {
|
} else if (window.localStorage.authToken) {
|
||||||
window.hassConnection = init(window.localStorage.authToken);
|
window.hassConnection = init(window.localStorage.authToken);
|
||||||
} else {
|
} else {
|
||||||
window.hassConnection = null;
|
window.hassConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.clientId) {
|
||||||
|
main();
|
||||||
|
} else {
|
||||||
|
mainLegacy();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('error', (e) => {
|
window.addEventListener('error', (e) => {
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es6-object-assign": "^1.1.0",
|
"es6-object-assign": "^1.1.0",
|
||||||
"fecha": "^2.3.3",
|
"fecha": "^2.3.3",
|
||||||
"home-assistant-js-websocket": "^1.1.2",
|
"home-assistant-js-websocket": "1.2.0",
|
||||||
"mdn-polyfills": "^5.5.0",
|
"mdn-polyfills": "^5.5.0",
|
||||||
"preact": "^8.2.6",
|
"preact": "^8.2.6",
|
||||||
"unfetch": "^3.0.0"
|
"unfetch": "^3.0.0"
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
||||||
<link rel="import" href='../../../src/components/ha-markdown.html'>
|
<link rel="import" href='../../../src/components/ha-markdown.html'>
|
||||||
<link rel='import' href='../../../src/resources/ha-style.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">
|
<dom-module id="ha-config-flow">
|
||||||
<template>
|
<template>
|
||||||
@ -57,10 +57,6 @@
|
|||||||
<ha-markdown content='[[_computeStepDescription(localize, step)]]'></ha-markdown>
|
<ha-markdown content='[[_computeStepDescription(localize, step)]]'></ha-markdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template is='dom-if' if='[[step.errors.base]]'>
|
|
||||||
<div class='error'>[[_computeBaseError(localize, step)]]</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ha-form
|
<ha-form
|
||||||
data='{{stepData}}'
|
data='{{stepData}}'
|
||||||
schema='[[step.data_schema]]'
|
schema='[[step.data_schema]]'
|
||||||
@ -196,10 +192,6 @@ class HaConfigFlow extends
|
|||||||
return localize(`component.${step.handler}.config.step.${step.step_id}.description`);
|
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) {
|
_computeLabelCallback(localize, step) {
|
||||||
// Returns a callback for ha-form to calculate labels per schema object
|
// 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}`);
|
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 frontend
|
||||||
BUILD_DEV=0 ./node_modules/.bin/gulp
|
BUILD_DEV=0 ./node_modules/.bin/gulp
|
||||||
|
BUILD_DEV=0 ./node_modules/.bin/gulp authorize authorize-es5
|
||||||
|
|
||||||
# Entry points
|
# Entry points
|
||||||
cp build/*.js build/*.html $OUTPUT_DIR
|
cp build/*.js build/*.html $OUTPUT_DIR
|
||||||
|
143
src/auth/ha-auth-flow.html
Normal file
143
src/auth/ha-auth-flow.html
Normal file
@ -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>
|
81
src/auth/ha-authorize.html
Normal file
81
src/auth/ha-authorize.html
Normal file
@ -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>
|
86
src/auth/ha-pick-auth-provider.html
Normal file
86
src/auth/ha-pick-auth-provider.html
Normal file
@ -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>
|
26
src/authorize.html
Normal file
26
src/authorize.html
Normal file
@ -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-input/paper-input.html'>
|
||||||
<link rel="import" href='../../../bower_components/paper-checkbox/paper-checkbox.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-dropdown-menu/paper-dropdown-menu.html'>
|
||||||
<link rel='import' href='../../../bower_components/paper-listbox/paper-listbox.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-item/paper-item.html'>
|
||||||
|
|
||||||
<link rel='import' href='../../../src/util/hass-mixins.html'>
|
<link rel='import' href='../util/hass-mixins.html'>
|
||||||
<link rel="import" href='../../../src/components/ha-paper-slider.html'>
|
<link rel="import" href='./ha-paper-slider.html'>
|
||||||
|
|
||||||
|
|
||||||
<dom-module id="ha-form">
|
<dom-module id="ha-form">
|
||||||
@ -18,6 +18,10 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<template is='dom-if' if='[[_isArray(schema)]]' restamp>
|
<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]]'>
|
<template is='dom-repeat' items='[[schema]]'>
|
||||||
<ha-form
|
<ha-form
|
||||||
data='[[_getValue(data, item)]]'
|
data='[[_getValue(data, item)]]'
|
||||||
@ -112,14 +116,14 @@ class HaForm extends window.hassMixins.EventsMixin(Polymer.Element) {
|
|||||||
// schema object.
|
// schema object.
|
||||||
computeLabel: {
|
computeLabel: {
|
||||||
type: Function,
|
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
|
// A function that will computes an error message to be displayed for a
|
||||||
// given error ID, and relevant schema object
|
// given error ID, and relevant schema object
|
||||||
computeError: {
|
computeError: {
|
||||||
type: Function,
|
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) => {
|
callApi: (method, path, parameters) => {
|
||||||
var host = window.location.protocol + '//' + window.location.host;
|
const host = window.location.protocol + '//' + window.location.host;
|
||||||
var auth = conn.options.authToken ? conn.options : {};
|
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);
|
return window.hassCallApi(host, auth, method, path, parameters);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}, this.$.storage.getStoredState());
|
}, this.$.storage.getStoredState());
|
||||||
|
|
||||||
@ -216,12 +224,21 @@ class HomeAssistant extends Polymer.Element {
|
|||||||
this.loadBackendTranslations();
|
this.loadBackendTranslations();
|
||||||
};
|
};
|
||||||
|
|
||||||
conn.addEventListener('ready', reconnected);
|
const disconnected = () => {
|
||||||
|
|
||||||
var disconnected = () => {
|
|
||||||
this._updateHass({ connected: false });
|
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);
|
conn.addEventListener('disconnected', disconnected);
|
||||||
|
|
||||||
var unsubEntities;
|
var unsubEntities;
|
||||||
@ -310,16 +327,9 @@ class HomeAssistant extends Polymer.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
delete localStorage.authToken;
|
this.connection.close();
|
||||||
var conn = this.connection;
|
localStorage.clear();
|
||||||
this.connectionPromise = null;
|
document.location = '/';
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTheme(event) {
|
setTheme(event) {
|
||||||
|
@ -33,6 +33,8 @@ window.hassCallApi = function (host, auth, method, path, parameters) {
|
|||||||
|
|
||||||
if (auth.authToken) {
|
if (auth.authToken) {
|
||||||
req.setRequestHeader('X-HA-access', auth.authToken);
|
req.setRequestHeader('X-HA-access', auth.authToken);
|
||||||
|
} else if (auth.accessToken) {
|
||||||
|
req.setRequestHeader('authorization', `Bearer ${auth.accessToken}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.onload = function () {
|
req.onload = function () {
|
||||||
|
18
test-mocha/common/util/parse_query_test.js
Normal file
18
test-mocha/common/util/parse_query_test.js
Normal file
@ -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",
|
"browserName": "safari",
|
||||||
"platform": "macOS 10.12",
|
"platform": "macOS 10.12",
|
||||||
"version": "10"
|
"version": "latest"
|
||||||
}, {
|
}, {
|
||||||
"browserName": "firefox",
|
"browserName": "firefox",
|
||||||
"platform": "Windows 10",
|
"platform": "Windows 10",
|
||||||
@ -17,7 +17,7 @@
|
|||||||
}, {
|
}, {
|
||||||
"browserName": "MicrosoftEdge",
|
"browserName": "MicrosoftEdge",
|
||||||
"platform": "Windows 10",
|
"platform": "Windows 10",
|
||||||
"version": "14.14393"
|
"version": "latest"
|
||||||
}, {
|
}, {
|
||||||
"deviceName": "Android GoogleAPI Emulator",
|
"deviceName": "Android GoogleAPI Emulator",
|
||||||
"platformName": "Android",
|
"platformName": "Android",
|
||||||
|
26
yarn.lock
26
yarn.lock
@ -2163,6 +2163,14 @@ commander@^2.8.1:
|
|||||||
version "2.12.2"
|
version "2.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
|
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:
|
commondir@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||||
@ -4234,9 +4242,9 @@ hoek@4.x.x:
|
|||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
|
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
|
||||||
|
|
||||||
home-assistant-js-websocket@^1.1.2:
|
home-assistant-js-websocket@1.2.0:
|
||||||
version "1.1.4"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-1.1.4.tgz#36da056be18210ada76abfa2bc1f247ecbebce11"
|
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-1.2.0.tgz#aa965a7ae47606ea82b919ce74a310ef18412cd7"
|
||||||
|
|
||||||
home-or-tmp@^2.0.0:
|
home-or-tmp@^2.0.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
|
||||||
|
|
||||||
uglify-es@^3.1.9:
|
uglify-es@^3.1.9:
|
||||||
version "3.1.9"
|
version "3.3.9"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.1.9.tgz#6c82df628ac9eb7af9c61fd70c744a084abe6161"
|
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
|
||||||
dependencies:
|
dependencies:
|
||||||
commander "~2.11.0"
|
commander "~2.13.0"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
uglify-js@2.6.x:
|
uglify-js@2.6.x:
|
||||||
@ -8227,10 +8235,10 @@ uglify-js@^3.0.5:
|
|||||||
source-map "~0.5.1"
|
source-map "~0.5.1"
|
||||||
|
|
||||||
uglify-js@^3.1.9:
|
uglify-js@^3.1.9:
|
||||||
version "3.1.9"
|
version "3.3.24"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.1.9.tgz#dffca799308cf327ec3ac77eeacb8e196ce3b452"
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.24.tgz#abeae7690c602ebd006f4567387a0c0c333bdc0d"
|
||||||
dependencies:
|
dependencies:
|
||||||
commander "~2.11.0"
|
commander "~2.15.0"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
uglify-to-browserify@~1.0.0:
|
uglify-to-browserify@~1.0.0:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user