core: show all applications a user can access in admin interface (#8343)

* core: show all applications a user can access in admin interface

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* minor adjustments

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add relative time

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use relative time in most places

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* improve admin dashboard scaling

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2024-01-30 01:56:33 +01:00
committed by GitHub
parent 0052e60643
commit 07ed5e1cd9
22 changed files with 245 additions and 49 deletions

View File

@ -11,6 +11,7 @@ from rest_framework.permissions import AllowAny
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import SecretKeyFilter from authentik.api.authorization import SecretKeyFilter
@ -57,6 +58,11 @@ class BrandSerializer(ModelSerializer):
"web_certificate", "web_certificate",
"attributes", "attributes",
] ]
extra_kwargs = {
# TODO: This field isn't unique on the database which is hard to backport
# hence we just validate the uniqueness here
"domain": {"validators": [UniqueValidator(Brand.objects.all())]},
}
class Themes(models.TextChoices): class Themes(models.TextChoices):

View File

@ -1,4 +1,5 @@
"""Application API Views""" """Application API Views"""
from copy import copy
from datetime import timedelta from datetime import timedelta
from typing import Optional from typing import Optional
@ -128,10 +129,16 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
queryset = backend().filter_queryset(self.request, queryset, self) queryset = backend().filter_queryset(self.request, queryset, self)
return queryset return queryset
def _get_allowed_applications(self, queryset: QuerySet) -> list[Application]: def _get_allowed_applications(
self, queryset: QuerySet, user: Optional[User] = None
) -> list[Application]:
applications = [] applications = []
request = self.request._request
if user:
request = copy(request)
request.user = user
for application in queryset: for application in queryset:
engine = PolicyEngine(application, self.request.user, self.request) engine = PolicyEngine(application, request.user, request)
engine.build() engine.build()
if engine.passing: if engine.passing:
applications.append(application) applications.append(application)
@ -188,20 +195,43 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
name="superuser_full_list", name="superuser_full_list",
location=OpenApiParameter.QUERY, location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL, type=OpenApiTypes.BOOL,
) ),
OpenApiParameter(
name="for_user",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
),
] ]
) )
def list(self, request: Request) -> Response: def list(self, request: Request) -> Response:
"""Custom list method that checks Policy based access instead of guardian""" """Custom list method that checks Policy based access instead of guardian"""
should_cache = request.GET.get("search", "") == "" should_cache = request.query_params.get("search", "") == ""
superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true" superuser_full_list = (
str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
)
if superuser_full_list and request.user.is_superuser: if superuser_full_list and request.user.is_superuser:
return super().list(request) return super().list(request)
queryset = self._filter_queryset_for_list(self.get_queryset()) queryset = self._filter_queryset_for_list(self.get_queryset())
self.paginate_queryset(queryset) self.paginate_queryset(queryset)
if "for_user" in request.query_params:
try:
for_user: int = int(request.query_params.get("for_user", 0))
for_user = (
get_objects_for_user(request.user, "authentik_core.view_user_applications")
.filter(pk=for_user)
.first()
)
if not for_user:
raise ValidationError({"for_user": "User not found"})
except ValueError as exc:
raise ValidationError from exc
allowed_applications = self._get_allowed_applications(queryset, user=for_user)
serializer = self.get_serializer(allowed_applications, many=True)
return self.get_paginated_response(serializer.data)
allowed_applications = [] allowed_applications = []
if not should_cache: if not should_cache:
allowed_applications = self._get_allowed_applications(queryset) allowed_applications = self._get_allowed_applications(queryset)

View File

@ -2658,6 +2658,10 @@ paths:
operationId: core_applications_list operationId: core_applications_list
description: Custom list method that checks Policy based access instead of guardian description: Custom list method that checks Policy based access instead of guardian
parameters: parameters:
- in: query
name: for_user
schema:
type: integer
- in: query - in: query
name: group name: group
schema: schema:
@ -2931,8 +2935,14 @@ paths:
schema: schema:
$ref: '#/components/schemas/PolicyTestResult' $ref: '#/components/schemas/PolicyTestResult'
description: '' description: ''
'404':
description: for_user user not found
'400': '400':
description: Bad request content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403': '403':
content: content:
application/json: application/json:

View File

@ -79,7 +79,9 @@ export class AdminOverviewPage extends AKElement {
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter"> <div class="pf-l-grid pf-m-gutter">
<!-- row 1 --> <!-- row 1 -->
<div class="pf-l-grid__item pf-m-6-col pf-l-grid pf-m-gutter"> <div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter"
>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl"> <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
<ak-aggregate-card <ak-aggregate-card
icon="fa fa-share" icon="fa fa-share"
@ -167,7 +169,7 @@ export class AdminOverviewPage extends AKElement {
<ak-admin-status-card-workers> </ak-admin-status-card-workers> <ak-admin-status-card-workers> </ak-admin-status-card-workers>
</div> </div>
</div> </div>
<div class="pf-l-grid__item pf-m-6-col"> <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl">
<ak-recent-events pageSize="6"></ak-recent-events> <ak-recent-events pageSize="6"></ak-recent-events>
</div> </div>
<div class="pf-l-grid__item pf-m-12-col"> <div class="pf-l-grid__item pf-m-12-col">

View File

@ -47,6 +47,9 @@ export class RecentEventsCard extends Table<Event> {
--pf-c-card__title--FontSize: var(--pf-global--FontSize--md); --pf-c-card__title--FontSize: var(--pf-global--FontSize--md);
--pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold); --pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold);
} }
* {
word-break: break-all;
}
`, `,
); );
} }

View File

@ -9,7 +9,6 @@ import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import { getURLParam } from "@goauthentik/elements/router/RouteMatch"; import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
// import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -25,6 +24,22 @@ import { Application, CoreApi } from "@goauthentik/api";
import "./ApplicationWizardHint"; import "./ApplicationWizardHint";
export const applicationListStyle = css`
/* Fix alignment issues with images in tables */
.pf-c-table tbody > tr > * {
vertical-align: middle;
}
tr td:first-child {
width: auto;
min-width: 0px;
text-align: center;
vertical-align: middle;
}
.pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * {
margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2);
}
`;
@customElement("ak-application-list") @customElement("ak-application-list")
export class ApplicationListPage extends TablePage<Application> { export class ApplicationListPage extends TablePage<Application> {
searchEnabled(): boolean { searchEnabled(): boolean {
@ -59,24 +74,7 @@ export class ApplicationListPage extends TablePage<Application> {
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return super.styles.concat( return super.styles.concat(PFCard, applicationListStyle);
PFCard,
css`
/* Fix alignment issues with images in tables */
.pf-c-table tbody > tr > * {
vertical-align: middle;
}
tr td:first-child {
width: auto;
min-width: 0px;
text-align: center;
vertical-align: middle;
}
.pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * {
margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2);
}
`,
);
} }
columns(): TableColumn[] { columns(): TableColumn[] {
@ -97,7 +95,6 @@ export class ApplicationListPage extends TablePage<Application> {
renderSidebarAfter(): TemplateResult { renderSidebarAfter(): TemplateResult {
// Rendering the wizard with .open here, as if we set the attribute in // Rendering the wizard with .open here, as if we set the attribute in
// renderObjectCreate() it'll open two wizards, since that function gets called twice // renderObjectCreate() it'll open two wizards, since that function gets called twice
return html`<div class="pf-c-sidebar__panel pf-m-width-25"> return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__body"> <div class="pf-c-card__body">

View File

@ -1,4 +1,5 @@
import "@goauthentik/admin/blueprints/BlueprintForm"; import "@goauthentik/admin/blueprints/BlueprintForm";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
@ -144,7 +145,8 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
html`<div>${item.name}</div> html`<div>${item.name}</div>
${description ? html`<small>${description}</small>` : html``}`, ${description ? html`<small>${description}</small>` : html``}`,
html`${BlueprintStatus(item)}`, html`${BlueprintStatus(item)}`,
html`${item.lastApplied.toLocaleString()}`, html`<div>${getRelativeTime(item.lastApplied)}</div>
<small>${item.lastApplied.toLocaleString()}</small>`,
html`<ak-status-label ?good=${item.enabled}></ak-status-label>`, html`<ak-status-label ?good=${item.enabled}></ak-status-label>`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>

View File

@ -30,7 +30,7 @@ export class BrandListPage extends TablePage<Brand> {
return msg("Configure visual settings and defaults for different domains."); return msg("Configure visual settings and defaults for different domains.");
} }
pageIcon(): string { pageIcon(): string {
return "pf-icon pf-icon-brand"; return "pf-icon pf-icon-tenant";
} }
checkbox = true; checkbox = true;
@ -51,6 +51,7 @@ export class BrandListPage extends TablePage<Brand> {
columns(): TableColumn[] { columns(): TableColumn[] {
return [ return [
new TableColumn(msg("Domain"), "domain"), new TableColumn(msg("Domain"), "domain"),
new TableColumn(msg("Brand name"), "branding_title"),
new TableColumn(msg("Default?"), "default"), new TableColumn(msg("Default?"), "default"),
new TableColumn(msg("Actions")), new TableColumn(msg("Actions")),
]; ];
@ -84,6 +85,7 @@ export class BrandListPage extends TablePage<Brand> {
row(item: Brand): TemplateResult[] { row(item: Brand): TemplateResult[] {
return [ return [
html`${item.domain}`, html`${item.domain}`,
html`${item.brandingTitle}`,
html`<ak-status-label ?good=${item._default}></ak-status-label>`, html`<ak-status-label ?good=${item._default}></ak-status-label>`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>

View File

@ -1,4 +1,5 @@
import "@goauthentik/admin/enterprise/EnterpriseLicenseForm"; import "@goauthentik/admin/enterprise/EnterpriseLicenseForm";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import { PFColor } from "@goauthentik/elements/Label"; import { PFColor } from "@goauthentik/elements/Label";
@ -202,7 +203,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
subtext=${msg("Cumulative license expiry")} subtext=${msg("Cumulative license expiry")}
> >
${this.summary?.hasLicense ${this.summary?.hasLicense
? this.summary.latestValid.toLocaleString() ? html`<div>${getRelativeTime(this.summary.latestValid)}</div>
<small>${this.summary.latestValid.toLocaleString()}</small>`
: "-"} : "-"}
</ak-aggregate-card> </ak-aggregate-card>
</div> </div>

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils"; import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
@ -49,7 +49,10 @@ export class MemberSelectTable extends TableModal<User> {
html`<div>${item.username}</div> html`<div>${item.username}</div>
<small>${item.name}</small>`, <small>${item.name}</small>`,
html` <ak-status-label type="warning" ?good=${item.isActive}></ak-status-label>`, html` <ak-status-label type="warning" ?good=${item.isActive}></ak-status-label>`,
html`${first(item.lastLogin?.toLocaleString(), msg("-"))}`, html`${item.lastLogin
? html`<div>${getRelativeTime(item.lastLogin)}</div>
<small>${item.lastLogin.toLocaleString()}</small>`
: msg("-")}`,
]; ];
} }

View File

@ -7,7 +7,7 @@ import { me } from "@goauthentik/app/common/users";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils"; import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { import {
@ -199,7 +199,10 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
<small>${item.name}</small> <small>${item.name}</small>
</a>`, </a>`,
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`, html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
html`${first(item.lastLogin?.toLocaleString(), msg("-"))}`, html`${item.lastLogin
? html`<div>${getRelativeTime(item.lastLogin)}</div>
<small>${item.lastLogin.toLocaleString()}</small>`
: msg("-")}`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update User")} </span> <span slot="header"> ${msg("Update User")} </span>

View File

@ -1,3 +1,4 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/ModalButton";
@ -92,7 +93,8 @@ export class ReputationListPage extends TablePage<Reputation> {
: html``} : html``}
${item.ip}`, ${item.ip}`,
html`${item.score}`, html`${item.score}`,
html`${item.updated.toLocaleString()}`, html`<div>${getRelativeTime(item.updated)}</div>
<small>${item.updated.toLocaleString()}</small>`,
html` html`
<ak-rbac-object-permission-modal <ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.PoliciesReputationReputationpolicy} model=${RbacPermissionsAssignedByUsersListModelEnum.PoliciesReputationReputationpolicy}

View File

@ -1,4 +1,5 @@
import { uiConfig } from "@goauthentik/app/common/ui/config"; import { uiConfig } from "@goauthentik/app/common/ui/config";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { PFColor } from "@goauthentik/elements/Label"; import { PFColor } from "@goauthentik/elements/Label";
@ -111,7 +112,8 @@ export class SystemTaskListPage extends TablePage<SystemTask> {
return [ return [
html`${item.name}${item.uid ? `:${item.uid}` : ""}`, html`${item.name}${item.uid ? `:${item.uid}` : ""}`,
html`${item.description}`, html`${item.description}`,
html`${item.finishTimestamp.toLocaleString()}`, html`<div>${getRelativeTime(item.finishTimestamp)}</div>
<small>${item.finishTimestamp.toLocaleString()}</small>`,
this.taskStatus(item), this.taskStatus(item),
html`<ak-action-button html`<ak-action-button
class="pf-m-plain" class="pf-m-plain"

View File

@ -1,4 +1,5 @@
import "@goauthentik/admin/tokens/TokenForm"; import "@goauthentik/admin/tokens/TokenForm";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { intentToLabel } from "@goauthentik/common/labels"; import { intentToLabel } from "@goauthentik/common/labels";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
@ -111,7 +112,10 @@ export class TokenListPage extends TablePage<Token> {
: html``}`, : html``}`,
html`<a href="#/identity/users/${item.userObj?.pk}">${item.userObj?.username}</a>`, html`<a href="#/identity/users/${item.userObj?.pk}">${item.userObj?.username}</a>`,
html`<ak-status-label type="warning" ?good=${item.expiring}></ak-status-label>`, html`<ak-status-label type="warning" ?good=${item.expiring}></ak-status-label>`,
html`${item.expiring ? item.expires?.toLocaleString() : msg("-")}`, html`${item.expires
? html`<div>${getRelativeTime(item.expires)}</div>
<small>${item.expires.toLocaleString()}</small>`
: msg("-")}`,
html`${intentToLabel(item.intent ?? IntentEnum.Api)}`, html`${intentToLabel(item.intent ?? IntentEnum.Api)}`,
html` html`
${!item.managed ${!item.managed

View File

@ -0,0 +1,79 @@
import { applicationListStyle } from "@goauthentik/app/admin/applications/ApplicationListPage";
import { DEFAULT_CONFIG } from "@goauthentik/app/common/api/config";
import { uiConfig } from "@goauthentik/app/common/ui/config";
import { PFSize } from "@goauthentik/app/elements/Spinner";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/app/elements/table/Table";
import "@goauthentik/components/ak-app-icon";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Application, CoreApi, User } from "@goauthentik/api";
@customElement("ak-user-application-table")
export class UserApplicationTable extends Table<Application> {
@property({ attribute: false })
user?: User;
static get styles(): CSSResult[] {
return super.styles.concat(applicationListStyle);
}
async apiEndpoint(page: number): Promise<PaginatedResponse<Application>> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
forUser: this.user?.pk,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
ordering: this.order,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn(""),
new TableColumn(msg("Name"), "name"),
new TableColumn(msg("Group"), "group"),
new TableColumn(msg("Provider")),
new TableColumn(msg("Provider Type")),
new TableColumn(msg("Actions")),
];
}
row(item: Application): TemplateResult[] {
return [
html`<ak-app-icon size=${PFSize.Medium} .app=${item}></ak-app-icon>`,
html`<a href="#/core/applications/${item.slug}">
<div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}
</a>`,
html`${item.group || msg("-")}`,
item.provider
? html`<a href="#/core/providers/${item.providerObj?.pk}">
${item.providerObj?.name}
</a>`
: html`-`,
html`${item.providerObj?.verboseName || msg("-")}`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Application")} </span>
<ak-application-form slot="form" .instancePk=${item.slug}>
</ak-application-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
${item.launchUrl
? html`<a href=${item.launchUrl} target="_blank" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Open")}>
<i class="fas fa-share-square"></i>
</pf-tooltip>
</a>`
: html``}`,
];
}
}

View File

@ -9,7 +9,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { userTypeToLabel } from "@goauthentik/common/labels"; import { userTypeToLabel } from "@goauthentik/common/labels";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils"; import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
import { rootInterface } from "@goauthentik/elements/Base"; import { rootInterface } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
@ -159,6 +159,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
new TableColumn(msg("Name"), "username"), new TableColumn(msg("Name"), "username"),
new TableColumn(msg("Active"), "is_active"), new TableColumn(msg("Active"), "is_active"),
new TableColumn(msg("Last login"), "last_login"), new TableColumn(msg("Last login"), "last_login"),
new TableColumn(msg("Type"), "type"),
new TableColumn(msg("Actions")), new TableColumn(msg("Actions")),
]; ];
} }
@ -246,11 +247,15 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk; this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
return [ return [
html`<a href="#/identity/users/${item.pk}"> html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div> <div>${item.username}</div>
<small>${item.name === "" ? msg("<No name set>") : item.name}</small> </a <small>${item.name === "" ? msg("<No name set>") : item.name}</small>
>&nbsp;<small>${userTypeToLabel(item.type)}</small>`, </a>`,
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`, html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
html`${first(item.lastLogin?.toLocaleString(), msg("-"))}`, html`${item.lastLogin
? html`<div>${getRelativeTime(item.lastLogin)}</div>
<small>${item.lastLogin.toLocaleString()}</small>`
: msg("-")}`,
html`${userTypeToLabel(item.type)}`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update User")} </span> <span slot="header"> ${msg("Update User")} </span>

View File

@ -1,5 +1,6 @@
import "@goauthentik/admin/groups/RelatedGroupList"; import "@goauthentik/admin/groups/RelatedGroupList";
import "@goauthentik/admin/users/UserActiveForm"; import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserApplicationTable";
import "@goauthentik/admin/users/UserChart"; import "@goauthentik/admin/users/UserChart";
import "@goauthentik/admin/users/UserForm"; import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm"; import "@goauthentik/admin/users/UserPasswordForm";
@ -8,6 +9,7 @@ import {
requestRecoveryLink, requestRecoveryLink,
} from "@goauthentik/app/admin/users/UserListPage"; } from "@goauthentik/app/admin/users/UserListPage";
import { me } from "@goauthentik/app/common/users"; import { me } from "@goauthentik/app/common/users";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import "@goauthentik/app/elements/oauth/UserAccessTokenList"; import "@goauthentik/app/elements/oauth/UserAccessTokenList";
import "@goauthentik/app/elements/oauth/UserRefreshTokenList"; import "@goauthentik/app/elements/oauth/UserRefreshTokenList";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage"; import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
@ -147,7 +149,10 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
[msg("Username"), user.username], [msg("Username"), user.username],
[msg("Name"), user.name], [msg("Name"), user.name],
[msg("Email"), user.email || "-"], [msg("Email"), user.email || "-"],
[msg("Last login"), user.lastLogin?.toLocaleString()], [msg("Last login"), user.lastLogin
? html`<div>${getRelativeTime(user.lastLogin)}</div>
<small>${user.lastLogin.toLocaleString()}</small>`
: html`${msg("-")}`],
[msg("Active"), html`<ak-status-label type="warning" ?good=${user.isActive}></ak-status-label>`], [msg("Active"), html`<ak-status-label type="warning" ?good=${user.isActive}></ak-status-label>`],
[msg("Type"), userTypeToLabel(user.type)], [msg("Type"), userTypeToLabel(user.type)],
[msg("Superuser"), html`<ak-status-label type="warning" ?good=${user.isSuperuser}></ak-status-label>`], [msg("Superuser"), html`<ak-status-label type="warning" ?good=${user.isSuperuser}></ak-status-label>`],
@ -317,6 +322,14 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
`; `;
} }
renderTabApplications(user: User): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-application-table .user=${user}></ak-user-application-table>
</div>
</div>`;
}
renderBody() { renderBody() {
if (!this.user) { if (!this.user) {
return nothing; return nothing;
@ -399,6 +412,13 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
<section slot="page-credentials" data-tab-title="${msg("Credentials / Tokens")}"> <section slot="page-credentials" data-tab-title="${msg("Credentials / Tokens")}">
${this.renderTabCredentialsToken(this.user)} ${this.renderTabCredentialsToken(this.user)}
</section> </section>
<section
slot="page-applications"
data-tab-title="${msg("Applications")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
${this.renderTabApplications(this.user)}
</section>
<ak-rbac-object-permission-page <ak-rbac-object-permission-page
slot="page-permissions" slot="page-permissions"
data-tab-title="${msg("Permissions")}" data-tab-title="${msg("Permissions")}"

View File

@ -1,3 +1,4 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/common/constants";
import { actionToLabel } from "@goauthentik/common/labels"; import { actionToLabel } from "@goauthentik/common/labels";
@ -6,6 +7,7 @@ import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
@ -132,7 +134,9 @@ export class NotificationDrawer extends AKElement {
</div> </div>
<p class="pf-c-notification-drawer__list-item-description">${item.body}</p> <p class="pf-c-notification-drawer__list-item-description">${item.body}</p>
<small class="pf-c-notification-drawer__list-item-timestamp" <small class="pf-c-notification-drawer__list-item-timestamp"
>${item.created?.toLocaleString()}</small ><pf-tooltip position="top" .content=${item.created?.toLocaleString()}>
${getRelativeTime(item.created!)}
</pf-tooltip></small
> >
</li>`; </li>`;
} }

View File

@ -1,3 +1,4 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
@ -86,7 +87,10 @@ export class UserOAuthAccessTokenList extends Table<TokenModel> {
return [ return [
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`, html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`, html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`,
html`${item.expires?.toLocaleString()}`, html`${item.expires
? html`<div>${getRelativeTime(item.expires)}</div>
<small>${item.expires.toLocaleString()}</small>`
: msg("-")}`,
html`${item.scope.join(", ")}`, html`${item.scope.join(", ")}`,
]; ];
} }

View File

@ -1,3 +1,4 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
@ -87,7 +88,10 @@ export class UserOAuthRefreshTokenList extends Table<TokenModel> {
return [ return [
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`, html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`, html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`,
html`${item.expires?.toLocaleString()}`, html`${item.expires
? html`<div>${getRelativeTime(item.expires)}</div>
<small>${item.expires.toLocaleString()}</small>`
: msg("-")}`,
html`${item.scope.join(", ")}`, html`${item.scope.join(", ")}`,
]; ];
} }

View File

@ -1,3 +1,4 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
@ -61,7 +62,10 @@ export class UserConsentList extends Table<UserConsent> {
row(item: UserConsent): TemplateResult[] { row(item: UserConsent): TemplateResult[] {
return [ return [
html`${item.application.name}`, html`${item.application.name}`,
html`${item.expires?.toLocaleString()}`, html`${item.expires
? html`<div>${getRelativeTime(item.expires)}</div>
<small>${item.expires.toLocaleString()}</small>`
: msg("-")}`,
html`${item.permissions || "-"}`, html`${item.permissions || "-"}`,
]; ];
} }

View File

@ -1,3 +1,4 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { intentToLabel } from "@goauthentik/common/labels"; import { intentToLabel } from "@goauthentik/common/labels";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
@ -108,7 +109,14 @@ export class UserTokenList extends Table<Token> {
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text"> <div class="pf-c-description-list__text">
${item.expiring ? item.expires?.toLocaleString() : msg("-")} ${item.expiring
? html`<pf-tooltip
position="top"
.content=${item.expires?.toLocaleString()}
>
${getRelativeTime(item.expires!)}
</pf-tooltip>`
: msg("-")}
</div> </div>
</dd> </dd>
</div> </div>