Implement authentication system with access control UI
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { ReactNode, useState, useRef, useEffect } from 'react';
|
||||
import { Link, NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { GlobalSearch } from './GlobalSearch';
|
||||
import './Layout.css';
|
||||
|
||||
@@ -9,6 +10,31 @@ interface LayoutProps {
|
||||
|
||||
function Layout({ children }: LayoutProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { user, loading, logout } = useAuth();
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setShowUserMenu(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await logout();
|
||||
setShowUserMenu(false);
|
||||
navigate('/');
|
||||
} catch {
|
||||
// Error handled in context
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
@@ -60,6 +86,97 @@ function Layout({ children }: LayoutProps) {
|
||||
</svg>
|
||||
Docs
|
||||
</a>
|
||||
|
||||
{/* User Menu */}
|
||||
{loading ? (
|
||||
<div className="user-menu-loading">
|
||||
<div className="user-menu-spinner"></div>
|
||||
</div>
|
||||
) : user ? (
|
||||
<div className="user-menu" ref={menuRef}>
|
||||
<button
|
||||
className="user-menu-trigger"
|
||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||
aria-expanded={showUserMenu}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<div className="user-avatar">
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="user-name">{user.display_name || user.username}</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showUserMenu && (
|
||||
<div className="user-menu-dropdown">
|
||||
<div className="user-menu-header">
|
||||
<span className="user-menu-username">{user.username}</span>
|
||||
{user.is_admin && (
|
||||
<span className="user-menu-badge">Admin</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="user-menu-divider"></div>
|
||||
<NavLink
|
||||
to="/settings/api-keys"
|
||||
className="user-menu-item"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
||||
</svg>
|
||||
API Keys
|
||||
</NavLink>
|
||||
{user.is_admin && (
|
||||
<>
|
||||
<NavLink
|
||||
to="/admin/users"
|
||||
className="user-menu-item"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
User Management
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/admin/oidc"
|
||||
className="user-menu-item"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
SSO Configuration
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
<div className="user-menu-divider"></div>
|
||||
<button className="user-menu-item" onClick={handleLogout}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link to="/login" className="nav-login">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
|
||||
<polyline points="10 17 15 12 10 7"/>
|
||||
<line x1="15" y1="12" x2="3" y2="12"/>
|
||||
</svg>
|
||||
Login
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user