diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts
index 1857338ac7..457f9bf1c8 100644
--- a/src/panels/config/ha-panel-config.ts
+++ b/src/panels/config/ha-panel-config.ts
@@ -136,6 +136,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "ui.panel.config.users.caption",
iconPath: mdiBadgeAccountHorizontal,
core: true,
+ advancedOnly: true,
},
],
general: [
diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts
index 043d407607..5844a0374f 100644
--- a/src/panels/config/person/dialog-person-detail.ts
+++ b/src/panels/config/person/dialog-person-detail.ts
@@ -22,6 +22,22 @@ import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
+import "../../../components/ha-formfield";
+import { computeRTLDirection } from "../../../common/util/compute_rtl";
+import {
+ User,
+ SYSTEM_GROUP_ID_ADMIN,
+ deleteUser,
+ SYSTEM_GROUP_ID_USER,
+ updateUser,
+} from "../../../data/user";
+import {
+ showAlertDialog,
+ showPromptDialog,
+ showConfirmationDialog,
+} from "../../../dialogs/generic/show-dialog-box";
+import { adminChangePassword } from "../../../data/auth";
+import { showAddUserDialog } from "../users/show-dialog-add-user";
const includeDomains = ["device_tracker"];
@@ -39,6 +55,10 @@ class DialogPersonDetail extends LitElement {
@internalProperty() private _userId?: string;
+ @internalProperty() private _user?: User;
+
+ @internalProperty() private _isAdmin?: boolean;
+
@internalProperty() private _deviceTrackers!: string[];
@internalProperty() private _picture!: string | null;
@@ -64,9 +84,15 @@ class DialogPersonDetail extends LitElement {
this._userId = this._params.entry.user_id || undefined;
this._deviceTrackers = this._params.entry.device_trackers || [];
this._picture = this._params.entry.picture || null;
+ this._user = this._userId
+ ? this._params.users.find((user) => user.id === this._userId)
+ : undefined;
+ this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
} else {
this._name = "";
this._userId = undefined;
+ this._user = undefined;
+ this._isAdmin = undefined;
this._deviceTrackers = [];
this._picture = null;
}
@@ -115,15 +141,37 @@ class DialogPersonDetail extends LitElement {
@change=${this._pictureChanged}
>
-
+
+
+
+
+ ${this._user
+ ? html`
+
+
+ `
+ : ""}
${this._deviceTrackersAvailable(this.hass)
? html`
@@ -185,10 +233,21 @@ class DialogPersonDetail extends LitElement {
slot="secondaryAction"
class="warning"
@click="${this._deleteEntry}"
- .disabled=${this._submitting}
+ .disabled=${(this._user && this._user.is_owner) ||
+ this._submitting}
>
${this.hass!.localize("ui.panel.config.person.detail.delete")}
+ ${this._user && this.hass.user?.is_owner
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.users.editor.change_password"
+ )}
+ `
+ : ""}
`
: html``}
) {
- this._error = undefined;
- this._userId = ev.detail.value;
+ private async _adminChanged(ev): Promise {
+ this._isAdmin = ev.target.checked;
+ }
+
+ private async _allowLoginChanged(ev): Promise {
+ const target = ev.target;
+ if (target.checked) {
+ target.checked = false;
+ showAddUserDialog(this, {
+ userAddedCallback: async (user?: User) => {
+ if (user) {
+ target.checked = true;
+ this._user = user;
+ this._userId = user.id;
+ this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
+ this._params?.refreshUsers();
+ }
+ },
+ name: this._name,
+ });
+ } else if (this._userId) {
+ if (
+ !(await showConfirmationDialog(this, {
+ text: this.hass!.localize(
+ "ui.panel.config.person.detail.confirm_delete_user",
+ "name",
+ this._name
+ ),
+ confirmText: this.hass!.localize(
+ "ui.panel.config.person.detail.delete"
+ ),
+ dismissText: this.hass!.localize("ui.common.cancel"),
+ }))
+ ) {
+ target.checked = true;
+ return;
+ }
+ await deleteUser(this.hass, this._userId);
+ this._params?.refreshUsers();
+ this._userId = undefined;
+ }
}
private _deviceTrackersChanged(ev: PolymerChangedEvent) {
@@ -228,9 +325,70 @@ class DialogPersonDetail extends LitElement {
this._picture = (ev.target as HaPictureUpload).value;
}
+ private async _changePassword() {
+ if (!this._user) {
+ return;
+ }
+ const credential = this._user.credentials.find(
+ (cred) => cred.type === "homeassistant"
+ );
+ if (!credential) {
+ showAlertDialog(this, {
+ title: "No Home Assistant credentials found.",
+ });
+ return;
+ }
+ const newPassword = await showPromptDialog(this, {
+ title: this.hass.localize("ui.panel.config.users.editor.change_password"),
+ inputType: "password",
+ inputLabel: this.hass.localize(
+ "ui.panel.config.users.editor.new_password"
+ ),
+ });
+ if (!newPassword) {
+ return;
+ }
+ const confirmPassword = await showPromptDialog(this, {
+ title: this.hass.localize("ui.panel.config.users.editor.change_password"),
+ inputType: "password",
+ inputLabel: this.hass.localize(
+ "ui.panel.config.users.add_user.password_confirm"
+ ),
+ });
+ if (!confirmPassword) {
+ return;
+ }
+ if (newPassword !== confirmPassword) {
+ showAlertDialog(this, {
+ title: this.hass.localize(
+ "ui.panel.config.users.add_user.password_not_match"
+ ),
+ });
+ return;
+ }
+ await adminChangePassword(this.hass, this._user.id, newPassword);
+ showAlertDialog(this, {
+ title: this.hass.localize(
+ "ui.panel.config.users.editor.password_changed"
+ ),
+ });
+ }
+
private async _updateEntry() {
this._submitting = true;
try {
+ if (
+ (this._userId && this._name !== this._params!.entry?.name) ||
+ this._isAdmin !== this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN)
+ ) {
+ await updateUser(this.hass!, this._userId!, {
+ name: this._name.trim(),
+ group_ids: [
+ this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
+ ],
+ });
+ this._params?.refreshUsers();
+ }
const values: PersonMutableParams = {
name: this._name.trim(),
device_trackers: this._deviceTrackers,
@@ -254,6 +412,9 @@ class DialogPersonDetail extends LitElement {
this._submitting = true;
try {
if (await this._params!.removeEntry()) {
+ if (this._params!.entry!.user_id) {
+ deleteUser(this.hass, this._params!.entry!.user_id);
+ }
this._params = undefined;
}
} finally {
@@ -275,6 +436,10 @@ class DialogPersonDetail extends LitElement {
ha-picture-upload {
display: block;
}
+ ha-formfield {
+ display: block;
+ padding: 16px 0;
+ }
ha-user-picker {
margin-top: 16px;
}
diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts
index 3d1de87bd0..829b12278a 100644
--- a/src/panels/config/person/ha-config-person.ts
+++ b/src/panels/config/person/ha-config-person.ts
@@ -1,6 +1,7 @@
import { mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
+import "@material/mwc-fab";
import {
css,
CSSResult,
@@ -223,6 +224,9 @@ class HaConfigPerson extends LitElement {
return false;
}
},
+ refreshUsers: () => {
+ this._usersLoad = fetchUsers(this.hass!);
+ },
});
}
diff --git a/src/panels/config/person/show-dialog-person-detail.ts b/src/panels/config/person/show-dialog-person-detail.ts
index 4f2f3f0e1c..3ffd16577f 100644
--- a/src/panels/config/person/show-dialog-person-detail.ts
+++ b/src/panels/config/person/show-dialog-person-detail.ts
@@ -5,6 +5,7 @@ import { User } from "../../../data/user";
export interface PersonDetailDialogParams {
entry?: Person;
users: User[];
+ refreshUsers: () => void;
createEntry: (values: PersonMutableParams) => Promise;
updateEntry: (updates: Partial) => Promise;
removeEntry: () => Promise;
diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts
index 6d6fd0ce76..7d291e9307 100644
--- a/src/panels/config/users/dialog-add-user.ts
+++ b/src/panels/config/users/dialog-add-user.ts
@@ -50,15 +50,24 @@ export class DialogAddUser extends LitElement {
@internalProperty() private _isAdmin?: boolean;
+ @internalProperty() private _allowChangeName = true;
+
public showDialog(params: AddUserDialogParams) {
this._params = params;
- this._name = "";
+ this._name = this._params.name || "";
this._username = "";
this._password = "";
this._passwordConfirm = "";
this._isAdmin = false;
this._error = undefined;
this._loading = false;
+
+ if (this._params.name) {
+ this._allowChangeName = false;
+ this._maybePopulateUsername();
+ } else {
+ this._allowChangeName = true;
+ }
}
protected firstUpdated(changedProperties: PropertyValues) {
@@ -84,19 +93,22 @@ export class DialogAddUser extends LitElement {
>
${this._error ? html`
${this._error}
` : ""}
-
-
+ ${this._allowChangeName
+ ? html`
`
+ : ""}
diff --git a/src/panels/config/users/show-dialog-add-user.ts b/src/panels/config/users/show-dialog-add-user.ts
index 5ff0e375e9..4ed4b18f9e 100644
--- a/src/panels/config/users/show-dialog-add-user.ts
+++ b/src/panels/config/users/show-dialog-add-user.ts
@@ -3,6 +3,7 @@ import { User } from "../../../data/user";
export interface AddUserDialogParams {
userAddedCallback: (user: User) => void;
+ name?: string;
}
export const loadAddUserDialog = () =>
diff --git a/src/translations/en.json b/src/translations/en.json
index d23e6b68d3..7a28805113 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1656,7 +1656,10 @@
"device_tracker_pick": "Pick device to track",
"delete": "Delete",
"create": "Create",
- "update": "Update"
+ "update": "Update",
+ "confirm_delete_user": "Are you sure you want to delete the user account for {name}? You can still track the user, but the person will no longer be able to login.",
+ "admin": "[%key:ui::panel::config::users::editor::admin%]",
+ "allow_login": "Allow person to login"
}
},
"zone": {
@@ -1676,7 +1679,7 @@
"new_zone": "New Zone",
"name": "Name",
"icon": "Icon",
- "icon_error_msg": "Icon should be in the format prefix:iconname, for example: mdi:home",
+ "icon_error_msg": "Icon should be in the format ''prefix:iconname'', for example: ''mdi:home''",
"radius": "Radius",
"latitude": "Latitude",
"longitude": "Longitude",