mirror of
https://github.com/home-assistant/frontend.git
synced 2025-05-05 02:28:42 +00:00
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
This commit is contained in:
parent
ec79e12bf3
commit
7cc3fc728b
@ -6,6 +6,7 @@ export default superClass => class extends superClass {
|
|||||||
ready() {
|
ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
this.addEventListener('hass-logout', () => this._handleLogout());
|
this.addEventListener('hass-logout', () => this._handleLogout());
|
||||||
|
this.addEventListener('hass-refresh-current-user', () => this._getCurrentUser());
|
||||||
|
|
||||||
afterNextRender(null, () => {
|
afterNextRender(null, () => {
|
||||||
if (askWrite()) {
|
if (askWrite()) {
|
||||||
@ -20,6 +21,10 @@ export default superClass => class extends superClass {
|
|||||||
hassConnected() {
|
hassConnected() {
|
||||||
super.hassConnected();
|
super.hassConnected();
|
||||||
|
|
||||||
|
this._getCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCurrentUser() {
|
||||||
// only for new auth
|
// only for new auth
|
||||||
if (this.hass.connection.options.accessToken) {
|
if (this.hass.connection.options.accessToken) {
|
||||||
this.hass.callWS({
|
this.hass.callWS({
|
||||||
|
271
src/panels/profile/ha-mfa-module-setup-flow.js
Normal file
271
src/panels/profile/ha-mfa-module-setup-flow.js
Normal file
@ -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`
|
||||||
|
<style include="ha-style-dialog">
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
paper-dialog {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
ha-markdown img:first-child:last-child {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.init-spinner {
|
||||||
|
padding: 10px 100px 34px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.submit-spinner {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<paper-dialog id="dialog" with-backdrop="" opened="{{_opened}}" on-opened-changed="_openedChanged">
|
||||||
|
<h2>
|
||||||
|
<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')]]">
|
||||||
|
[[_computeStepTitle(localize, _step)]]
|
||||||
|
</template>
|
||||||
|
</h2>
|
||||||
|
<paper-dialog-scrollable>
|
||||||
|
<template is="dom-if" if="[[_errorMsg]]">
|
||||||
|
<div class='error'>[[_errorMsg]]</div>
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[!_step]]">
|
||||||
|
<div class='init-spinner'><paper-spinner active></paper-spinner></div>
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[_step]]">
|
||||||
|
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
||||||
|
<ha-markdown content="[[_computeStepAbortedReason(localize, _step)]]"></ha-markdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
||||||
|
<p>Setup done for [[_step.title]]</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
||||||
|
<template is="dom-if" if="[[_computeStepDescription(localize, _step)]]">
|
||||||
|
<ha-markdown content="[[_computeStepDescription(localize, _step)]]"></ha-markdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ha-form
|
||||||
|
data="{{_stepData}}"
|
||||||
|
schema="[[_step.data_schema]]"
|
||||||
|
error="[[_step.errors]]"
|
||||||
|
compute-label="[[_computeLabelCallback(localize, _step)]]"
|
||||||
|
compute-error="[[_computeErrorCallback(localize, _step)]]"
|
||||||
|
></ha-form>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</paper-dialog-scrollable>
|
||||||
|
<div class="buttons">
|
||||||
|
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
||||||
|
<paper-button on-click="_flowDone">Close</paper-button>
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
||||||
|
<paper-button on-click="_flowDone">Close</paper-button>
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
||||||
|
<template is="dom-if" if="[[_loading]]">
|
||||||
|
<div class='submit-spinner'><paper-spinner active></paper-spinner></div>
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[!_loading]]">
|
||||||
|
<paper-button on-click="_submitStep">Submit</paper-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</paper-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
119
src/panels/profile/ha-mfa-modules-card.js
Normal file
119
src/panels/profile/ha-mfa-modules-card.js
Normal file
@ -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`
|
||||||
|
<style include="iron-flex ha-style">
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.error, .status {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
|
paper-card {
|
||||||
|
display: block;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
paper-button {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: -.57em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<paper-card heading="Multi-factor Authenication Modules">
|
||||||
|
<template is="dom-repeat" items="[[mfaModules]]" as="module">
|
||||||
|
<paper-item>
|
||||||
|
<paper-item-body two-line="">
|
||||||
|
<div>[[module.name]]</div>
|
||||||
|
<div secondary="">[[module.id]]</div>
|
||||||
|
</paper-item-body>
|
||||||
|
<template is="dom-if" if="[[module.enabled]]">
|
||||||
|
<paper-button on-click="_disable">Disable</paper-button>
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[!module.enabled]]">
|
||||||
|
<paper-button on-click="_enable">Enable</paper-button>
|
||||||
|
</template>
|
||||||
|
</paper-item>
|
||||||
|
</template>
|
||||||
|
</paper-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
@ -14,6 +14,7 @@ import '../../resources/ha-style.js';
|
|||||||
import EventsMixin from '../../mixins/events-mixin.js';
|
import EventsMixin from '../../mixins/events-mixin.js';
|
||||||
|
|
||||||
import './ha-change-password-card.js';
|
import './ha-change-password-card.js';
|
||||||
|
import './ha-mfa-modules-card.js';
|
||||||
import './ha-pick-language-row.js';
|
import './ha-pick-language-row.js';
|
||||||
import './ha-pick-theme-row.js';
|
import './ha-pick-theme-row.js';
|
||||||
import './ha-push-notifications-row.js';
|
import './ha-push-notifications-row.js';
|
||||||
@ -83,6 +84,7 @@ class HaPanelProfile extends EventsMixin(PolymerElement) {
|
|||||||
<ha-change-password-card hass="[[hass]]"></ha-change-password-card>
|
<ha-change-password-card hass="[[hass]]"></ha-change-password-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<ha-mfa-modules-card hass='[[hass]]' mfa-modules='[[hass.user.mfa_modules]]'></ha-mfa-modules-card>
|
||||||
</div>
|
</div>
|
||||||
</app-header-layout>
|
</app-header-layout>
|
||||||
`;
|
`;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user