feat: 集成 Keycloak 认证 — 前后端 + K8s 配置

This commit is contained in:
2026-06-15 17:30:57 +08:00
parent eeea55e9e4
commit 23f07dd3a7
13 changed files with 268 additions and 16 deletions
+12 -2
View File
@@ -32,11 +32,21 @@ async function request<T>(
if (parts.length) url += '?' + parts.join('&');
}
const init: RequestInit = { method };
// Build headers with optional Bearer token
const headers: Record<string, string> = {};
try {
const { getToken } = await import('./auth');
const token = getToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
} catch { /* auth not initialized */ }
if (!options.formData && options.body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const init: RequestInit = { method, headers };
if (options.formData) {
init.body = options.formData;
} else if (options.body !== undefined) {
init.headers = { 'Content-Type': 'application/json' };
init.body = JSON.stringify(options.body);
}
+59
View File
@@ -0,0 +1,59 @@
import Keycloak from 'keycloak-js';
const keycloak = new Keycloak({
url: 'https://keycloak.plfai.cn',
realm: 'plfai',
clientId: 'graphrag-frontend',
});
let _initialized = false;
export async function initKeycloak(): Promise<boolean> {
if (_initialized) return keycloak.authenticated ?? false;
try {
const authenticated = await keycloak.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
});
_initialized = true;
return authenticated;
} catch {
_initialized = true;
return false;
}
}
export function login(): void {
keycloak.login({ redirectUri: window.location.href });
}
export function logout(): void {
keycloak.logout({ redirectUri: window.location.origin });
}
export function getToken(): string | undefined {
return keycloak.token;
}
export function isAuthenticated(): boolean {
return keycloak.authenticated ?? false;
}
export function getUser(): {
username: string;
email: string;
name: string;
} | null {
if (!keycloak.tokenParsed) return null;
const p = keycloak.tokenParsed as Record<string, string>;
return {
username: p.preferred_username ?? '',
email: p.email ?? '',
name: p.name ?? '',
};
}
export default keycloak;
+28 -3
View File
@@ -1,13 +1,15 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { Menu, Search, X } from 'lucide-react';
import { Menu, Search, X, LogIn, LogOut, User } from 'lucide-react';
import { useAppState, type KGNode } from '../../store';
import { api } from '../../api';
import { TYPE_COLORS } from '../../mock-data';
import { isAuthenticated, getUser, login, logout } from '../../auth';
export function Header() {
const { sidebarCollapsed, setSidebarCollapsed, health } = useAppState();
const [query, setQuery] = useState('');
const [authed, setAuthed] = useState(isAuthenticated());
const [showSuggestions, setShowSuggestions] = useState(false);
const [suggestions, setSuggestions] = useState<KGNode[]>([]);
const navigate = useNavigate();
@@ -132,8 +134,31 @@ export function Header() {
)}
</form>
{/* Right */}
<div className="flex items-center gap-2" style={{ whiteSpace: 'nowrap' }}>
{/* Right — Auth + Health */}
<div className="flex items-center gap-3" style={{ whiteSpace: 'nowrap' }}>
{authed ? (
<>
<User size={14} style={{ color: 'var(--text-3)' }} />
<span style={{ color: 'var(--text-2)', fontSize: 12 }}>
{getUser()?.username ?? 'User'}
</span>
<button
onClick={() => { logout(); setAuthed(false); }}
className="flex items-center gap-1 px-2 py-1 rounded cursor-pointer"
style={{ background: 'var(--bg-s2)', border: '1px solid var(--border-main)', color: 'var(--text-3)', fontSize: 11 }}
>
<LogOut size={12} />
</button>
</>
) : (
<button
onClick={() => login()}
className="flex items-center gap-1 px-3 py-1 rounded cursor-pointer font-medium"
style={{ background: 'var(--green-btn)', color: '#fff', fontSize: 12 }}
>
<LogIn size={12} />
</button>
)}
<span
className="inline-block w-2 h-2 rounded-full"
style={{ background: allOk ? 'var(--green)' : 'var(--red)' }}
+9 -5
View File
@@ -1,7 +1,11 @@
import { createRoot } from 'react-dom/client';
import App from './app/App.tsx';
import './styles/index.css';
import { initKeycloak } from './app/auth.ts';
import { createRoot } from "react-dom/client";
import App from "./app/App.tsx";
import "./styles/index.css";
async function bootstrap() {
await initKeycloak();
createRoot(document.getElementById('root')!).render(<App />);
}
createRoot(document.getElementById("root")!).render(<App />);
bootstrap();