From fadc14eddca2e3b2b314696a17097a38a9688bab Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:58:23 +0200 Subject: [PATCH] web: Fix stale clipboard tokens, untranslated labels (#23063) * web: Fix stale clipboard tokens, untranslated labels. * Fix tooltip. * Fix type error. * Update types. * Fix types. Clean up composite. * Fix label names. * Fix broken HTML. * Fix labels, formatters. * Clean up properties, lifecyle. --- web/packages/lex/index.d.ts | 23 ++ web/packages/lex/index.js | 9 +- web/packages/lex/package.json | 2 + .../connectors/ConnectorsListPage.ts | 3 + .../agent/EnrollmentTokenListPage.ts | 3 + .../admin/endpoints/devices/DeviceListPage.ts | 2 +- .../admin/lifecycle/ObjectReviewIteration.ts | 2 +- .../providers/rac/ConnectionTokenList.ts | 3 + .../invitation/wizard/InvitationWizard.ts | 2 +- web/src/admin/tokens/TokenForm.ts | 16 +- web/src/admin/tokens/TokenListPage.ts | 6 +- web/src/admin/users/UserDevicesTable.ts | 3 + web/src/admin/users/UserTokenList.ts | 172 +++++++++++ web/src/admin/users/UserViewPage.ts | 277 +++++++++--------- .../admin/users/oauth/UserAccessTokenList.ts | 3 + .../admin/users/oauth/UserRefreshTokenList.ts | 3 + web/src/common/clipboard.ts | 3 +- web/src/common/labels.ts | 141 +++++---- web/src/common/messages.ts | 4 +- web/src/common/ui/locale/format.ts | 10 +- web/src/elements/ak-table/ak-simple-table.ts | 4 +- web/src/elements/buttons/IconCopyButton.ts | 7 +- .../elements/buttons/IconTokenCopyButton.ts | 36 ++- web/src/elements/forms/Form.ts | 4 +- web/src/elements/table/Table.ts | 122 ++++++-- web/src/elements/table/TablePage.ts | 2 +- web/src/elements/user/SessionList.ts | 3 + web/src/elements/user/UserConsentList.ts | 3 + web/src/elements/user/UserReputationList.ts | 3 + web/src/elements/wizard/CreateWizard.ts | 8 +- web/src/elements/wizard/Wizard.ts | 8 +- .../user-settings/tokens/UserTokenList.ts | 6 +- web/tsconfig.json | 8 +- 33 files changed, 640 insertions(+), 261 deletions(-) create mode 100644 web/packages/lex/index.d.ts create mode 100644 web/src/admin/users/UserTokenList.ts diff --git a/web/packages/lex/index.d.ts b/web/packages/lex/index.d.ts new file mode 100644 index 0000000000..9931fa55b4 --- /dev/null +++ b/web/packages/lex/index.d.ts @@ -0,0 +1,23 @@ +export type Token = unknown; + +export type LexerAction = ( + this: Lexer, + match: string, + ...captures: string[] +) => Token | Token[] | null | void; + +export type DefunctHandler = (this: Lexer, chr: string) => Token | Token[] | null | void; + +export class Lexer { + state: number; + index: number; + input: string; + reject: boolean; + + constructor(defunct?: DefunctHandler); + addRule(pattern: RegExp, action: LexerAction, start?: number[]): this; + setInput(input: string): this; + lex(): Token | null; +} + +export default Lexer; diff --git a/web/packages/lex/index.js b/web/packages/lex/index.js index 6a92a377c0..4f99524459 100644 --- a/web/packages/lex/index.js +++ b/web/packages/lex/index.js @@ -174,10 +174,11 @@ export class Lexer { this.reject = false; this.#remove++; - let token = match.action.apply( - this, - /** @type {string[]} */ (/** @type {unknown} */ (result)), - ); + // TypeScript Native's assessment is correct. + // but TypeScript Node can't make the connection between regex and the array. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Remove after TypeScript Native is mainline. + let token = match.action.apply(this, result); if (this.reject) { this.index = result.index; diff --git a/web/packages/lex/package.json b/web/packages/lex/package.json index 8aae13859b..2e151e2434 100644 --- a/web/packages/lex/package.json +++ b/web/packages/lex/package.json @@ -5,8 +5,10 @@ "license": "MIT", "private": true, "type": "module", + "types": "./index.d.ts", "exports": { ".": { + "types": "./index.d.ts", "import": "./index.js" } } diff --git a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts index d057ce9d0c..65b5ac4bea 100644 --- a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts +++ b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts @@ -22,6 +22,9 @@ import { customElement } from "lit/decorators.js"; @customElement("ak-endpoints-connectors-list") export class ConnectorsListPage extends TablePage { + public static override verboseName = msg("Connector"); + public static override verboseNamePlural = msg("Connectors"); + public override searchPlaceholder = msg("Search connectors by name or type..."); public override pageIcon = "pf-icon pf-icon-data-source"; public override pageTitle = msg("Connectors"); diff --git a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts index f14daaf1e0..b2f72b6906 100644 --- a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts +++ b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts @@ -26,6 +26,9 @@ import { customElement, property } from "lit/decorators.js"; export class EnrollmentTokenListPage extends Table { #api = aki(EndpointsApi); + public static override verboseName = msg("Enrollment Token"); + public static override verboseNamePlural = msg("Enrollment Tokens"); + protected override searchEnabled = true; protected emptyStateMessage = msg("No enrollment tokens found for this connector."); diff --git a/web/src/admin/endpoints/devices/DeviceListPage.ts b/web/src/admin/endpoints/devices/DeviceListPage.ts index d07fadd83e..170cf4b722 100644 --- a/web/src/admin/endpoints/devices/DeviceListPage.ts +++ b/web/src/admin/endpoints/devices/DeviceListPage.ts @@ -64,7 +64,7 @@ export class DeviceListPage extends TablePage { ${inner ? inner : html`${msg("No objects found.")} + >${this.formatEmptyStateMessage()}
${this.search ? this.renderEmptyClearSearch() : nothing}

diff --git a/web/src/admin/lifecycle/ObjectReviewIteration.ts b/web/src/admin/lifecycle/ObjectReviewIteration.ts index e1316c5a00..3c2d6b4518 100644 --- a/web/src/admin/lifecycle/ObjectReviewIteration.ts +++ b/web/src/admin/lifecycle/ObjectReviewIteration.ts @@ -244,7 +244,7 @@ export class ObjectReviewIteration extends Table { protected override renderEmpty(): SlottedTemplateResult { return super.renderEmpty( html` ${this.emptyStateMessage}${this.formatEmptyStateMessage()}`, ); } diff --git a/web/src/admin/providers/rac/ConnectionTokenList.ts b/web/src/admin/providers/rac/ConnectionTokenList.ts index d4ea90885c..ea661933d5 100644 --- a/web/src/admin/providers/rac/ConnectionTokenList.ts +++ b/web/src/admin/providers/rac/ConnectionTokenList.ts @@ -18,6 +18,9 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList @customElement("ak-rac-connection-token-list") export class ConnectionTokenListPage extends Table { + public static override verboseName = msg("Connection Token"); + public static override verboseNamePlural = msg("Connection Tokens"); + checkbox = true; clearOnRefresh = true; diff --git a/web/src/admin/stages/invitation/wizard/InvitationWizard.ts b/web/src/admin/stages/invitation/wizard/InvitationWizard.ts index 1757921ad8..ac5694a198 100644 --- a/web/src/admin/stages/invitation/wizard/InvitationWizard.ts +++ b/web/src/admin/stages/invitation/wizard/InvitationWizard.ts @@ -30,7 +30,7 @@ export class InvitationWizard extends AKElement implements TransclusionChildElem protected override render(): SlottedTemplateResult { return html` diff --git a/web/src/admin/tokens/TokenForm.ts b/web/src/admin/tokens/TokenForm.ts index 3e44b12251..ef9d771885 100644 --- a/web/src/admin/tokens/TokenForm.ts +++ b/web/src/admin/tokens/TokenForm.ts @@ -16,7 +16,7 @@ import { CoreApi, CoreUsersListRequest, IntentEnum, Token, User } from "@goauthe import { msg } from "@lit/localize"; import { html, TemplateResult } from "lit"; -import { customElement, state } from "lit/decorators.js"; +import { customElement, property, state } from "lit/decorators.js"; const EXPIRATION_DURATION = 30 * 60 * 1000; // 30 minutes @@ -25,6 +25,12 @@ export class TokenForm extends ModelForm { public static override verboseName = msg("Token"); public static override verboseNamePlural = msg("Tokens"); + /** + * Pre-selected user for new tokens, e.g. when creating a token from a user's detail page. + */ + @property({ attribute: false }) + public defaultUser: User | null = null; + protected expirationMinimumDate = new Date(); @state() @@ -114,7 +120,7 @@ export class TokenForm extends ModelForm { } const users = await aki(CoreApi).coreUsersList(args); - const instanceUser = this.instance?.userObj; + const instanceUser = this.instance?.userObj ?? this.defaultUser; if (!instanceUser) { return users.results; @@ -136,7 +142,11 @@ export class TokenForm extends ModelForm { return user?.pk; }} .selected=${(user: User): boolean => { - return this.instance?.user === user.pk; + if (this.instance) { + return this.instance.user === user.pk; + } + + return this.defaultUser?.pk === user.pk; }} > diff --git a/web/src/admin/tokens/TokenListPage.ts b/web/src/admin/tokens/TokenListPage.ts index e3a032f756..db4dfa46b0 100644 --- a/web/src/admin/tokens/TokenListPage.ts +++ b/web/src/admin/tokens/TokenListPage.ts @@ -8,7 +8,7 @@ import "#elements/forms/ModalForm"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { aki } from "#common/api/client"; -import { intentToLabel } from "#common/labels"; +import { formatIntentLabel } from "#common/labels"; import { IconTokenCopyButton } from "#elements/buttons/IconTokenCopyButton"; import { IconEditButton, ModalInvokerButton } from "#elements/dialogs"; @@ -94,7 +94,7 @@ export class TokenListPage extends TablePage { html`${item.userObj?.username}`, html``, Timestamp(item.expires && item.expiring ? item.expires : null), - html`${intentToLabel(item.intent ?? IntentEnum.Api)}`, + html`${formatIntentLabel(item.intent ?? IntentEnum.Api)}`, html`

${!item.managed ? IconEditButton(TokenForm, item.identifier, item.identifier) @@ -110,7 +110,7 @@ export class TokenListPage extends TablePage { model: ModelEnum.AuthentikCoreToken, objectPk: item.pk, })} - ${IconTokenCopyButton(item.identifier)} + ${IconTokenCopyButton(item)}
`, ]; } diff --git a/web/src/admin/users/UserDevicesTable.ts b/web/src/admin/users/UserDevicesTable.ts index 4dcd2907a8..7cb9200f6e 100644 --- a/web/src/admin/users/UserDevicesTable.ts +++ b/web/src/admin/users/UserDevicesTable.ts @@ -16,6 +16,9 @@ import { customElement, property } from "lit/decorators.js"; @customElement("ak-user-device-table") export class UserDeviceTable extends Table { + public static override verboseName = msg("Device"); + public static override verboseNamePlural = msg("Devices"); + @property({ type: Number }) userId?: number; diff --git a/web/src/admin/users/UserTokenList.ts b/web/src/admin/users/UserTokenList.ts new file mode 100644 index 0000000000..dc455e42c7 --- /dev/null +++ b/web/src/admin/users/UserTokenList.ts @@ -0,0 +1,172 @@ +import "#admin/rbac/ObjectPermissionModal"; +import "#admin/tokens/TokenForm"; +import "#components/ak-status-label"; +import "#elements/buttons/Dropdown"; +import "#elements/buttons/TokenCopyButton/index"; +import "#elements/forms/DeleteBulkForm"; +import "#elements/forms/ModalForm"; +import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; + +import { aki } from "#common/api/client"; +import { formatIntentLabel } from "#common/labels"; + +import { IconTokenCopyButton } from "#elements/buttons/IconTokenCopyButton"; +import { IconEditButton, ModalInvokerButton } from "#elements/dialogs"; +import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton"; +import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; +import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table"; +import { SlottedTemplateResult } from "#elements/types"; + +import { TokenForm } from "#admin/tokens/TokenForm"; + +import { CoreApi, IntentEnum, ModelEnum, Token, User } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { html, nothing, PropertyValues } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("ak-admin-user-token-list") +export class AdminUserTokenList extends Table { + public static override verboseName = msg("Token"); + public static override verboseNamePlural = msg("Tokens"); + + @property({ type: Number, attribute: "user-id", useDefault: true }) + public userID: number | null = null; + + @property({ attribute: false, useDefault: true }) + public user: User | null = null; + + protected override searchEnabled = true; + + protected override rowLabel(item: Token): string | null { + return item.identifier; + } + + public override checkbox = true; + public override clearOnRefresh = true; + public override order = "expires"; + + //#region Lifecycle + + protected override async apiEndpoint(): Promise> { + if (!this.user) { + await this.refresh(); + } + + if (!this.user) { + throw new TypeError("User is not set, cannot fetch tokens."); + } + + return aki(CoreApi).coreTokensList({ + ...(await this.defaultEndpointConfig()), + userUsername: this.user.username, + }); + } + + public refresh = () => { + if (!this.userID) { + return; + } + + return aki(CoreApi) + .coreUsersRetrieve({ + id: this.userID!, + }) + .then((user) => { + this.user = user; + }) + .catch(showAPIErrorMessage); + }; + + protected override updated(changed: PropertyValues) { + super.updated(changed); + + if (changed.has("userID") && this.userID !== null) { + this.refresh(); + } + } + + //#region + + //#endregion + + //#region Rendering + + protected override renderObjectCreate(): SlottedTemplateResult { + if (!this.user) { + return null; + } + + return ModalInvokerButton(TokenForm, { defaultUser: this.user }); + } + + protected columns: TableColumn[] = [ + [msg("Identifier"), "identifier"], + [msg("Expires?"), "expiring"], + [msg("Expiry date"), "expires"], + [msg("Intent"), "intent"], + [msg("Actions"), null, msg("Row Actions")], + ]; + + protected override renderToolbarSelected(): SlottedTemplateResult { + const disabled = this.selectedElements.length < 1; + return html` { + return [{ key: msg("Identifier"), value: item.identifier }]; + }} + .usedBy=${(item: Token) => { + return aki(CoreApi).coreTokensUsedByList({ + identifier: item.identifier, + }); + }} + .delete=${(item: Token) => { + return aki(CoreApi).coreTokensDestroy({ + identifier: item.identifier, + }); + }} + > + + `; + } + + protected override row(item: Token): SlottedTemplateResult[] { + return [ + html`
${item.identifier}
+ ${item.managed + ? html`${msg("Token is managed by authentik.")}` + : nothing}`, + html``, + Timestamp(item.expires && item.expiring ? item.expires : null), + html`${formatIntentLabel(item.intent ?? IntentEnum.Api)}`, + html`
+ ${!item.managed + ? IconEditButton(TokenForm, item.identifier, item.identifier) + : html``} + ${IconPermissionButton(item.identifier, { + model: ModelEnum.AuthentikCoreToken, + objectPk: item.pk, + })} + ${IconTokenCopyButton(item)} +
`, + ]; + } + + //#endregion +} + +declare global { + interface HTMLElementTagNameMap { + "ak-admin-user-token-list": AdminUserTokenList; + } +} diff --git a/web/src/admin/users/UserViewPage.ts b/web/src/admin/users/UserViewPage.ts index 24873cec9d..a5c02f6012 100644 --- a/web/src/admin/users/UserViewPage.ts +++ b/web/src/admin/users/UserViewPage.ts @@ -8,6 +8,7 @@ import "#admin/users/UserChart"; import "#admin/users/UserForm"; import "#admin/users/UserImpersonateForm"; import "#admin/users/UserPasswordForm"; +import "#admin/users/UserTokenList"; import "#admin/users/oauth/UserAccessTokenList"; import "#admin/users/oauth/UserRefreshTokenList"; import "#components/DescriptionList"; @@ -245,116 +246,123 @@ export class UserViewPage extends WithLicenseSummary( } protected renderTabCredentialsToken(user: User): TemplateResult { - return html` - -
-
- - -
+ return html` +
+
+
-
-
- - -
+
+
+
+
- +
+
+
-
-
- - -
+
+