feat: SSO双路径认证 + 端口修正 5177→6177

- 中间件支持 X-Remote-User (SSO) + JWT 双路径
- lib/auth.ts 新增 getSession() 统一会话获取
- 所有 API 路由改用 getSession(),支持 SSO header 回退
- 退出登录同时清除 SSO cookie
- 侧边栏根据角色显示/隐藏系统设置
- layout.tsx 支持 SSO 用户自动创建
- package.json 端口 5177→6177
- 跨站点引用 localhost 端口全部修正
This commit is contained in:
gitadmin 2026-05-09 17:14:36 +08:00
parent 3e7f94b014
commit c6a92ed33a
27 changed files with 176 additions and 119 deletions

View File

@ -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

View File

@ -13,7 +13,7 @@ assets-ai 是基于 Next.js + SQLite 的 IT 设备资产管理系统CMDB
| 站点域名 | `assets.tlyq.ai` |
| 服务器 | txjpIP: 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
```
---

View File

@ -3,7 +3,7 @@ services:
build: .
container_name: assets-ai
ports:
- "5177:3000"
- "6177:3000"
volumes:
- assets-data:/app/data
- assets-uploads:/app/uploads

View File

@ -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",

View File

@ -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)}` : '')
// 路径 1SSO来自 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 <AppShell user={{ display_name: user.display_name, role: user.role }}>{children}</AppShell>
}
}
// 路径 2JWT 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 <AppShell user={user}>{children}</AppShell>
}

View File

@ -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()

View File

@ -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()

View File

@ -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') || ''

View File

@ -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',

View File

@ -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 })

View File

@ -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)

View File

@ -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()

View File

@ -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') || ''

View File

@ -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()

View File

@ -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 cookiedomain 匹配才会生效)
cookieStore.set('authelia_session', '', { maxAge: 0, path: '/', domain: '127.0.0.1' })
return NextResponse.json({ success: true })
}

View File

@ -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()
// 路径 1SSO来自 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<string, unknown> | 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
}
}
// 路径 2JWT 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<string, unknown> | 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<string, unknown> | 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 })
}
}

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -9,7 +9,7 @@ interface AppShellProps { children: ReactNode; user?: { display_name: string; ro
export default function AppShell({ children, user }: AppShellProps) {
return (
<ThemeProvider>
<Sidebar />
<Sidebar role={user?.role} />
<TopBar user={user} />
<main className="ml-60 pt-14 min-h-screen bg-slate-50 dark:bg-slate-950">
<div className="p-6">{children}</div>

View File

@ -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 (
<aside className="fixed left-0 top-0 bottom-0 w-60 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 flex flex-col z-40">
<div className="h-14 flex items-center px-5 border-b border-slate-200 dark:border-slate-800">
@ -28,6 +29,7 @@ export default function Sidebar() {
const Icon = item.icon
return (<Link key={item.href} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`}><Icon size={18} />{item.label}</Link>)
})}
{isAdmin && (
<div className="pt-3 border-t border-slate-200 dark:border-slate-800 mt-3">
<div className="flex items-center gap-3 px-3 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
<Settings size={14} />
@ -38,6 +40,7 @@ export default function Sidebar() {
return (<Link key={item.href} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`}><Icon size={18} />{item.label}</Link>)
})}
</div>
)}
</nav>
</aside>
)

View File

@ -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 (

View File

@ -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<SessionPayload | null> {
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 headernginx 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)

View File

@ -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 || ''
// ---------------------------------------------------------------------------

View File

@ -13,6 +13,37 @@ function decodeJwtPayload(token: string): Record<string, unknown> | 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/')) {