From 86548052e5293f5110142a906f3e085dfb26dd49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 11 Mar 2019 12:08:09 -0700 Subject: [PATCH] Allow changing group (#2908) * Allow changing group * Styling + rename * Fix type --- src/components/ha-sidebar.ts | 113 ++++----- src/components/user/ha-user-badge.ts | 2 +- src/components/user/ha-user-picker.ts | 2 +- src/data/auth.ts | 19 +- src/data/user.ts | 49 ++++ src/fake_data/provide_hass.ts | 1 + src/panels/config/person/ha-config-person.ts | 2 +- .../person/show-dialog-person-detail.ts | 2 +- .../config/users/ha-config-user-picker.js | 6 +- src/panels/config/users/ha-config-users.js | 2 +- src/panels/config/users/ha-user-editor.js | 113 --------- src/panels/config/users/ha-user-editor.ts | 217 ++++++++++++++++++ src/translations/en.json | 5 + src/types.ts | 1 + 14 files changed, 343 insertions(+), 191 deletions(-) create mode 100644 src/data/user.ts delete mode 100644 src/panels/config/users/ha-user-editor.js create mode 100644 src/panels/config/users/ha-user-editor.ts diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 6e6d5fc658..e51371dc51 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -22,6 +22,7 @@ import { DEFAULT_PANEL } from "../common/const"; const computeUrl = (urlPath) => `/${urlPath}`; const computePanels = (hass: HomeAssistant) => { + const isAdmin = hass.user.is_admin; const panels = hass.panels; const sortValue = { map: 1, @@ -30,9 +31,9 @@ const computePanels = (hass: HomeAssistant) => { }; const result: Panel[] = []; - Object.keys(panels).forEach((key) => { - if (panels[key].title) { - result.push(panels[key]); + Object.values(panels).forEach((panel) => { + if (panel.title && (panel.component_name !== "config" || isAdmin)) { + result.push(panel); } }); @@ -129,62 +130,66 @@ class HaSidebar extends LitElement { : html``} -
-
+ ${!hass.user.is_admin + ? "" + : html` +
+
-
- ${hass.localize("ui.sidebar.developer_tools")} -
+
+ ${hass.localize("ui.sidebar.developer_tools")} +
-
- - - - - - - - - - - - - ${isComponentLoaded(hass, "mqtt") - ? html` - +
+ - ` - : html``} - - - -
-
+ + + + + + + + + + ${isComponentLoaded(hass, "mqtt") + ? html` + + + + ` + : html``} + + + +
+
+ `} `; } diff --git a/src/components/user/ha-user-badge.ts b/src/components/user/ha-user-badge.ts index a4bb8a5a31..8df39f752b 100644 --- a/src/components/user/ha-user-badge.ts +++ b/src/components/user/ha-user-badge.ts @@ -8,7 +8,7 @@ import { customElement, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; -import { User } from "../../data/auth"; +import { User } from "../../data/user"; import { CurrentUser } from "../../types"; const computeInitials = (name: string) => { diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts index 9fcc6db530..d98186cea0 100644 --- a/src/components/user/ha-user-picker.ts +++ b/src/components/user/ha-user-picker.ts @@ -15,7 +15,7 @@ import { } from "lit-element"; import { HomeAssistant } from "../../types"; import { fireEvent } from "../../common/dom/fire_event"; -import { User, fetchUsers } from "../../data/auth"; +import { User, fetchUsers } from "../../data/user"; import compare from "../../common/string/compare"; class HaEntityPicker extends LitElement { diff --git a/src/data/auth.ts b/src/data/auth.ts index 4ec1e57146..92a44130e2 100644 --- a/src/data/auth.ts +++ b/src/data/auth.ts @@ -1,26 +1,9 @@ -import { HomeAssistant } from "../types"; - export interface AuthProvider { name: string; id: string; type: string; } -interface Credential { +export interface Credential { type: string; } - -export interface User { - id: string; - name: string; - is_owner: boolean; - is_active: boolean; - system_generated: boolean; - group_ids: string[]; - credentials: Credential[]; -} - -export const fetchUsers = async (hass: HomeAssistant) => - hass.callWS({ - type: "config/auth/list", - }); diff --git a/src/data/user.ts b/src/data/user.ts new file mode 100644 index 0000000000..41045906c4 --- /dev/null +++ b/src/data/user.ts @@ -0,0 +1,49 @@ +import { HomeAssistant } from "../types"; +import { Credential } from "./auth"; + +export const SYSTEM_GROUP_ID_ADMIN = "system-admin"; +export const SYSTEM_GROUP_ID_USER = "system-users"; +export const SYSTEM_GROUP_ID_READ_ONLY = "system-read-only"; + +export interface User { + id: string; + name: string; + is_owner: boolean; + is_active: boolean; + system_generated: boolean; + group_ids: string[]; + credentials: Credential[]; +} + +interface UpdateUserParams { + name?: User["name"]; + group_ids?: User["group_ids"]; +} + +export const fetchUsers = async (hass: HomeAssistant) => + hass.callWS({ + type: "config/auth/list", + }); + +export const createUser = async (hass: HomeAssistant, name: string) => + hass.callWS<{ user: User }>({ + type: "config/auth/create", + name, + }); + +export const updateUser = async ( + hass: HomeAssistant, + userId: string, + params: UpdateUserParams +) => + hass.callWS<{ user: User }>({ + ...params, + type: "config/auth/update", + user_id: userId, + }); + +export const deleteUser = async (hass: HomeAssistant, userId: string) => + hass.callWS({ + type: "config/auth/delete", + user_id: userId, + }); diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index b8f9edd169..0adf94c8c9 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -130,6 +130,7 @@ export const provideHass = ( user: { credentials: [], id: "abcd", + is_admin: true, is_owner: true, mfa_modules: [], name: "Demo User", diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 25071c52b7..bc9ea04ca2 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -27,7 +27,7 @@ import { showPersonDetailDialog, loadPersonDetailDialog, } from "./show-dialog-person-detail"; -import { User, fetchUsers } from "../../../data/auth"; +import { User, fetchUsers } from "../../../data/user"; class HaConfigPerson extends LitElement { public hass?: HomeAssistant; diff --git a/src/panels/config/person/show-dialog-person-detail.ts b/src/panels/config/person/show-dialog-person-detail.ts index 96aead9702..89744a7838 100644 --- a/src/panels/config/person/show-dialog-person-detail.ts +++ b/src/panels/config/person/show-dialog-person-detail.ts @@ -1,6 +1,6 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { Person, PersonMutableParams } from "../../../data/person"; -import { User } from "../../../data/auth"; +import { User } from "../../../data/user"; export interface PersonDetailDialogParams { entry?: Person; diff --git a/src/panels/config/users/ha-config-user-picker.js b/src/panels/config/users/ha-config-user-picker.js index aa028cf4c3..356ad9169f 100644 --- a/src/panels/config/users/ha-config-user-picker.js +++ b/src/panels/config/users/ha-config-user-picker.js @@ -66,7 +66,7 @@ class HaUserPicker extends EventsMixin(
[[_withDefault(user.name, 'Unnamed User')]]
- [[user.id]] + [[_computeGroup(localize, user)]] @@ -124,6 +124,10 @@ class HaUserPicker extends EventsMixin( return `/config/users/${user.id}`; } + _computeGroup(localize, user) { + return localize(`groups.${user.group_ids[0]}`); + } + _computeRTL(hass) { return computeRTL(hass); } diff --git a/src/panels/config/users/ha-config-users.js b/src/panels/config/users/ha-config-users.js index 16cfe6d5b9..6778f282cb 100644 --- a/src/panels/config/users/ha-config-users.js +++ b/src/panels/config/users/ha-config-users.js @@ -9,7 +9,7 @@ import NavigateMixin from "../../../mixins/navigate-mixin"; import "./ha-config-user-picker"; import "./ha-user-editor"; import { fireEvent } from "../../../common/dom/fire_event"; -import { fetchUsers } from "../../../data/auth"; +import { fetchUsers } from "../../../data/user"; /* * @appliesMixin NavigateMixin diff --git a/src/panels/config/users/ha-user-editor.js b/src/panels/config/users/ha-user-editor.js deleted file mode 100644 index 15e1e9ec74..0000000000 --- a/src/panels/config/users/ha-user-editor.js +++ /dev/null @@ -1,113 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../layouts/hass-subpage"; -import LocalizeMixin from "../../../mixins/localize-mixin"; -import NavigateMixin from "../../../mixins/navigate-mixin"; -import EventsMixin from "../../../mixins/events-mixin"; - -/* - * @appliesMixin LocalizeMixin - * @appliesMixin NavigateMixin - * @appliesMixin EventsMixin - */ -class HaUserEditor extends EventsMixin( - NavigateMixin(LocalizeMixin(PolymerElement)) -) { - static get template() { - return html` - - - - - - - - - - - - - - - - - - - - - -
ID[[user.id]]
Owner[[user.is_owner]]
Active[[user.is_active]]
System generated[[user.system_generated]]
-
- -
- - [[localize('ui.panel.config.users.editor.delete_user')]] - - -
-
-
- `; - } - - static get properties() { - return { - hass: Object, - user: Object, - }; - } - - _computeName(user) { - return user && (user.name || "Unnamed user"); - } - - async _deleteUser(ev) { - if ( - !confirm( - `Are you sure you want to delete ${this._computeName(this.user)}` - ) - ) { - ev.target.blur(); - return; - } - try { - await this.hass.callWS({ - type: "config/auth/delete", - user_id: this.user.id, - }); - } catch (err) { - alert(err.code); - return; - } - this.fire("reload-users"); - this.navigate("/config/users"); - } -} - -customElements.define("ha-user-editor", HaUserEditor); diff --git a/src/panels/config/users/ha-user-editor.ts b/src/panels/config/users/ha-user-editor.ts new file mode 100644 index 0000000000..245902d84f --- /dev/null +++ b/src/panels/config/users/ha-user-editor.ts @@ -0,0 +1,217 @@ +import { + LitElement, + TemplateResult, + html, + customElement, + CSSResultArray, + css, + property, +} from "lit-element"; +import { until } from "lit-html/directives/until"; +import "@material/mwc-button"; + +import "../../../layouts/hass-subpage"; +import { haStyle } from "../../../resources/styles"; +import "../../../components/ha-card"; +import { HomeAssistant } from "../../../types"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { navigate } from "../../../common/navigate"; +import { + User, + deleteUser, + updateUser, + SYSTEM_GROUP_ID_USER, + SYSTEM_GROUP_ID_ADMIN, +} from "../../../data/user"; + +declare global { + interface HASSDomEvents { + "reload-users": undefined; + } +} + +const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN]; + +@customElement("ha-user-editor") +class HaUserEditor extends LitElement { + @property() public hass?: HomeAssistant; + @property() public user?: User; + + protected render(): TemplateResult | void { + const hass = this.hass; + const user = this.user; + if (!hass || !user) { + return html``; + } + + return html` + + + + + + + + + + + + + + + + ${user.group_ids[0] === SYSTEM_GROUP_ID_USER + ? html` + + + + ` + : ""} + + + + + + + + + +
ID${user.id}
Owner${user.is_owner}
Group + +
+ The users group is a work in progress. The user will be + unable to administer the instance via the UI. We're still + auditing all management API endpoints to ensure that they + correctly limit access to administrators. +
Active${user.is_active}
System generated${user.system_generated}
+ +
+ + ${hass.localize("ui.panel.config.users.editor.rename_user")} + + + ${hass.localize("ui.panel.config.users.editor.delete_user")} + + ${user.system_generated + ? html` + Unable to remove system generated users. + ` + : ""} +
+
+
+ `; + } + + private get _name() { + return this.user && (this.user.name || "Unnamed user"); + } + + private async _handleRenameUser(ev): Promise { + ev.currentTarget.blur(); + const newName = prompt("New name?", this.user!.name); + if (newName === null || newName === this.user!.name) { + return; + } + + try { + await updateUser(this.hass!, this.user!.id, { + name: newName, + }); + fireEvent(this, "reload-users"); + } catch (err) { + alert(`User rename failed: ${err.message}`); + } + } + + private async _handleGroupChange(ev): Promise { + const selectEl = ev.currentTarget as HTMLSelectElement; + const newGroup = selectEl.value; + try { + await updateUser(this.hass!, this.user!.id, { + group_ids: [newGroup], + }); + fireEvent(this, "reload-users"); + } catch (err) { + alert(`Group update failed: ${err.message}`); + selectEl.value = this.user!.group_ids[0]; + } + } + + private async _deleteUser(ev): Promise { + if (!confirm(`Are you sure you want to delete ${this._name}`)) { + ev.target.blur(); + return; + } + try { + await deleteUser(this.hass!, this.user!.id); + } catch (err) { + alert(err.code); + return; + } + fireEvent(this, "reload-users"); + navigate(this, "/config/users"); + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + ha-card { + display: block; + max-width: 600px; + margin: 0 auto 16px; + } + ha-card:first-child { + margin-top: 16px; + } + ha-card:last-child { + margin-bottom: 16px; + } + .card-content { + padding: 0 16px 16px; + } + .card-actions { + padding: 0 8px; + } + hass-subpage ha-card:first-of-type { + direction: ltr; + } + table { + width: 100%; + } + td { + vertical-align: top; + } + .user-experiment { + padding: 8px 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-user-editor": HaUserEditor; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 3fe3ab60d6..d28a834f45 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -355,6 +355,11 @@ "not_home": "[%key:state::person::not_home%]" } }, + "groups": { + "system-admin": "Administrators", + "system-users": "Users", + "system-read-only": "Read-Only Users" + }, "ui": { "auth_store": { "ask": "Do you want to save this login?", diff --git a/src/types.ts b/src/types.ts index 946f5e8137..bcefb5418a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,7 @@ export interface MFAModule { export interface CurrentUser { id: string; is_owner: boolean; + is_admin: boolean; name: string; credentials: Credential[]; mfa_modules: MFAModule[];