@@ -28,6 +29,7 @@ export default function Sidebar() {
const Icon = item.icon
return (
系统设置
@@ -38,6 +40,7 @@ export default function Sidebar() {
return ({item.label})
})}
+ )}
)
diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx
index 558795b..51f28fd 100644
--- a/src/components/layout/TopBar.tsx
+++ b/src/components/layout/TopBar.tsx
@@ -10,6 +10,7 @@ export default function TopBar({ user }: TopBarProps) {
const { theme, toggleTheme } = useTheme()
async function handleLogout() {
await fetch('/api/auth/logout', { method: 'POST' })
+ // 清除所有 cookies 后跳转登录页,下次请求将触发 SSO 重新认证
router.push('/login'); router.refresh()
}
return (
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 77a7aa4..7da93d4 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -37,6 +37,28 @@ export function hashApiKey(key: string): string { return crypto.createHash('sha2
export function generateApiKey(): string { return `ak_${crypto.randomBytes(32).toString('hex')}` }
export function verifySession(token: string): SessionPayload | null { return verifyJwt(token) }
+// 统一获取当前会话:先查 JWT,再查 SSO header(解决首次加载时 JWT 尚未签发的竞态问题)
+import { cookies, headers } from 'next/headers'
+export async function getSession(): Promise
{
+ const cookieStore = await cookies()
+ // 1. JWT
+ const token = cookieStore.get('session_assets')?.value
+ if (token) {
+ const payload = verifyJwt(token)
+ if (payload) return payload
+ }
+ // 2. SSO header(nginx auth_request 注入,首次请求时 JWT 可能尚未签发)
+ const headersList = await headers()
+ const ssoUsername = headersList.get('x-remote-user')
+ if (ssoUsername) {
+ const user = db.prepare(
+ 'SELECT id, username, role FROM users WHERE username = ? AND is_active = 1'
+ ).get(ssoUsername) as SessionPayload | undefined
+ if (user) return user
+ }
+ return null
+}
+
export function verifyApiKey(key: string): { id: number; name: string; permissions: string[] } | null {
if (!key.startsWith('ak_')) return null
const keyHash = hashApiKey(key)
diff --git a/src/lib/issue-client.ts b/src/lib/issue-client.ts
index b7888bc..4eeeb81 100644
--- a/src/lib/issue-client.ts
+++ b/src/lib/issue-client.ts
@@ -3,11 +3,11 @@
* 封装对 issue-ai 工单系统的 API 调用
*
* 环境变量:
- * ISSUE_API_URL — issue-ai API 地址(默认 http://localhost:5176/api,云上必须设 http://issue-ai:3000/api)
+ * ISSUE_API_URL — issue-ai API 地址(默认 http://localhost:6176/api,云上必须设 http://issue-ai:3000/api)
* ISSUE_API_KEY — API 密钥(可选,如不设置则需要 Cookie 认证)
*/
-const API_BASE = process.env.ISSUE_API_URL || 'http://localhost:5176/api'
+const API_BASE = process.env.ISSUE_API_URL || 'http://localhost:6176/api'
const API_KEY = process.env.ISSUE_API_KEY || ''
// ---------------------------------------------------------------------------
diff --git a/src/middleware.ts b/src/middleware.ts
index 28b281f..109dc98 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -13,6 +13,37 @@ function decodeJwtPayload(token: string): Record | null {
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
+
+ // 清除外部注入的 trust proxy headers(防伪造)
+ const requestHeaders = new Headers(request.headers)
+ requestHeaders.delete('x-remote-user')
+ requestHeaders.delete('x-remote-groups')
+
+ // SSO 代理认证路径:检测 X-Remote-User header + 代理密钥验证
+ const remoteUser = request.headers.get('x-remote-user')
+ const proxyKey = request.headers.get('x-auth-proxy-key')
+ const isFromNginx = proxyKey === 'internal-auth-key-tlyq-2026'
+
+ if (remoteUser && isFromNginx) {
+ // logout 路径不设置 SSO session(避免清除后又重新设置)
+ if (pathname === '/api/auth/logout') return NextResponse.next()
+
+ const response = pathname === '/login' || pathname.startsWith('/api/auth/login')
+ ? NextResponse.redirect(new URL('/', request.url))
+ : NextResponse.next()
+
+ response.cookies.set('session', JSON.stringify({ username: remoteUser }), {
+ httpOnly: true,
+ sameSite: 'lax',
+ path: '/',
+ })
+ if (pathname.startsWith('/api/')) {
+ response.headers.set('x-original-pathname', pathname + (request.nextUrl.search || ''))
+ }
+ return response
+ }
+
+ // 回退:现有 JWT 认证路径
if (pathname === '/login' || pathname.startsWith('/api/auth/login')) return NextResponse.next()
if (pathname.startsWith('/api/')) {