diff --git a/.env.example b/.env.example index 89c778e..057b9ba 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,6 @@ NODE_ENV=development # issue-ai API 配置(用于故障历史功能) # NEXT_PUBLIC_ 前缀:构建时内嵌到客户端 JS,云上必须通过 deploy-ai.sh 设置 -# 本地开发:http://localhost:5176/tickets +# 本地开发:http://localhost:6176/tickets # 云上生产:https://issue.tlyq.ai/tickets -NEXT_PUBLIC_ISSUE_URL=http://localhost:5176/tickets +NEXT_PUBLIC_ISSUE_URL=http://localhost:6176/tickets diff --git a/CLAUDE.md b/CLAUDE.md index 92b227a..583f800 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ assets-ai 是基于 Next.js + SQLite 的 IT 设备资产管理系统(CMDB) | 站点域名 | `assets.tlyq.ai` | | 服务器 | txjp(IP: 43.133.38.210) | | 代码路径 | `/root/docker/assets-ai/` | -| 本地端口 | 5177 | +| 本地端口 | 6177 | | 容器名 | `assets-ai` | | 数据库 | SQLite:`data/assets.db` | | 默认账号 | `admin` / `admin123` | @@ -140,9 +140,9 @@ npm run import # 导入设备数据 | 环境变量 | 本地开发 | 云服务器(txjp) | 说明 | |---------|---------|----------------|------| -| `ISSUE_API_URL` | `http://localhost:5176/api` | `http://issue-ai:3000/api` | 调用 issue API 地址 | +| `ISSUE_API_URL` | `http://localhost:6176/api` | `http://issue-ai:3000/api` | 调用 issue API 地址 | | `ISSUE_API_KEY` | 本地 issue-ai 生成 | 云上 issue-ai 生成 | **每个环境独立,不可跨环境使用** | -| `NEXT_PUBLIC_ISSUE_URL` | `http://localhost:5176/tickets` | `https://issue.tlyq.ai` | 前端跳转链接(构建时内嵌) | +| `NEXT_PUBLIC_ISSUE_URL` | `http://localhost:6176/tickets` | `https://issue.tlyq.ai` | 前端跳转链接(构建时内嵌) | | `ALLOWED_API_KEYS` | issue-ai 调用本系统时需要的 Key | 云上 issue-ai 生成的 Key | 仅 issue→assets 方向需要 | | `JWT_SECRET` | `dev-secret-key-local` | `${ASSETS_JWT_SECRET}` | 生产必须强密钥 | | `DATABASE_PATH` | `./data/assets.db` | `/app/data/assets.db` | Docker volume 挂载 | @@ -154,9 +154,9 @@ npm run import # 导入设备数据 DATABASE_PATH=./data/assets.db JWT_SECRET=dev-secret-key-local NODE_ENV=development -ISSUE_API_URL=http://localhost:5176/api +ISSUE_API_URL=http://localhost:6176/api ISSUE_API_KEY=ak_<32字节十六进制> -NEXT_PUBLIC_ISSUE_URL=http://localhost:5176/tickets +NEXT_PUBLIC_ISSUE_URL=http://localhost:6176/tickets ``` --- diff --git a/docker-compose.yml b/docker-compose.yml index b75d71c..9ec6009 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: build: . container_name: assets-ai ports: - - "5177:3000" + - "6177:3000" volumes: - assets-data:/app/data - assets-uploads:/app/uploads diff --git a/package.json b/package.json index e1dfff5..06b873c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "NODE_OPTIONS='--max-old-space-size=2048' next dev --port 5177", + "dev": "NODE_OPTIONS='--max-old-space-size=2048' next dev --port 6177", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 748142e..9b43ac3 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -12,11 +12,34 @@ export default async function AppLayout({ children }: { children: React.ReactNod const originalPath = headersList.get('x-original-pathname') || '' const loginUrl = '/login' + (originalPath ? `?redirect=${encodeURIComponent(originalPath)}` : '') + // 路径 1:SSO(来自 nginx auth_request 代理认证) + let ssoUsername = '' + const ssoSession = cookieStore.get('session')?.value + if (ssoSession) { + try { ssoUsername = JSON.parse(ssoSession).username || '' } catch { } + } + if (!ssoUsername) ssoUsername = headersList.get('x-remote-user') || '' + + if (ssoUsername) { + db.prepare( + "INSERT OR IGNORE INTO users (username, password_hash, display_name, role, is_active) VALUES (?, '__SSO__', ?, 'viewer', 1)" + ).run(ssoUsername, ssoUsername) + const user = db.prepare( + 'SELECT id, username, display_name, role FROM users WHERE username = ? AND is_active = 1' + ).get(ssoUsername) as { id: number; username: string; display_name: string; role: string } | undefined + if (user) { + return {children} + } + } + + // 路径 2:JWT cookie(本地开发 / @fallback 紧急绕过) const token = cookieStore.get('session_assets')?.value if (!token) redirect(loginUrl) const payload = verifyJwt(token) if (!payload) redirect(loginUrl) - const user = db.prepare('SELECT display_name, role FROM users WHERE id = ? AND is_active = 1').get(payload.userId) as { display_name: string; role: string } | undefined + const user = db.prepare( + 'SELECT display_name, role FROM users WHERE id = ? AND is_active = 1' + ).get(payload.userId) as { display_name: string; role: string } | undefined if (!user) redirect(loginUrl) return {children} } diff --git a/src/app/api/api-keys/[id]/route.ts b/src/app/api/api-keys/[id]/route.ts index 67e4a14..0297a3e 100644 --- a/src/app/api/api-keys/[id]/route.ts +++ b/src/app/api/api-keys/[id]/route.ts @@ -1,15 +1,10 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { const session = await getSession() diff --git a/src/app/api/api-keys/route.ts b/src/app/api/api-keys/route.ts index 7e24e3d..7690b5c 100644 --- a/src/app/api/api-keys/route.ts +++ b/src/app/api/api-keys/route.ts @@ -1,15 +1,9 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt, generateApiKey, hashApiKey } from '@/lib/auth' +import { getSession, generateApiKey, hashApiKey } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + export async function GET() { const session = await getSession() diff --git a/src/app/api/assets/[id]/route.ts b/src/app/api/assets/[id]/route.ts index 783c02f..fa0afb4 100644 --- a/src/app/api/assets/[id]/route.ts +++ b/src/app/api/assets/[id]/route.ts @@ -1,15 +1,9 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt, verifyApiKey } from '@/lib/auth' +import { getSession, verifyApiKey } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + function getApiKeyAuth(request: Request) { const auth = request.headers.get('Authorization') || '' diff --git a/src/app/api/assets/batch/route.ts b/src/app/api/assets/batch/route.ts index 583ee0c..a439b41 100644 --- a/src/app/api/assets/batch/route.ts +++ b/src/app/api/assets/batch/route.ts @@ -1,15 +1,10 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + const UPDATABLE_FIELDS = [ 'device_type', 'device_purpose', 'room', 'rack_position', 'status', diff --git a/src/app/api/assets/export/route.ts b/src/app/api/assets/export/route.ts index eeaecec..8144ec5 100644 --- a/src/app/api/assets/export/route.ts +++ b/src/app/api/assets/export/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' import { exportAssetsToBuffer } from '@/lib/excel' @@ -30,7 +30,7 @@ export async function GET(request: Request) { const cookieStore = await cookies() const token = cookieStore.get('session_assets')?.value if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) - const payload = verifyJwt(token) + const payload = await getSession() if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) if (!payload.role) return NextResponse.json({ error: '会话数据异常,请重新登录' }, { status: 401 }) diff --git a/src/app/api/assets/field-values/route.ts b/src/app/api/assets/field-values/route.ts index 43b74d7..0548a11 100644 --- a/src/app/api/assets/field-values/route.ts +++ b/src/app/api/assets/field-values/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' const ALLOWED_FIELDS = new Set([ 'device_type', 'device_purpose', 'room', 'rack_position', 'node_name', @@ -24,7 +24,7 @@ export async function GET(request: Request) { const cookieStore = await cookies() const token = cookieStore.get('session_assets')?.value if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) - const payload = verifyJwt(token) + const payload = await getSession() if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) const { searchParams } = new URL(request.url) diff --git a/src/app/api/assets/import/route.ts b/src/app/api/assets/import/route.ts index d513d92..f74e35c 100644 --- a/src/app/api/assets/import/route.ts +++ b/src/app/api/assets/import/route.ts @@ -1,16 +1,11 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' import { parseImportBuffer } from '@/lib/excel' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + export async function POST(request: Request) { const session = await getSession() diff --git a/src/app/api/assets/route.ts b/src/app/api/assets/route.ts index bd2fc8f..1c7d9b5 100644 --- a/src/app/api/assets/route.ts +++ b/src/app/api/assets/route.ts @@ -1,15 +1,9 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt, verifyApiKey } from '@/lib/auth' +import { getSession, verifyApiKey } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + function getApiKeyAuth(request: Request) { const auth = request.headers.get('Authorization') || '' diff --git a/src/app/api/assets/template/route.ts b/src/app/api/assets/template/route.ts index 6ba0e24..02eb7b0 100644 --- a/src/app/api/assets/template/route.ts +++ b/src/app/api/assets/template/route.ts @@ -1,13 +1,13 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' import { generateTemplateBuffer } from '@/lib/excel' export async function GET() { const cookieStore = await cookies() const token = cookieStore.get('session_assets')?.value if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) - const payload = verifyJwt(token) + const payload = await getSession() if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) const buffer = generateTemplateBuffer() diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 8ff59fc..946b06b 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,7 +1,12 @@ import { NextResponse } from 'next/server' import { cookies } from 'next/headers' + export async function POST() { const cookieStore = await cookies() cookieStore.set('session_assets', '', { maxAge: 0, path: '/' }) + cookieStore.set('session', '', { maxAge: 0, path: '/' }) + // 清除 Authelia SSO cookie(domain 匹配才会生效) + cookieStore.set('authelia_session', '', { maxAge: 0, path: '/', domain: '127.0.0.1' }) + return NextResponse.json({ success: true }) } diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index 1f42ce1..72cd477 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -1,19 +1,58 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' +import { cookies, headers } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession, verifyJwt, signJwt } from '@/lib/auth' export async function GET() { try { const cookieStore = await cookies() + + // 路径 1:SSO(来自 nginx auth_request 代理) + // 优先读 cookie,若没有则直接读 header(首次请求时 cookie 可能尚未写入) + let ssoUsername = '' + try { + const ssoSession = cookieStore.get('session')?.value + if (ssoSession) ssoUsername = JSON.parse(ssoSession).username || '' + } catch { } + if (!ssoUsername) { + const headersList = await headers() + ssoUsername = headersList.get('x-remote-user') || '' + } + + if (ssoUsername) { + db.prepare( + "INSERT OR IGNORE INTO users (username, password_hash, display_name, role, is_active) VALUES (?, '__SSO__', ?, ?, 1)" + ).run(ssoUsername, ssoUsername, 'viewer') + const user = db.prepare( + 'SELECT id, username, display_name, email, role FROM users WHERE username = ? AND is_active = 1' + ).get(ssoUsername) as Record | undefined + if (user) { + const roleRow = db.prepare( + 'SELECT permissions FROM roles WHERE name = ?' + ).get(user.role) as { permissions: string } | undefined + const permissions: string[] = roleRow ? JSON.parse(roleRow.permissions) : [] + const jwt = signJwt({ userId: user.id as number, username: user.username as string, role: user.role as string }) + const response = NextResponse.json({ user: { ...user, permissions } }) + response.cookies.set('session_assets', jwt, { httpOnly: true, sameSite: 'lax', path: '/', maxAge: 86400 }) + return response + } + } + + // 路径 2:JWT cookie(本地开发 / @fallback 紧急绕过) const token = cookieStore.get('session_assets')?.value if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) const payload = verifyJwt(token) if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) - const user = db.prepare('SELECT id, username, display_name, email, role FROM users WHERE id = ? AND is_active = 1').get(payload.userId) as Record | undefined + const user = db.prepare( + 'SELECT id, username, display_name, email, role FROM users WHERE id = ? AND is_active = 1' + ).get(payload.userId) as Record | undefined if (!user) return NextResponse.json({ error: '用户不存在' }, { status: 401 }) - const roleRow = db.prepare('SELECT permissions FROM roles WHERE name = ?').get(user.role) as { permissions: string } | undefined + const roleRow = db.prepare( + 'SELECT permissions FROM roles WHERE name = ?' + ).get(user.role) as { permissions: string } | undefined const permissions: string[] = roleRow ? JSON.parse(roleRow.permissions) : [] return NextResponse.json({ user: { ...user, permissions } }) - } catch { return NextResponse.json({ error: '获取用户信息失败' }, { status: 500 }) } + } catch { + return NextResponse.json({ error: '获取用户信息失败' }, { status: 500 }) + } } diff --git a/src/app/api/roles/[id]/route.ts b/src/app/api/roles/[id]/route.ts index f406fea..77f9e21 100644 --- a/src/app/api/roles/[id]/route.ts +++ b/src/app/api/roles/[id]/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' import { initDatabase } from '@/lib/db-schema' @@ -12,11 +11,8 @@ export async function PUT( { params }: { params: Promise<{ id: string }> } ) { initDatabase() - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) - const session = verifyJwt(token) - if (!session) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) + const session = await getSession() + if (!session) return NextResponse.json({ error: '未登录' }, { status: 401 }) if (!checkPermission(session.role, 'roles:write')) return NextResponse.json({ error: '权限不足' }, { status: 403 }) const { id } = await params @@ -45,11 +41,8 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { initDatabase() - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) - const session = verifyJwt(token) - if (!session) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) + const session = await getSession() + if (!session) return NextResponse.json({ error: '未登录' }, { status: 401 }) if (!checkPermission(session.role, 'roles:write')) return NextResponse.json({ error: '权限不足' }, { status: 403 }) const { id } = await params diff --git a/src/app/api/roles/route.ts b/src/app/api/roles/route.ts index 22a11ea..4566703 100644 --- a/src/app/api/roles/route.ts +++ b/src/app/api/roles/route.ts @@ -1,17 +1,13 @@ import { NextRequest, NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' +import { getSession } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' import { initDatabase } from '@/lib/db-schema' export async function GET() { initDatabase() - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) - const session = verifyJwt(token) - if (!session) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) + const session = await getSession() + if (!session) return NextResponse.json({ error: '未登录' }, { status: 401 }) const roles = db.prepare('SELECT * FROM roles ORDER BY id').all() return NextResponse.json({ roles }) @@ -19,11 +15,8 @@ export async function GET() { export async function POST(request: NextRequest) { initDatabase() - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return NextResponse.json({ error: '未授权' }, { status: 401 }) - const session = verifyJwt(token) - if (!session) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) + const session = await getSession() + if (!session) return NextResponse.json({ error: '未登录' }, { status: 401 }) if (!checkPermission(session.role, 'roles:write')) return NextResponse.json({ error: '权限不足' }, { status: 403 }) const body = await request.json() diff --git a/src/app/api/stats/route.ts b/src/app/api/stats/route.ts index 794bac8..fa52ab9 100644 --- a/src/app/api/stats/route.ts +++ b/src/app/api/stats/route.ts @@ -1,14 +1,6 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt } from '@/lib/auth' - -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} +import { getSession } from '@/lib/auth' export async function GET() { const session = await getSession() diff --git a/src/app/api/users/[id]/route.ts b/src/app/api/users/[id]/route.ts index 233335f..b339aa9 100644 --- a/src/app/api/users/[id]/route.ts +++ b/src/app/api/users/[id]/route.ts @@ -1,15 +1,9 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt, hashPassword } from '@/lib/auth' +import { getSession, hashPassword } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) { const session = await getSession() diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 87562ef..18b923c 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -1,15 +1,9 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' import db from '@/lib/db' -import { verifyJwt, hashPassword } from '@/lib/auth' +import { getSession, hashPassword } from '@/lib/auth' import { checkPermission } from '@/lib/permissions' -async function getSession() { - const cookieStore = await cookies() - const token = cookieStore.get('session_assets')?.value - if (!token) return null - return verifyJwt(token) -} + export async function GET() { const session = await getSession() diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 4eadb5c..5cef9a6 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -9,7 +9,7 @@ interface AppShellProps { children: ReactNode; user?: { display_name: string; ro export default function AppShell({ children, user }: AppShellProps) { return ( - +
{children}
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index e88867b..86ee2bf 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -15,8 +15,9 @@ const settingsItems = [ { href: '/settings/api-keys', label: 'API Key', icon: Key }, ] -export default function Sidebar() { +export default function Sidebar({ role }: { role?: string }) { const pathname = usePathname() + const isAdmin = role === 'admin' return ( ) 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/')) {