fix: #26 auth/ldap 认证对齐 + CLAUDE.md 更新

This commit is contained in:
gitadmin 2026-07-15 16:00:23 +08:00
parent da542e95e7
commit 6c426757e0
4 changed files with 64 additions and 4 deletions

View File

@ -97,6 +97,8 @@ npm run import # 导入设备数据
| POST | `/api/auth/logout` | 登出(清除两个 cookie |
| GET | `/api/auth/me` | 当前用户信息 |
| GET | `/api/internal/roles` | 内部 API返回角色列表x-internal-key 鉴权) |
| GET | `/api/internal/users` | 内部 API返回用户列表x-internal-key 鉴权) |
| POST | `/api/internal/users` | 内部 APIOA 同步用户角色x-internal-key 鉴权) |
### 资产

View File

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

View File

@ -40,7 +40,7 @@ export function verifySession(token: string): SessionPayload | null { return ver
// 统一获取当前会话:优先 tlyq_session共享 JWT回退 session_assets本地 JWT
import { cookies } from 'next/headers'
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> {
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)
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 displayName = ldapInfo?.displayName || sharedPayload.displayName
const email = ldapInfo?.email ?? null
const role = (await ldapIsAdmin(sharedPayload.username)) ? 'admin' : 'viewer'
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'))"
).run(sharedPayload.username, displayName, email)
"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, role)
const newRow = db.prepare(
'SELECT id, username, role FROM users WHERE username = ? AND is_active = 1'
).get(sharedPayload.username) as { id: number; username: string; role: string } | undefined

View File

@ -65,6 +65,22 @@ export async function ldapGetUserInfo(username: string): Promise<{ displayName:
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 保证容错)
export async function ldapUserExists(username: string): Promise<boolean> {
const adminDn = process.env.LDAP_ADMIN_DN || 'uid=admin,ou=people,dc=tlyq,dc=ai'