diff --git a/src/auth/ha-auth-flow.js b/src/auth/ha-auth-flow.js deleted file mode 100644 index d397aa4f4e..0000000000 --- a/src/auth/ha-auth-flow.js +++ /dev/null @@ -1,270 +0,0 @@ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "@material/mwc-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import "../components/ha-form"; -import "../components/ha-markdown"; -import { localizeLiteMixin } from "../mixins/localize-lite-mixin"; - -class HaAuthFlow extends localizeLiteMixin(PolymerElement) { - static get template() { - return html` - -
- - - -
- `; - } - - static get properties() { - return { - authProvider: { - type: Object, - observer: "_providerChanged", - }, - clientId: String, - redirectUri: String, - oauth2State: String, - _state: { - type: String, - value: "loading", - }, - _stepData: { - type: Object, - value: () => ({}), - }, - _step: { - type: Object, - notify: true, - }, - _errorMsg: String, - }; - } - - ready() { - super.ready(); - - this.addEventListener("keypress", (ev) => { - if (ev.keyCode === 13) { - this._handleSubmit(ev); - } - }); - } - - async _providerChanged(newProvider, oldProvider) { - if (oldProvider && this._step && this._step.type === "form") { - fetch(`/auth/login_flow/${this._step.flow_id}`, { - method: "DELETE", - credentials: "same-origin", - }).catch(() => {}); - } - - try { - const response = await fetch("/auth/login_flow", { - method: "POST", - credentials: "same-origin", - body: JSON.stringify({ - client_id: this.clientId, - handler: [newProvider.type, newProvider.id], - redirect_uri: this.redirectUri, - }), - }); - - const data = await response.json(); - - if (response.ok) { - // allow auth provider bypass the login form - if (data.type === "create_entry") { - this._redirect(data.result); - return; - } - - this._updateStep(data); - } else { - this.setProperties({ - _state: "error", - _errorMsg: data.message, - }); - } - } catch (err) { - // eslint-disable-next-line - console.error("Error starting auth flow", err); - this.setProperties({ - _state: "error", - _errorMsg: this.localize("ui.panel.page-authorize.form.unknown_error"), - }); - } - } - - _redirect(authCode) { - // OAuth 2: 3.1.2 we need to retain query component of a redirect URI - let url = this.redirectUri; - if (!url.includes("?")) { - url += "?"; - } else if (!url.endsWith("&")) { - url += "&"; - } - - url += `code=${encodeURIComponent(authCode)}`; - - if (this.oauth2State) { - url += `&state=${encodeURIComponent(this.oauth2State)}`; - } - - document.location = url; - } - - _updateStep(step) { - const props = { - _step: step, - _state: "step", - }; - - if ( - this._step && - (step.flow_id !== this._step.flow_id || - step.step_id !== this._step.step_id) - ) { - props._stepData = {}; - } - - this.setProperties(props); - } - - _equals(a, b) { - return a === b; - } - - _computeSubmitCaption(stepType) { - return stepType === "form" ? "Next" : "Start over"; - } - - _computeStepAbortedReason(localize, step) { - return localize( - `ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${ - step.reason - }` - ); - } - - _computeStepDescription(localize, step) { - const args = [ - `ui.panel.page-authorize.form.providers.${step.handler[0]}.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( - `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${ - step.step_id - }.data.${schema.name}` - ); - } - - _computeErrorCallback(localize, step) { - // Returns a callback for ha-form to calculate error messages - return (error) => - localize( - `ui.panel.page-authorize.form.providers.${ - step.handler[0] - }.error.${error}` - ); - } - - async _handleSubmit(ev) { - ev.preventDefault(); - if (this._step.type !== "form") { - this._providerChanged(this.authProvider, null); - return; - } - this._state = "loading"; - // To avoid a jumping UI. - this.style.setProperty("min-height", `${this.offsetHeight}px`); - - const postData = Object.assign({}, this._stepData, { - client_id: this.clientId, - }); - - try { - const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, { - method: "POST", - credentials: "same-origin", - body: JSON.stringify(postData), - }); - - const newStep = await response.json(); - - if (newStep.type === "create_entry") { - this._redirect(newStep.result); - return; - } - this._updateStep(newStep); - } catch (err) { - // eslint-disable-next-line - console.error("Error submitting step", err); - this._state = "error-loading"; - } finally { - this.style.setProperty("min-height", ""); - } - } -} -customElements.define("ha-auth-flow", HaAuthFlow); diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts new file mode 100644 index 0000000000..72e488117e --- /dev/null +++ b/src/auth/ha-auth-flow.ts @@ -0,0 +1,296 @@ +import { LitElement, html, property, PropertyValues } from "lit-element"; +import "@material/mwc-button"; +import "../components/ha-form"; +import "../components/ha-markdown"; +import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; +import { AuthProvider } from "../data/auth"; +import { ConfigFlowStep, ConfigFlowStepForm } from "../data/config_entries"; + +type State = "loading" | "error" | "step"; + +class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { + @property() public authProvider?: AuthProvider; + @property() public clientId?: string; + @property() public redirectUri?: string; + @property() public oauth2State?: string; + @property() private _state: State = "loading"; + @property() private _stepData: any = {}; + @property() private _step?: ConfigFlowStep; + @property() private _errorMessage?: string; + + protected render() { + return html` + +
+ ${this._renderForm()} +
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + if (this.clientId == null || this.redirectUri == null) { + // tslint:disable-next-line: no-console + console.error( + "clientId and redirectUri must not be null", + this.clientId, + this.redirectUri + ); + this._state = "error"; + this._errorMessage = this._unknownError(); + return; + } + + this.addEventListener("keypress", (ev) => { + if (ev.keyCode === 13) { + this._handleSubmit(ev); + } + }); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("authProvider")) { + this._providerChanged(this.authProvider); + } + } + + private _renderForm() { + switch (this._state) { + case "step": + if (this._step == null) { + return html``; + } + return html` + ${this._renderStep(this._step)} +
+ ${this._step.type === "form" ? "Next" : "Start over"} +
+ `; + case "error": + return html` +
Error: ${this._errorMessage}
+ `; + case "loading": + return html` + ${this.localize("ui.panel.page-authorize.form.working")} + `; + } + } + + private _renderStep(step: ConfigFlowStep) { + switch (step.type) { + case "abort": + return html` + ${this.localize("ui.panel.page-authorize.abort_intro")}: + + `; + case "form": + return html` + ${this._computeStepDescription(step) + ? html` + + ` + : html``} + + `; + default: + return html``; + } + } + + private async _providerChanged(newProvider?: AuthProvider) { + if (this._step && this._step.type === "form") { + fetch(`/auth/login_flow/${this._step.flow_id}`, { + method: "DELETE", + credentials: "same-origin", + }).catch((err) => { + // tslint:disable-next-line: no-console + console.error("Error delete obsoleted auth flow", err); + }); + } + + if (newProvider == null) { + // tslint:disable-next-line: no-console + console.error("No auth provider"); + this._state = "error"; + this._errorMessage = this._unknownError(); + return; + } + + try { + const response = await fetch("/auth/login_flow", { + method: "POST", + credentials: "same-origin", + body: JSON.stringify({ + client_id: this.clientId, + handler: [newProvider.type, newProvider.id], + redirect_uri: this.redirectUri, + }), + }); + + const data = await response.json(); + + if (response.ok) { + // allow auth provider bypass the login form + if (data.type === "create_entry") { + this._redirect(data.result); + return; + } + + this._updateStep(data); + } else { + this._state = "error"; + this._errorMessage = data.message; + } + } catch (err) { + // tslint:disable-next-line: no-console + console.error("Error starting auth flow", err); + this._state = "error"; + this._errorMessage = this._unknownError(); + } + } + + private _redirect(authCode: string) { + // OAuth 2: 3.1.2 we need to retain query component of a redirect URI + let url = this.redirectUri!!; + if (!url.includes("?")) { + url += "?"; + } else if (!url.endsWith("&")) { + url += "&"; + } + + url += `code=${encodeURIComponent(authCode)}`; + + if (this.oauth2State) { + url += `&state=${encodeURIComponent(this.oauth2State)}`; + } + + document.location.assign(url); + } + + private _updateStep(step: ConfigFlowStep) { + let stepData: any = null; + if ( + this._step && + (step.flow_id !== this._step.flow_id || + (step.type === "form" && + this._step.type === "form" && + step.step_id !== this._step.step_id)) + ) { + stepData = {}; + } + this._step = step; + this._state = "step"; + if (stepData != null) { + this._stepData = stepData; + } + } + + private _computeStepDescription(step: ConfigFlowStepForm) { + const resourceKey = `ui.panel.page-authorize.form.providers.${ + step.handler[0] + }.step.${step.step_id}.description`; + const args: string[] = []; + const placeholders = step.description_placeholders || {}; + Object.keys(placeholders).forEach((key) => { + args.push(key); + args.push(placeholders[key]); + }); + return this.localize(resourceKey, ...args); + } + + private _computeLabelCallback(step: ConfigFlowStepForm) { + // Returns a callback for ha-form to calculate labels per schema object + return (schema) => + this.localize( + `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${ + step.step_id + }.data.${schema.name}` + ); + } + + private _computeErrorCallback(step: ConfigFlowStepForm) { + // Returns a callback for ha-form to calculate error messages + return (error) => + this.localize( + `ui.panel.page-authorize.form.providers.${ + step.handler[0] + }.error.${error}` + ); + } + + private _unknownError() { + return this.localize("ui.panel.page-authorize.form.unknown_error"); + } + + private async _handleSubmit(ev: Event) { + ev.preventDefault(); + if (this._step == null) { + return; + } + if (this._step.type !== "form") { + this._providerChanged(this.authProvider); + return; + } + this._state = "loading"; + // To avoid a jumping UI. + this.style.setProperty("min-height", `${this.offsetHeight}px`); + + const postData = { ...this._stepData, client_id: this.clientId }; + + try { + const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(postData), + }); + + const newStep = await response.json(); + + if (newStep.type === "create_entry") { + this._redirect(newStep.result); + return; + } + this._updateStep(newStep); + } catch (err) { + // tslint:disable-next-line: no-console + console.error("Error submitting step", err); + this._state = "error"; + this._errorMessage = this._unknownError(); + } finally { + this.style.setProperty("min-height", ""); + } + } +} +customElements.define("ha-auth-flow", HaAuthFlow); diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 4935fac9dd..0a7ce2a70a 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -108,7 +108,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) { .resources="${this.resources}" .clientId="${this.clientId}" .authProviders="${inactiveProviders}" - @pick="${this._handleAuthProviderPick}" + @pick-auth-provider="${this._handleAuthProviderPick}" > ` : ""} diff --git a/src/auth/ha-pick-auth-provider.js b/src/auth/ha-pick-auth-provider.js deleted file mode 100644 index d1c6fd00f8..0000000000 --- a/src/auth/ha-pick-auth-provider.js +++ /dev/null @@ -1,54 +0,0 @@ -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import { EventsMixin } from "../mixins/events-mixin"; -import { localizeLiteMixin } from "../mixins/localize-lite-mixin"; -import "../components/ha-icon-next"; - -/* - * @appliesMixin EventsMixin - */ -class HaPickAuthProvider extends EventsMixin( - localizeLiteMixin(PolymerElement) -) { - static get template() { - return html` - -

[[localize('ui.panel.page-authorize.pick_auth_provider')]]:

- - `; - } - - static get properties() { - return { - _state: { - type: String, - value: "loading", - }, - authProviders: Array, - }; - } - - _handlePick(ev) { - this.fire("pick", ev.model.item); - } - - _equal(a, b) { - return a === b; - } -} -customElements.define("ha-pick-auth-provider", HaPickAuthProvider); diff --git a/src/auth/ha-pick-auth-provider.ts b/src/auth/ha-pick-auth-provider.ts new file mode 100644 index 0000000000..942fb5b4cc --- /dev/null +++ b/src/auth/ha-pick-auth-provider.ts @@ -0,0 +1,44 @@ +import { LitElement, html, property } from "lit-element"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; +import { fireEvent } from "../common/dom/fire_event"; +import "../components/ha-icon-next"; +import { AuthProvider } from "../data/auth"; + +declare global { + interface HASSDomEvents { + "pick-auth-provider": AuthProvider; + } +} + +class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) { + @property() public authProviders: AuthProvider[] = []; + + protected render() { + return html` + +

${this.localize("ui.panel.page-authorize.pick_auth_provider")}:

+ ${this.authProviders.map( + (provider) => html` + + ${provider.name} + + + ` + )} + `; + } + + private _handlePick(ev) { + fireEvent(this, "pick-auth-provider", ev.currentTarget.auth_provider); + } +} +customElements.define("ha-pick-auth-provider", HaPickAuthProvider);