mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
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:
@@ -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:
|
||||
|
||||
@@ -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'."]}},
|
||||
)
|
||||
|
||||
Generated
+300
-3
@@ -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 'weeks=3;days=2;hours=3,seconds=2'
|
||||
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
|
||||
}
|
||||
|
||||
Generated
+196
@@ -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)
|
||||
}
|
||||
Generated
+1
-1
@@ -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
@@ -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", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_device {
|
||||
req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_model_app {
|
||||
req_builder = req_builder.query(&[("context_model_app", ¶m_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", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_device {
|
||||
req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_model_app {
|
||||
req_builder = req_builder.query(&[("context_model_app", ¶m_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", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_actions {
|
||||
req_builder = match "multi" {
|
||||
"multi" => req_builder.query(
|
||||
¶m_value
|
||||
.into_iter()
|
||||
.map(|p| ("actions".to_owned(), p.to_string()))
|
||||
.collect::<Vec<(std::string::String, std::string::String)>>(),
|
||||
),
|
||||
_ => req_builder.query(&[(
|
||||
"actions",
|
||||
¶m_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", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_client_ip {
|
||||
req_builder = req_builder.query(&[("client_ip", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_authorized_app {
|
||||
req_builder = req_builder.query(&[("context_authorized_app", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_device {
|
||||
req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_model_app {
|
||||
req_builder = req_builder.query(&[("context_model_app", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_model_name {
|
||||
req_builder = req_builder.query(&[("context_model_name", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_model_pk {
|
||||
req_builder = req_builder.query(&[("context_model_pk", ¶m_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", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_search {
|
||||
req_builder = req_builder.query(&[("search", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_username {
|
||||
req_builder = req_builder.query(&[("username", ¶m_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", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_device {
|
||||
req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_context_model_app {
|
||||
req_builder = req_builder.query(&[("context_model_app", ¶m_value.to_string())]);
|
||||
}
|
||||
|
||||
+33
@@ -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
@@ -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,
|
||||
|
||||
Generated
+2
@@ -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;
|
||||
|
||||
Generated
+138
@@ -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
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
Generated
+1
@@ -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
@@ -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>`,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user