From d43b5d33375a4cc8d0204bbd24e36369a5bfe599 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Sep 2020 15:22:40 +0200 Subject: [PATCH] Integrate user management in person (#7170) --- src/panels/config/ha-panel-config.ts | 1 + .../config/person/dialog-person-detail.ts | 191 ++++++++++++++++-- src/panels/config/person/ha-config-person.ts | 4 + .../person/show-dialog-person-detail.ts | 1 + src/panels/config/users/dialog-add-user.ts | 40 ++-- src/panels/config/users/dialog-user-detail.ts | 4 +- .../config/users/show-dialog-add-user.ts | 1 + src/translations/en.json | 7 +- 8 files changed, 218 insertions(+), 31 deletions(-) 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",