mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 08:16:36 +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,
|
||||
} from 'home-assistant-js-websocket';
|
||||
|
||||
import fetchToken from '../common/auth/fetch_token.js';
|
||||
import refreshToken_ from '../common/auth/refresh_token.js';
|
||||
import { redirectLogin, resolveCode, refreshToken } from '../common/auth/token.js';
|
||||
// import refreshToken_ from '../common/auth/refresh_token.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 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() {
|
||||
if (location.search) {
|
||||
const query = parseQuery(location.search.substr(1));
|
||||
if (query.code) {
|
||||
resolveCode(query.code);
|
||||
window.hassConnection = resolveCode(query.code).then(newTokens => init(null, newTokens));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -103,14 +47,14 @@ function main() {
|
||||
|
||||
if (Date.now() + 30000 > tokens.expires) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
window.hassConnection = init(null, tokens).catch((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 { askWrite } from '../../common/auth/token_storage.js';
|
||||
|
||||
export default superClass => class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
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() {
|
||||
|
@ -9,6 +9,7 @@ import translationMetadata from '../../../build-translations/translationMetadata
|
||||
import LocalizeMixin from '../../mixins/localize-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 { getActiveTranslation } from '../../util/hass-translation.js';
|
||||
import hassCallApi from '../../util/hass-call-api.js';
|
||||
@ -103,7 +104,7 @@ export default superClass =>
|
||||
try {
|
||||
// Refresh token if it will expire in 30 seconds
|
||||
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.expires = accessToken.expires;
|
||||
}
|
||||
@ -112,7 +113,7 @@ export default superClass =>
|
||||
if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
|
||||
|
||||
// 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.expires = accessToken.expires;
|
||||
return await hassCallApi(host, auth, method, path, parameters);
|
||||
@ -159,7 +160,7 @@ export default superClass =>
|
||||
while (this.unsubFuncs.length) {
|
||||
this.unsubFuncs.pop()();
|
||||
}
|
||||
const accessToken = await window.refreshToken();
|
||||
const accessToken = await refreshToken();
|
||||
this._handleNewConnProm(window.createHassConnection(null, accessToken));
|
||||
};
|
||||
|
||||
|
@ -18,6 +18,7 @@ export default superClass => class extends superClass {
|
||||
|
||||
provideHass(el) {
|
||||
this.__provideHass.push(el);
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
async _updateHass(obj) {
|
||||
|
@ -191,6 +191,12 @@ documentContainer.innerHTML = `<custom-style>
|
||||
.card-actions ha-call-service-button.warning:not([disabled]) {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
|
||||
.card-actions paper-button[primary] {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
</style>
|
||||
</template>
|
||||
</dom-module><dom-module id="ha-style-dialog">
|
||||
|
@ -326,6 +326,11 @@
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"auth_store": {
|
||||
"ask": "Do you want to save this login?",
|
||||
"decline": "No thanks",
|
||||
"confirm": "Save login"
|
||||
},
|
||||
"card": {
|
||||
"alarm_control_panel": {
|
||||
"code": "Code",
|
||||
|
Loading…
x
Reference in New Issue
Block a user