From a169883d83f15c6f513fea2b3ebdb9c65c9e6fc9 Mon Sep 17 00:00:00 2001 From: "lorenzo.padovani@padosoft.com" Date: Fri, 3 Jul 2026 22:09:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(console):=20rotation=20alerts=20=E2=80=94?= =?UTF-8?q?=20global=20banner=20+=20dashboard=20widget=20(v1.11.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumes metrics/clients (v1.11.0). A global banner (in the app shell) warns "N app secrets need rotation" whenever any confidential client secret is expiring/expired, linking to Applications. The Dashboard gains a "Client secrets to rotate" widget listing the most-urgent clients (app, client_id, status, expiry). Both hide themselves when there is nothing to act on, and fail-soft if the operator lacks iam:metrics.read. Green: SPA build (tsc) + oxlint, pint, Playwright golden-path. Co-Authored-By: Claude Opus 4.8 (1M context) --- composer.lock | 12 ++++---- resources/console/src/components/Layout.tsx | 17 +++++++++++ .../console/src/hooks/useRotationAlerts.ts | 29 +++++++++++++++++++ resources/console/src/pages/Dashboard.tsx | 29 +++++++++++++++++++ 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 resources/console/src/hooks/useRotationAlerts.ts diff --git a/composer.lock b/composer.lock index bcd72dc..f2af5cb 100644 --- a/composer.lock +++ b/composer.lock @@ -3276,16 +3276,16 @@ }, { "name": "padosoft/laravel-iam-server", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/padosoft/laravel-iam-server.git", - "reference": "25dd0027fd54cc5d90399c4ca64fb08bfa858cdd" + "reference": "df218c84fe615b1400cdfcc98d3aa116d31a16e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/padosoft/laravel-iam-server/zipball/25dd0027fd54cc5d90399c4ca64fb08bfa858cdd", - "reference": "25dd0027fd54cc5d90399c4ca64fb08bfa858cdd", + "url": "https://api.github.com/repos/padosoft/laravel-iam-server/zipball/df218c84fe615b1400cdfcc98d3aa116d31a16e8", + "reference": "df218c84fe615b1400cdfcc98d3aa116d31a16e8", "shasum": "" }, "require": { @@ -3336,9 +3336,9 @@ "description": "Server Laravel IAM: identity, organizations, Application Registry + manifest, PDP (RBAC+ABAC+ReBAC), OAuth (league/oauth2-server) + OIDC layer, audit tamper-evident, governance/IGA, Admin API + panel.", "support": { "issues": "https://github.com/padosoft/laravel-iam-server/issues", - "source": "https://github.com/padosoft/laravel-iam-server/tree/v1.10.0" + "source": "https://github.com/padosoft/laravel-iam-server/tree/v1.11.0" }, - "time": "2026-07-03T19:23:39+00:00" + "time": "2026-07-03T19:57:05+00:00" }, { "name": "paragonie/constant_time_encoding", diff --git a/resources/console/src/components/Layout.tsx b/resources/console/src/components/Layout.tsx index 121dcc3..9bd9b26 100644 --- a/resources/console/src/components/Layout.tsx +++ b/resources/console/src/components/Layout.tsx @@ -5,6 +5,7 @@ import { NavLink, Outlet } from 'react-router-dom' import { apiPost, errorMessage } from '../lib/api' import { cx, initials } from '../lib/format' import { useCurrentUser } from '../hooks/useCurrentUser' +import { useRotationAlerts } from '../hooks/useRotationAlerts' import { Button } from './ui' import { useToast } from './toast-context' @@ -60,6 +61,21 @@ function NavItemLink({ item, onNavigate }: { item: NavItem; onNavigate?: () => v ) } +// Global alert: OAuth client secrets that are expiring/expired and need rotation (from GET metrics/clients). +function RotationBanner() { + const alerts = useRotationAlerts() + if (!alerts || alerts.needs_rotation <= 0) { + return null + } + const n = alerts.needs_rotation + return ( +
+ ⚠ {n} app secret{n > 1 ? 's' : ''} need rotation{alerts.expired > 0 ? ` — ${alerts.expired} already expired` : ''}. + Review applications → +
+ ) +} + export default function Layout() { const user = useCurrentUser() const toast = useToast() @@ -133,6 +149,7 @@ export default function Layout() {
+
diff --git a/resources/console/src/hooks/useRotationAlerts.ts b/resources/console/src/hooks/useRotationAlerts.ts new file mode 100644 index 0000000..da9468e --- /dev/null +++ b/resources/console/src/hooks/useRotationAlerts.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react' +import { apiGet } from '../lib/api' + +export interface RotationAlerts { + expired: number + expiring: number + in_grace: number + needs_rotation: number + items: Array> +} + +/** + * Fetches the client-secret rotation alerts (GET metrics/clients) — how many OAuth client secrets are + * expired / expiring / in a rotation grace, plus the most-urgent items. Best-effort: a failure (e.g. the + * operator lacks iam:metrics.read) resolves to null and the banner/widget simply hide. + */ +export function useRotationAlerts(): RotationAlerts | null { + const [data, setData] = useState(null) + useEffect(() => { + let alive = true + apiGet('metrics/clients') + .then((d) => alive && setData(d)) + .catch(() => {}) + return () => { + alive = false + } + }, []) + return data +} diff --git a/resources/console/src/pages/Dashboard.tsx b/resources/console/src/pages/Dashboard.tsx index 29434c2..6be1704 100644 --- a/resources/console/src/pages/Dashboard.tsx +++ b/resources/console/src/pages/Dashboard.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react' import { Link } from 'react-router-dom' import { apiGet } from '../lib/api' import { useResource, useCursorList } from '../hooks/useApi' +import { useRotationAlerts } from '../hooks/useRotationAlerts' import { asText, formatDate, pick } from '../lib/format' import PageHeader from '../components/PageHeader' import { Card, CardHeader, EmptyState, ErrorState, Loading, Table, Td, Th, Badge } from '../components/ui' @@ -62,6 +63,33 @@ function StatGrid({ title, to, metrics, loading, error, note }: { title: string; ) } +// Client secrets needing rotation (expired/expiring) — hidden when there's nothing to act on. +function ClientSecretsCard() { + const alerts = useRotationAlerts() + if (!alerts || alerts.needs_rotation <= 0) { + return null + } + return ( + + Applications} + /> + }> + {alerts.items.map((c, i) => ( + + + + + + + ))} +
ApplicationClientStatusExpires
{asText(pick(c, ['application_key']))}{asText(pick(c, ['client_id']))}{asText(pick(c, ['status']))}{formatDate(pick(c, ['secret_expires_at']))}
+
+ ) +} + export default function Dashboard() { const users = useResource(() => apiGet('metrics/users'), []) const decisions = useResource(() => apiGet('metrics/decisions'), []) @@ -74,6 +102,7 @@ export default function Dashboard() {
+