mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 16:26:43 +00:00
Update design of login page (#18770)
This commit is contained in:
parent
31c91cea9a
commit
2e1fb9df66
@ -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;
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -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
@ -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": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user