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:
parent
3e7f94b014
commit
c6a92ed33a
|
|
@ -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
|
||||
|
|
|
|||
10
CLAUDE.md
10
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
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ services:
|
|||
build: .
|
||||
container_name: assets-ai
|
||||
ports:
|
||||
- "5177:3000"
|
||||
- "6177:3000"
|
||||
volumes:
|
||||
- assets-data:/app/data
|
||||
- assets-uploads:/app/uploads
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 <AppShell user={{ display_name: user.display_name, role: user.role }}>{children}</AppShell>
|
||||
}
|
||||
}
|
||||
|
||||
// 路径 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 <AppShell user={user}>{children}</AppShell>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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') || ''
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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') || ''
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
|
||||
// 路径 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<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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 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)
|
||||
|
|
|
|||
|
|
@ -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 || ''
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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/')) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue