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

View File

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

View File

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

View File

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

View File

@ -90,7 +90,7 @@ export class HaConfigUsers extends LitElement {
width: "80px",
template: (is_active) =>
is_active
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>`
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: "",
},
system_generated: {
@ -103,9 +103,20 @@ export class HaConfigUsers extends LitElement {
width: "160px",
template: (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;

View File

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