diff --git a/src/panels/profile/dialog-ha-mfa-module-setup-flow.ts b/src/panels/profile/dialog-ha-mfa-module-setup-flow.ts new file mode 100644 index 000000000000..bb3aa6758776 --- /dev/null +++ b/src/panels/profile/dialog-ha-mfa-module-setup-flow.ts @@ -0,0 +1,281 @@ +import "@material/mwc-button"; +import { + css, + CSSResult, + customElement, + internalProperty, + LitElement, + property, +} from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { localizeKey } from "../../common/translations/localize"; +import "../../components/ha-circular-progress"; +import "../../components/ha-form/ha-form"; +import "../../components/ha-markdown"; +import { + DataEntryFlowStep, + DataEntryFlowStepForm, +} from "../../data/data_entry_flow"; +import { haStyleDialog } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; +import "../../components/ha-dialog"; + +let instance = 0; + +@customElement("ha-mfa-module-setup-flow") +class HaMfaModuleSetupFlow extends LitElement { + @property() public hass!: HomeAssistant; + + @internalProperty() private _dialogClosedCallback?: (params: { + flowFinished: boolean; + }) => void; + + @internalProperty() private _instance?: number; + + @internalProperty() private _loading = false; + + @internalProperty() private _opened = false; + + @internalProperty() private _stepData: any = {}; + + @internalProperty() private _step?: DataEntryFlowStep; + + @internalProperty() private _errorMessage?: string; + + public showDialog({ continueFlowId, mfaModuleId, dialogClosedCallback }) { + this._instance = instance++; + this._dialogClosedCallback = dialogClosedCallback; + 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); + }); + } + + public closeDialog() { + // Closed dialog by clicking on the overlay + if (this._step) { + this._flowDone(); + } + this._opened = false; + } + + protected render(): TemplateResult { + if (!this._opened) { + return html``; + } + return html` + +
+ ${this._errorMessage + ? html`
${this._errorMessage}
` + : ""} + ${!this._step + ? html`
+ +
` + : html`${this._step.type === "abort" + ? html` ` + : this._step.type === "create_entry" + ? html`

+ ${this.hass.localize( + "ui.panel.profile.mfa_setup.step_done", + "step", + this._step.title + )} +

` + : this._step.type === "form" + ? html` + ` + : ""}`} +
+ ${["abort", "create_entry"].includes(this._step?.type || "") + ? html`${this.hass.localize( + "ui.panel.profile.mfa_setup.close" + )}` + : ""} + ${this._step?.type === "form" + ? html`${this.hass.localize( + "ui.panel.profile.mfa_setup.submit" + )}` + : ""} +
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + .error { + color: red; + } + ha-dialog { + max-width: 500px; + } + ha-markdown { + --markdown-svg-background-color: white; + --markdown-svg-color: black; + display: block; + margin: 0 auto; + } + ha-markdown a { + color: var(--primary-color); + } + .init-spinner { + padding: 10px 100px 34px; + text-align: center; + } + .submit-spinner { + margin-right: 16px; + } + `, + ]; + } + + protected firstUpdated(changedProperties) { + super.firstUpdated(changedProperties); + this.hass.loadBackendTranslation("mfa_setup", "auth"); + this.addEventListener("keypress", (ev) => { + if (ev.key === "Enter") { + this._submitStep(); + } + }); + } + + private _stepDataChanged(ev: CustomEvent) { + this._stepData = ev.detail.value; + } + + private _submitStep() { + this._loading = true; + this._errorMessage = undefined; + + 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._errorMessage = + (err && err.body && err.body.message) || "Unknown error occurred"; + this._loading = false; + } + ); + } + + private _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 = {}; + } + } + + private _flowDone() { + const flowFinished = Boolean( + this._step && ["create_entry", "abort"].includes(this._step.type) + ); + + this._dialogClosedCallback!({ + flowFinished, + }); + + this._errorMessage = undefined; + this._step = undefined; + this._stepData = {}; + this._dialogClosedCallback = undefined; + this.closeDialog(); + } + + private _computeStepTitle() { + return this._step?.type === "abort" + ? this.hass.localize("ui.panel.profile.mfa_setup.title_aborted") + : this._step?.type === "create_entry" + ? this.hass.localize("ui.panel.profile.mfa_setup.title_success") + : this._step?.type === "form" + ? this.hass.localize( + `component.auth.mfa_setup.${this._step.handler}.step.${this._step.step_id}.title` + ) + : ""; + } + + private _computeLabel = (schema) => + this.hass.localize( + `component.auth.mfa_setup.${this._step!.handler}.step.${ + (this._step! as DataEntryFlowStepForm).step_id + }.data.${schema.name}` + ) || schema.name; + + private _computeError = (error) => + this.hass.localize( + `component.auth.mfa_setup.${this._step!.handler}.error.${error}` + ) || error; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-mfa-module-setup-flow": HaMfaModuleSetupFlow; + } +} diff --git a/src/panels/profile/ha-mfa-module-setup-flow.js b/src/panels/profile/ha-mfa-module-setup-flow.js deleted file mode 100644 index 2e4a66fd1f15..000000000000 --- a/src/panels/profile/ha-mfa-module-setup-flow.js +++ /dev/null @@ -1,322 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../components/dialog/ha-paper-dialog"; -import "../../components/ha-circular-progress"; -import "../../components/ha-form/ha-form"; -import "../../components/ha-markdown"; -import { EventsMixin } from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../styles/polymer-ha-style-dialog"; - -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.hass.loadBackendTranslation("mfa_setup", "auth"); - 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 deleted file mode 100644 index a118d6e1ccb0..000000000000 --- a/src/panels/profile/ha-mfa-modules-card.js +++ /dev/null @@ -1,130 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../components/ha-card"; -import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; -import { EventsMixin } from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../styles/polymer-ha-style"; - -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"), - }); - } - } - - _enable(ev) { - this.fire("show-mfa-module-setup-flow", { - hass: this.hass, - mfaModuleId: ev.model.module.id, - dialogClosedCallback: () => this._refreshCurrentUser(), - }); - } - - async _disable(ev) { - const mfamodule = ev.model.module; - if ( - !(await showConfirmationDialog(this, { - text: this.localize( - "ui.panel.profile.mfa.confirm_disable", - "name", - mfamodule.name - ), - })) - ) { - return; - } - - const mfaModuleId = mfamodule.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-mfa-modules-card.ts b/src/panels/profile/ha-mfa-modules-card.ts new file mode 100644 index 000000000000..f5680cb12725 --- /dev/null +++ b/src/panels/profile/ha-mfa-modules-card.ts @@ -0,0 +1,101 @@ +import "@material/mwc-button"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-card"; +import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import { HomeAssistant, MFAModule } from "../../types"; +import { showMfaModuleSetupFlowDialog } from "./show-ha-mfa-module-setup-flow-dialog"; + +@customElement("ha-mfa-modules-card") +class HaMfaModulesCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public mfaModules!: MFAModule[]; + + protected render(): TemplateResult { + return html` + + ${this.mfaModules.map( + (module) => html` + +
${module.name}
+
${module.id}
+
+ ${module.enabled + ? html`${this.hass.localize( + "ui.panel.profile.mfa.disable" + )}` + : html`${this.hass.localize( + "ui.panel.profile.mfa.enable" + )}`} +
` + )} +
+ `; + } + + static get styles(): CSSResult { + return css` + mwc-button { + margin-right: -0.57em; + } + `; + } + + private _enable(ev) { + showMfaModuleSetupFlowDialog(this, { + mfaModuleId: ev.currentTarget.module.id, + dialogClosedCallback: () => this._refreshCurrentUser(), + }); + } + + private async _disable(ev) { + const mfamodule = ev.currentTarget.module; + if ( + !(await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.profile.mfa.confirm_disable", + "name", + mfamodule.name + ), + })) + ) { + return; + } + + const mfaModuleId = mfamodule.id; + + this.hass + .callWS({ + type: "auth/depose_mfa", + mfa_module_id: mfaModuleId, + }) + .then(() => { + this._refreshCurrentUser(); + }); + } + + private _refreshCurrentUser() { + fireEvent(this, "hass-refresh-current-user"); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-mfa-modules-card": HaMfaModulesCard; + } +} diff --git a/src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts b/src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts new file mode 100644 index 000000000000..6c5e6495cb18 --- /dev/null +++ b/src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +export interface MfaModuleSetupFlowDialogParams { + continueFlowId?: string; + mfaModuleId?: string; + dialogClosedCallback: (params: { flowFinished: boolean }) => void; +} + +export const loadMfaModuleSetupFlowDialog = () => + import("./dialog-ha-mfa-module-setup-flow"); + +export const showMfaModuleSetupFlowDialog = ( + element: HTMLElement, + dialogParams: MfaModuleSetupFlowDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-mfa-module-setup-flow", + dialogImport: loadMfaModuleSetupFlowDialog, + dialogParams, + }); +};