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}
+ />
+ | Application | Client | Status | Expires | >}>
+ {alerts.items.map((c, i) => (
+
+ | {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() {
+