diff --git a/authentik/stages/authenticator_mobile/api/device.py b/authentik/stages/authenticator_mobile/api/device.py index d0b5b2006..072534d22 100644 --- a/authentik/stages/authenticator_mobile/api/device.py +++ b/authentik/stages/authenticator_mobile/api/device.py @@ -5,7 +5,7 @@ from django_filters.rest_framework.backends import DjangoFilterBackend from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer from rest_framework import mixins from rest_framework.decorators import action -from rest_framework.fields import CharField, ChoiceField, JSONField, UUIDField +from rest_framework.fields import CharField, ChoiceField, JSONField, UUIDField, DateTimeField from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAdminUser from rest_framework.request import Request @@ -40,6 +40,7 @@ class MobileDeviceSerializer(DeviceSerializer): """Serializer for Mobile authenticator devices""" state = MobileDeviceInfoSerializer(read_only=True) + last_checkin = DateTimeField(read_only=True) class Meta: model = MobileDevice diff --git a/schema.yml b/schema.yml index e47b46d36..ce13d0c9a 100644 --- a/schema.yml +++ b/schema.yml @@ -35322,8 +35322,13 @@ components: allOf: - $ref: '#/components/schemas/MobileDeviceInfo' readOnly: true + last_checkin: + type: string + format: date-time + readOnly: true required: - confirmed + - last_checkin - meta_model_name - name - pk diff --git a/schemas/authentik-cloud-gateway.yml b/schemas/authentik-cloud-gateway.yml index 059546f01..6df354e4f 100644 --- a/schemas/authentik-cloud-gateway.yml +++ b/schemas/authentik-cloud-gateway.yml @@ -453,8 +453,13 @@ components: allOf: - $ref: '#/components/schemas/MobileDeviceInfo' readOnly: true + last_checkin: + type: string + format: date-time + readOnly: true required: - confirmed + - last_checkin - meta_model_name - name - pk diff --git a/web/src/admin/users/UserDevicesTable.ts b/web/src/admin/users/UserDevicesTable.ts index 8fc0e1acc..d23820360 100644 --- a/web/src/admin/users/UserDevicesTable.ts +++ b/web/src/admin/users/UserDevicesTable.ts @@ -5,8 +5,11 @@ import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table"; import { msg } from "@lit/localize"; -import { TemplateResult, html } from "lit"; +import { CSSResult, TemplateResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { until } from "lit/directives/until.js"; + +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import { AuthenticatorsApi, Device } from "@goauthentik/api"; @@ -16,6 +19,11 @@ export class UserDeviceTable extends Table { userId?: number; checkbox = true; + expandable = true; + + static get styles(): CSSResult[] { + return super.styles.concat(PFDescriptionList); + } async apiEndpoint(): Promise> { return new AuthenticatorsApi(DEFAULT_CONFIG) @@ -95,6 +103,91 @@ export class UserDeviceTable extends Table { >`; } + renderExpanded(item: Device): TemplateResult { + return html` + +
+
+ ${until( + new AuthenticatorsApi(DEFAULT_CONFIG) + .authenticatorsMobileRetrieve({ + uuid: item.pk, + }) + .then((device) => { + return html` +
+
+ ${msg("Last check-in")} +
+
+
+ ${device.lastCheckin.toLocaleString()} +
+
+
+
+
+ ${msg("App version")} +
+
+
+ ${device.state.appVersion} +
+
+
+
+
+ ${msg("Device model")} +
+
+
+ ${device.state.model} +
+
+
+
+
+ ${msg("OS Version")} +
+
+
+ ${device.state.osVersion} +
+
+
+
+
+ ${msg("Platform")} +
+
+
+ ${device.state.platform} +
+
+
+ `; + }), + )} +
+
+ + `; + } + + rowExpandable(item: Device): boolean { + return item.type.toLowerCase() === "authentik_stages_authenticator_mobile.mobiledevice"; + } + row(item: Device): TemplateResult[] { return [ html`${item.name}`, diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index 82fb9f5ae..0db52b789 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -149,6 +149,11 @@ export abstract class Table extends AKElement implements TableLike { @property({ type: Boolean }) expandable = false; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + rowExpandable(item: T): boolean { + return this.expandable; + } + @property({ attribute: false }) expandedElements: T[] = []; @@ -375,7 +380,11 @@ export abstract class Table extends AKElement implements TableLike { : itemSelectHandler} > ${this.checkbox ? renderCheckbox() : html``} - ${this.expandable ? renderExpansion() : html``} + ${this.rowExpandable(item) + ? renderExpansion() + : this.expandable + ? html`` + : html``} ${this.row(item).map((col) => { return html`${col}`; })}