91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
import { Client, InvalidCredentialsError } from 'ldapts'
|
||
import { execFileSync } from 'child_process'
|
||
|
||
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 密码,避免明文存于多个 .env
|
||
// 需要容器挂载 /var/run/docker.sock
|
||
function getLdapAdminPassword(): string {
|
||
try {
|
||
return execFileSync('docker', ['exec', 'lldap', 'printenv', 'LLDAP_ADMIN_PASSWORD'],
|
||
{ timeout: 3000 }).toString().trim()
|
||
} catch { return '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() }
|
||
}
|
||
|
||
// 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()
|
||
}
|
||
}
|