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;