From 7cc3fc728bf37e507258071d58a9dc470bd725fe Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 24 Aug 2018 10:35:33 -0700 Subject: [PATCH] Add ha-mfa-modules-card and setup flow (#1590) * Add ha-mfa-modules-card and setup flow * Add hass-refresh-current-user event * Address code review comment --- src/layouts/app/auth-mixin.js | 5 + .../profile/ha-mfa-module-setup-flow.js | 271 ++++++++++++++++++ src/panels/profile/ha-mfa-modules-card.js | 119 ++++++++ src/panels/profile/ha-panel-profile.js | 2 + 4 files changed, 397 insertions(+) create mode 100644 src/panels/profile/ha-mfa-module-setup-flow.js create mode 100644 src/panels/profile/ha-mfa-modules-card.js diff --git a/src/layouts/app/auth-mixin.js b/src/layouts/app/auth-mixin.js index a4de9e7f8..430ccc634 100644 --- a/src/layouts/app/auth-mixin.js +++ b/src/layouts/app/auth-mixin.js @@ -6,6 +6,7 @@ export default superClass => class extends superClass { ready() { super.ready(); this.addEventListener('hass-logout', () => this._handleLogout()); + this.addEventListener('hass-refresh-current-user', () => this._getCurrentUser()); afterNextRender(null, () => { if (askWrite()) { @@ -20,6 +21,10 @@ export default superClass => class extends superClass { hassConnected() { super.hassConnected(); + this._getCurrentUser(); + } + + _getCurrentUser() { // only for new auth if (this.hass.connection.options.accessToken) { this.hass.callWS({ diff --git a/src/panels/profile/ha-mfa-module-setup-flow.js b/src/panels/profile/ha-mfa-module-setup-flow.js new file mode 100644 index 000000000..da40f9b8d --- /dev/null +++ b/src/panels/profile/ha-mfa-module-setup-flow.js @@ -0,0 +1,271 @@ +import '@polymer/paper-button/paper-button.js'; +import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js'; +import '@polymer/paper-dialog/paper-dialog.js'; +import '@polymer/paper-spinner/paper-spinner.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import '../../components/ha-form.js'; +import '../../components/ha-markdown.js'; +import '../../resources/ha-style.js'; + +import EventsMixin from '../../mixins/events-mixin.js'; +import LocalizeMixin from '../../mixins/localize-mixin.js'; + +let instance = 0; + +/* + * @appliesMixin LocalizeMixin + * @appliesMixin EventsMixin + */ +class HaMfaModuleSetupFlow extends + LocalizeMixin(EventsMixin(PolymerElement)) { + static get template() { + return html` + + +

+ + + +

+ + + + + +
+ + + +
+
+`; + } + + static get properties() { + return { + _hass: Object, + _dialogClosedCallback: Function, + _instance: Number, + + _loading: { + type: Boolean, + value: false, + }, + + // Error message when can't talk to server etc + _errorMsg: String, + + _opened: { + type: Boolean, + value: false, + }, + + _step: { + type: Object, + value: null, + }, + + /* + * Store user entered data. + */ + _stepData: Object, + }; + } + + ready() { + super.ready(); + this.addEventListener('keypress', (ev) => { + if (ev.keyCode === 13) { + this._submitStep(); + } + }); + } + + showDialog({ hass, continueFlowId, mfaModuleId, dialogClosedCallback }) { + this.hass = hass; + this._instance = instance++; + this._dialogClosedCallback = dialogClosedCallback; + this._createdFromHandler = !!mfaModuleId; + this._loading = true; + this._opened = true; + + const fetchStep = continueFlowId ? + this.hass.callWS({ + type: 'auth/setup_mfa', + flow_id: continueFlowId, + }) : + this.hass.callWS({ + type: 'auth/setup_mfa', + mfa_module_id: mfaModuleId, + }); + + const curInstance = this._instance; + + fetchStep.then((step) => { + if (curInstance !== this._instance) return; + + this._processStep(step); + this._loading = false; + // When the flow changes, center the dialog. + // Don't do it on each step or else the dialog keeps bouncing. + setTimeout(() => this.$.dialog.center(), 0); + }); + } + + _submitStep() { + this._loading = true; + this._errorMsg = null; + + const curInstance = this._instance; + + this.hass.callWS({ + type: 'auth/setup_mfa', + flow_id: this._step.flow_id, + user_input: this._stepData, + }).then( + (step) => { + if (curInstance !== this._instance) return; + + this._processStep(step); + this._loading = false; + }, + (err) => { + this._errorMsg = (err && err.body && err.body.message) || 'Unknown error occurred'; + this._loading = false; + } + ); + } + + _processStep(step) { + if (!step.errors) step.errors = {}; + this._step = step; + // We got a new form if there are no errors. + if (Object.keys(step.errors).length === 0) { + this._stepData = {}; + } + } + + _flowDone() { + this._opened = false; + const flowFinished = this._step && ['create_entry', 'abort'].includes(this._step.type); + + if (this._step && !flowFinished && this._createdFromHandler) { + // console.log('flow not finish'); + } + + this._dialogClosedCallback({ + flowFinished, + }); + + this._errorMsg = null; + this._step = null; + this._stepData = {}; + this._dialogClosedCallback = null; + } + + _equals(a, b) { + return a === b; + } + + _openedChanged(ev) { + // Closed dialog by clicking on the overlay + if (this._step && !ev.detail.value) { + this._flowDone(); + } + } + + _computeStepAbortedReason(localize, step) { + return localize(`component.auth.mfa_setup.${step.handler}.abort.${step.reason}`); + } + + _computeStepTitle(localize, step) { + return localize(`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.title`) + || 'Setup Multi-factor Authentication'; + } + + _computeStepDescription(localize, step) { + const args = [`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.description`]; + const placeholders = step.description_placeholders || {}; + Object.keys(placeholders).forEach((key) => { + args.push(key); + args.push(placeholders[key]); + }); + return localize(...args); + } + + _computeLabelCallback(localize, step) { + // Returns a callback for ha-form to calculate labels per schema object + return schema => localize(`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.data.${schema.name}`) + || schema.name; + } + + _computeErrorCallback(localize, step) { + // Returns a callback for ha-form to calculate error messages + return error => localize(`component.auth.mfa_setup.${step.handler}.error.${error}`) || error; + } +} + +customElements.define('ha-mfa-module-setup-flow', HaMfaModuleSetupFlow); diff --git a/src/panels/profile/ha-mfa-modules-card.js b/src/panels/profile/ha-mfa-modules-card.js new file mode 100644 index 000000000..84ce9c204 --- /dev/null +++ b/src/panels/profile/ha-mfa-modules-card.js @@ -0,0 +1,119 @@ +import '@polymer/paper-button/paper-button.js'; +import '@polymer/paper-card/paper-card.js'; +import '@polymer/paper-item/paper-item-body.js'; +import '@polymer/paper-item/paper-item.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import '../../resources/ha-style.js'; + +import EventsMixin from '../../mixins/events-mixin.js'; +import LocalizeMixin from '../../mixins/localize-mixin.js'; + +let registeredDialog = false; + +/* + * @appliesMixin EventsMixin + * @appliesMixin LocalizeMixin + */ +class HaMfaModulesCard extends EventsMixin(LocalizeMixin(PolymerElement)) { + static get template() { + return html` + + + + +`; + } + + static get properties() { + return { + hass: Object, + + _loading: { + type: Boolean, + value: false, + }, + + // Error message when can't talk to server etc + _statusMsg: String, + _errorMsg: String, + + mfaModules: Array, + }; + } + + connectedCallback() { + super.connectedCallback(); + + if (!registeredDialog) { + registeredDialog = true; + this.fire('register-dialog', { + dialogShowEvent: 'show-mfa-module-setup-flow', + dialogTag: 'ha-mfa-module-setup-flow', + dialogImport: () => import('./ha-mfa-module-setup-flow.js'), + }); + } + } + + _enable(ev) { + this.fire('show-mfa-module-setup-flow', { + hass: this.hass, + mfaModuleId: ev.model.module.id, + dialogClosedCallback: () => this._refreshCurrentUser(), + }); + } + + _disable(ev) { + if (!confirm(`Are you sure you want to disable ${ev.model.module.name}?`)) return; + + const mfaModuleId = ev.model.module.id; + + this.hass.callWS({ + type: 'auth/depose_mfa', + mfa_module_id: mfaModuleId, + }).then(() => { + this._refreshCurrentUser(); + }); + } + + _refreshCurrentUser() { + this.fire('hass-refresh-current-user'); + } +} + +customElements.define('ha-mfa-modules-card', HaMfaModulesCard); diff --git a/src/panels/profile/ha-panel-profile.js b/src/panels/profile/ha-panel-profile.js index fc3744419..e4e6d8442 100644 --- a/src/panels/profile/ha-panel-profile.js +++ b/src/panels/profile/ha-panel-profile.js @@ -14,6 +14,7 @@ import '../../resources/ha-style.js'; import EventsMixin from '../../mixins/events-mixin.js'; import './ha-change-password-card.js'; +import './ha-mfa-modules-card.js'; import './ha-pick-language-row.js'; import './ha-pick-theme-row.js'; import './ha-push-notifications-row.js'; @@ -83,6 +84,7 @@ class HaPanelProfile extends EventsMixin(PolymerElement) { + `;