Enhancement: add authorizationsLast24H field to Authentik widget, opt… (#6989)
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
Zhelyan Radoev
2026-08-16 17:21:51 +03:00
committed by GitHub
parent 198c86fb49
commit 600c9eee78
5 changed files with 57 additions and 25 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ description: Authentik Widget Configuration
Learn more about [Authentik](https://github.com/goauthentik/authentik).
This widget reads the number of active users in the system, as well as logins for the last 24 hours.
This widget reads the number of active users in the system, as well as logins for the last 24 hours, and authorizations when using widget version 2.
You will need to generate an API token for an existing user under `Admin Portal` > `Directory` > `Tokens & App passwords`.
Make sure to set Intent to "API Token".
@@ -15,7 +15,7 @@ The account you made the API token for also needs the following **Assigned globa
- authentik Core -> Can view User (Model: User)
- authentik Events -> Can view Event (Model: Event)
Allowed fields: `["users", "loginsLast24H", "failedLoginsLast24H"]`.
Allowed fields: `["users", "loginsLast24H", "failedLoginsLast24H", "authorizationsLast24H"]` (`authorizationsLast24H` works with v2 only).
| Authentik Version | Homepage Widget Version |
| ----------------- | ----------------------- |
+2 -1
View File
@@ -471,7 +471,8 @@
"authentik": {
"users": "Users",
"loginsLast24H": "Logins (24h)",
"failedLoginsLast24H": "Failed Logins (24h)"
"failedLoginsLast24H": "Failed Logins (24h)",
"authorizationsLast24H": "Auths (24h)"
},
"proxmox": {
"mem": "MEM",
+38 -12
View File
@@ -8,32 +8,40 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const isV2 = widget.version === 2;
const { data: usersData, error: usersError } = useWidgetAPI(widget, "users");
const loginsEndpoint = widget.version === 2 ? "loginv2" : "login";
const loginsEndpoint = isV2 ? "" : "login";
const { data: loginsData, error: loginsError } = useWidgetAPI(widget, loginsEndpoint);
const failedLoginsEndpoint = widget.version === 2 ? "login_failedv2" : "login_failed";
const failedLoginsEndpoint = isV2 ? "" : "login_failed";
const { data: failedLoginsData, error: failedLoginsError } = useWidgetAPI(widget, failedLoginsEndpoint);
if (usersError || loginsError || failedLoginsError) {
const finalError = usersError ?? loginsError ?? failedLoginsError;
const eventsDataEndpoint = isV2 ? "datav2" : "";
const { data: eventsData, error: eventsDataError } = useWidgetAPI(widget, eventsDataEndpoint);
if (usersError || loginsError || failedLoginsError || eventsDataError) {
const finalError = usersError ?? loginsError ?? failedLoginsError ?? eventsDataError;
return <Container service={service} error={finalError} />;
}
if (!usersData || !loginsData || !failedLoginsData) {
const hasNoData = isV2 ? !usersData || !eventsData : !usersData || !loginsData || !failedLoginsData;
if (hasNoData) {
return (
<Container service={service}>
<Block label="authentik.users" />
<Block label="authentik.loginsLast24H" />
<Block label="authentik.failedLoginsLast24H" />
{isV2 && <Block label="authentik.authorizationsLast24H" />}
</Container>
);
}
let loginsLast24H;
let failedLoginsLast24H;
let authorizationsLast24H;
switch (widget.version) {
// v1 is default
default:
@@ -48,13 +56,28 @@ export default function Component({ service }) {
);
break;
case 2:
loginsLast24H =
loginsData.reduce?.(
(total, current) => (current?.count && current?.action === "login" ? total + current.count : total),
0,
) || 0;
failedLoginsLast24H =
failedLoginsData.reduce?.((total, current) => (current?.count ? total + current.count : total), 0) || 0;
const events = Array.isArray(eventsData) ? eventsData : [];
const result = events.reduce(
(acc, current) => {
if (!current?.count || !current?.action) {
return acc;
}
if (current.action === "login") {
acc.logins += current.count;
} else if (current.action === "login_failed") {
acc.failed += current.count;
} else if (current.action === "authorize_application") {
acc.authorizations += current.count;
}
return acc;
},
{ logins: 0, failed: 0, authorizations: 0 },
);
loginsLast24H = result.logins;
failedLoginsLast24H = result.failed;
authorizationsLast24H = result.authorizations;
break;
}
@@ -63,6 +86,9 @@ export default function Component({ service }) {
<Block label="authentik.users" value={t("common.number", { value: usersData.pagination.count })} />
<Block label="authentik.loginsLast24H" value={t("common.number", { value: loginsLast24H })} />
<Block label="authentik.failedLoginsLast24H" value={t("common.number", { value: failedLoginsLast24H })} />
{isV2 && (
<Block label="authentik.authorizationsLast24H" value={t("common.number", { value: authorizationsLast24H })} />
)}
</Container>
);
}
+13 -5
View File
@@ -30,24 +30,31 @@ describe("widgets/authentik/component", () => {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("authentik.users")).toBeInTheDocument();
expect(screen.getByText("authentik.loginsLast24H")).toBeInTheDocument();
expect(screen.getByText("authentik.failedLoginsLast24H")).toBeInTheDocument();
expect(screen.getByText("authentik.authorizationsLast24H")).toBeInTheDocument();
});
it("computes v2 login/failed counts from action data", () => {
useWidgetAPI.mockImplementation((widget, endpoint) => {
if (endpoint === "users") return { data: { pagination: { count: 10 } }, error: undefined };
if (endpoint === "loginv2")
if (endpoint === "datav2")
return {
data: [
{ action: "login", count: 2 },
{ action: "login", count: 4 },
{ action: "logout", count: 9 },
{ action: "login_failed", count: 1 },
{ action: "login_failed", count: 2 },
{ action: "authorize_application", count: 10 },
{ action: "authorize_application", count: 8 },
{ action: null, count: null },
{ action: "login", count: null },
],
error: undefined,
};
if (endpoint === "login_failedv2") return { data: [{ count: 3 }, { count: null }], error: undefined };
return { data: undefined, error: undefined };
});
@@ -55,10 +62,11 @@ describe("widgets/authentik/component", () => {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "authentik.users", 10);
expectBlockValue(container, "authentik.loginsLast24H", 2);
expectBlockValue(container, "authentik.loginsLast24H", 6);
expectBlockValue(container, "authentik.failedLoginsLast24H", 3);
expectBlockValue(container, "authentik.authorizationsLast24H", 18);
});
it("computes v1 login/failed counts for entries within the last 24h window", () => {
+2 -5
View File
@@ -11,14 +11,11 @@ const widget = {
login: {
endpoint: "events/events/per_month/?action=login",
},
loginv2: {
endpoint: "events/events/volume/?action=login&&history_days=1",
},
login_failed: {
endpoint: "events/events/per_month/?action=login_failed",
},
login_failedv2: {
endpoint: "events/events/volume/?action=login_failed&&history_days=1",
datav2: {
endpoint: "events/events/volume/?actions=login&actions=login_failed&actions=authorize_application&history_days=1",
},
},
};