mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 16:26:43 +00:00
parent
63c7c55843
commit
310299367b
@ -1,17 +0,0 @@
|
|||||||
export default function fetchToken(clientId, code) {
|
|
||||||
const data = new FormData();
|
|
||||||
data.append('client_id', clientId);
|
|
||||||
data.append('grant_type', 'authorization_code');
|
|
||||||
data.append('code', code);
|
|
||||||
return fetch('/auth/token', {
|
|
||||||
credentials: 'same-origin',
|
|
||||||
method: 'POST',
|
|
||||||
body: data,
|
|
||||||
}).then((resp) => {
|
|
||||||
if (!resp.ok) throw new Error('Unable to fetch tokens');
|
|
||||||
return resp.json().then((tokens) => {
|
|
||||||
tokens.expires = (tokens.expires_in * 1000) + Date.now();
|
|
||||||
return tokens;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export default function refreshAccessToken(clientId, refreshToken) {
|
|
||||||
const data = new FormData();
|
|
||||||
data.append('client_id', clientId);
|
|
||||||
data.append('grant_type', 'refresh_token');
|
|
||||||
data.append('refresh_token', refreshToken);
|
|
||||||
return fetch('/auth/token', {
|
|
||||||
credentials: 'same-origin',
|
|
||||||
method: 'POST',
|
|
||||||
body: data,
|
|
||||||
}).then((resp) => {
|
|
||||||
if (!resp.ok) throw new Error('Unable to fetch tokens');
|
|
||||||
return resp.json().then((tokens) => {
|
|
||||||
tokens.expires = (tokens.expires_in * 1000) + Date.now();
|
|
||||||
return tokens;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
76
src/common/auth/token.js
Normal file
76
src/common/auth/token.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { storeTokens, loadTokens } from './token_storage.js';
|
||||||
|
|
||||||
|
function genClientId() {
|
||||||
|
return `${location.protocol}//${location.host}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function redirectLogin() {
|
||||||
|
document.location.href = `/auth/authorize?response_type=code&client_id=${encodeURIComponent(genClientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
|
||||||
|
return new Promise((() => {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function fetchTokenRequest(code) {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('client_id', genClientId());
|
||||||
|
data.append('grant_type', 'authorization_code');
|
||||||
|
data.append('code', code);
|
||||||
|
return fetch('/auth/token', {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}).then((resp) => {
|
||||||
|
if (!resp.ok) throw new Error('Unable to fetch tokens');
|
||||||
|
return resp.json().then((tokens) => {
|
||||||
|
tokens.expires = (tokens.expires_in * 1000) + Date.now();
|
||||||
|
return tokens;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTokenRequest(tokens) {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('client_id', genClientId());
|
||||||
|
data.append('grant_type', 'refresh_token');
|
||||||
|
data.append('refresh_token', tokens.refresh_token);
|
||||||
|
return fetch('/auth/token', {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}).then((resp) => {
|
||||||
|
if (!resp.ok) throw new Error('Unable to fetch tokens');
|
||||||
|
return resp.json().then((newTokens) => {
|
||||||
|
newTokens.expires = (newTokens.expires_in * 1000) + Date.now();
|
||||||
|
return newTokens;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCode(code) {
|
||||||
|
return fetchTokenRequest(code).then((tokens) => {
|
||||||
|
storeTokens(tokens);
|
||||||
|
history.replaceState(null, null, location.pathname);
|
||||||
|
return tokens;
|
||||||
|
}, (err) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error('Resolve token failed', err);
|
||||||
|
alert('Unable to fetch tokens');
|
||||||
|
redirectLogin();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function refreshToken() {
|
||||||
|
const tokens = loadTokens();
|
||||||
|
|
||||||
|
if (tokens === null) {
|
||||||
|
return redirectLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshTokenRequest(tokens).then((accessTokenResp) => {
|
||||||
|
const newTokens = Object.assign({}, tokens, accessTokenResp);
|
||||||
|
storeTokens(newTokens);
|
||||||
|
return newTokens;
|
||||||
|
}, () => redirectLogin());
|
||||||
|
}
|
45
src/common/auth/token_storage.js
Normal file
45
src/common/auth/token_storage.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
const storage = window.localStorage || {};
|
||||||
|
|
||||||
|
// So that core.js and main app hit same shared object.
|
||||||
|
let tokenCache = window.__tokenCache;
|
||||||
|
if (!tokenCache) {
|
||||||
|
tokenCache = window.__tokenCache = {
|
||||||
|
tokens: undefined,
|
||||||
|
writeEnabled: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function askWrite() {
|
||||||
|
return tokenCache.writeEnabled === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeTokens(tokens) {
|
||||||
|
tokenCache.tokens = tokens;
|
||||||
|
if (tokenCache.writeEnabled) {
|
||||||
|
try {
|
||||||
|
storage.tokens = JSON.stringify(tokens);
|
||||||
|
} catch (err) {} // eslint-disable-line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableWrite() {
|
||||||
|
tokenCache.writeEnabled = true;
|
||||||
|
storeTokens(tokenCache.tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadTokens() {
|
||||||
|
if (tokenCache.tokens === undefined) {
|
||||||
|
try {
|
||||||
|
const tokens = storage.tokens;
|
||||||
|
if (tokens) {
|
||||||
|
tokenCache.tokens = JSON.parse(tokens);
|
||||||
|
tokenCache.writeEnabled = true;
|
||||||
|
} else {
|
||||||
|
tokenCache.tokens = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
tokenCache.tokens = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokenCache.tokens;
|
||||||
|
}
|
69
src/dialogs/ha-store-auth-card.js
Normal file
69
src/dialogs/ha-store-auth-card.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import '@polymer/paper-card/paper-card.js';
|
||||||
|
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||||
|
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||||
|
|
||||||
|
import { enableWrite } from '../common/auth/token_storage.js';
|
||||||
|
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||||
|
|
||||||
|
import '../resources/ha-style.js';
|
||||||
|
|
||||||
|
class HaStoreAuth extends LocalizeMixin(PolymerElement) {
|
||||||
|
static get template() {
|
||||||
|
return html`
|
||||||
|
<style include='ha-style'>
|
||||||
|
paper-card {
|
||||||
|
position: fixed;
|
||||||
|
padding: 8px 0;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
text-align: right;
|
||||||
|
border-top: 0;
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.small) paper-card {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<paper-card elevation="4">
|
||||||
|
<div class='card-content'>
|
||||||
|
[[localize('ui.auth_store.ask')]]
|
||||||
|
</div>
|
||||||
|
<div class='card-actions'>
|
||||||
|
<paper-button on-click='_done'>[[localize('ui.auth_store.decline')]]</paper-button>
|
||||||
|
<paper-button primary on-click='_save'>[[localize('ui.auth_store.confirm')]]</paper-button>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ready() {
|
||||||
|
super.ready();
|
||||||
|
this.classList.toggle('small', window.innerWidth < 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
_save() {
|
||||||
|
enableWrite();
|
||||||
|
this._done();
|
||||||
|
}
|
||||||
|
|
||||||
|
_done() {
|
||||||
|
const card = this.shadowRoot.querySelector('paper-card');
|
||||||
|
card.style.transition = 'bottom .25s';
|
||||||
|
card.style.bottom = `-${card.offsetHeight + 8}px`;
|
||||||
|
setTimeout(() => this.parentNode.removeChild(this), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('ha-store-auth-card', HaStoreAuth);
|
@ -5,9 +5,10 @@ import {
|
|||||||
subscribeEntities,
|
subscribeEntities,
|
||||||
} from 'home-assistant-js-websocket';
|
} from 'home-assistant-js-websocket';
|
||||||
|
|
||||||
import fetchToken from '../common/auth/fetch_token.js';
|
import { redirectLogin, resolveCode, refreshToken } from '../common/auth/token.js';
|
||||||
import refreshToken_ from '../common/auth/refresh_token.js';
|
// import refreshToken_ from '../common/auth/refresh_token.js';
|
||||||
import parseQuery from '../common/util/parse_query.js';
|
import parseQuery from '../common/util/parse_query.js';
|
||||||
|
import { loadTokens } from '../common/auth/token_storage.js';
|
||||||
|
|
||||||
const init = window.createHassConnection = function (password, accessToken) {
|
const init = window.createHassConnection = function (password, accessToken) {
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
@ -29,68 +30,11 @@ const init = window.createHassConnection = function (password, accessToken) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function clientId() {
|
|
||||||
return `${location.protocol}//${location.host}/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectLogin() {
|
|
||||||
document.location.href = `/auth/authorize?response_type=code&client_id=${encodeURIComponent(clientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
|
|
||||||
return new Promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
let tokenCache;
|
|
||||||
|
|
||||||
function storeTokens(tokens) {
|
|
||||||
tokenCache = tokens;
|
|
||||||
try {
|
|
||||||
localStorage.tokens = JSON.stringify(tokens);
|
|
||||||
} catch (err) {} // eslint-disable-line
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTokens() {
|
|
||||||
if (tokenCache === undefined) {
|
|
||||||
try {
|
|
||||||
const tokens = localStorage.tokens;
|
|
||||||
tokenCache = tokens ? JSON.parse(tokens) : null;
|
|
||||||
} catch (err) {
|
|
||||||
tokenCache = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tokenCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.refreshToken = () => {
|
|
||||||
const tokens = loadTokens();
|
|
||||||
|
|
||||||
if (tokens === null) {
|
|
||||||
return redirectLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
return refreshToken_(clientId(), tokens.refresh_token).then((accessTokenResp) => {
|
|
||||||
const newTokens = Object.assign({}, tokens, accessTokenResp);
|
|
||||||
storeTokens(newTokens);
|
|
||||||
return newTokens;
|
|
||||||
}, () => redirectLogin());
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveCode(code) {
|
|
||||||
fetchToken(clientId(), code).then((tokens) => {
|
|
||||||
storeTokens(tokens);
|
|
||||||
// Refresh the page and have tokens in place.
|
|
||||||
document.location.href = location.pathname;
|
|
||||||
}, (err) => {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.error('Resolve token failed', err);
|
|
||||||
alert('Unable to fetch tokens');
|
|
||||||
redirectLogin();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
if (location.search) {
|
if (location.search) {
|
||||||
const query = parseQuery(location.search.substr(1));
|
const query = parseQuery(location.search.substr(1));
|
||||||
if (query.code) {
|
if (query.code) {
|
||||||
resolveCode(query.code);
|
window.hassConnection = resolveCode(query.code).then(newTokens => init(null, newTokens));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,14 +47,14 @@ function main() {
|
|||||||
|
|
||||||
if (Date.now() + 30000 > tokens.expires) {
|
if (Date.now() + 30000 > tokens.expires) {
|
||||||
// refresh access token if it will expire in 30 seconds to avoid invalid auth event
|
// refresh access token if it will expire in 30 seconds to avoid invalid auth event
|
||||||
window.hassConnection = window.refreshToken().then(newTokens => init(null, newTokens));
|
window.hassConnection = refreshToken().then(newTokens => init(null, newTokens));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.hassConnection = init(null, tokens).catch((err) => {
|
window.hassConnection = init(null, tokens).catch((err) => {
|
||||||
if (err !== ERR_INVALID_AUTH) throw err;
|
if (err !== ERR_INVALID_AUTH) throw err;
|
||||||
|
|
||||||
return window.refreshToken().then(newTokens => init(null, newTokens));
|
return refreshToken().then(newTokens => init(null, newTokens));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
|
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
||||||
import { clearState } from '../../util/ha-pref-storage.js';
|
import { clearState } from '../../util/ha-pref-storage.js';
|
||||||
|
import { askWrite } from '../../common/auth/token_storage.js';
|
||||||
|
|
||||||
export default superClass => class extends superClass {
|
export default superClass => class extends superClass {
|
||||||
ready() {
|
ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
this.addEventListener('hass-logout', () => this._handleLogout());
|
this.addEventListener('hass-logout', () => this._handleLogout());
|
||||||
|
|
||||||
|
afterNextRender(null, () => {
|
||||||
|
if (askWrite()) {
|
||||||
|
const el = document.createElement('ha-store-auth-card');
|
||||||
|
this.shadowRoot.appendChild(el);
|
||||||
|
this.provideHass(el);
|
||||||
|
import(/* webpackChunkName: "ha-store-auth-card" */ '../../dialogs/ha-store-auth-card.js');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hassConnected() {
|
hassConnected() {
|
||||||
|
@ -9,6 +9,7 @@ import translationMetadata from '../../../build-translations/translationMetadata
|
|||||||
import LocalizeMixin from '../../mixins/localize-mixin.js';
|
import LocalizeMixin from '../../mixins/localize-mixin.js';
|
||||||
import EventsMixin from '../../mixins/events-mixin.js';
|
import EventsMixin from '../../mixins/events-mixin.js';
|
||||||
|
|
||||||
|
import { refreshToken } from '../../common/auth/token.js';
|
||||||
import { getState } from '../../util/ha-pref-storage.js';
|
import { getState } from '../../util/ha-pref-storage.js';
|
||||||
import { getActiveTranslation } from '../../util/hass-translation.js';
|
import { getActiveTranslation } from '../../util/hass-translation.js';
|
||||||
import hassCallApi from '../../util/hass-call-api.js';
|
import hassCallApi from '../../util/hass-call-api.js';
|
||||||
@ -103,7 +104,7 @@ export default superClass =>
|
|||||||
try {
|
try {
|
||||||
// Refresh token if it will expire in 30 seconds
|
// Refresh token if it will expire in 30 seconds
|
||||||
if (auth.accessToken && Date.now() + 30000 > auth.expires) {
|
if (auth.accessToken && Date.now() + 30000 > auth.expires) {
|
||||||
const accessToken = await window.refreshToken();
|
const accessToken = await refreshToken();
|
||||||
conn.options.accessToken = accessToken.access_token;
|
conn.options.accessToken = accessToken.access_token;
|
||||||
conn.options.expires = accessToken.expires;
|
conn.options.expires = accessToken.expires;
|
||||||
}
|
}
|
||||||
@ -112,7 +113,7 @@ export default superClass =>
|
|||||||
if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
|
if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
|
||||||
|
|
||||||
// If we connect with access token and get 401, refresh token and try again
|
// If we connect with access token and get 401, refresh token and try again
|
||||||
const accessToken = await window.refreshToken();
|
const accessToken = await refreshToken();
|
||||||
conn.options.accessToken = accessToken.access_token;
|
conn.options.accessToken = accessToken.access_token;
|
||||||
conn.options.expires = accessToken.expires;
|
conn.options.expires = accessToken.expires;
|
||||||
return await hassCallApi(host, auth, method, path, parameters);
|
return await hassCallApi(host, auth, method, path, parameters);
|
||||||
@ -159,7 +160,7 @@ export default superClass =>
|
|||||||
while (this.unsubFuncs.length) {
|
while (this.unsubFuncs.length) {
|
||||||
this.unsubFuncs.pop()();
|
this.unsubFuncs.pop()();
|
||||||
}
|
}
|
||||||
const accessToken = await window.refreshToken();
|
const accessToken = await refreshToken();
|
||||||
this._handleNewConnProm(window.createHassConnection(null, accessToken));
|
this._handleNewConnProm(window.createHassConnection(null, accessToken));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export default superClass => class extends superClass {
|
|||||||
|
|
||||||
provideHass(el) {
|
provideHass(el) {
|
||||||
this.__provideHass.push(el);
|
this.__provideHass.push(el);
|
||||||
|
el.hass = this.hass;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateHass(obj) {
|
async _updateHass(obj) {
|
||||||
|
@ -191,6 +191,12 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
.card-actions ha-call-service-button.warning:not([disabled]) {
|
.card-actions ha-call-service-button.warning:not([disabled]) {
|
||||||
color: var(--google-red-500);
|
color: var(--google-red-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-actions paper-button[primary] {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</template>
|
</template>
|
||||||
</dom-module><dom-module id="ha-style-dialog">
|
</dom-module><dom-module id="ha-style-dialog">
|
||||||
|
@ -326,6 +326,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
|
"auth_store": {
|
||||||
|
"ask": "Do you want to save this login?",
|
||||||
|
"decline": "No thanks",
|
||||||
|
"confirm": "Save login"
|
||||||
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"alarm_control_panel": {
|
"alarm_control_panel": {
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user