feat(auth): OIDC login UI + binding management + OIDC-only mode

- useAuthConfig fetches public /auth/config; LoginPage hides the
  password form when oidc_only and shows an SSO button when enabled.
- /oidc/callback route applies the returned JWT (sign-in) or shows the
  link result; oidc_error surfaced on LoginPage.
- UsersPage: hides password fields in OIDC-only mode; admin OIDC
  bind/unbind UI per user. Sidebar self-service "Link OIDC account"
  (non-OIDC_ONLY).
- Dockerfile ARG/ENV HARBORFORGE_OIDC_ONLY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-17 20:22:14 +01:00
parent aaf36a4f5c
commit 8f8d6d5465
9 changed files with 312 additions and 34 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import api from '@/services/api'
import { useAuthConfig, oidcLinkHref } from '@/hooks/useAuthConfig'
import type { User } from '@/types'
interface Props {
@@ -11,6 +12,7 @@ interface Props {
export default function Sidebar({ user, onLogout }: Props) {
const { pathname } = useLocation()
const navigate = useNavigate()
const { config: authCfg } = useAuthConfig()
const [unreadCount, setUnreadCount] = useState(0)
useEffect(() => {
@@ -64,6 +66,11 @@ export default function Sidebar({ user, onLogout }: Props) {
<button onClick={() => navigate('/login')}>Log in</button>
)}
</div>
{user && authCfg.oidcEnabled && !authCfg.oidcOnly && (
<div className="sidebar-footer" style={{ borderTop: 'none', paddingTop: 0 }}>
<a href={oidcLinkHref()} title="Link your account to an OIDC identity">🔗 Link OIDC account</a>
</div>
)}
</nav>
)
}