diff --git a/authentik/admin/api/metrics.py b/authentik/admin/api/metrics.py
index c7529d536..bf64e9a80 100644
--- a/authentik/admin/api/metrics.py
+++ b/authentik/admin/api/metrics.py
@@ -23,6 +23,7 @@ class LoginMetricsSerializer(PassiveSerializer):
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
+ authorizations_per_1h = SerializerMethodField()
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_per_1h(self, _):
@@ -44,6 +45,16 @@ class LoginMetricsSerializer(PassiveSerializer):
.get_events_per_hour()
)
+ @extend_schema_field(CoordinateSerializer(many=True))
+ def get_authorizations_per_1h(self, _):
+ """Get successful authorizations per hour for the last 24 hours"""
+ user = self.context["user"]
+ return (
+ get_objects_for_user(user, "authentik_events.view_event")
+ .filter(action=EventAction.AUTHORIZE_APPLICATION)
+ .get_events_per_hour()
+ )
+
class AdministrationMetricsViewSet(APIView):
"""Login Metrics per 1h"""
diff --git a/schema.yml b/schema.yml
index 2330cc223..26b961c15 100644
--- a/schema.yml
+++ b/schema.yml
@@ -28967,7 +28967,13 @@ components:
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
+ authorizations_per_1h:
+ type: array
+ items:
+ $ref: '#/components/schemas/Coordinate'
+ readOnly: true
required:
+ - authorizations_per_1h
- logins_failed_per_1h
- logins_per_1h
LoginSource:
diff --git a/web/src/admin/admin-overview/AdminOverviewPage.ts b/web/src/admin/admin-overview/AdminOverviewPage.ts
index 0518fa9f8..3ea572030 100644
--- a/web/src/admin/admin-overview/AdminOverviewPage.ts
+++ b/web/src/admin/admin-overview/AdminOverviewPage.ts
@@ -3,6 +3,7 @@ import "@goauthentik/admin/admin-overview/cards/AdminStatusCard";
import "@goauthentik/admin/admin-overview/cards/SystemStatusCard";
import "@goauthentik/admin/admin-overview/cards/VersionStatusCard";
import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
+import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
import "@goauthentik/admin/admin-overview/charts/FlowStatusChart";
import "@goauthentik/admin/admin-overview/charts/GroupCountStatusChart";
import "@goauthentik/admin/admin-overview/charts/LDAPSyncStatusChart";
@@ -13,7 +14,6 @@ import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/cards/AggregatePromiseCard";
-import "@goauthentik/elements/charts/AdminLoginsChart";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
import { t } from "@lingui/macro";
@@ -189,9 +189,9 @@ export class AdminOverviewPage extends AKElement {
>
-
+
{
+ apiRequest(): Promise {
+ return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
+ }
+
+ getChartData(data: LoginMetrics): ChartData {
+ return {
+ datasets: [
+ {
+ label: t`Authorizations`,
+ backgroundColor: new RGBAColor(43, 154, 243, 0.5).toString(),
+ borderColor: new RGBAColor(43, 154, 243, 1).toString(),
+ spanGaps: true,
+ fill: "origin",
+ cubicInterpolationMode: "monotone",
+ tension: 0.4,
+ data: data.authorizationsPer1h.map((cord) => {
+ return {
+ x: cord.xCord,
+ y: cord.yCord,
+ };
+ }),
+ },
+ {
+ label: t`Failed Logins`,
+ backgroundColor: new RGBAColor(201, 24, 11, 0.5).toString(),
+ borderColor: new RGBAColor(201, 24, 11, 1).toString(),
+ spanGaps: true,
+ fill: "origin",
+ cubicInterpolationMode: "monotone",
+ tension: 0.4,
+ data: data.loginsFailedPer1h.map((cord) => {
+ return {
+ x: cord.xCord,
+ y: cord.yCord,
+ };
+ }),
+ },
+ {
+ label: t`Successful Logins`,
+ backgroundColor: new RGBAColor(62, 134, 53, 0.5).toString(),
+ borderColor: new RGBAColor(62, 134, 53, 1).toString(),
+ spanGaps: true,
+ fill: "origin",
+ cubicInterpolationMode: "monotone",
+ tension: 0.4,
+ data: data.loginsPer1h.map((cord) => {
+ return {
+ x: cord.xCord,
+ y: cord.yCord,
+ };
+ }),
+ },
+ ],
+ };
+ }
+}
diff --git a/web/src/elements/charts/AdminLoginsChart.ts b/web/src/elements/charts/AdminLoginsChart.ts
deleted file mode 100644
index f4b3b756c..000000000
--- a/web/src/elements/charts/AdminLoginsChart.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
-import { AKChart } from "@goauthentik/elements/charts/Chart";
-import { ChartData } from "chart.js";
-
-import { t } from "@lingui/macro";
-
-import { customElement } from "lit/decorators.js";
-
-import { AdminApi, LoginMetrics } from "@goauthentik/api";
-
-@customElement("ak-charts-admin-login")
-export class AdminLoginsChart extends AKChart {
- apiRequest(): Promise {
- return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
- }
-
- getChartData(data: LoginMetrics): ChartData {
- return {
- datasets: [
- {
- label: t`Failed Logins`,
- backgroundColor: "rgba(201, 25, 11, .5)",
- spanGaps: true,
- data:
- data.loginsFailedPer1h?.map((cord) => {
- return {
- x: cord.xCord || 0,
- y: cord.yCord || 0,
- };
- }) || [],
- },
- {
- label: t`Successful Logins`,
- backgroundColor: "rgba(189, 229, 184, .5)",
- spanGaps: true,
- data:
- data.loginsPer1h?.map((cord) => {
- return {
- x: cord.xCord || 0,
- y: cord.yCord || 0,
- };
- }) || [],
- },
- ],
- };
- }
-}
diff --git a/web/src/elements/charts/Chart.ts b/web/src/elements/charts/Chart.ts
index 95443ea88..c0eaabc4b 100644
--- a/web/src/elements/charts/Chart.ts
+++ b/web/src/elements/charts/Chart.ts
@@ -1,6 +1,16 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
-import { Chart, ChartConfiguration, ChartData, ChartOptions, Plugin, Tick } from "chart.js";
+import {
+ Chart,
+ ChartConfiguration,
+ ChartData,
+ ChartOptions,
+ Filler,
+ LineElement,
+ Plugin,
+ PointElement,
+ Tick,
+} from "chart.js";
import { Legend, Tooltip } from "chart.js";
import { BarController, DoughnutController, LineController } from "chart.js";
import { ArcElement, BarElement } from "chart.js";
@@ -14,12 +24,33 @@ import { property } from "lit/decorators.js";
Chart.register(Legend, Tooltip);
Chart.register(LineController, BarController, DoughnutController);
-Chart.register(ArcElement, BarElement);
-Chart.register(TimeScale, LinearScale);
+Chart.register(ArcElement, BarElement, PointElement, LineElement);
+Chart.register(TimeScale, LinearScale, Filler);
export const FONT_COLOUR_DARK_MODE = "#fafafa";
export const FONT_COLOUR_LIGHT_MODE = "#151515";
+export class RGBAColor {
+ constructor(public r: number, public g: number, public b: number, public a: number = 1) {}
+ toString(): string {
+ return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
+ }
+}
+
+export function getColorFromString(stringInput: string): RGBAColor {
+ let hash = 0;
+ for (let i = 0; i < stringInput.length; i++) {
+ hash = stringInput.charCodeAt(i) + ((hash << 5) - hash);
+ hash = hash & hash;
+ }
+ const rgb = [0, 0, 0];
+ for (let i = 0; i < 3; i++) {
+ const value = (hash >> (i * 8)) & 255;
+ rgb[i] = value;
+ }
+ return new RGBAColor(rgb[0], rgb[1], rgb[2]);
+}
+
export abstract class AKChart extends AKElement {
abstract apiRequest(): Promise;
abstract getChartData(data: T): ChartData;