Add support for local only users (#10784)

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
Bram Kragten 2021-12-03 16:34:34 +01:00 committed by GitHub
parent 0bcb4d0e09
commit a54a2a54f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 33 deletions

View File

@ -13,6 +13,7 @@ export interface User {
name: string; name: string;
is_owner: boolean; is_owner: boolean;
is_active: boolean; is_active: boolean;
local_only: boolean;
system_generated: boolean; system_generated: boolean;
group_ids: string[]; group_ids: string[];
credentials: Credential[]; credentials: Credential[];
@ -22,6 +23,7 @@ export interface UpdateUserParams {
name?: User["name"]; name?: User["name"];
is_active?: User["is_active"]; is_active?: User["is_active"];
group_ids?: User["group_ids"]; group_ids?: User["group_ids"];
local_only?: boolean;
} }
export const fetchUsers = async (hass: HomeAssistant) => export const fetchUsers = async (hass: HomeAssistant) =>
@ -33,12 +35,14 @@ export const createUser = async (
hass: HomeAssistant, hass: HomeAssistant,
name: string, name: string,
// eslint-disable-next-line: variable-name // eslint-disable-next-line: variable-name
group_ids?: User["group_ids"] group_ids?: User["group_ids"],
local_only?: boolean
) => ) =>
hass.callWS<{ user: User }>({ hass.callWS<{ user: User }>({
type: "config/auth/create", type: "config/auth/create",
name, name,
group_ids, group_ids,
local_only,
}); });
export const updateUser = async ( export const updateUser = async (

View File

@ -51,6 +51,8 @@ class DialogPersonDetail extends LitElement {
@state() private _isAdmin?: boolean; @state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _deviceTrackers!: string[]; @state() private _deviceTrackers!: string[];
@state() private _picture!: string | null; @state() private _picture!: string | null;
@ -83,12 +85,14 @@ class DialogPersonDetail extends LitElement {
? this._params.users.find((user) => user.id === this._userId) ? this._params.users.find((user) => user.id === this._userId)
: undefined; : undefined;
this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = this._user?.local_only;
} else { } else {
this._personExists = false; this._personExists = false;
this._name = ""; this._name = "";
this._userId = undefined; this._userId = undefined;
this._user = undefined; this._user = undefined;
this._isAdmin = undefined; this._isAdmin = undefined;
this._localOnly = undefined;
this._deviceTrackers = []; this._deviceTrackers = [];
this._picture = null; this._picture = null;
} }
@ -152,19 +156,31 @@ class DialogPersonDetail extends LitElement {
${this._user ${this._user
? html`<ha-formfield ? html`<ha-formfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.person.detail.admin" "ui.panel.config.person.detail.local_only"
)} )}
.dir=${computeRTLDirection(this.hass)} .dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${this._user.system_generated ||
this._user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
> >
</ha-switch> <ha-switch
</ha-formfield>` .checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.person.detail.admin"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${this._user.system_generated ||
this._user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>`
: ""} : ""}
${this._deviceTrackersAvailable(this.hass) ${this._deviceTrackersAvailable(this.hass)
? html` ? html`
@ -266,10 +282,14 @@ class DialogPersonDetail extends LitElement {
this._name = ev.detail.value; this._name = ev.detail.value;
} }
private async _adminChanged(ev): Promise<void> { private _adminChanged(ev): void {
this._isAdmin = ev.target.checked; this._isAdmin = ev.target.checked;
} }
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private async _allowLoginChanged(ev): Promise<void> { private async _allowLoginChanged(ev): Promise<void> {
const target = ev.target; const target = ev.target;
if (target.checked) { if (target.checked) {
@ -281,6 +301,7 @@ class DialogPersonDetail extends LitElement {
this._user = user; this._user = user;
this._userId = user.id; this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only;
this._params?.refreshUsers(); this._params?.refreshUsers();
} }
}, },
@ -373,13 +394,16 @@ class DialogPersonDetail extends LitElement {
try { try {
if ( if (
(this._userId && this._name !== this._params!.entry?.name) || (this._userId && this._name !== this._params!.entry?.name) ||
this._isAdmin !== this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) this._isAdmin !==
this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) ||
this._localOnly !== this._user?.local_only
) { ) {
await updateUser(this.hass!, this._userId!, { await updateUser(this.hass!, this._userId!, {
name: this._name.trim(), name: this._name.trim(),
group_ids: [ group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
], ],
local_only: this._localOnly,
}); });
this._params?.refreshUsers(); this._params?.refreshUsers();
} }

View File

@ -48,6 +48,8 @@ export class DialogAddUser extends LitElement {
@state() private _isAdmin?: boolean; @state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _allowChangeName = true; @state() private _allowChangeName = true;
public showDialog(params: AddUserDialogParams) { public showDialog(params: AddUserDialogParams) {
@ -57,6 +59,7 @@ export class DialogAddUser extends LitElement {
this._password = ""; this._password = "";
this._passwordConfirm = ""; this._passwordConfirm = "";
this._isAdmin = false; this._isAdmin = false;
this._localOnly = false;
this._error = undefined; this._error = undefined;
this._loading = false; this._loading = false;
@ -153,14 +156,32 @@ export class DialogAddUser extends LitElement {
"ui.panel.config.users.add_user.password_not_match" "ui.panel.config.users.add_user.password_not_match"
)} )}
></paper-input> ></paper-input>
<div class="row">
<ha-formfield <ha-formfield
.label=${this.hass.localize("ui.panel.config.users.editor.admin")} .label=${this.hass.localize(
.dir=${computeRTLDirection(this.hass)} "ui.panel.config.users.editor.local_only"
> )}
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}> .dir=${computeRTLDirection(this.hass)}
</ha-switch> >
</ha-formfield> <ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row">
<ha-formfield
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>
</div>
${!this._isAdmin ${!this._isAdmin
? html` ? html`
<br /> <br />
@ -218,6 +239,10 @@ export class DialogAddUser extends LitElement {
this._isAdmin = ev.target.checked; this._isAdmin = ev.target.checked;
} }
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private async _createUser(ev) { private async _createUser(ev) {
ev.preventDefault(); ev.preventDefault();
if (!this._name || !this._username || !this._password) { if (!this._name || !this._username || !this._password) {
@ -229,9 +254,12 @@ export class DialogAddUser extends LitElement {
let user: User; let user: User;
try { try {
const userResponse = await createUser(this.hass, this._name, [ const userResponse = await createUser(
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, this.hass,
]); this._name,
[this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER],
this._localOnly
);
user = userResponse.user; user = userResponse.user;
} catch (err: any) { } catch (err: any) {
this._loading = false; this._loading = false;
@ -266,8 +294,9 @@ export class DialogAddUser extends LitElement {
--mdc-dialog-max-width: 500px; --mdc-dialog-max-width: 500px;
--dialog-z-index: 10; --dialog-z-index: 10;
} }
ha-switch { .row {
margin-top: 8px; display: flex;
padding: 8px 0;
} }
`, `,
]; ];

View File

@ -30,6 +30,8 @@ class DialogUserDetail extends LitElement {
@state() private _isAdmin?: boolean; @state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _isActive?: boolean; @state() private _isActive?: boolean;
@state() private _error?: string; @state() private _error?: string;
@ -43,6 +45,7 @@ class DialogUserDetail extends LitElement {
this._error = undefined; this._error = undefined;
this._name = params.entry.name || ""; this._name = params.entry.name || "";
this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = params.entry.local_only;
this._isActive = params.entry.is_active; this._isActive = params.entry.is_active;
await this.updateComplete; await this.updateComplete;
} }
@ -95,6 +98,20 @@ class DialogUserDetail extends LitElement {
@value-changed=${this._nameChanged} @value-changed=${this._nameChanged}
label=${this.hass!.localize("ui.panel.config.users.editor.name")} label=${this.hass!.localize("ui.panel.config.users.editor.name")}
></paper-input> ></paper-input>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.users.editor.local_only"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row"> <div class="row">
<ha-formfield <ha-formfield
.label=${this.hass.localize( .label=${this.hass.localize(
@ -198,11 +215,15 @@ class DialogUserDetail extends LitElement {
this._name = ev.detail.value; this._name = ev.detail.value;
} }
private async _adminChanged(ev): Promise<void> { private _adminChanged(ev): void {
this._isAdmin = ev.target.checked; this._isAdmin = ev.target.checked;
} }
private async _activeChanged(ev): Promise<void> { private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private _activeChanged(ev): void {
this._isActive = ev.target.checked; this._isActive = ev.target.checked;
} }
@ -215,6 +236,7 @@ class DialogUserDetail extends LitElement {
group_ids: [ group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
], ],
local_only: this._localOnly,
}); });
this._close(); this._close();
} catch (err: any) { } catch (err: any) {

View File

@ -90,7 +90,7 @@ export class HaConfigUsers extends LitElement {
width: "80px", width: "80px",
template: (is_active) => template: (is_active) =>
is_active is_active
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>` ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: "", : "",
}, },
system_generated: { system_generated: {
@ -103,9 +103,20 @@ export class HaConfigUsers extends LitElement {
width: "160px", width: "160px",
template: (generated) => template: (generated) =>
generated generated
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>` ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: "", : "",
}, },
local_only: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.local"
),
type: "icon",
sortable: true,
filterable: true,
width: "160px",
template: (local) =>
local ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
},
}; };
return columns; return columns;

View File

@ -2297,6 +2297,7 @@
"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.", "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%]", "admin": "[%key:ui::panel::config::users::editor::admin%]",
"local_only": "[%key:ui::panel::config::users::editor::local_only%]",
"allow_login": "Allow person to login" "allow_login": "Allow person to login"
} }
}, },
@ -2456,7 +2457,8 @@
"group": "Group", "group": "Group",
"system": "System generated", "system": "System generated",
"is_active": "Active", "is_active": "Active",
"is_owner": "Owner" "is_owner": "Owner",
"local": "Local only"
}, },
"add_user": "Add user" "add_user": "Add user"
}, },
@ -2476,6 +2478,7 @@
"admin": "Administrator", "admin": "Administrator",
"group": "Group", "group": "Group",
"active": "Active", "active": "Active",
"local_only": "Can only login from the local network",
"system_generated": "System generated", "system_generated": "System generated",
"system_generated_users_not_removable": "Unable to remove system generated users.", "system_generated_users_not_removable": "Unable to remove system generated users.",
"system_generated_users_not_editable": "Unable to update system generated users.", "system_generated_users_not_editable": "Unable to update system generated users.",
@ -2488,6 +2491,7 @@
"password": "Password", "password": "Password",
"password_confirm": "Confirm Password", "password_confirm": "Confirm Password",
"password_not_match": "Passwords don't match", "password_not_match": "Passwords don't match",
"local_only": "Local only",
"create": "Create" "create": "Create"
} }
}, },