- ${this.localize("ui.panel.page-authorize.logging_in_with", {
- authProviderName: html`${this._authProvider!.name}`,
- })}
-
`
- : nothing}
-
+ ${!this._authProvider
+ ? html`
+ ${this.localize("ui.panel.page-authorize.initializing")}
+
`
+ : !this._forceDefaultLogin &&
+ this._authProvider!.users &&
+ this.clientId != null &&
+ this.redirectUri != null
+ ? html`
`
+ : html`
+ ${inactiveProviders!.length > 0
+ ? html`
+
+ `
+ : ""}`}
+
+
`;
}
@@ -266,4 +321,15 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
private async _handleAuthProviderPick(ev) {
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
+ }
+ }
}
diff --git a/src/auth/ha-local-auth-flow.ts b/src/auth/ha-local-auth-flow.ts
index ce15e0405679..eeae4c6d413d 100644
--- a/src/auth/ha-local-auth-flow.ts
+++ b/src/auth/ha-local-auth-flow.ts
@@ -1,16 +1,25 @@
/* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button";
+import { mdiEye, mdiEyeOff } from "@mdi/js";
import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
+import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
+import "../components/ha-button";
+import "../components/ha-icon-button";
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 "./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")
export class HaLocalAuthFlow extends LitElement {
@@ -24,6 +33,8 @@ export class HaLocalAuthFlow extends LitElement {
@property() public oauth2State?: string;
+ @property({ type: Boolean }) public ownInstance = false;
+
@property() public localize!: LocalizeFunc;
@state() private _error?: string;
@@ -36,6 +47,8 @@ export class HaLocalAuthFlow extends LitElement {
@state() private _selectedUser?: string;
+ @state() private _unmaskedPassword = false;
+
createRenderRoot() {
return this;
}
@@ -52,43 +65,122 @@ export class HaLocalAuthFlow extends LitElement {
if (!this.authProvider?.users || !this._persons) {
return nothing;
}
+ const userIds = Object.keys(this.authProvider.users);
return html`
${this._error
? html`
- ${Object.keys(this.authProvider.users).map((userId) => {
+ : html`
+ ${this.localize("ui.panel.page-authorize.welcome_home")}
+
+
+ ${this.localize("ui.panel.page-authorize.who_is_logging_in")}
+
+
+ ${userIds.map((userId) => {
const person = this._persons![userId];
return html`
`;
})}
-
- Other options
-
- `}
+
+
+
`}
`;
}
@@ -176,24 +320,41 @@ export class HaLocalAuthFlow extends LitElement {
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) {
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 {
- const flowResponse = await fetch("/auth/login_flow", {
- method: "POST",
- credentials: "same-origin",
- body: JSON.stringify({
- client_id: this.clientId,
- handler: ["trusted_networks", null],
- redirect_uri: this.redirectUri,
- }),
- });
+ const flowResponse = await createLoginFlow(
+ this.clientId,
+ this.redirectUri,
+ ["trusted_networks", null]
+ );
const data = await flowResponse.json();
if (data.type === "create_entry") {
- this._redirect(data.result);
+ redirectWithAuthCode(
+ this.redirectUri!,
+ data.result,
+ this.oauth2State
+ );
return;
}
@@ -204,27 +365,24 @@ export class HaLocalAuthFlow extends LitElement {
const postData = { user: userId, client_id: this.clientId };
- const response = await fetch(`/auth/login_flow/${data.flow_id}`, {
- method: "POST",
- credentials: "same-origin",
- body: JSON.stringify(postData),
- });
+ const response = await submitLoginFlow(data.flow_id, postData);
if (response.ok) {
const result = await response.json();
if (result.type === "create_entry") {
- this._redirect(result.result);
+ redirectWithAuthCode(
+ this.redirectUri!,
+ result.result,
+ this.oauth2State
+ );
return;
}
} else {
throw new Error("Invalid response");
}
} catch {
- fetch(`/auth/login_flow/${data.flow_id}`, {
- method: "DELETE",
- credentials: "same-origin",
- }).catch((err) => {
+ deleteLoginFlow(data.flow_id).catch((err) => {
// eslint-disable-next-line no-console
console.error("Error delete obsoleted auth flow", err);
});
@@ -243,17 +401,14 @@ export class HaLocalAuthFlow extends LitElement {
return;
}
+ this._error = undefined;
this._submitting = true;
- const flowResponse = await fetch("/auth/login_flow", {
- method: "POST",
- credentials: "same-origin",
- body: JSON.stringify({
- client_id: this.clientId,
- handler: ["homeassistant", null],
- redirect_uri: this.redirectUri,
- }),
- });
+ const flowResponse = await createLoginFlow(
+ this.clientId,
+ this.redirectUri,
+ ["homeassistant", null]
+ );
const data = await flowResponse.json();
@@ -265,11 +420,7 @@ export class HaLocalAuthFlow extends LitElement {
};
try {
- const response = await fetch(`/auth/login_flow/${data.flow_id}`, {
- method: "POST",
- credentials: "same-origin",
- body: JSON.stringify(postData),
- });
+ const response = await submitLoginFlow(data.flow_id, postData);
const newStep = await response.json();
@@ -279,7 +430,11 @@ export class HaLocalAuthFlow extends LitElement {
}
if (newStep.type === "create_entry") {
- this._redirect(newStep.result);
+ redirectWithAuthCode(
+ this.redirectUri!,
+ newStep.result,
+ this.oauth2State
+ );
return;
}
@@ -287,38 +442,25 @@ export class HaLocalAuthFlow extends LitElement {
this._error = this.localize(
`ui.panel.page-authorize.form.providers.homeassistant.error.${newStep.errors.base}`
);
- return;
+ throw new Error(this._error);
}
this._step = newStep;
- } catch (err: any) {
- // eslint-disable-next-line no-console
- console.error("Error submitting step", err);
- this._error = this.localize("ui.panel.page-authorize.form.unknown_error");
+ } catch {
+ deleteLoginFlow(data.flow_id).catch((err) => {
+ // eslint-disable-next-line no-console
+ console.error("Error delete obsoleted auth flow", err);
+ });
+ if (!this._error) {
+ this._error = this.localize(
+ "ui.panel.page-authorize.form.unknown_error"
+ );
+ }
} finally {
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() {
fireEvent(this, "default-login-flow");
}
diff --git a/src/auth/ha-pick-auth-provider.ts b/src/auth/ha-pick-auth-provider.ts
index 01878a1c2236..90a5c238b67b 100644
--- a/src/auth/ha-pick-auth-provider.ts
+++ b/src/auth/ha-pick-auth-provider.ts
@@ -21,7 +21,11 @@ export class HaPickAuthProvider extends LitElement {
protected render() {
return html`
-
${this.localize("ui.panel.page-authorize.pick_auth_provider")}:
+
+ ${this.localize("ui.panel.page-authorize.pick_auth_provider")}
+
${this.authProviders.map(
(provider) => html`
@@ -45,12 +49,34 @@ export class HaPickAuthProvider extends LitElement {
}
static styles = css`
- p {
- margin-top: 0;
+ h3 {
+ 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 {
- margin: 0 -16px;
- --mdc-list-side-padding: 16px;
+ margin: 16px -16px 0;
+ --mdc-list-side-padding: 24px;
}
`;
}
diff --git a/src/data/auth.ts b/src/data/auth.ts
index 17309f491be8..ea7d6f2666b3 100644
--- a/src/data/auth.ts
+++ b/src/data/auth.ts
@@ -49,6 +49,56 @@ export const fetchAuthProviders = () =>
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) =>
+ 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 (
hass: HomeAssistant,
userId: string,
diff --git a/src/html/authorize.html.template b/src/html/authorize.html.template
index 2770e1a32a31..11724890bf73 100644
--- a/src/html/authorize.html.template
+++ b/src/html/authorize.html.template
@@ -5,26 +5,45 @@
<%= renderTemplate("_header.html.template") %>
<%= renderTemplate("_style_base.html.template") %>
@@ -32,11 +51,7 @@
diff --git a/src/translations/en.json b/src/translations/en.json
index 8392b17f2f7c..7367bb8bc8fb 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -5579,16 +5579,24 @@
"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_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",
"abort_intro": "Login aborted",
"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": {
"working": "Please wait",
"unknown_error": "Something went wrong",
"next": "Log in",
+ "previous": "Previous",
"start_over": "Start over",
"error": "Error: {error}",
+ "hide_password": "Hide password",
+ "show_password": "Show password",
"providers": {
"command_line": {
"step": {