feat: 集成 Keycloak 认证 — 前后端 + K8s 配置
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
"date-fns": "3.6.0",
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"input-otp": "1.4.2",
|
||||
"keycloak-js": "^26.2.4",
|
||||
"lucide-react": "0.487.0",
|
||||
"marked": "^17.0.4",
|
||||
"motion": "12.23.24",
|
||||
|
||||
Generated
+8
@@ -128,6 +128,9 @@ importers:
|
||||
input-otp:
|
||||
specifier: 1.4.2
|
||||
version: 1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
keycloak-js:
|
||||
specifier: ^26.2.4
|
||||
version: 26.2.4
|
||||
lucide-react:
|
||||
specifier: 0.487.0
|
||||
version: 0.487.0(react@19.2.7)
|
||||
@@ -2004,6 +2007,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
keycloak-js@26.2.4:
|
||||
resolution: {integrity: sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw==}
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -4233,6 +4239,8 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
keycloak-js@26.2.4: {}
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+12
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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)' }}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user