migrate more

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer
2025-06-06 03:40:17 +02:00
parent eb63e22bf5
commit 24f852b8d8
5 changed files with 49 additions and 212 deletions

View File

@ -6,7 +6,6 @@ from typing import Any
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic from django.db.transaction import atomic
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -52,7 +51,6 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -317,53 +315,6 @@ class SessionUserSerializer(PassiveSerializer):
original = UserSelfSerializer(required=False) original = UserSelfSerializer(required=False)
class UserMetricsSerializer(PassiveSerializer):
"""User Metrics"""
logins = SerializerMethodField()
logins_failed = SerializerMethodField()
authorizations = SerializerMethodField()
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins(self, _):
"""Get successful logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
return (
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.LOGIN, user__pk=user.pk
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
return (
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.LOGIN_FAILED, context__username=user.username
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
return (
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
class UsersFilter(FilterSet): class UsersFilter(FilterSet):
"""Filter for users""" """Filter for users"""
@ -607,17 +558,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
update_session_auth_hash(self.request, user) update_session_auth_hash(self.request, user)
return Response(status=204) return Response(status=204)
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
def metrics(self, request: Request, pk: int) -> Response:
"""User metrics per 1h"""
user: User = self.get_object()
serializer = UserMetricsSerializer(instance={})
serializer.context["user"] = user
serializer.context["request"] = request
return Response(serializer.data)
@permission_required("authentik_core.reset_user_password") @permission_required("authentik_core.reset_user_password")
@extend_schema( @extend_schema(
responses={ responses={

View File

@ -62,7 +62,7 @@ class EventsFilter(django_filters.FilterSet):
"""Filter for events""" """Filter for events"""
username = django_filters.CharFilter( username = django_filters.CharFilter(
field_name="user", lookup_expr="username", label="Username" field_name="user", label="Username", method="filter_username"
) )
context_model_pk = django_filters.CharFilter( context_model_pk = django_filters.CharFilter(
field_name="context", field_name="context",
@ -97,6 +97,9 @@ class EventsFilter(django_filters.FilterSet):
label="Brand name", label="Brand name",
) )
def filter_username(self, queryset, name, value):
return queryset.filter(Q(user__username=value) | Q(context__username=value))
def filter_context_model_pk(self, queryset, name, value): def filter_context_model_pk(self, queryset, name, value):
"""Because we store the PK as UUID.hex, """Because we store the PK as UUID.hex,
we need to remove the dashes that a client may send. We can't use a we need to remove the dashes that a client may send. We can't use a

View File

@ -4109,42 +4109,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/core/applications/{slug}/metrics/:
get:
operationId: core_applications_metrics_list
description: Metrics for application logins
parameters:
- in: path
name: slug
schema:
type: string
description: Internal application name, used in URLs.
required: true
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Coordinate'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/applications/{slug}/set_icon/: /core/applications/{slug}/set_icon/:
post: post:
operationId: core_applications_set_icon_create operationId: core_applications_set_icon_create
@ -6044,40 +6008,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/core/users/{id}/metrics/:
get:
operationId: core_users_metrics_retrieve
description: User metrics per 1h
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this User.
required: true
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/UserMetrics'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/{id}/recovery/: /core/users/{id}/recovery/:
post: post:
operationId: core_users_recovery_create operationId: core_users_recovery_create
@ -60771,29 +60701,6 @@ components:
- username_link - username_link
- username_deny - username_deny
type: string type: string
UserMetrics:
type: object
description: User Metrics
properties:
logins:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
logins_failed:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
authorizations:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
required:
- authorizations
- logins
- logins_failed
UserOAuthSourceConnection: UserOAuthSourceConnection:
type: object type: object
description: User source connection description: User source connection

View File

@ -1,71 +1,58 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart"; import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData, Tick } from "chart.js"; import { ChartData } from "chart.js";
import { msg, str } from "@lit/localize"; import { msg } from "@lit/localize";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { CoreApi, UserMetrics } from "@goauthentik/api"; import { EventActions, EventVolume, EventsApi } from "@goauthentik/api";
@customElement("ak-charts-user") @customElement("ak-charts-user")
export class UserChart extends AKChart<UserMetrics> { export class UserChart extends AKChart<EventVolume[]> {
@property({ type: Number }) @property()
userId?: number; username?: string;
async apiRequest(): Promise<UserMetrics> { async apiRequest(): Promise<EventVolume[]> {
return new CoreApi(DEFAULT_CONFIG).coreUsersMetricsRetrieve({ return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({
id: this.userId || 0, actions: [
EventActions.Login,
EventActions.LoginFailed,
EventActions.AuthorizeApplication,
],
username: this.username,
}); });
} }
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string { getChartData(data: EventVolume[]): ChartData {
const valueStamp = ticks[index]; return this.eventVolume(
const delta = Date.now() - valueStamp.value; data,
const ago = Math.round(delta / 1000 / 3600 / 24); new Map([
return msg(str`${ago} days ago`); [
} EventActions.LoginFailed,
getChartData(data: UserMetrics): ChartData {
return {
datasets: [
{ {
label: msg("Failed Logins"), label: msg("Failed Logins"),
backgroundColor: "rgba(201, 25, 11, .5)", backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true, spanGaps: true,
data:
data.loginsFailed?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
}, },
],
[
EventActions.Login,
{ {
label: msg("Successful Logins"), label: msg("Successful Logins"),
backgroundColor: "rgba(189, 229, 184, .5)", backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true, spanGaps: true,
data:
data.logins?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
}, },
],
[
EventActions.AuthorizeApplication,
{ {
label: msg("Application authorizations"), label: msg("Application authorizations"),
backgroundColor: "rgba(43, 154, 243, .5)", backgroundColor: "rgba(43, 154, 243, .5)",
spanGaps: true, spanGaps: true,
data:
data.authorizations?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
}, },
], ],
}; ]),
);
} }
} }

View File

@ -389,7 +389,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
${msg("Actions over the last week (per 8 hours)")} ${msg("Actions over the last week (per 8 hours)")}
</div> </div>
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-charts-user userId=${this.user.pk || 0}> </ak-charts-user> <ak-charts-user username=${this.user.username}> </ak-charts-user>
</div> </div>
</div> </div>
<div <div