Update design of login page (#18770)

This commit is contained in:
Bram Kragten 2023-11-27 23:06:51 +01:00 committed by GitHub
parent 31c91cea9a
commit 2e1fb9df66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 539 additions and 213 deletions

View File

@ -8,7 +8,14 @@ import "../components/ha-alert";
import "../components/ha-checkbox"; import "../components/ha-checkbox";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data"; import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
import "../components/ha-formfield"; import "../components/ha-formfield";
import { AuthProvider, autocompleteLoginFields } from "../data/auth"; import {
AuthProvider,
autocompleteLoginFields,
createLoginFlow,
deleteLoginFlow,
redirectWithAuthCode,
submitLoginFlow,
} from "../data/auth";
import { import {
DataEntryFlowStep, DataEntryFlowStep,
DataEntryFlowStepForm, DataEntryFlowStepForm,
@ -86,6 +93,25 @@ export class HaAuthFlow extends LitElement {
margin-top: 10px; margin-top: 10px;
margin-left: -16px; margin-left: -16px;
} }
a.forgot-password {
color: var(--primary-color);
text-decoration: none;
font-size: 0.875rem;
}
.space-between {
display: flex;
justify-content: space-between;
align-items: center;
}
form {
text-align: center;
max-width: 336px;
width: 100%;
}
ha-auth-form {
display: block;
margin-top: 16px;
}
</style> </style>
<form>${this._renderForm()}</form> <form>${this._renderForm()}</form>
`; `;
@ -189,6 +215,11 @@ export class HaAuthFlow extends LitElement {
`; `;
case "form": case "form":
return html` return html`
<h1>
${!["select_mfa_module", "mfa"].includes(step.step_id)
? this.localize("ui.panel.page-authorize.welcome_home")
: this.localize("ui.panel.page-authorize.just_checking")}
</h1>
${this._computeStepDescription(step)} ${this._computeStepDescription(step)}
<ha-auth-form <ha-auth-form
.data=${this._stepData} .data=${this._stepData}
@ -202,15 +233,28 @@ export class HaAuthFlow extends LitElement {
${this.clientId === genClientId() && ${this.clientId === genClientId() &&
!["select_mfa_module", "mfa"].includes(step.step_id) !["select_mfa_module", "mfa"].includes(step.step_id)
? html` ? html`
<ha-formfield <div class="space-between">
class="store-token" <ha-formfield
.label=${this.localize("ui.panel.page-authorize.store_token")} class="store-token"
> .label=${this.localize(
<ha-checkbox "ui.panel.page-authorize.store_token"
.checked=${this.storeToken} )}
@change=${this._storeTokenChanged} >
></ha-checkbox> <ha-checkbox
</ha-formfield> .checked=${this.storeToken}
@change=${this._storeTokenChanged}
></ha-checkbox>
</ha-formfield>
<a
class="forgot-password"
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
target="_blank"
rel="noreferrer noopener"
>${this.localize(
"ui.panel.page-authorize.forgot_password"
)}</a
>
</div>
` `
: ""} : ""}
`; `;
@ -225,10 +269,7 @@ export class HaAuthFlow extends LitElement {
private async _providerChanged(newProvider?: AuthProvider) { private async _providerChanged(newProvider?: AuthProvider) {
if (this.step && this.step.type === "form") { if (this.step && this.step.type === "form") {
fetch(`/auth/login_flow/${this.step.flow_id}`, { deleteLoginFlow(this.step.flow_id).catch((err) => {
method: "DELETE",
credentials: "same-origin",
}).catch((err) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Error delete obsoleted auth flow", err); console.error("Error delete obsoleted auth flow", err);
}); });
@ -243,22 +284,21 @@ export class HaAuthFlow extends LitElement {
} }
try { try {
const response = await fetch("/auth/login_flow", { const response = await createLoginFlow(this.clientId, this.redirectUri, [
method: "POST", newProvider.type,
credentials: "same-origin", newProvider.id,
body: JSON.stringify({ ]);
client_id: this.clientId,
handler: [newProvider.type, newProvider.id],
redirect_uri: this.redirectUri,
}),
});
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
// allow auth provider bypass the login form // allow auth provider bypass the login form
if (data.type === "create_entry") { if (data.type === "create_entry") {
this._redirect(data.result); redirectWithAuthCode(
this.redirectUri!,
data.result,
this.oauth2State
);
return; return;
} }
@ -276,27 +316,6 @@ export class HaAuthFlow extends LitElement {
} }
} }
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)}`;
}
if (this.storeToken) {
url += `&storeToken=true`;
}
document.location.assign(url);
}
private _stepDataChanged(ev: CustomEvent) { private _stepDataChanged(ev: CustomEvent) {
this._stepData = ev.detail.value; this._stepData = ev.detail.value;
} }
@ -345,11 +364,7 @@ export class HaAuthFlow extends LitElement {
const postData = { ...this._stepData, client_id: this.clientId }; const postData = { ...this._stepData, client_id: this.clientId };
try { try {
const response = await fetch(`/auth/login_flow/${this.step.flow_id}`, { const response = await submitLoginFlow(this.step.flow_id, postData);
method: "POST",
credentials: "same-origin",
body: JSON.stringify(postData),
});
const newStep = await response.json(); const newStep = await response.json();
@ -360,7 +375,11 @@ export class HaAuthFlow extends LitElement {
} }
if (newStep.type === "create_entry") { if (newStep.type === "create_entry") {
this._redirect(newStep.result); redirectWithAuthCode(
this.redirectUri!,
newStep.result,
this.oauth2State
);
return; return;
} }
this.step = newStep; this.step = newStep;

View File

@ -5,6 +5,7 @@ import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-language-picker";
import { import {
AuthProvider, AuthProvider,
AuthUrlSearchParams, AuthUrlSearchParams,
@ -71,19 +72,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
`; `;
} }
if (!this._authProviders) { const inactiveProviders = this._authProviders?.filter(
return html`
<style>
ha-authorize p {
font-size: 14px;
line-height: 20px;
}
</style>
<p>${this.localize("ui.panel.page-authorize.initializing")}</p>
`;
}
const inactiveProviders = this._authProviders.filter(
(prv) => prv !== this._authProvider (prv) => prv !== this._authProvider
); );
@ -92,13 +81,16 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
return html` return html`
<style> <style>
ha-pick-auth-provider { ha-pick-auth-provider {
display: block;
margin-top: 48px;
}
ha-auth-flow {
display: block; display: block;
margin-top: 24px; margin-top: 24px;
} }
ha-auth-flow,
ha-local-auth-flow {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
ha-alert { ha-alert {
display: block; display: block;
margin: 16px 0; margin: 16px 0;
@ -107,6 +99,55 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
} }
.card-content {
background: var(
--ha-card-background,
var(--card-background-color, white)
);
box-shadow: var(--ha-card-box-shadow, none);
box-sizing: border-box;
border-radius: var(--ha-card-border-radius, 12px);
border-width: var(--ha-card-border-width, 1px);
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
color: var(--primary-text-color);
transition: all 0.3s ease-out;
position: relative;
padding: 16px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-language-picker {
width: 200px;
margin-top: 8px;
border-radius: 4px;
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
.footer a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
}
h1 {
font-size: 28px;
font-weight: 400;
margin-top: 16px;
margin-bottom: 16px;
}
</style> </style>
${!this._ownInstance ${!this._ownInstance
@ -123,44 +164,58 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
>`, >`,
})} })}
</ha-alert>` </ha-alert>`
: html`<p>${this.localize("ui.panel.page-authorize.authorizing")}</p>`} : nothing}
${!this._forceDefaultLogin &&
this._authProvider!.users && <div class="card-content">
this.clientId != null && ${!this._authProvider
this.redirectUri != null ? html`<p>
? html`<ha-local-auth-flow ${this.localize("ui.panel.page-authorize.initializing")}
.clientId=${this.clientId} </p> `
.redirectUri=${this.redirectUri} : !this._forceDefaultLogin &&
.oauth2State=${this.oauth2State} this._authProvider!.users &&
.authProvider=${this._authProvider} this.clientId != null &&
.authProviders=${this._authProviders} this.redirectUri != null
.localize=${this.localize} ? html`<ha-local-auth-flow
@default-login-flow=${this._handleDefaultLoginFlow} .clientId=${this.clientId}
></ha-local-auth-flow>` .redirectUri=${this.redirectUri}
: html`${inactiveProviders.length > 0 .oauth2State=${this.oauth2State}
? html`<p> .authProvider=${this._authProvider}
${this.localize("ui.panel.page-authorize.logging_in_with", { .authProviders=${this._authProviders}
authProviderName: html`<b>${this._authProvider!.name}</b>`, .localize=${this.localize}
})} @default-login-flow=${this._handleDefaultLoginFlow}
</p>` ></ha-local-auth-flow>`
: nothing} : html`<ha-auth-flow
<ha-auth-flow .clientId=${this.clientId}
.clientId=${this.clientId} .redirectUri=${this.redirectUri}
.redirectUri=${this.redirectUri} .oauth2State=${this.oauth2State}
.oauth2State=${this.oauth2State} .authProvider=${this._authProvider}
.authProvider=${this._authProvider} .localize=${this.localize}
.localize=${this.localize} ></ha-auth-flow>
></ha-auth-flow> ${inactiveProviders!.length > 0
${inactiveProviders.length > 0 ? html`
? html` <ha-pick-auth-provider
<ha-pick-auth-provider .localize=${this.localize}
.localize=${this.localize} .clientId=${this.clientId}
.clientId=${this.clientId} .authProviders=${inactiveProviders}
.authProviders=${inactiveProviders} @pick-auth-provider=${this._handleAuthProviderPick}
@pick-auth-provider=${this._handleAuthProviderPick} ></ha-pick-auth-provider>
></ha-pick-auth-provider> `
` : ""}`}
: ""}`} </div>
<div class="footer">
<ha-language-picker
.value=${this.language}
.label=${""}
nativeName
@value-changed=${this._languageChanged}
></ha-language-picker>
<a
href="https://www.home-assistant.io/docs/authentication/"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-authorize.help")}</a
>
</div>
`; `;
} }
@ -266,4 +321,15 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
private async _handleAuthProviderPick(ev) { private async _handleAuthProviderPick(ev) {
this._authProvider = ev.detail; this._authProvider = ev.detail;
} }
private _languageChanged(ev: CustomEvent) {
const language = ev.detail.value;
this.language = language;
try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
} catch (err: any) {
// Ignore
}
}
} }

View File

@ -1,16 +1,25 @@
/* eslint-disable lit/prefer-static-styles */ /* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { html, LitElement, nothing, PropertyValues } from "lit"; import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-button";
import "../components/ha-icon-button";
import "../components/user/ha-person-badge"; import "../components/user/ha-person-badge";
import { AuthProvider } from "../data/auth"; import {
AuthProvider,
createLoginFlow,
deleteLoginFlow,
redirectWithAuthCode,
submitLoginFlow,
} from "../data/auth";
import { DataEntryFlowStep } from "../data/data_entry_flow";
import { listPersons } from "../data/person"; import { listPersons } from "../data/person";
import "./ha-auth-textfield"; import "./ha-auth-textfield";
import type { HaAuthTextField } from "./ha-auth-textfield"; import type { HaAuthTextField } from "./ha-auth-textfield";
import { DataEntryFlowStep } from "../data/data_entry_flow";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-local-auth-flow") @customElement("ha-local-auth-flow")
export class HaLocalAuthFlow extends LitElement { export class HaLocalAuthFlow extends LitElement {
@ -24,6 +33,8 @@ export class HaLocalAuthFlow extends LitElement {
@property() public oauth2State?: string; @property() public oauth2State?: string;
@property({ type: Boolean }) public ownInstance = false;
@property() public localize!: LocalizeFunc; @property() public localize!: LocalizeFunc;
@state() private _error?: string; @state() private _error?: string;
@ -36,6 +47,8 @@ export class HaLocalAuthFlow extends LitElement {
@state() private _selectedUser?: string; @state() private _selectedUser?: string;
@state() private _unmaskedPassword = false;
createRenderRoot() { createRenderRoot() {
return this; return this;
} }
@ -52,43 +65,122 @@ export class HaLocalAuthFlow extends LitElement {
if (!this.authProvider?.users || !this._persons) { if (!this.authProvider?.users || !this._persons) {
return nothing; return nothing;
} }
const userIds = Object.keys(this.authProvider.users);
return html` return html`
<style> <style>
.content {
max-width: 560px;
}
.persons { .persons {
margin-top: 32px;
display: flex; display: flex;
flex-wrap: wrap;
gap: 16px; gap: 16px;
justify-content: center;
}
.persons.force-small {
max-width: 350px;
} }
.person { .person {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0; flex-shrink: 0;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
width: 80px;
}
.person[role="button"] {
outline: none;
padding: 8px;
border-radius: 4px;
}
.person[role="button"]:focus-visible {
background: rgba(var(--rgb-primary-color), 0.1);
} }
.person p { .person p {
margin-bottom: 0; margin-bottom: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
} }
ha-person-badge { ha-person-badge {
width: 120px; width: 80px;
height: 120px; height: 80px;
--person-badge-font-size: 3em; --person-badge-font-size: 2em;
}
form {
width: 100%;
}
ha-auth-textfield {
display: block !important;
position: relative;
}
ha-auth-textfield ha-icon-button {
position: absolute;
top: 4px;
right: 4px;
z-index: 9;
} }
.login-form { .login-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%;
max-width: 336px;
margin-top: 24px;
} }
.login-form .person { .login-form .person {
cursor: default; cursor: default;
width: auto;
} }
.login-form ha-auth-textfield { .login-form .person p {
margin-top: 16px; font-size: 28px;
margin-top: 24px;
margin-bottom: 32px;
line-height: normal;
}
.login-form ha-person-badge {
width: 120px;
height: 120px;
--person-badge-font-size: 3em;
} }
.action { .action {
margin: 24px 0 8px; margin: 16px 0 8px;
text-align: center; display: flex;
width: 100%;
max-width: 336px;
justify-content: center;
}
.space-between {
justify-content: space-between;
} }
ha-list-item { ha-list-item {
margin-top: 16px; margin-top: 16px;
} }
ha-button {
--mdc-typography-button-text-transform: none;
}
a.forgot-password {
color: var(--primary-color);
text-decoration: none;
font-size: 0.875rem;
}
button {
color: var(--primary-color);
background: none;
border: none;
padding: 8px;
font: inherit;
text-align: left;
cursor: pointer;
outline: none;
border-radius: 4px;
}
button:focus-visible {
background: rgba(var(--rgb-primary-color), 0.1);
}
</style> </style>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@ -117,12 +209,40 @@ export class HaLocalAuthFlow extends LitElement {
.value=${this.authProvider.users[this._selectedUser]} .value=${this.authProvider.users[this._selectedUser]}
/> />
<ha-auth-textfield <ha-auth-textfield
type="password" .type=${this._unmaskedPassword ? "text" : "password"}
id="password" id="password"
></ha-auth-textfield> name="password"
.label=${this.localize(
"ui.panel.page-authorize.form.providers.homeassistant.step.init.data.password"
)}
required
autoValidate
autocomplete
iconTrailing
validationMessage="Required"
>
<ha-icon-button
toggles
.label=${
this.localize(
this._unmaskedPassword
? "ui.panel.page-authorize.form.hide_password"
: "ui.panel.page-authorize.form.show_password"
) || (this._unmaskedPassword ? "Hide password" : "Show password")
}
@click=${this._toggleUnmaskedPassword}
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>
</ha-auth-textfield>
</div> </div>
<div class="action"> <div class="action space-between">
<mwc-button <mwc-button
@click=${this._restart}
.disabled=${this._submitting}
>
${this.localize("ui.panel.page-authorize.form.previous")}
</mwc-button>
<mwc-button
raised raised
@click=${this._handleSubmit} @click=${this._handleSubmit}
.disabled=${this._submitting} .disabled=${this._submitting}
@ -130,24 +250,48 @@ export class HaLocalAuthFlow extends LitElement {
${this.localize("ui.panel.page-authorize.form.next")} ${this.localize("ui.panel.page-authorize.form.next")}
</mwc-button> </mwc-button>
</div> </div>
<div class="action">
<a class="forgot-password"
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
target="_blank"
rel="noreferrer noopener"
>${this.localize(
"ui.panel.page-authorize.forgot_password"
)}</a
>
</div>
</form>` </form>`
: html`<div class="persons"> : html`<h1>
${Object.keys(this.authProvider.users).map((userId) => { ${this.localize("ui.panel.page-authorize.welcome_home")}
</h1>
<p>
${this.localize("ui.panel.page-authorize.who_is_logging_in")}
</p>
<div
class="persons ${userIds.length < 10 && userIds.length % 4 === 1
? "force-small"
: ""}"
>
${userIds.map((userId) => {
const person = this._persons![userId]; const person = this._persons![userId];
return html`<div return html`<div
class="person" class="person"
.userId=${userId} .userId=${userId}
@click=${this._personSelected} @click=${this._personSelected}
@keyup=${this._handleKeyUp}
role="button"
tabindex="0"
> >
<ha-person-badge .person=${person}></ha-person-badge> <ha-person-badge .person=${person}></ha-person-badge>
<p>${person.name}</p> <p>${person.name}</p>
</div>`; </div>`;
})} })}
</div> </div>
<ha-list-item hasMeta role="button" @click=${this._otherLogin}> <div class="action">
Other options <button @click=${this._otherLogin} tabindex="0">
<ha-icon-next slot="meta"></ha-icon-next> ${this.localize("ui.panel.page-authorize.other_options")}
</ha-list-item>`} </button>
</div>`}
`; `;
} }
@ -176,24 +320,41 @@ export class HaLocalAuthFlow extends LitElement {
this._persons = await (await listPersons()).json(); this._persons = await (await listPersons()).json();
} }
private _restart() {
this._selectedUser = undefined;
}
private _toggleUnmaskedPassword() {
this._unmaskedPassword = !this._unmaskedPassword;
}
private _handleKeyUp(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
this._personSelected(ev);
}
}
private async _personSelected(ev) { private async _personSelected(ev) {
const userId = ev.currentTarget.userId; const userId = ev.currentTarget.userId;
if (this.authProviders?.find((prv) => prv.type === "trusted_networks")) { if (
this.ownInstance &&
this.authProviders?.find((prv) => prv.type === "trusted_networks")
) {
try { try {
const flowResponse = await fetch("/auth/login_flow", { const flowResponse = await createLoginFlow(
method: "POST", this.clientId,
credentials: "same-origin", this.redirectUri,
body: JSON.stringify({ ["trusted_networks", null]
client_id: this.clientId, );
handler: ["trusted_networks", null],
redirect_uri: this.redirectUri,
}),
});
const data = await flowResponse.json(); const data = await flowResponse.json();
if (data.type === "create_entry") { if (data.type === "create_entry") {
this._redirect(data.result); redirectWithAuthCode(
this.redirectUri!,
data.result,
this.oauth2State
);
return; return;
} }
@ -204,27 +365,24 @@ export class HaLocalAuthFlow extends LitElement {
const postData = { user: userId, client_id: this.clientId }; const postData = { user: userId, client_id: this.clientId };
const response = await fetch(`/auth/login_flow/${data.flow_id}`, { const response = await submitLoginFlow(data.flow_id, postData);
method: "POST",
credentials: "same-origin",
body: JSON.stringify(postData),
});
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
if (result.type === "create_entry") { if (result.type === "create_entry") {
this._redirect(result.result); redirectWithAuthCode(
this.redirectUri!,
result.result,
this.oauth2State
);
return; return;
} }
} else { } else {
throw new Error("Invalid response"); throw new Error("Invalid response");
} }
} catch { } catch {
fetch(`/auth/login_flow/${data.flow_id}`, { deleteLoginFlow(data.flow_id).catch((err) => {
method: "DELETE",
credentials: "same-origin",
}).catch((err) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Error delete obsoleted auth flow", err); console.error("Error delete obsoleted auth flow", err);
}); });
@ -243,17 +401,14 @@ export class HaLocalAuthFlow extends LitElement {
return; return;
} }
this._error = undefined;
this._submitting = true; this._submitting = true;
const flowResponse = await fetch("/auth/login_flow", { const flowResponse = await createLoginFlow(
method: "POST", this.clientId,
credentials: "same-origin", this.redirectUri,
body: JSON.stringify({ ["homeassistant", null]
client_id: this.clientId, );
handler: ["homeassistant", null],
redirect_uri: this.redirectUri,
}),
});
const data = await flowResponse.json(); const data = await flowResponse.json();
@ -265,11 +420,7 @@ export class HaLocalAuthFlow extends LitElement {
}; };
try { try {
const response = await fetch(`/auth/login_flow/${data.flow_id}`, { const response = await submitLoginFlow(data.flow_id, postData);
method: "POST",
credentials: "same-origin",
body: JSON.stringify(postData),
});
const newStep = await response.json(); const newStep = await response.json();
@ -279,7 +430,11 @@ export class HaLocalAuthFlow extends LitElement {
} }
if (newStep.type === "create_entry") { if (newStep.type === "create_entry") {
this._redirect(newStep.result); redirectWithAuthCode(
this.redirectUri!,
newStep.result,
this.oauth2State
);
return; return;
} }
@ -287,38 +442,25 @@ export class HaLocalAuthFlow extends LitElement {
this._error = this.localize( this._error = this.localize(
`ui.panel.page-authorize.form.providers.homeassistant.error.${newStep.errors.base}` `ui.panel.page-authorize.form.providers.homeassistant.error.${newStep.errors.base}`
); );
return; throw new Error(this._error);
} }
this._step = newStep; this._step = newStep;
} catch (err: any) { } catch {
// eslint-disable-next-line no-console deleteLoginFlow(data.flow_id).catch((err) => {
console.error("Error submitting step", err); // eslint-disable-next-line no-console
this._error = this.localize("ui.panel.page-authorize.form.unknown_error"); console.error("Error delete obsoleted auth flow", err);
});
if (!this._error) {
this._error = this.localize(
"ui.panel.page-authorize.form.unknown_error"
);
}
} finally { } finally {
this._submitting = false; this._submitting = false;
} }
} }
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)}`;
}
url += `&storeToken=true`;
document.location.assign(url);
}
private _otherLogin() { private _otherLogin() {
fireEvent(this, "default-login-flow"); fireEvent(this, "default-login-flow");
} }

View File

@ -21,7 +21,11 @@ export class HaPickAuthProvider extends LitElement {
protected render() { protected render() {
return html` return html`
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p> <h3>
<span
>${this.localize("ui.panel.page-authorize.pick_auth_provider")}</span
>
</h3>
<mwc-list> <mwc-list>
${this.authProviders.map( ${this.authProviders.map(
(provider) => html` (provider) => html`
@ -45,12 +49,34 @@ export class HaPickAuthProvider extends LitElement {
} }
static styles = css` static styles = css`
p { h3 {
margin-top: 0; margin: 0 -16px;
position: relative;
z-index: 1;
text-align: center;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
h3:before {
border-top: 1px solid var(--divider-color);
content: "";
margin: 0 auto;
position: absolute;
top: 50%;
left: 0;
right: 0;
bottom: 0;
width: 100%;
z-index: -1;
}
h3 span {
background: var(--card-background-color);
padding: 0 15px;
} }
mwc-list { mwc-list {
margin: 0 -16px; margin: 16px -16px 0;
--mdc-list-side-padding: 16px; --mdc-list-side-padding: 24px;
} }
`; `;
} }

View File

@ -49,6 +49,56 @@ export const fetchAuthProviders = () =>
credentials: "same-origin", credentials: "same-origin",
}); });
export const createLoginFlow = (
client_id: string | undefined,
redirect_uri: string | undefined,
handler: (string | null)[]
) =>
fetch("/auth/login_flow", {
method: "POST",
credentials: "same-origin",
body: JSON.stringify({
client_id,
handler,
redirect_uri,
}),
});
export const submitLoginFlow = (flow_id: string, data: Record<string, any>) =>
fetch(`/auth/login_flow/${flow_id}`, {
method: "POST",
credentials: "same-origin",
body: JSON.stringify(data),
});
export const deleteLoginFlow = (flow_id) =>
fetch(`/auth/login_flow/${flow_id}`, {
method: "DELETE",
credentials: "same-origin",
});
export const redirectWithAuthCode = (
url: string,
authCode: string,
oauth2State: string | undefined
) => {
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
if (!url.includes("?")) {
url += "?";
} else if (!url.endsWith("&")) {
url += "&";
}
url += `code=${encodeURIComponent(authCode)}`;
if (oauth2State) {
url += `&state=${encodeURIComponent(oauth2State)}`;
}
url += `&storeToken=true`;
document.location.assign(url);
};
export const createAuthForUser = async ( export const createAuthForUser = async (
hass: HomeAssistant, hass: HomeAssistant,
userId: string, userId: string,

File diff suppressed because one or more lines are too long

View File

@ -5579,16 +5579,24 @@
"authorizing": "Log in to your Home Assistant instance", "authorizing": "Log in to your Home Assistant instance",
"authorizing_app": "You're about to give the Home Assistant Companion app for {app} access to your Home Assistant instance.", "authorizing_app": "You're about to give the Home Assistant Companion app for {app} access to your Home Assistant instance.",
"authorizing_client": "You're about to give {clientId} access to your Home Assistant instance.", "authorizing_client": "You're about to give {clientId} access to your Home Assistant instance.",
"logging_in_with": "Logging in with {authProviderName}.",
"pick_auth_provider": "Or log in with", "pick_auth_provider": "Or log in with",
"abort_intro": "Login aborted", "abort_intro": "Login aborted",
"store_token": "Keep me logged in", "store_token": "Keep me logged in",
"help": "Help",
"welcome_home": "Welcome home!",
"just_checking": "Just checking",
"who_is_logging_in": "Who is logging in?",
"other_options": "Other login options",
"forgot_password": "Forgot password?",
"form": { "form": {
"working": "Please wait", "working": "Please wait",
"unknown_error": "Something went wrong", "unknown_error": "Something went wrong",
"next": "Log in", "next": "Log in",
"previous": "Previous",
"start_over": "Start over", "start_over": "Start over",
"error": "Error: {error}", "error": "Error: {error}",
"hide_password": "Hide password",
"show_password": "Show password",
"providers": { "providers": {
"command_line": { "command_line": {
"step": { "step": {