core: Application stats, device events & cleanup (#21225)

* core: app stats

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

* refctor

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

* rework to generic API

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

* oops

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

* handling

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

* fix docs

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

* more docs

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

* unrelated fix

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

* allow filtering events by device

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

* show device events on device page

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

* simply event tables

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

* tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-03-29 20:58:12 +01:00
committed by GitHub
parent a62c6c92a8
commit d1c997b2fe
22 changed files with 1446 additions and 351 deletions
+76 -16
View File
@@ -9,30 +9,49 @@ from django.db.models import DateTimeField as DjangoDateTimeField
from django.db.models.fields.json import KeyTextTransform, KeyTransform
from django.db.models.functions import TruncHour
from django.db.models.query_utils import Q
from django.utils.text import slugify
from django.utils.timezone import now
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import ChoiceField, DateTimeField, DictField, IntegerField
from rest_framework.fields import (
CharField,
ChoiceField,
DateTimeField,
DictField,
IntegerField,
ListField,
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.api.validation import validate
from authentik.core.api.object_types import TypeCreateSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.events.models import Event, EventAction
from authentik.lib.utils.reflection import ConditionalInheritance
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
AGGR_MAX_AGE = timedelta(days=90)
class EventVolumeSerializer(PassiveSerializer):
"""Count of events of action created on day"""
"""Count of events of action created on day for a single event action"""
action = ChoiceField(choices=EventAction.choices)
time = DateTimeField()
count = IntegerField()
class EventStatsSerializer(PassiveSerializer):
"""Count of unique users in events and aggregated counts per specified deltas"""
unique_users = IntegerField()
count_step = DictField()
class EventSerializer(ModelSerializer):
"""Event Serializer"""
@@ -84,6 +103,11 @@ class EventsFilter(django_filters.FilterSet):
lookup_expr="authorized_application__pk",
label="Context Authorized application",
)
context_device = django_filters.CharFilter(
field_name="context",
lookup_expr="device__pk",
label="Context Device Primary Key",
)
action = django_filters.CharFilter(
field_name="action",
lookup_expr="icontains",
@@ -123,6 +147,16 @@ class EventViewSet(
):
"""Event Read-Only Viewset"""
class EventVolumeParameters(PassiveSerializer):
history_days = IntegerField(default=7, required=False)
class EventStatsParameters(PassiveSerializer):
count_steps = ListField(
child=CharField(validators=[timedelta_string_validator]),
required=True,
help_text="Timedelta, format of 'weeks=3;days=2;hours=3,seconds=2'",
)
queryset = Event.objects.all()
serializer_class = EventSerializer
ordering = ["-created"]
@@ -225,24 +259,16 @@ class EventViewSet(
@extend_schema(
responses={200: EventVolumeSerializer(many=True)},
parameters=[
OpenApiParameter(
"history_days",
type=OpenApiTypes.NUMBER,
location=OpenApiParameter.QUERY,
required=False,
default=7,
),
],
parameters=[EventVolumeParameters],
)
@action(detail=False, methods=["GET"], pagination_class=None)
def volume(self, request: Request) -> Response:
@validate(EventVolumeParameters, "query")
def volume(self, request: Request, query: EventVolumeParameters) -> Response:
"""Get event volume for specified filters and timeframe"""
queryset: QuerySet[Event] = self.filter_queryset(self.get_queryset())
delta = timedelta(days=7)
time_delta = request.query_params.get("history_days", 7)
if time_delta:
delta = timedelta(days=min(int(time_delta), 60))
delta = timedelta(days=query.validated_data.get("history_days", 7))
if delta.total_seconds() > AGGR_MAX_AGE:
delta = AGGR_MAX_AGE
return Response(
queryset.filter(created__gte=now() - delta)
.annotate(hour=TruncHour("created"))
@@ -257,6 +283,40 @@ class EventViewSet(
.order_by("time", "action")
)
@extend_schema(
responses={200: EventStatsSerializer()},
parameters=[EventStatsParameters],
filters=True,
)
@action(detail=False, methods=["GET"], pagination_class=None)
@validate(EventStatsParameters, "query")
def stats(self, request: Request, query: EventStatsParameters) -> Response:
"""Get event stats for specified filters and count steps"""
_now = now()
aggrs = {
"unique_users": Count("user__pk", distinct=True),
}
largest_delta = 0
for step in query.validated_data.get("count_steps"):
delta = timedelta_from_string(step)
if delta.total_seconds() > AGGR_MAX_AGE.total_seconds():
delta = AGGR_MAX_AGE
largest_delta = max(largest_delta, delta.total_seconds())
aggrs[slugify(step).replace("-", "_")] = Count(
"event_uuid", filter=Q(created__gte=_now - delta)
)
data = (
self.filter_queryset(self.get_queryset())
.filter(created__gte=now() - timedelta(days=60))
.aggregate(**aggrs)
)
return Response(
{
"unique_users": data.pop("unique_users"),
"count_step": data,
}
)
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def actions(self, request: Request) -> Response:
+25
View File
@@ -1,8 +1,10 @@
"""Event API tests"""
from datetime import timedelta
from json import loads
from django.urls import reverse
from django.utils.timezone import now
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
@@ -91,3 +93,26 @@ class TestEventsAPI(APITestCase):
},
)
self.assertEqual(response.status_code, 400)
def test_stats(self):
Event.objects.all().delete()
Event.new(EventAction.LOGIN).set_user(self.user).save()
evt = Event.new(EventAction.LOGIN).set_user(self.user)
evt.created = now() - timedelta(days=6)
evt.save()
res = self.client.get(
reverse("authentik_api:event-stats")
+ "?count_steps=hours=24&count_steps=days=7&count_steps=days=240"
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content, {"unique_users": 1, "count_step": {"hours24": 2, "days7": 2, "days240": 2}}
)
def test_stats_invalid(self):
res = self.client.get(reverse("authentik_api:event-stats") + "?count_steps=24")
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(
res.content,
{"count_steps": {"0": ["24 is not in the correct format of 'hours=3;minutes=1'."]}},
)
+300 -3
View File
@@ -399,6 +399,7 @@ type ApiEventsEventsExportCreateRequest struct {
brandName *string
clientIp *string
contextAuthorizedApp *string
contextDevice *string
contextModelApp *string
contextModelName *string
contextModelPk *string
@@ -434,6 +435,12 @@ func (r ApiEventsEventsExportCreateRequest) ContextAuthorizedApp(contextAuthoriz
return r
}
// Context Device Primary Key
func (r ApiEventsEventsExportCreateRequest) ContextDevice(contextDevice string) ApiEventsEventsExportCreateRequest {
r.contextDevice = &contextDevice
return r
}
// Context Model App
func (r ApiEventsEventsExportCreateRequest) ContextModelApp(contextModelApp string) ApiEventsEventsExportCreateRequest {
r.contextModelApp = &contextModelApp
@@ -538,6 +545,9 @@ func (a *EventsAPIService) EventsEventsExportCreateExecute(r ApiEventsEventsExpo
if r.contextAuthorizedApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "")
}
if r.contextDevice != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "")
}
if r.contextModelApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "")
}
@@ -639,6 +649,7 @@ type ApiEventsEventsListRequest struct {
brandName *string
clientIp *string
contextAuthorizedApp *string
contextDevice *string
contextModelApp *string
contextModelName *string
contextModelPk *string
@@ -676,6 +687,12 @@ func (r ApiEventsEventsListRequest) ContextAuthorizedApp(contextAuthorizedApp st
return r
}
// Context Device Primary Key
func (r ApiEventsEventsListRequest) ContextDevice(contextDevice string) ApiEventsEventsListRequest {
r.contextDevice = &contextDevice
return r
}
// Context Model App
func (r ApiEventsEventsListRequest) ContextModelApp(contextModelApp string) ApiEventsEventsListRequest {
r.contextModelApp = &contextModelApp
@@ -788,6 +805,9 @@ func (a *EventsAPIService) EventsEventsListExecute(r ApiEventsEventsListRequest)
if r.contextAuthorizedApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "")
}
if r.contextDevice != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "")
}
if r.contextModelApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "")
}
@@ -1145,6 +1165,273 @@ func (a *EventsAPIService) EventsEventsRetrieveExecute(r ApiEventsEventsRetrieve
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiEventsEventsStatsRetrieveRequest struct {
ctx context.Context
ApiService *EventsAPIService
countSteps *[]string
action *string
actions *[]EventActions
brandName *string
clientIp *string
contextAuthorizedApp *string
contextDevice *string
contextModelApp *string
contextModelName *string
contextModelPk *string
ordering *string
search *string
username *string
}
// Timedelta, format of &#39;weeks&#x3D;3;days&#x3D;2;hours&#x3D;3,seconds&#x3D;2&#39;
func (r ApiEventsEventsStatsRetrieveRequest) CountSteps(countSteps []string) ApiEventsEventsStatsRetrieveRequest {
r.countSteps = &countSteps
return r
}
func (r ApiEventsEventsStatsRetrieveRequest) Action(action string) ApiEventsEventsStatsRetrieveRequest {
r.action = &action
return r
}
func (r ApiEventsEventsStatsRetrieveRequest) Actions(actions []EventActions) ApiEventsEventsStatsRetrieveRequest {
r.actions = &actions
return r
}
// Brand name
func (r ApiEventsEventsStatsRetrieveRequest) BrandName(brandName string) ApiEventsEventsStatsRetrieveRequest {
r.brandName = &brandName
return r
}
func (r ApiEventsEventsStatsRetrieveRequest) ClientIp(clientIp string) ApiEventsEventsStatsRetrieveRequest {
r.clientIp = &clientIp
return r
}
// Context Authorized application
func (r ApiEventsEventsStatsRetrieveRequest) ContextAuthorizedApp(contextAuthorizedApp string) ApiEventsEventsStatsRetrieveRequest {
r.contextAuthorizedApp = &contextAuthorizedApp
return r
}
// Context Device Primary Key
func (r ApiEventsEventsStatsRetrieveRequest) ContextDevice(contextDevice string) ApiEventsEventsStatsRetrieveRequest {
r.contextDevice = &contextDevice
return r
}
// Context Model App
func (r ApiEventsEventsStatsRetrieveRequest) ContextModelApp(contextModelApp string) ApiEventsEventsStatsRetrieveRequest {
r.contextModelApp = &contextModelApp
return r
}
// Context Model Name
func (r ApiEventsEventsStatsRetrieveRequest) ContextModelName(contextModelName string) ApiEventsEventsStatsRetrieveRequest {
r.contextModelName = &contextModelName
return r
}
// Context Model Primary Key
func (r ApiEventsEventsStatsRetrieveRequest) ContextModelPk(contextModelPk string) ApiEventsEventsStatsRetrieveRequest {
r.contextModelPk = &contextModelPk
return r
}
// Which field to use when ordering the results.
func (r ApiEventsEventsStatsRetrieveRequest) Ordering(ordering string) ApiEventsEventsStatsRetrieveRequest {
r.ordering = &ordering
return r
}
// A search term.
func (r ApiEventsEventsStatsRetrieveRequest) Search(search string) ApiEventsEventsStatsRetrieveRequest {
r.search = &search
return r
}
// Username
func (r ApiEventsEventsStatsRetrieveRequest) Username(username string) ApiEventsEventsStatsRetrieveRequest {
r.username = &username
return r
}
func (r ApiEventsEventsStatsRetrieveRequest) Execute() (*EventStats, *http.Response, error) {
return r.ApiService.EventsEventsStatsRetrieveExecute(r)
}
/*
EventsEventsStatsRetrieve Method for EventsEventsStatsRetrieve
Get event stats for specified filters and count steps
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@return ApiEventsEventsStatsRetrieveRequest
*/
func (a *EventsAPIService) EventsEventsStatsRetrieve(ctx context.Context) ApiEventsEventsStatsRetrieveRequest {
return ApiEventsEventsStatsRetrieveRequest{
ApiService: a,
ctx: ctx,
}
}
// Execute executes the request
//
// @return EventStats
func (a *EventsAPIService) EventsEventsStatsRetrieveExecute(r ApiEventsEventsStatsRetrieveRequest) (*EventStats, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodGet
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *EventStats
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "EventsAPIService.EventsEventsStatsRetrieve")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/events/events/stats/"
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.countSteps == nil {
return localVarReturnValue, nil, reportError("countSteps is required and must be specified")
}
if r.action != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "action", r.action, "form", "")
}
if r.actions != nil {
t := *r.actions
if reflect.TypeOf(t).Kind() == reflect.Slice {
s := reflect.ValueOf(t)
for i := 0; i < s.Len(); i++ {
parameterAddToHeaderOrQuery(localVarQueryParams, "actions", s.Index(i).Interface(), "form", "multi")
}
} else {
parameterAddToHeaderOrQuery(localVarQueryParams, "actions", t, "form", "multi")
}
}
if r.brandName != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "brand_name", r.brandName, "form", "")
}
if r.clientIp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "client_ip", r.clientIp, "form", "")
}
if r.contextAuthorizedApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "")
}
if r.contextDevice != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "")
}
if r.contextModelApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "")
}
if r.contextModelName != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_name", r.contextModelName, "form", "")
}
if r.contextModelPk != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_pk", r.contextModelPk, "form", "")
}
{
t := *r.countSteps
if reflect.TypeOf(t).Kind() == reflect.Slice {
s := reflect.ValueOf(t)
for i := 0; i < s.Len(); i++ {
parameterAddToHeaderOrQuery(localVarQueryParams, "count_steps", s.Index(i).Interface(), "form", "multi")
}
} else {
parameterAddToHeaderOrQuery(localVarQueryParams, "count_steps", t, "form", "multi")
}
}
if r.ordering != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "ordering", r.ordering, "form", "")
}
if r.search != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "search", r.search, "form", "")
}
if r.username != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "username", r.username, "form", "")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 400 {
var v ValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
return localVarReturnValue, localVarHTTPResponse, newErr
}
if localVarHTTPResponse.StatusCode == 403 {
var v GenericError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiEventsEventsTopPerUserListRequest struct {
ctx context.Context
ApiService *EventsAPIService
@@ -1428,10 +1715,11 @@ type ApiEventsEventsVolumeListRequest struct {
brandName *string
clientIp *string
contextAuthorizedApp *string
contextDevice *string
contextModelApp *string
contextModelName *string
contextModelPk *string
historyDays *float32
historyDays *int32
ordering *string
search *string
username *string
@@ -1464,6 +1752,12 @@ func (r ApiEventsEventsVolumeListRequest) ContextAuthorizedApp(contextAuthorized
return r
}
// Context Device Primary Key
func (r ApiEventsEventsVolumeListRequest) ContextDevice(contextDevice string) ApiEventsEventsVolumeListRequest {
r.contextDevice = &contextDevice
return r
}
// Context Model App
func (r ApiEventsEventsVolumeListRequest) ContextModelApp(contextModelApp string) ApiEventsEventsVolumeListRequest {
r.contextModelApp = &contextModelApp
@@ -1482,7 +1776,7 @@ func (r ApiEventsEventsVolumeListRequest) ContextModelPk(contextModelPk string)
return r
}
func (r ApiEventsEventsVolumeListRequest) HistoryDays(historyDays float32) ApiEventsEventsVolumeListRequest {
func (r ApiEventsEventsVolumeListRequest) HistoryDays(historyDays int32) ApiEventsEventsVolumeListRequest {
r.historyDays = &historyDays
return r
}
@@ -1569,6 +1863,9 @@ func (a *EventsAPIService) EventsEventsVolumeListExecute(r ApiEventsEventsVolume
if r.contextAuthorizedApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "")
}
if r.contextDevice != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "")
}
if r.contextModelApp != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "")
}
@@ -1581,7 +1878,7 @@ func (a *EventsAPIService) EventsEventsVolumeListExecute(r ApiEventsEventsVolume
if r.historyDays != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "history_days", r.historyDays, "form", "")
} else {
var defaultValue float32 = 7
var defaultValue int32 = 7
parameterAddToHeaderOrQuery(localVarQueryParams, "history_days", defaultValue, "form", "")
r.historyDays = &defaultValue
}
+196
View File
@@ -0,0 +1,196 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// checks if the EventStats type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &EventStats{}
// EventStats Count of unique users in events and aggregated counts per specified deltas
type EventStats struct {
UniqueUsers int32 `json:"unique_users"`
CountStep map[string]interface{} `json:"count_step"`
AdditionalProperties map[string]interface{}
}
type _EventStats EventStats
// NewEventStats instantiates a new EventStats object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewEventStats(uniqueUsers int32, countStep map[string]interface{}) *EventStats {
this := EventStats{}
this.UniqueUsers = uniqueUsers
this.CountStep = countStep
return &this
}
// NewEventStatsWithDefaults instantiates a new EventStats object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewEventStatsWithDefaults() *EventStats {
this := EventStats{}
return &this
}
// GetUniqueUsers returns the UniqueUsers field value
func (o *EventStats) GetUniqueUsers() int32 {
if o == nil {
var ret int32
return ret
}
return o.UniqueUsers
}
// GetUniqueUsersOk returns a tuple with the UniqueUsers field value
// and a boolean to check if the value has been set.
func (o *EventStats) GetUniqueUsersOk() (*int32, bool) {
if o == nil {
return nil, false
}
return &o.UniqueUsers, true
}
// SetUniqueUsers sets field value
func (o *EventStats) SetUniqueUsers(v int32) {
o.UniqueUsers = v
}
// GetCountStep returns the CountStep field value
func (o *EventStats) GetCountStep() map[string]interface{} {
if o == nil {
var ret map[string]interface{}
return ret
}
return o.CountStep
}
// GetCountStepOk returns a tuple with the CountStep field value
// and a boolean to check if the value has been set.
func (o *EventStats) GetCountStepOk() (map[string]interface{}, bool) {
if o == nil {
return map[string]interface{}{}, false
}
return o.CountStep, true
}
// SetCountStep sets field value
func (o *EventStats) SetCountStep(v map[string]interface{}) {
o.CountStep = v
}
func (o EventStats) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o EventStats) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["unique_users"] = o.UniqueUsers
toSerialize["count_step"] = o.CountStep
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
}
return toSerialize, nil
}
func (o *EventStats) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"unique_users",
"count_step",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err
}
for _, requiredProperty := range requiredProperties {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varEventStats := _EventStats{}
err = json.Unmarshal(data, &varEventStats)
if err != nil {
return err
}
*o = EventStats(varEventStats)
additionalProperties := make(map[string]interface{})
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "unique_users")
delete(additionalProperties, "count_step")
o.AdditionalProperties = additionalProperties
}
return err
}
type NullableEventStats struct {
value *EventStats
isSet bool
}
func (v NullableEventStats) Get() *EventStats {
return v.value
}
func (v *NullableEventStats) Set(val *EventStats) {
v.value = val
v.isSet = true
}
func (v NullableEventStats) IsSet() bool {
return v.isSet
}
func (v *NullableEventStats) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableEventStats(val *EventStats) *NullableEventStats {
return &NullableEventStats{value: val, isSet: true}
}
func (v NullableEventStats) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableEventStats) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
// checks if the EventVolume type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &EventVolume{}
// EventVolume Count of events of action created on day
// EventVolume Count of events of action created on day for a single event action
type EventVolume struct {
Action EventActions `json:"action"`
Time time.Time `json:"time"`
+175 -1
View File
@@ -75,6 +75,15 @@ pub enum EventsEventsRetrieveError {
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`events_events_stats_retrieve`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EventsEventsStatsRetrieveError {
Status400(models::ValidationError),
Status403(models::GenericError),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`events_events_top_per_user_list`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
@@ -467,6 +476,7 @@ pub async fn events_events_export_create(
brand_name: Option<&str>,
client_ip: Option<&str>,
context_authorized_app: Option<&str>,
context_device: Option<&str>,
context_model_app: Option<&str>,
context_model_name: Option<&str>,
context_model_pk: Option<&str>,
@@ -480,6 +490,7 @@ pub async fn events_events_export_create(
let p_query_brand_name = brand_name;
let p_query_client_ip = client_ip;
let p_query_context_authorized_app = context_authorized_app;
let p_query_context_device = context_device;
let p_query_context_model_app = context_model_app;
let p_query_context_model_name = context_model_name;
let p_query_context_model_pk = context_model_pk;
@@ -523,6 +534,9 @@ pub async fn events_events_export_create(
if let Some(ref param_value) = p_query_context_authorized_app {
req_builder = req_builder.query(&[("context_authorized_app", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_device {
req_builder = req_builder.query(&[("context_device", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_model_app {
req_builder = req_builder.query(&[("context_model_app", &param_value.to_string())]);
}
@@ -595,6 +609,7 @@ pub async fn events_events_list(
brand_name: Option<&str>,
client_ip: Option<&str>,
context_authorized_app: Option<&str>,
context_device: Option<&str>,
context_model_app: Option<&str>,
context_model_name: Option<&str>,
context_model_pk: Option<&str>,
@@ -610,6 +625,7 @@ pub async fn events_events_list(
let p_query_brand_name = brand_name;
let p_query_client_ip = client_ip;
let p_query_context_authorized_app = context_authorized_app;
let p_query_context_device = context_device;
let p_query_context_model_app = context_model_app;
let p_query_context_model_name = context_model_name;
let p_query_context_model_pk = context_model_pk;
@@ -653,6 +669,9 @@ pub async fn events_events_list(
if let Some(ref param_value) = p_query_context_authorized_app {
req_builder = req_builder.query(&[("context_authorized_app", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_device {
req_builder = req_builder.query(&[("context_device", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_model_app {
req_builder = req_builder.query(&[("context_model_app", &param_value.to_string())]);
}
@@ -850,6 +869,156 @@ pub async fn events_events_retrieve(
}
}
/// Get event stats for specified filters and count steps
pub async fn events_events_stats_retrieve(
configuration: &configuration::Configuration,
count_steps: Vec<String>,
action: Option<&str>,
actions: Option<Vec<models::EventActions>>,
brand_name: Option<&str>,
client_ip: Option<&str>,
context_authorized_app: Option<&str>,
context_device: Option<&str>,
context_model_app: Option<&str>,
context_model_name: Option<&str>,
context_model_pk: Option<&str>,
ordering: Option<&str>,
search: Option<&str>,
username: Option<&str>,
) -> Result<models::EventStats, Error<EventsEventsStatsRetrieveError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_query_count_steps = count_steps;
let p_query_action = action;
let p_query_actions = actions;
let p_query_brand_name = brand_name;
let p_query_client_ip = client_ip;
let p_query_context_authorized_app = context_authorized_app;
let p_query_context_device = context_device;
let p_query_context_model_app = context_model_app;
let p_query_context_model_name = context_model_name;
let p_query_context_model_pk = context_model_pk;
let p_query_ordering = ordering;
let p_query_search = search;
let p_query_username = username;
let uri_str = format!("{}/events/events/stats/", configuration.base_path);
let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
if let Some(ref param_value) = p_query_action {
req_builder = req_builder.query(&[("action", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_actions {
req_builder = match "multi" {
"multi" => req_builder.query(
&param_value
.into_iter()
.map(|p| ("actions".to_owned(), p.to_string()))
.collect::<Vec<(std::string::String, std::string::String)>>(),
),
_ => req_builder.query(&[(
"actions",
&param_value
.into_iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(",")
.to_string(),
)]),
};
}
if let Some(ref param_value) = p_query_brand_name {
req_builder = req_builder.query(&[("brand_name", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_client_ip {
req_builder = req_builder.query(&[("client_ip", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_authorized_app {
req_builder = req_builder.query(&[("context_authorized_app", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_device {
req_builder = req_builder.query(&[("context_device", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_model_app {
req_builder = req_builder.query(&[("context_model_app", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_model_name {
req_builder = req_builder.query(&[("context_model_name", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_model_pk {
req_builder = req_builder.query(&[("context_model_pk", &param_value.to_string())]);
}
req_builder = match "multi" {
"multi" => req_builder.query(
&p_query_count_steps
.into_iter()
.map(|p| ("count_steps".to_owned(), p.to_string()))
.collect::<Vec<(std::string::String, std::string::String)>>(),
),
_ => req_builder.query(&[(
"count_steps",
&p_query_count_steps
.into_iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(",")
.to_string(),
)]),
};
if let Some(ref param_value) = p_query_ordering {
req_builder = req_builder.query(&[("ordering", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_search {
req_builder = req_builder.query(&[("search", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_username {
req_builder = req_builder.query(&[("username", &param_value.to_string())]);
}
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => {
return Err(Error::from(serde_json::Error::custom(
"Received `text/plain` content type response that cannot be converted to \
`models::EventStats`",
)));
}
ContentType::Unsupported(unknown_type) => {
return Err(Error::from(serde_json::Error::custom(format!(
"Received `{unknown_type}` content type response that cannot be converted to \
`models::EventStats`"
))));
}
}
} else {
let content = resp.text().await?;
let entity: Option<EventsEventsStatsRetrieveError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Get the top_n events grouped by user count
pub async fn events_events_top_per_user_list(
configuration: &configuration::Configuration,
@@ -987,10 +1156,11 @@ pub async fn events_events_volume_list(
brand_name: Option<&str>,
client_ip: Option<&str>,
context_authorized_app: Option<&str>,
context_device: Option<&str>,
context_model_app: Option<&str>,
context_model_name: Option<&str>,
context_model_pk: Option<&str>,
history_days: Option<f64>,
history_days: Option<i32>,
ordering: Option<&str>,
search: Option<&str>,
username: Option<&str>,
@@ -1001,6 +1171,7 @@ pub async fn events_events_volume_list(
let p_query_brand_name = brand_name;
let p_query_client_ip = client_ip;
let p_query_context_authorized_app = context_authorized_app;
let p_query_context_device = context_device;
let p_query_context_model_app = context_model_app;
let p_query_context_model_name = context_model_name;
let p_query_context_model_pk = context_model_pk;
@@ -1043,6 +1214,9 @@ pub async fn events_events_volume_list(
if let Some(ref param_value) = p_query_context_authorized_app {
req_builder = req_builder.query(&[("context_authorized_app", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_device {
req_builder = req_builder.query(&[("context_device", &param_value.to_string())]);
}
if let Some(ref param_value) = p_query_context_model_app {
req_builder = req_builder.query(&[("context_model_app", &param_value.to_string())]);
}
+33
View File
@@ -0,0 +1,33 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
/// EventStats : Count of unique users in events and aggregated counts per specified deltas
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct EventStats {
#[serde(rename = "unique_users")]
pub unique_users: i32,
#[serde(rename = "count_step")]
pub count_step: std::collections::HashMap<String, serde_json::Value>,
}
impl EventStats {
/// Count of unique users in events and aggregated counts per specified deltas
pub fn new(
unique_users: i32,
count_step: std::collections::HashMap<String, serde_json::Value>,
) -> EventStats {
EventStats {
unique_users,
count_step,
}
}
}
+2 -2
View File
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use crate::models;
/// EventVolume : Count of events of action created on day
/// EventVolume : Count of events of action created on day for a single event action
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct EventVolume {
#[serde(rename = "action")]
@@ -22,7 +22,7 @@ pub struct EventVolume {
}
impl EventVolume {
/// Count of events of action created on day
/// Count of events of action created on day for a single event action
pub fn new(action: models::EventActions, time: String, count: i32) -> EventVolume {
EventVolume {
action,
+2
View File
@@ -342,6 +342,8 @@ pub mod event_matcher_policy_request;
pub use self::event_matcher_policy_request::EventMatcherPolicyRequest;
pub mod event_request;
pub use self::event_request::EventRequest;
pub mod event_stats;
pub use self::event_stats::EventStats;
pub mod event_top_per_user;
pub use self::event_top_per_user::EventTopPerUser;
pub mod event_volume;
+138
View File
@@ -19,6 +19,7 @@ import type {
Event,
EventActions,
EventRequest,
EventStats,
EventTopPerUser,
EventVolume,
GenericError,
@@ -52,6 +53,8 @@ import {
EventActionsToJSON,
EventRequestFromJSON,
EventRequestToJSON,
EventStatsFromJSON,
EventStatsToJSON,
EventTopPerUserFromJSON,
EventTopPerUserToJSON,
EventVolumeFromJSON,
@@ -114,6 +117,7 @@ export interface EventsEventsExportCreateRequest {
brandName?: string;
clientIp?: string;
contextAuthorizedApp?: string;
contextDevice?: string;
contextModelApp?: string;
contextModelName?: string;
contextModelPk?: string;
@@ -128,6 +132,7 @@ export interface EventsEventsListRequest {
brandName?: string;
clientIp?: string;
contextAuthorizedApp?: string;
contextDevice?: string;
contextModelApp?: string;
contextModelName?: string;
contextModelPk?: string;
@@ -147,6 +152,22 @@ export interface EventsEventsRetrieveRequest {
eventUuid: string;
}
export interface EventsEventsStatsRetrieveRequest {
countSteps: Array<string>;
action?: string;
actions?: Array<EventActions>;
brandName?: string;
clientIp?: string;
contextAuthorizedApp?: string;
contextDevice?: string;
contextModelApp?: string;
contextModelName?: string;
contextModelPk?: string;
ordering?: string;
search?: string;
username?: string;
}
export interface EventsEventsTopPerUserListRequest {
action?: string;
topN?: number;
@@ -163,6 +184,7 @@ export interface EventsEventsVolumeListRequest {
brandName?: string;
clientIp?: string;
contextAuthorizedApp?: string;
contextDevice?: string;
contextModelApp?: string;
contextModelName?: string;
contextModelPk?: string;
@@ -467,6 +489,10 @@ export class EventsApi extends runtime.BaseAPI {
queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp'];
}
if (requestParameters['contextDevice'] != null) {
queryParameters['context_device'] = requestParameters['contextDevice'];
}
if (requestParameters['contextModelApp'] != null) {
queryParameters['context_model_app'] = requestParameters['contextModelApp'];
}
@@ -556,6 +582,10 @@ export class EventsApi extends runtime.BaseAPI {
queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp'];
}
if (requestParameters['contextDevice'] != null) {
queryParameters['context_device'] = requestParameters['contextDevice'];
}
if (requestParameters['contextModelApp'] != null) {
queryParameters['context_model_app'] = requestParameters['contextModelApp'];
}
@@ -736,6 +766,110 @@ export class EventsApi extends runtime.BaseAPI {
return await response.value();
}
/**
* Creates request options for eventsEventsStatsRetrieve without sending the request
*/
async eventsEventsStatsRetrieveRequestOpts(requestParameters: EventsEventsStatsRetrieveRequest): Promise<runtime.RequestOpts> {
if (requestParameters['countSteps'] == null) {
throw new runtime.RequiredError(
'countSteps',
'Required parameter "countSteps" was null or undefined when calling eventsEventsStatsRetrieve().'
);
}
const queryParameters: any = {};
if (requestParameters['action'] != null) {
queryParameters['action'] = requestParameters['action'];
}
if (requestParameters['actions'] != null) {
queryParameters['actions'] = requestParameters['actions'];
}
if (requestParameters['brandName'] != null) {
queryParameters['brand_name'] = requestParameters['brandName'];
}
if (requestParameters['clientIp'] != null) {
queryParameters['client_ip'] = requestParameters['clientIp'];
}
if (requestParameters['contextAuthorizedApp'] != null) {
queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp'];
}
if (requestParameters['contextDevice'] != null) {
queryParameters['context_device'] = requestParameters['contextDevice'];
}
if (requestParameters['contextModelApp'] != null) {
queryParameters['context_model_app'] = requestParameters['contextModelApp'];
}
if (requestParameters['contextModelName'] != null) {
queryParameters['context_model_name'] = requestParameters['contextModelName'];
}
if (requestParameters['contextModelPk'] != null) {
queryParameters['context_model_pk'] = requestParameters['contextModelPk'];
}
if (requestParameters['countSteps'] != null) {
queryParameters['count_steps'] = requestParameters['countSteps'];
}
if (requestParameters['ordering'] != null) {
queryParameters['ordering'] = requestParameters['ordering'];
}
if (requestParameters['search'] != null) {
queryParameters['search'] = requestParameters['search'];
}
if (requestParameters['username'] != null) {
queryParameters['username'] = requestParameters['username'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/events/events/stats/`;
return {
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
};
}
/**
* Get event stats for specified filters and count steps
*/
async eventsEventsStatsRetrieveRaw(requestParameters: EventsEventsStatsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<EventStats>> {
const requestOptions = await this.eventsEventsStatsRetrieveRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => EventStatsFromJSON(jsonValue));
}
/**
* Get event stats for specified filters and count steps
*/
async eventsEventsStatsRetrieve(requestParameters: EventsEventsStatsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<EventStats> {
const response = await this.eventsEventsStatsRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for eventsEventsTopPerUserList without sending the request
*/
@@ -878,6 +1012,10 @@ export class EventsApi extends runtime.BaseAPI {
queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp'];
}
if (requestParameters['contextDevice'] != null) {
queryParameters['context_device'] = requestParameters['contextDevice'];
}
if (requestParameters['contextModelApp'] != null) {
queryParameters['context_model_app'] = requestParameters['contextModelApp'];
}
+75
View File
@@ -0,0 +1,75 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
* Count of unique users in events and aggregated counts per specified deltas
* @export
* @interface EventStats
*/
export interface EventStats {
/**
*
* @type {number}
* @memberof EventStats
*/
uniqueUsers: number;
/**
*
* @type {{ [key: string]: any; }}
* @memberof EventStats
*/
countStep: { [key: string]: any; };
}
/**
* Check if a given object implements the EventStats interface.
*/
export function instanceOfEventStats(value: object): value is EventStats {
if (!('uniqueUsers' in value) || value['uniqueUsers'] === undefined) return false;
if (!('countStep' in value) || value['countStep'] === undefined) return false;
return true;
}
export function EventStatsFromJSON(json: any): EventStats {
return EventStatsFromJSONTyped(json, false);
}
export function EventStatsFromJSONTyped(json: any, ignoreDiscriminator: boolean): EventStats {
if (json == null) {
return json;
}
return {
'uniqueUsers': json['unique_users'],
'countStep': json['count_step'],
};
}
export function EventStatsToJSON(json: any): EventStats {
return EventStatsToJSONTyped(json, false);
}
export function EventStatsToJSONTyped(value?: EventStats | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'unique_users': value['uniqueUsers'],
'count_step': value['countStep'],
};
}
+1 -1
View File
@@ -22,7 +22,7 @@ import {
} from './EventActions';
/**
* Count of events of action created on day
* Count of events of action created on day for a single event action
* @export
* @interface EventVolume
*/
+1
View File
@@ -172,6 +172,7 @@ export * from './EventActions';
export * from './EventMatcherPolicy';
export * from './EventMatcherPolicyRequest';
export * from './EventRequest';
export * from './EventStats';
export * from './EventTopPerUser';
export * from './EventVolume';
export * from './EventsRequestedEnum';
+112 -2
View File
@@ -7094,6 +7094,11 @@ paths:
schema:
type: string
description: Context Authorized application
- in: query
name: context_device
schema:
type: string
description: Context Device Primary Key
- in: query
name: context_model_app
schema:
@@ -7326,6 +7331,11 @@ paths:
schema:
type: string
description: Context Authorized application
- in: query
name: context_device
schema:
type: string
description: Context Device Primary Key
- in: query
name: context_model_app
schema:
@@ -7363,6 +7373,88 @@ paths:
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/events/events/stats/:
get:
operationId: events_events_stats_retrieve
description: Get event stats for specified filters and count steps
parameters:
- in: query
name: action
schema:
type: string
- in: query
name: actions
schema:
type: array
items:
$ref: '#/components/schemas/EventActions'
explode: true
style: form
- in: query
name: brand_name
schema:
type: string
description: Brand name
- in: query
name: client_ip
schema:
type: string
- in: query
name: context_authorized_app
schema:
type: string
description: Context Authorized application
- in: query
name: context_device
schema:
type: string
description: Context Device Primary Key
- in: query
name: context_model_app
schema:
type: string
description: Context Model App
- in: query
name: context_model_name
schema:
type: string
description: Context Model Name
- in: query
name: context_model_pk
schema:
type: string
description: Context Model Primary Key
- in: query
name: count_steps
schema:
type: array
items:
type: string
minLength: 1
description: Timedelta, format of 'weeks=3;days=2;hours=3,seconds=2'
required: true
- $ref: '#/components/parameters/QueryPaginationOrdering'
- $ref: '#/components/parameters/QuerySearch'
- in: query
name: username
schema:
type: string
description: Username
tags:
- events
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/EventStats'
description: ''
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/events/events/top_per_user/:
get:
operationId: events_events_top_per_user_list
@@ -7424,6 +7516,11 @@ paths:
schema:
type: string
description: Context Authorized application
- in: query
name: context_device
schema:
type: string
description: Context Device Primary Key
- in: query
name: context_model_app
schema:
@@ -7442,7 +7539,7 @@ paths:
- in: query
name: history_days
schema:
type: number
type: integer
default: 7
- $ref: '#/components/parameters/QueryPaginationOrdering'
- $ref: '#/components/parameters/QuerySearch'
@@ -38168,6 +38265,19 @@ components:
required:
- action
- app
EventStats:
type: object
description: Count of unique users in events and aggregated counts per specified
deltas
properties:
unique_users:
type: integer
count_step:
type: object
additionalProperties: {}
required:
- count_step
- unique_users
EventTopPerUser:
type: object
description: Response object of Event's top_per_user
@@ -38185,7 +38295,7 @@ components:
- unique_users
EventVolume:
type: object
description: Count of events of action created on day
description: Count of events of action created on day for a single event action
properties:
action:
$ref: '#/components/schemas/EventActions'
@@ -5,17 +5,10 @@ import "#elements/buttons/Dropdown";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/index";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EventWithContext } from "#common/events";
import { actionToLabel } from "#common/labels";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import Styles from "#admin/admin-overview/cards/RecentEventsCard.css";
import { EventGeo, renderEventUser } from "#admin/events/utils";
import { SimpleEventTable } from "#admin/events/SimpleEventTable";
import { Event, EventsApi } from "@goauthentik/api";
import { EventsEventsListRequest } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
@@ -24,10 +17,11 @@ import { customElement, property } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
@customElement("ak-recent-events")
export class RecentEventsCard extends Table<Event> {
export class RecentEventsCard extends SimpleEventTable {
public override role = "region";
public override ariaLabel = msg("Recent events");
public override label = msg("Events");
public override expandable = false;
@property()
order = "-created";
@@ -35,11 +29,10 @@ export class RecentEventsCard extends Table<Event> {
@property({ type: Number })
pageSize = 10;
async apiEndpoint(): Promise<PaginatedResponse<Event>> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
...(await this.defaultEndpointConfig()),
async apiParameters(): Promise<Partial<EventsEventsListRequest>> {
return {
pageSize: this.pageSize,
});
};
}
static styles: CSSResult[] = [
@@ -49,47 +42,12 @@ export class RecentEventsCard extends Table<Event> {
Styles,
];
protected override rowLabel(item: Event): string {
return actionToLabel(item.action);
}
protected columns: TableColumn[] = [
[msg("Action"), "action"],
[msg("User"), "user"],
[msg("Creation Date"), "created"],
[msg("Client IP"), "client_ip"],
];
renderToolbar(): TemplateResult {
return html`<h1 class="pf-c-card__title">
<i class="pf-icon pf-icon-catalog" aria-hidden="true"></i>
${msg("Recent events")}
</h1>`;
}
row(item: EventWithContext): SlottedTemplateResult[] {
return [
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
<small>${item.app}</small>`,
renderEventUser(item),
html`<ak-timestamp .timestamp=${item.created}></ak-timestamp>`,
html` <div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,
];
}
renderEmpty(inner?: SlottedTemplateResult): TemplateResult {
if (this.error) {
return super.renderEmpty(inner);
}
return super.renderEmpty(
html`<ak-empty-state
><span>${msg("No Events found.")}</span>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`,
);
}
}
declare global {
@@ -1,68 +1,20 @@
import "#components/ak-event-info";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EventWithContext } from "#common/events";
import { actionToLabel } from "#common/labels";
import { SimpleEventTable } from "#admin/events/SimpleEventTable";
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { EventsEventsListRequest } from "@goauthentik/api";
import { renderEventUser } from "#admin/events/utils";
import { Event, EventsApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-events-application")
export class ApplicationEvents extends Table<Event> {
expandable = true;
@property()
order = "-created";
export class ApplicationEvents extends SimpleEventTable {
@property({ attribute: "application-id" })
applicationId!: string;
async apiEndpoint(): Promise<PaginatedResponse<Event>> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
...(await this.defaultEndpointConfig()),
async apiParameters(): Promise<Partial<EventsEventsListRequest>> {
return {
contextAuthorizedApp: this.applicationId.replaceAll("-", ""),
});
}
protected override rowLabel(item: Event): string {
return actionToLabel(item.action);
}
protected columns: TableColumn[] = [
[msg("Action"), "action"],
[msg("User"), "enabled"],
[msg("Creation Date"), "created"],
[msg("Client IP"), "client_ip"],
];
row(item: EventWithContext): SlottedTemplateResult[] {
return [
html`${actionToLabel(item.action)}`,
renderEventUser(item),
Timestamp(item.created),
html`<span>${item.clientIp || msg("-")}</span>`,
];
}
renderExpanded(item: Event): TemplateResult {
return html`<ak-event-info .event=${item as EventWithContext}></ak-event-info>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state
><span>${msg("No Events found.")}</span>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`,
);
};
}
}
+136 -118
View File
@@ -19,11 +19,21 @@ import { AKElement } from "#elements/Base";
import { WithLicenseSummary } from "#elements/mixins/license";
import { setPageDetails } from "#components/ak-page-navbar";
import renderDescriptionList from "#components/DescriptionList";
import { Application, ContentTypeEnum, CoreApi, ModelEnum, OutpostsApi } from "@goauthentik/api";
import {
Application,
ContentTypeEnum,
CoreApi,
EventActions,
EventsApi,
EventStats,
ModelEnum,
OutpostsApi,
} from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
import { css, CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
@@ -48,6 +58,11 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
PFGrid,
PFFlex,
PFCard,
css`
.big-number {
font-size: 250%;
}
`,
];
//#region Properties
@@ -61,6 +76,9 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
@state()
protected application?: Application;
@state()
protected stats?: EventStats;
@state()
protected error?: APIError;
@@ -98,6 +116,15 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
) {
this.fetchIsMissingOutpost([app.provider || 0]);
}
return new EventsApi(DEFAULT_CONFIG)
.eventsEventsStatsRetrieve({
action: EventActions.AuthorizeApplication,
contextAuthorizedApp: app.pk.replaceAll("-", ""),
countSteps: ["hours=24", "days=7", "days=30"],
})
.then((stats) => {
this.stats = stats;
});
})
.catch(async (error) => {
this.error = await parseAPIResponseError(error);
@@ -120,125 +147,116 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-2-col-on-xl pf-m-2-col-on-2xl">
<div class="pf-c-card__title">${msg("Related")}</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list">
${this.application.providerObj
? html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Provider")}</span
${renderDescriptionList([
[
msg("Provider"),
this.application.providerObj
? html` <a
href="#/core/providers/${this.application.providerObj?.pk}"
>
${this.application.providerObj?.name}
(${this.application.providerObj?.verboseName})
</a>`
: html`-`,
],
[
msg("Backchannel Providers"),
(this.application.backchannelProvidersObj || []).length > 0
? html`<ul class="pf-c-list">
${this.application.backchannelProvidersObj.map((provider) => {
return html`
<li>
<a href="#/core/providers/${provider.pk}">
${provider.name} (${provider.verboseName})
</a>
</li>
`;
})}
</ul>`
: html`-`,
],
[
msg("Policy engine mode"),
html`${this.application.policyEngineMode?.toUpperCase()}`,
],
[
msg("Related actions"),
html`<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header"> ${msg("Update Application")} </span>
<ak-application-form
slot="form"
.instancePk=${this.application.slug}
>
</ak-application-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary pf-m-block"
>
${msg("Edit")}
</button>
</ak-forms-modal>
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Check")}</span>
<span slot="header"> ${msg("Check Application access")} </span>
<ak-application-check-access-form
slot="form"
.application=${this.application}
>
</ak-application-check-access-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary pf-m-block"
>
${msg("Check access")}
</button>
</ak-forms-modal>
${this.application.launchUrl
? html`<a
target="_blank"
href=${this.application.launchUrl}
slot="trigger"
class="pf-c-button pf-m-secondary pf-m-block"
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a
href="#/core/providers/${this.application.providerObj
?.pk}"
>
${this.application.providerObj?.name}
(${this.application.providerObj?.verboseName})
</a>
</div>
</dd>
</div>`
: nothing}
${(this.application.backchannelProvidersObj || []).length > 0
? html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Backchannel Providers")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ul class="pf-c-list">
${this.application.backchannelProvidersObj.map(
(provider) => {
return html`
<li>
<a
href="#/core/providers/${provider.pk}"
>
${provider.name}
(${provider.verboseName})
</a>
</li>
`;
},
)}
</ul>
</div>
</dd>
</div>`
: nothing}
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Policy engine mode")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text pf-m-monospace">
${this.application.policyEngineMode?.toUpperCase()}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Related actions")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header"> ${msg("Update Application")} </span>
<ak-application-form
slot="form"
.instancePk=${this.application.slug}
>
</ak-application-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary pf-m-block"
>
${msg("Edit")}
</button>
</ak-forms-modal>
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Check")}</span>
<span slot="header">
${msg("Check Application access")}
</span>
<ak-application-check-access-form
slot="form"
.application=${this.application}
>
</ak-application-check-access-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary pf-m-block"
>
${msg("Check access")}
</button>
</ak-forms-modal>
${this.application.launchUrl
? html`<a
target="_blank"
href=${this.application.launchUrl}
slot="trigger"
class="pf-c-button pf-m-secondary pf-m-block"
>
${msg("Launch")}
</a>`
: nothing}
</div>
</dd>
</div>
</dl>
${msg("Launch")}
</a>`
: nothing}`,
],
])}
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl">
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-2-col-on-xl pf-m-2-col-on-2xl">
<div class="pf-c-card__title">${msg("Statistics")}</div>
<div class="pf-c-card__body">
${renderDescriptionList([
[
msg("Users"),
html`<p class="big-number">
${this.stats ? this.stats?.uniqueUsers : "-"}
</p>`,
],
[
msg("Authorizations (24h)"),
html`<p class="big-number">
${this.stats ? this.stats?.countStep.hours24 : "-"}
</p>`,
],
[
msg("Authorizations (7d)"),
html`<p class="big-number">
${this.stats ? this.stats?.countStep.days7 : "-"}
</p>`,
],
[
msg("Authorizations (1m)"),
html`<p class="big-number">
${this.stats ? this.stats?.countStep.days30 : "-"}
</p>`,
],
])}
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl pf-m-8-col-on-2xl">
<div class="pf-c-card__title">
${msg("Logins over the last week (per 8 hours)")}
</div>
@@ -0,0 +1,25 @@
import "#components/ak-event-info";
import { SimpleEventTable } from "#admin/events/SimpleEventTable";
import { EventsEventsListRequest } from "@goauthentik/api";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-events-device")
export class DeviceEvents extends SimpleEventTable {
@property({ attribute: "device-id" })
deviceId!: string;
async apiParameters(): Promise<Partial<EventsEventsListRequest>> {
return {
contextDevice: this.deviceId.replaceAll("-", ""),
};
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-events-device": DeviceEvents;
}
}
@@ -6,6 +6,7 @@ import "#admin/endpoints/devices/facts/DeviceUserTable";
import "#admin/endpoints/devices/facts/DeviceSoftwareTable";
import "#admin/endpoints/devices/facts/DeviceGroupTable";
import "#admin/endpoints/devices/DeviceForm";
import "#admin/endpoints/devices/DeviceEvents";
import "#elements/forms/ModalForm";
import "#elements/Tabs";
@@ -31,6 +32,7 @@ import PFCard from "@patternfly/patternfly/components/Card/card.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 PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
@customElement("ak-endpoints-device-view")
export class DeviceViewPage extends AKElement {
@@ -43,7 +45,7 @@ export class DeviceViewPage extends AKElement {
@state()
protected error?: APIError;
static styles: CSSResult[] = [PFCard, PFPage, PFGrid, PFButton, PFDescriptionList];
static styles: CSSResult[] = [PFCard, PFPage, PFGrid, PFStack, PFButton, PFDescriptionList];
protected fetchDevice(id: string) {
new EndpointsApi(DEFAULT_CONFIG)
@@ -74,49 +76,52 @@ export class DeviceViewPage extends AKElement {
osFamilyToLabel(this.device.facts.data.os.family),
this.device.facts.data.os?.version,
].join(" ")
: undefined,
: "-",
icon: "fa fa-laptop",
});
}
renderOverview() {
if (!this.device) {
return nothing;
}
renderDetails() {
const _rootDisk =
this.device.facts.data.disks?.filter(
this.device?.facts.data.disks?.filter(
(d) => d.mountpoint === "/" || d.mountpoint === "C:",
) || [];
let rootDisk: Disk | undefined = undefined;
if (_rootDisk?.length > 0) {
rootDisk = _rootDisk[0];
}
return html`<div class="pf-l-grid pf-m-gutter">
<div class="pf-l-grid__item pf-m-4-col pf-c-card">
return html`<div class="pf-l-stack pf-m-gutter">
<div class="pf-l-stack__item pf-c-card">
<div class="pf-c-card__title">${msg("Device details")}</div>
<div class="pf-c-card__body">
${renderDescriptionList(
[
[msg("Name"), this.device.name],
[msg("Hostname"), this.device.facts.data.network?.hostname ?? "-"],
[msg("Serial number"), this.device.facts.data.hardware?.serial ?? "-"],
[msg("Name"), this.device?.name],
[msg("Hostname"), this.device?.facts.data.network?.hostname ?? "-"],
[msg("Serial number"), this.device?.facts.data.hardware?.serial ?? "-"],
[
msg("Operating system"),
this.device.facts.data.os
this.device?.facts.data.os
? [
this.device.facts.data.os?.name ||
osFamilyToLabel(this.device.facts.data.os.family),
this.device.facts.data.os?.version,
this.device?.facts.data.os?.name ||
osFamilyToLabel(this.device?.facts.data.os.family),
this.device?.facts.data.os?.version,
].join(" ")
: "-",
],
[
msg("Firewall enabled"),
html`<ak-status-label
?good=${this.device.facts.data.network?.firewallEnabled}
?good=${this.device?.facts.data.network?.firewallEnabled}
></ak-status-label>`,
],
[msg("Device access group"), this.device.accessGroupObj?.name ?? "-"],
[
msg("Disk encryption"),
html`<ak-status-label
?good=${rootDisk?.encryptionEnabled}
></ak-status-label>`,
],
[msg("Device access group"), this.device?.accessGroupObj?.name ?? "-"],
[
msg("Actions"),
html`<ak-forms-modal>
@@ -124,7 +129,7 @@ export class DeviceViewPage extends AKElement {
<span slot="header">${msg("Update Device")}</span>
<ak-endpoints-device-form
slot="form"
.instancePk=${this.device.deviceUuid}
.instancePk=${this.device?.deviceUuid}
>
</ak-endpoints-device-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
@@ -137,37 +142,31 @@ export class DeviceViewPage extends AKElement {
)}
</div>
</div>
<div class="pf-l-grid__item pf-m-4-col pf-c-card">
<div class="pf-l-stack__item pf-c-card">
<div class="pf-c-card__title">${msg("Hardware")}</div>
<div class="pf-c-card__body">
${renderDescriptionList(
[
[
msg("Manufacturer"),
this.device.facts.data.hardware?.manufacturer ?? "-",
this.device?.facts.data.hardware?.manufacturer ?? "-",
],
[msg("Model"), this.device.facts.data.hardware?.model ?? "-"],
[msg("Model"), this.device?.facts.data.hardware?.model ?? "-"],
[
msg("CPU"),
this.device.facts.data.hardware?.cpuCount &&
this.device.facts.data.hardware?.cpuName
this.device?.facts.data.hardware?.cpuCount &&
this.device?.facts.data.hardware?.cpuName
? msg(
str`${this.device.facts.data.hardware?.cpuCount} x ${this.device.facts.data.hardware?.cpuName}`,
str`${this.device?.facts.data.hardware?.cpuCount} x ${this.device?.facts.data.hardware?.cpuName}`,
)
: "-",
],
[
msg("Memory"),
this.device.facts.data.hardware?.memoryBytes
? getSize(this.device.facts.data.hardware?.memoryBytes)
this.device?.facts.data.hardware?.memoryBytes
? getSize(this.device?.facts.data.hardware?.memoryBytes)
: "-",
],
[
msg("Disk encryption"),
html`<ak-status-label
?good=${rootDisk?.encryptionEnabled}
></ak-status-label>`,
],
[
msg("Primary disk size"),
rootDisk?.capacityTotalBytes
@@ -192,11 +191,11 @@ export class DeviceViewPage extends AKElement {
)}
</div>
</div>
<div class="pf-l-grid__item pf-m-4-col pf-c-card">
<div class="pf-l-stack__item pf-c-card">
<div class="pf-c-card__title">${msg("Connections")}</div>
<div class="pf-c-card__body">
${renderDescriptionList(
this.device.connectionsObj.map((conn) => {
this.device?.connectionsObj.map((conn) => {
return [
html`${conn.connectorObj.name}`,
html`<div class="pf-c-description-list__text">
@@ -215,18 +214,6 @@ export class DeviceViewPage extends AKElement {
)}
</div>
</div>
<div class="pf-l-grid__item pf-m-12-col pf-c-card">
<div class="pf-c-card__title">${msg("Users / Groups")}</div>
<ak-bound-device-users-list
.target=${this.device.pbmUuid}
></ak-bound-device-users-list>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<ak-object-attributes-card
.objectAttributes=${this.device.attributes}
.excludeNotes=${false}
></ak-object-attributes-card>
</div>
</div>`;
}
@@ -288,7 +275,27 @@ export class DeviceViewPage extends AKElement {
aria-label="${msg("Overview")}"
class="pf-c-page__main-section"
>
${this.renderOverview()}
<div class="pf-l-grid pf-m-gutter">
<div class="pf-l-grid__item pf-m-4-col">${this.renderDetails()}</div>
<div class="pf-l-stack pf-m-gutter pf-m-8-col">
<div class="pf-l-stack__item pf-c-card">
<div class="pf-c-card__title">${msg("Events")}</div>
<ak-events-device .deviceId=${this.deviceId}></ak-events-device>
</div>
<div class="pf-l-stack__item pf-c-card">
<div class="pf-c-card__title">${msg("Users / Groups")}</div>
<ak-bound-device-users-list
.target=${this.device?.pbmUuid}
></ak-bound-device-users-list>
</div>
<div class="pf-l-stack__item pf-c-card">
<ak-object-attributes-card
.objectAttributes=${this.device?.attributes}
.excludeNotes=${false}
></ak-object-attributes-card>
</div>
</div>
</div>
</div>
<div
role="tabpanel"
+67
View File
@@ -0,0 +1,67 @@
import "#components/ak-event-info";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EventWithContext } from "#common/events";
import { actionToLabel } from "#common/labels";
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { EventGeo, renderEventUser } from "#admin/events/utils";
import { Event, EventsApi, EventsEventsListRequest } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit-html";
import { property } from "lit/decorators.js";
export abstract class SimpleEventTable extends Table<Event> {
abstract apiParameters(): Promise<Partial<EventsEventsListRequest>>;
expandable = true;
@property()
order = "-created";
async apiEndpoint(): Promise<PaginatedResponse<Event>> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
...(await this.defaultEndpointConfig()),
...(await this.apiParameters()),
});
}
protected override rowLabel(item: Event): string {
return actionToLabel(item.action);
}
protected columns: TableColumn[] = [
[msg("Action"), "action"],
[msg("User"), "enabled"],
[msg("Creation Date"), "created"],
[msg("Client IP"), "client_ip"],
];
row(item: EventWithContext): SlottedTemplateResult[] {
return [
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
<small>${item.app}</small>`,
renderEventUser(item),
Timestamp(item.created),
html`<div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,
];
}
renderExpanded(item: Event): TemplateResult {
return html`<ak-event-info .event=${item as EventWithContext}></ak-event-info>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state
><span>${msg("No Events found.")}</span>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`,
);
}
}
+6 -54
View File
@@ -1,68 +1,20 @@
import "#components/ak-event-info";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EventWithContext } from "#common/events";
import { actionToLabel } from "#common/labels";
import { SimpleEventTable } from "#admin/events/SimpleEventTable";
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { EventsEventsListRequest } from "@goauthentik/api";
import { renderEventUser } from "#admin/events/utils";
import { Event, EventsApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-events-user")
export class UserEvents extends Table<Event> {
expandable = true;
@property()
order = "-created";
export class UserEvents extends SimpleEventTable {
@property()
targetUser!: string;
async apiEndpoint(): Promise<PaginatedResponse<Event>> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
...(await this.defaultEndpointConfig()),
async apiParameters(): Promise<Partial<EventsEventsListRequest>> {
return {
username: this.targetUser,
});
}
protected override rowLabel(item: Event): string {
return actionToLabel(item.action);
}
protected columns: TableColumn[] = [
[msg("Action"), "action"],
[msg("User"), "enabled"],
[msg("Creation Date"), "created"],
[msg("Client IP"), "client_ip"],
];
row(item: EventWithContext): SlottedTemplateResult[] {
return [
html`${actionToLabel(item.action)}`,
renderEventUser(item),
Timestamp(item.created),
html`<span>${item.clientIp || msg("-")}</span>`,
];
}
renderExpanded(item: Event): TemplateResult {
return html`<ak-event-info .event=${item as EventWithContext}></ak-event-info>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state
><span>${msg("No Events found.")}</span>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`,
);
};
}
}
@@ -10,7 +10,7 @@ import { SlottedTemplateResult } from "#elements/types";
import { Provider } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html } from "lit";
import { html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-outposts-provider-list")
@@ -19,7 +19,7 @@ export class OutpostsProviderList extends StaticTable<Provider> {
protected columns: TableColumn[] = [
// ---
[msg("Name")],
[msg("ID")],
[msg("Application")],
];
row(item: Provider): SlottedTemplateResult[] {
@@ -27,6 +27,11 @@ export class OutpostsProviderList extends StaticTable<Provider> {
html`<a href="#/core/providers/${item.pk}">
<div>${item.name}</div>
</a>`,
item.assignedApplicationName
? html`<a href="#/core/applications/${item.assignedApplicationSlug}">
<div>${item.assignedApplicationName}</div>
</a>`
: nothing,
];
}
}