fix: #26 auth/ldap 认证对齐 + CLAUDE.md 更新
This commit is contained in:
parent
da542e95e7
commit
6c426757e0
|
|
@ -97,6 +97,8 @@ npm run import # 导入设备数据
|
||||||
| POST | `/api/auth/logout` | 登出(清除两个 cookie) |
|
| POST | `/api/auth/logout` | 登出(清除两个 cookie) |
|
||||||
| GET | `/api/auth/me` | 当前用户信息 |
|
| GET | `/api/auth/me` | 当前用户信息 |
|
||||||
| GET | `/api/internal/roles` | 内部 API:返回角色列表(x-internal-key 鉴权) |
|
| GET | `/api/internal/roles` | 内部 API:返回角色列表(x-internal-key 鉴权) |
|
||||||
|
| GET | `/api/internal/users` | 内部 API:返回用户列表(x-internal-key 鉴权) |
|
||||||
|
| POST | `/api/internal/users` | 内部 API:OA 同步用户角色(x-internal-key 鉴权) |
|
||||||
|
|
||||||
### 资产
|
### 资产
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// GET /api/internal/users — 供 OA 查询 assets 用户列表
|
||||||
|
// POST /api/internal/users — 供 OA 同步用户角色
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import db from '@/lib/db'
|
||||||
|
|
||||||
|
const INTERNAL_KEY = process.env.INTERNAL_API_KEY || 'oa-internal-key-tlyq-2026'
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const key = request.headers.get('x-internal-key')
|
||||||
|
if (key !== INTERNAL_KEY) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
|
||||||
|
const users = db.prepare(
|
||||||
|
'SELECT username, display_name, role FROM users WHERE is_active = 1 ORDER BY username'
|
||||||
|
).all() as { username: string; display_name: string; role: string }[]
|
||||||
|
return NextResponse.json({ users })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const key = request.headers.get('x-internal-key')
|
||||||
|
if (key !== INTERNAL_KEY) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const { username, displayName, role } = body
|
||||||
|
if (!username) {
|
||||||
|
return NextResponse.json({ error: 'username 必填' }, { status: 400 })
|
||||||
|
}
|
||||||
|
const VALID_ROLES = ['admin', 'editor', 'viewer']
|
||||||
|
const safeRole = VALID_ROLES.includes(role) ? role : 'viewer'
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO users (username, password_hash, display_name, role, is_active, created_at, updated_at)
|
||||||
|
VALUES (?, '', ?, ?, 1, datetime('now', '+8 hours'), datetime('now', '+8 hours'))
|
||||||
|
ON CONFLICT(username) DO UPDATE SET role = ?, display_name = ?,
|
||||||
|
updated_at = datetime('now', '+8 hours')`
|
||||||
|
).run(username, displayName || username, safeRole, safeRole, displayName || username)
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ error: '同步失败' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ export function verifySession(token: string): SessionPayload | null { return ver
|
||||||
// 统一获取当前会话:优先 tlyq_session(共享 JWT),回退 session_assets(本地 JWT)
|
// 统一获取当前会话:优先 tlyq_session(共享 JWT),回退 session_assets(本地 JWT)
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import { verifySharedJwt } from '@/lib/jwt'
|
import { verifySharedJwt } from '@/lib/jwt'
|
||||||
import { ldapUserExists, ldapGetUserInfo } from '@/lib/ldap'
|
import { ldapUserExists, ldapGetUserInfo, ldapIsAdmin } from '@/lib/ldap'
|
||||||
|
|
||||||
export async function getSession(): Promise<SessionPayload | null> {
|
export async function getSession(): Promise<SessionPayload | null> {
|
||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
|
|
@ -67,13 +67,14 @@ export async function getSession(): Promise<SessionPayload | null> {
|
||||||
db.prepare("UPDATE users SET last_login_at = datetime('now', '+8 hours'), last_active_at = datetime('now', '+8 hours') WHERE id = ?").run(row.id)
|
db.prepare("UPDATE users SET last_login_at = datetime('now', '+8 hours'), last_active_at = datetime('now', '+8 hours') WHERE id = ?").run(row.id)
|
||||||
return { userId: row.id, username: row.username, role: row.role }
|
return { userId: row.id, username: row.username, role: row.role }
|
||||||
}
|
}
|
||||||
// SSO 免登录:LLDAP 验证通过但本地无记录 → 自动创建(viewer 角色)
|
// SSO 免登录:LLDAP 验证通过但本地无记录 → 自动创建(从 LLDAP 判断角色,oa-ai 同步更新)
|
||||||
const ldapInfo = await ldapGetUserInfo(sharedPayload.username)
|
const ldapInfo = await ldapGetUserInfo(sharedPayload.username)
|
||||||
const displayName = ldapInfo?.displayName || sharedPayload.displayName
|
const displayName = ldapInfo?.displayName || sharedPayload.displayName
|
||||||
const email = ldapInfo?.email ?? null
|
const email = ldapInfo?.email ?? null
|
||||||
|
const role = (await ldapIsAdmin(sharedPayload.username)) ? 'admin' : 'viewer'
|
||||||
db.prepare(
|
db.prepare(
|
||||||
"INSERT OR IGNORE INTO users (username, display_name, email, role, is_active, created_at, updated_at) VALUES (?, ?, ?, 'viewer', 1, datetime('now', '+8 hours'), datetime('now', '+8 hours'))"
|
"INSERT OR IGNORE INTO users (username, display_name, email, role, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, 1, datetime('now', '+8 hours'), datetime('now', '+8 hours'))"
|
||||||
).run(sharedPayload.username, displayName, email)
|
).run(sharedPayload.username, displayName, email, role)
|
||||||
const newRow = db.prepare(
|
const newRow = db.prepare(
|
||||||
'SELECT id, username, role FROM users WHERE username = ? AND is_active = 1'
|
'SELECT id, username, role FROM users WHERE username = ? AND is_active = 1'
|
||||||
).get(sharedPayload.username) as { id: number; username: string; role: string } | undefined
|
).get(sharedPayload.username) as { id: number; username: string; role: string } | undefined
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,22 @@ export async function ldapGetUserInfo(username: string): Promise<{ displayName:
|
||||||
finally { await client.unbind() }
|
finally { await client.unbind() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查 LLDAP 用户是否为 lldap_admin 组成员
|
||||||
|
export async function ldapIsAdmin(username: string): Promise<boolean> {
|
||||||
|
if (username === 'admin') return true
|
||||||
|
const adminDn = process.env.LDAP_ADMIN_DN || 'uid=admin,ou=people,dc=tlyq,dc=ai'
|
||||||
|
const adminPass = getLdapAdminPassword()
|
||||||
|
const client = new Client({ url: LDAP_URL, timeout: 5000 })
|
||||||
|
try {
|
||||||
|
await client.bind(adminDn, adminPass)
|
||||||
|
const { searchEntries } = await client.search(`ou=groups,${LDAP_BASE_DN}`, {
|
||||||
|
scope: 'sub', filter: `(&(cn=lldap_admin)(member=uid=${username},ou=people,${LDAP_BASE_DN}))`, timeLimit: 3,
|
||||||
|
})
|
||||||
|
return searchEntries.length > 0
|
||||||
|
} catch { return false }
|
||||||
|
finally { try { await client.unbind() } catch { /* */ } }
|
||||||
|
}
|
||||||
|
|
||||||
// Q1: 检查 LLDAP 中用户是否存在(用 admin bind 搜索,不在/不可达均返回 true 保证容错)
|
// Q1: 检查 LLDAP 中用户是否存在(用 admin bind 搜索,不在/不可达均返回 true 保证容错)
|
||||||
export async function ldapUserExists(username: string): Promise<boolean> {
|
export async function ldapUserExists(username: string): Promise<boolean> {
|
||||||
const adminDn = process.env.LDAP_ADMIN_DN || 'uid=admin,ou=people,dc=tlyq,dc=ai'
|
const adminDn = process.env.LDAP_ADMIN_DN || 'uid=admin,ou=people,dc=tlyq,dc=ai'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue