102 lines
3.7 KiB
TypeScript
102 lines
3.7 KiB
TypeScript
import { Client, InvalidCredentialsError } from 'ldapts'
|
||
|
||
const LDAP_URL = process.env.LDAP_URL || 'ldap://localhost:3890'
|
||
const LDAP_BASE_DN = process.env.LDAP_BASE_DN || 'dc=tlyq,dc=ai'
|
||
|
||
// 从环境变量获取 LLDAP admin 密码(容器内无法执行 docker exec,见 LESSONS-LEARNED #41)
|
||
function getLdapAdminPassword(): string {
|
||
return process.env.LLDAP_ADMIN_PASSWORD || 'admin123'
|
||
}
|
||
|
||
export interface LdapResult {
|
||
success: boolean
|
||
unreachable: boolean
|
||
username?: string
|
||
displayName?: string
|
||
email?: string
|
||
}
|
||
|
||
export async function ldapAuth(
|
||
username: string,
|
||
password: string
|
||
): Promise<LdapResult> {
|
||
const userDn = `uid=${username},ou=people,${LDAP_BASE_DN}`
|
||
const client = new Client({ url: LDAP_URL, timeout: 5000 })
|
||
|
||
try {
|
||
await client.bind(userDn, password)
|
||
try {
|
||
const { searchEntries } = await client.search(LDAP_BASE_DN, {
|
||
scope: 'sub',
|
||
filter: `(uid=${username})`,
|
||
attributes: ['displayName', 'mail'],
|
||
timeLimit: 3,
|
||
})
|
||
const entry = searchEntries[0] as any
|
||
const displayName = entry?.displayName || username
|
||
const email = entry?.mail || null
|
||
return { success: true, unreachable: false, username, displayName, email }
|
||
} catch {
|
||
return { success: true, unreachable: false, username, displayName: username }
|
||
}
|
||
} catch (err) {
|
||
if (err instanceof InvalidCredentialsError) {
|
||
return { success: false, unreachable: false }
|
||
}
|
||
return { success: false, unreachable: true }
|
||
} finally {
|
||
await client.unbind()
|
||
}
|
||
}
|
||
|
||
// 从 LLDAP 获取用户信息(displayName + email),不可达返回 null
|
||
export async function ldapGetUserInfo(username: string): Promise<{ displayName: string; email: string | null } | null> {
|
||
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(LDAP_BASE_DN, {
|
||
scope: 'sub', filter: `(uid=${username})`, attributes: ['displayName', 'mail'], timeLimit: 3,
|
||
})
|
||
const entry = searchEntries[0] as any
|
||
return entry ? { displayName: entry.displayName || username, email: entry.mail || null } : null
|
||
} catch { return null }
|
||
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'
|
||
const adminPass = getLdapAdminPassword()
|
||
const client = new Client({ url: LDAP_URL, timeout: 5000 })
|
||
|
||
try {
|
||
await client.bind(adminDn, adminPass)
|
||
const { searchEntries } = await client.search(LDAP_BASE_DN, {
|
||
scope: 'sub', filter: `(uid=${username})`, timeLimit: 3,
|
||
})
|
||
return searchEntries.length > 0
|
||
} catch {
|
||
return true // LLDAP 不可达 → 不阻断,容错放行
|
||
} finally {
|
||
await client.unbind()
|
||
}
|
||
}
|