diff --git a/web/src/elements/oauth/UserCodeList.ts b/web/src/elements/oauth/UserCodeList.ts deleted file mode 100644 index 62d3b6e3d..000000000 --- a/web/src/elements/oauth/UserCodeList.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { t } from "@lingui/macro"; - -import { TemplateResult, html } from "lit"; -import { customElement, property } from "lit/decorators.js"; - -import { ExpiringBaseGrantModel, Oauth2Api } from "@goauthentik/api"; - -import { AKResponse } from "../../api/Client"; -import { DEFAULT_CONFIG } from "../../api/Config"; -import { uiConfig } from "../../common/config"; -import "../forms/DeleteBulkForm"; -import { Table, TableColumn } from "../table/Table"; - -@customElement("ak-user-oauth-code-list") -export class UserOAuthCodeList extends Table { - @property({ type: Number }) - userId?: number; - - async apiEndpoint(page: number): Promise> { - return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesList({ - user: this.userId, - ordering: "expires", - page: page, - pageSize: (await uiConfig()).pagination.perPage, - }); - } - - checkbox = true; - order = "-expires"; - - columns(): TableColumn[] { - return [ - new TableColumn(t`Provider`, "provider"), - new TableColumn(t`Expires`, "expires"), - new TableColumn(t`Scopes`, "scope"), - ]; - } - - renderToolbarSelected(): TemplateResult { - const disabled = this.selectedElements.length < 1; - return html` { - return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesUsedByList({ - id: item.pk, - }); - }} - .delete=${(item: ExpiringBaseGrantModel) => { - return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesDestroy({ - id: item.pk, - }); - }} - > - - `; - } - - row(item: ExpiringBaseGrantModel): TemplateResult[] { - return [ - html` ${item.provider?.name} `, - html`${item.expires?.toLocaleString()}`, - html`${item.scope.join(", ")}`, - ]; - } -} diff --git a/web/src/pages/groups/GroupListPage.ts b/web/src/pages/groups/GroupListPage.ts index b83a59e61..91c097a98 100644 --- a/web/src/pages/groups/GroupListPage.ts +++ b/web/src/pages/groups/GroupListPage.ts @@ -33,7 +33,7 @@ export class GroupListPage extends TablePage { } @property() - order = "slug"; + order = "name"; async apiEndpoint(page: number): Promise> { return new CoreApi(DEFAULT_CONFIG).coreGroupsList({ @@ -78,7 +78,7 @@ export class GroupListPage extends TablePage { row(item: Group): TemplateResult[] { return [ - html`${item.name}`, + html`${item.name}`, html`${item.parentName || t`-`}`, html`${Array.from(item.users || []).length}`, html` diff --git a/web/src/pages/groups/GroupViewPage.ts b/web/src/pages/groups/GroupViewPage.ts new file mode 100644 index 000000000..37c851acc --- /dev/null +++ b/web/src/pages/groups/GroupViewPage.ts @@ -0,0 +1,168 @@ +import { t } from "@lingui/macro"; + +import { CSSResult, LitElement, TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import AKGlobal from "../../authentik.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFContent from "@patternfly/patternfly/components/Content/content.css"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; +import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; +import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css"; +import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css"; + +import { CoreApi, Group } from "@goauthentik/api"; + +import { DEFAULT_CONFIG } from "../../api/Config"; +import { EVENT_REFRESH } from "../../constants"; +import "../../elements/CodeMirror"; +import { PFColor } from "../../elements/Label"; +import "../../elements/PageHeader"; +import "../../elements/Tabs"; +import "../../elements/buttons/ActionButton"; +import "../../elements/buttons/SpinnerButton"; +import "../../elements/events/ObjectChangelog"; +import "../../elements/forms/ModalForm"; +import "../users/RelatedUserList"; +import "./GroupForm"; + +@customElement("ak-group-view") +export class GroupViewPage extends LitElement { + @property({ type: String }) + set groupId(id: string) { + new CoreApi(DEFAULT_CONFIG) + .coreGroupsRetrieve({ + groupUuid: id, + }) + .then((group) => { + this.group = group; + }); + } + + @property({ attribute: false }) + group?: Group; + + static get styles(): CSSResult[] { + return [ + PFBase, + PFPage, + PFFlex, + PFButton, + PFDisplay, + PFGrid, + PFContent, + PFCard, + PFDescriptionList, + PFSizing, + AKGlobal, + ]; + } + + constructor() { + super(); + this.addEventListener(EVENT_REFRESH, () => { + if (!this.group?.pk) return; + this.groupId = this.group?.pk; + }); + } + + render(): TemplateResult { + return html` + + ${this.renderBody()}`; + } + + renderBody(): TemplateResult { + if (!this.group) { + return html``; + } + return html` +
+
+
+
${t`Group Info`}
+
+
+
+
+ ${t`Name`} +
+
+
+ ${this.group.name} +
+
+
+
+
+ ${t`Superuser`} +
+
+
+ +
+
+
+
+
+ +
+
+
${t`Changelog`}
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
`; + } +} diff --git a/web/src/pages/groups/RelatedGroupList.ts b/web/src/pages/groups/RelatedGroupList.ts new file mode 100644 index 000000000..9f29a9209 --- /dev/null +++ b/web/src/pages/groups/RelatedGroupList.ts @@ -0,0 +1,93 @@ +import { t } from "@lingui/macro"; + +import { TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import { CoreApi, Group } from "@goauthentik/api"; + +import { AKResponse } from "../../api/Client"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { uiConfig } from "../../common/config"; +import { PFColor } from "../../elements/Label"; +import "../../elements/buttons/SpinnerButton"; +import "../../elements/forms/DeleteBulkForm"; +import "../../elements/forms/ModalForm"; +import { Table, TableColumn } from "../../elements/table/Table"; +import "./GroupForm"; + +@customElement("ak-group-related-list") +export class RelatedGroupList extends Table { + checkbox = true; + searchEnabled(): boolean { + return true; + } + + @property() + order = "name"; + + @property({ type: Number }) + targetUser?: number; + + async apiEndpoint(page: number): Promise> { + return new CoreApi(DEFAULT_CONFIG).coreGroupsList({ + ordering: this.order, + page: page, + pageSize: (await uiConfig()).pagination.perPage, + search: this.search || "", + membersByPk: this.targetUser ? [this.targetUser] : [], + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn(t`Name`, "name"), + new TableColumn(t`Parent`, "parent"), + new TableColumn(t`Superuser privileges?`), + new TableColumn(t`Actions`), + ]; + } + + renderToolbarSelected(): TemplateResult { + const disabled = this.selectedElements.length < 1; + return html` { + return new CoreApi(DEFAULT_CONFIG).coreGroupsUsedByList({ + groupUuid: item.pk, + }); + }} + .delete=${(item: Group) => { + return new CoreApi(DEFAULT_CONFIG).coreGroupsDestroy({ + groupUuid: item.pk, + }); + }} + > + + `; + } + + row(item: Group): TemplateResult[] { + return [ + html`${item.name}`, + html`${item.parentName || t`-`}`, + html` + ${item.isSuperuser ? t`Yes` : t`No`} + `, + html` + ${t`Update`} + ${t`Update Group`} + + + `, + ]; + } + + renderToolbar(): TemplateResult { + return html` ${super.renderToolbar()} `; + } +} diff --git a/web/src/pages/users/RelatedUserList.ts b/web/src/pages/users/RelatedUserList.ts new file mode 100644 index 000000000..7b11390ca --- /dev/null +++ b/web/src/pages/users/RelatedUserList.ts @@ -0,0 +1,336 @@ +import { t } from "@lingui/macro"; + +import { CSSResult, TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { until } from "lit/directives/until.js"; + +import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; + +import { CoreApi, User } from "@goauthentik/api"; + +import { AKResponse } from "../../api/Client"; +import { DEFAULT_CONFIG, tenant } from "../../api/Config"; +import { me } from "../../api/Users"; +import { uiConfig } from "../../common/config"; +import { PFColor } from "../../elements/Label"; +import "../../elements/buttons/ActionButton"; +import "../../elements/forms/DeleteBulkForm"; +import "../../elements/forms/ModalForm"; +import { MessageLevel } from "../../elements/messages/Message"; +import { showMessage } from "../../elements/messages/MessageContainer"; +import { getURLParam, updateURLParams } from "../../elements/router/RouteMatch"; +import { Table, TableColumn } from "../../elements/table/Table"; +import { first } from "../../utils"; +import "./ServiceAccountForm"; +import "./UserActiveForm"; +import "./UserForm"; +import "./UserPasswordForm"; +import "./UserResetEmailForm"; + +@customElement("ak-user-related-list") +export class RelatedUserList extends Table { + expandable = true; + checkbox = true; + + searchEnabled(): boolean { + return true; + } + + @property() + groupUuid?: string; + + @property() + order = "last_login"; + + @property({ type: Boolean }) + hideServiceAccounts = getURLParam("hideServiceAccounts", true); + + static get styles(): CSSResult[] { + return super.styles.concat(PFDescriptionList, PFAlert); + } + + async apiEndpoint(page: number): Promise> { + return new CoreApi(DEFAULT_CONFIG).coreUsersList({ + ordering: this.order, + page: page, + pageSize: (await uiConfig()).pagination.perPage, + search: this.search || "", + groupsByPk: this.groupUuid ? [this.groupUuid] : [], + attributes: this.hideServiceAccounts + ? JSON.stringify({ + "goauthentik.io/user/service-account__isnull": true, + }) + : undefined, + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn(t`Name`, "username"), + new TableColumn(t`Active`, "active"), + new TableColumn(t`Last login`, "last_login"), + new TableColumn(t`Actions`), + ]; + } + + renderToolbarSelected(): TemplateResult { + const disabled = this.selectedElements.length < 1; + return html` { + return [ + { key: t`Username`, value: item.username }, + { key: t`ID`, value: item.pk.toString() }, + { key: t`UID`, value: item.uid }, + ]; + }} + .usedBy=${(item: User) => { + return new CoreApi(DEFAULT_CONFIG).coreUsersUsedByList({ + id: item.pk, + }); + }} + .delete=${(item: User) => { + return new CoreApi(DEFAULT_CONFIG).coreUsersDestroy({ + id: item.pk, + }); + }} + > + ${until( + me().then((user) => { + const shouldShowWarning = this.selectedElements.find((el) => { + return el.pk === user.user.pk || el.pk == user.original?.pk; + }); + if (shouldShowWarning) { + return html` +
+
+
+ +
+

+ ${t`Warning: You're about to delete the user you're logged in as (${shouldShowWarning.username}). Proceed at your own risk.`} +

+
+
+ `; + } + return html``; + }), + )} + +
`; + } + + row(item: User): TemplateResult[] { + return [ + html` +
${item.username}
+ ${item.name} +
`, + html` + ${item.isActive ? t`Yes` : t`No`} + `, + html`${first(item.lastLogin?.toLocaleString(), t`-`)}`, + html` + ${t`Update`} + ${t`Update User`} + + + + + ${t`Impersonate`} + `, + ]; + } + + renderExpanded(item: User): TemplateResult { + return html` +
+
+
+
+ ${t`User status`} +
+
+
+ ${item.isActive ? t`Active` : t`Inactive`} +
+
+ ${item.isSuperuser ? t`Superuser` : t`Regular user`} +
+
+
+
+
+ ${t`Change status`} +
+
+
+ { + return new CoreApi( + DEFAULT_CONFIG, + ).coreUsersPartialUpdate({ + id: item.pk || 0, + patchedUserRequest: { + isActive: !item.isActive, + }, + }); + }} + > + + +
+
+
+
+
+ ${t`Recovery`} +
+
+
+ + ${t`Update password`} + ${t`Update password`} + + + + ${until( + tenant().then((tenant) => { + if (!tenant.flowRecovery) { + return html` +

+ ${t`To let a user directly reset a their password, configure a recovery flow on the currently active tenant.`} +

+ `; + } + return html` + { + return new CoreApi(DEFAULT_CONFIG) + .coreUsersRecoveryRetrieve({ + id: item.pk || 0, + }) + .then((rec) => { + showMessage({ + level: MessageLevel.success, + message: t`Successfully generated recovery link`, + description: rec.link, + }); + }) + .catch((ex: Response) => { + ex.json().then(() => { + showMessage({ + level: MessageLevel.error, + message: t`No recovery flow is configured.`, + }); + }); + }); + }} + > + ${t`Copy recovery link`} + + ${item.email + ? html` + + ${t`Send link`} + + + ${t`Send recovery link to user`} + + + + + ` + : html`${t`Recovery link cannot be emailed, user has no email address saved.`}`} + `; + }), + )} +
+
+
+
+
+ + + `; + } + + renderToolbar(): TemplateResult { + return html` + + ${t`Create`} + ${t`Create User`} + + + + + ${t`Create`} + ${t`Create Service account`} + + + + ${super.renderToolbar()} + `; + } + + renderToolbarAfter(): TemplateResult { + return html`  +
+
+
+
+ { + this.hideServiceAccounts = !this.hideServiceAccounts; + this.page = 1; + this.fetch(); + updateURLParams({ + hideServiceAccounts: this.hideServiceAccounts, + }); + }} + /> + +
+
+
+
`; + } +} diff --git a/web/src/pages/users/UserViewPage.ts b/web/src/pages/users/UserViewPage.ts index 67fc2227c..e94cd3c64 100644 --- a/web/src/pages/users/UserViewPage.ts +++ b/web/src/pages/users/UserViewPage.ts @@ -31,10 +31,10 @@ import "../../elements/events/UserEvents"; import "../../elements/forms/ModalForm"; import { MessageLevel } from "../../elements/messages/Message"; import { showMessage } from "../../elements/messages/MessageContainer"; -import "../../elements/oauth/UserCodeList"; import "../../elements/oauth/UserRefreshList"; import "../../elements/user/SessionList"; import "../../elements/user/UserConsentList"; +import "../groups/RelatedGroupList"; import "./UserActiveForm"; import "./UserForm"; @@ -283,6 +283,17 @@ export class UserViewPage extends LitElement { +
+
+
+ +
+
+
-
-
-
- -
-
-
`, ), new Route(new RegExp("^/identity/groups$"), html``), + new Route(new RegExp(`^/identity/groups/(?${UUID_REGEX})$`)).then((args) => { + return html``; + }), new Route(new RegExp("^/identity/users$"), html``), new Route(new RegExp(`^/identity/users/(?${ID_REGEX})$`)).then((args) => { return html``;