mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Allowing toggle of expiration date for refresh token (#20846)
* Allowing removal of expiration date for refresh token * Adjust wording and add icon * Update current * Update src/translations/en.json Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> * Update wording * Allow enable and disable * Better type * Better handle errors * Use relative date * Update src/panels/profile/ha-refresh-tokens-card.ts * Update API --------- Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
This commit is contained in:
parent
ccebae84a7
commit
7a7a355765
@ -11,6 +11,7 @@ export interface RefreshToken {
|
||||
client_id: string;
|
||||
client_name?: string;
|
||||
created_at: string;
|
||||
expire_at?: string;
|
||||
id: string;
|
||||
is_current: boolean;
|
||||
last_used_at?: string;
|
||||
|
@ -1,15 +1,33 @@
|
||||
import { mdiDelete } from "@mdi/js";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiAndroid,
|
||||
mdiApple,
|
||||
mdiClockCheckOutline,
|
||||
mdiClockRemoveOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiWeb,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { relativeTime } from "../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button-menu";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-settings-row";
|
||||
import "../../components/ha-icon-button";
|
||||
import { RefreshToken } from "../../data/refresh_token";
|
||||
import "../../components/ha-label";
|
||||
import "../../components/ha-settings-row";
|
||||
import { deleteAllRefreshTokens } from "../../data/auth";
|
||||
import { RefreshToken } from "../../data/refresh_token";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@ -17,6 +35,11 @@ import {
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
// Client ID used by iOS app
|
||||
const iOSclientId = "https://home-assistant.io/iOS";
|
||||
// Client ID used by Android app
|
||||
const androidClientId = "https://home-assistant.io/android";
|
||||
|
||||
const compareTokenLastUsedAt = (tokenA: RefreshToken, tokenB: RefreshToken) => {
|
||||
const timeA = tokenA.last_used_at ? new Date(tokenA.last_used_at) : 0;
|
||||
const timeB = tokenB.last_used_at ? new Date(tokenB.last_used_at) : 0;
|
||||
@ -38,95 +61,217 @@ class HaRefreshTokens extends LitElement {
|
||||
private _refreshTokens = memoizeOne(
|
||||
(refreshTokens: RefreshToken[]): RefreshToken[] =>
|
||||
refreshTokens
|
||||
?.filter((token) => token.type === "normal")
|
||||
.filter((token) => token.type === "normal")
|
||||
.sort(compareTokenLastUsedAt)
|
||||
);
|
||||
|
||||
private _formatTokenName = (token: RefreshToken): string => {
|
||||
if (token.client_id === iOSclientId) {
|
||||
return this.hass.localize("ui.panel.profile.refresh_tokens.ios_app");
|
||||
}
|
||||
if (token.client_id === androidClientId) {
|
||||
return this.hass.localize("ui.panel.profile.refresh_tokens.android_app");
|
||||
}
|
||||
return token.client_name || token.client_id;
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const refreshTokens = this._refreshTokens(this.refreshTokens!);
|
||||
return html`<ha-card
|
||||
.header=${this.hass.localize("ui.panel.profile.refresh_tokens.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.refresh_tokens.description")}
|
||||
${refreshTokens?.length
|
||||
? refreshTokens!.map(
|
||||
(token) =>
|
||||
html`<ha-settings-row three-line>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.token_title",
|
||||
{ clientId: token.client_id }
|
||||
)}
|
||||
</span>
|
||||
<div slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.created_at",
|
||||
{
|
||||
date: relativeTime(
|
||||
new Date(token.created_at),
|
||||
this.hass.locale
|
||||
),
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div slot="description">
|
||||
${token.last_used_at
|
||||
? this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.last_used",
|
||||
{
|
||||
date: relativeTime(
|
||||
new Date(token.last_used_at),
|
||||
this.hass.locale
|
||||
),
|
||||
location: token.last_used_ip,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.not_used"
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
${token.is_current
|
||||
? html`<simple-tooltip
|
||||
animation-delay="0"
|
||||
position="left"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.current_token_tooltip"
|
||||
const refreshTokens = this.refreshTokens
|
||||
? this._refreshTokens(this.refreshTokens)
|
||||
: [];
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.profile.refresh_tokens.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.refresh_tokens.description")}
|
||||
${refreshTokens.length
|
||||
? refreshTokens.map(
|
||||
(token) => html`
|
||||
<ha-settings-row three-line>
|
||||
<ha-svg-icon
|
||||
slot="prefix"
|
||||
.path=${token.client_id === iOSclientId
|
||||
? mdiApple
|
||||
: token.client_id === androidClientId
|
||||
? mdiAndroid
|
||||
: mdiWeb}
|
||||
></ha-svg-icon>
|
||||
<span slot="heading" class="primary">
|
||||
${this._formatTokenName(token)}
|
||||
</span>
|
||||
<div slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.created_at",
|
||||
{
|
||||
date: relativeTime(
|
||||
new Date(token.created_at),
|
||||
this.hass.locale
|
||||
),
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div slot="description">
|
||||
${token.is_current
|
||||
? html`
|
||||
<span class="current-session">
|
||||
<span class="dot"></span> ${this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.current_session"
|
||||
)}
|
||||
</span>
|
||||
`
|
||||
: token.last_used_at
|
||||
? this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.last_used",
|
||||
{
|
||||
date: relativeTime(
|
||||
new Date(token.last_used_at),
|
||||
this.hass.locale
|
||||
),
|
||||
location: token.last_used_ip,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.not_used"
|
||||
)}
|
||||
</div>
|
||||
<div slot="description">
|
||||
${token.expire_at
|
||||
? this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.expires_in",
|
||||
{
|
||||
date: relativeTime(
|
||||
new Date(token.expire_at),
|
||||
this.hass.locale
|
||||
),
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.never_expires"
|
||||
)}
|
||||
</simple-tooltip>`
|
||||
: ""}
|
||||
<ha-icon-button
|
||||
.token=${token}
|
||||
.disabled=${token.is_current}
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._deleteToken}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</ha-settings-row>`
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._deleteAllTokens}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.delete_all_tokens"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>`;
|
||||
</div>
|
||||
<div>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_END"
|
||||
menuCorner="END"
|
||||
@action=${this._handleAction}
|
||||
.token=${token}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${token.expire_at
|
||||
? mdiClockRemoveOutline
|
||||
: mdiClockCheckOutline}
|
||||
></ha-svg-icon>
|
||||
${token.expire_at
|
||||
? this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.disable_token_expiration"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.enable_token_expiration"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
class="warning"
|
||||
.disabled=${token.is_current}
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._deleteAllTokens}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.delete_all_tokens"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _deleteToken(ev: Event): Promise<void> {
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
const token = (ev.currentTarget as any).token;
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._toggleTokenExpiration(token);
|
||||
break;
|
||||
case 1:
|
||||
this._deleteToken(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _toggleTokenExpiration(token: RefreshToken): Promise<void> {
|
||||
const enable = !token.expire_at;
|
||||
if (!enable) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.confirm_disable_token_expiration_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.confirm_disable_token_expiration_text",
|
||||
{ name: this._formatTokenName(token) }
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.disable"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.hass.callWS({
|
||||
type: "auth/refresh_token_set_expiry",
|
||||
refresh_token_id: token.id,
|
||||
enable_expiry: enable,
|
||||
});
|
||||
fireEvent(this, "hass-refresh-tokens");
|
||||
} catch (err: unknown) {
|
||||
const message =
|
||||
typeof err === "object" && err !== null && "message" in err
|
||||
? (err.message as string)
|
||||
: String(err);
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.profile.refresh_tokens.${enable ? "enable" : "disable"}_expiration_failed`
|
||||
),
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteToken(token: RefreshToken): Promise<void> {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.confirm_delete",
|
||||
{ name: token.client_name || token.client_id }
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.confirm_delete_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.refresh_tokens.confirm_delete_text",
|
||||
{ name: this._formatTokenName(token) }
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
@ -183,6 +328,27 @@ class HaRefreshTokens extends LitElement {
|
||||
ha-icon-button {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-list-item[disabled],
|
||||
ha-list-item[disabled] ha-svg-icon {
|
||||
color: var(--disabled-text-color) !important;
|
||||
}
|
||||
ha-settings-row .current-session {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-settings-row .dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--success-color);
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
ha-settings-row > ha-svg-icon {
|
||||
margin-right: 12px;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -6371,11 +6371,22 @@
|
||||
"refresh_tokens": {
|
||||
"header": "Refresh tokens",
|
||||
"description": "Each refresh token represents a login session. Refresh tokens will be automatically removed when you click log out. Unused refresh tokens will be automatically removed after 90 days. The following refresh tokens are currently active for your account.",
|
||||
"token_title": "Refresh token for {clientId}",
|
||||
"ios_app": "iOS app",
|
||||
"android_app": "Android app",
|
||||
"current_session": "Your current session",
|
||||
"created_at": "Created {date}",
|
||||
"last_used": "Last used {date} from {location}",
|
||||
"not_used": "Has never been used",
|
||||
"confirm_delete": "Are you sure you want to delete the refresh token for {name}?",
|
||||
"expires_in": "Expires {date}",
|
||||
"never_expires": "Never expires",
|
||||
"disable_token_expiration": "Disable token expiration",
|
||||
"enable_token_expiration": "Enable token expiration",
|
||||
"confirm_disable_token_expiration_title": "Disable token expiration?",
|
||||
"confirm_disable_token_expiration_text": "The refresh token for ''{name}'' will never expire. Disabling token expiration can reduce security. Only disable token expiration on devices that are infrequently used (+90 days between use).",
|
||||
"disable_expiration_failed": "Failed to disable refresh token expiration",
|
||||
"enable_expiration_failed": "Failed to enable refresh token expiration",
|
||||
"confirm_delete_title": "Delete refresh token?",
|
||||
"confirm_delete_text": "The refresh token for ''{name}'' will be permanently deleted. This will end the login session on the associated device.",
|
||||
"delete_all_tokens": "Delete all tokens",
|
||||
"confirm_delete_all": "Are you sure you want to delete all refresh tokens? Your current session token will not be removed. Your long-lived access tokens will not be removed.",
|
||||
"delete_failed": "Failed to delete the refresh token.",
|
||||
|
Loading…
x
Reference in New Issue
Block a user