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() {
+