mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-18 00:40:46 +08:00
Query user profile via intranet direct HTTP; upgrade/clear trust sessions without department.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
74a86ff832
commit
9ff9635624
5 changed files with 262 additions and 148 deletions
163
uds-auth/lib/uds/user-search.js
Normal file
163
uds-auth/lib/uds/user-search.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Outbound HTTP(S) that must NOT use HTTP_PROXY / HTTPS_PROXY.
|
||||
* ZTE icenter / UAC user APIs are intranet-only; corporate forward proxies
|
||||
* typically return 401/403 and never reach the real service.
|
||||
*/
|
||||
import http from 'node:http'
|
||||
import https from 'node:https'
|
||||
|
||||
/**
|
||||
* @param {string|URL} url
|
||||
* @param {{ method?: string, headers?: Record<string,string|number>, body?: string|Buffer, timeoutMs?: number }} opts
|
||||
* @returns {Promise<{ statusCode: number, headers: object, body: string, json: any }>}
|
||||
*/
|
||||
export function directRequest(url, opts = {}) {
|
||||
const target = typeof url === 'string' ? new URL(url) : url
|
||||
const isHttps = target.protocol === 'https:'
|
||||
const lib = isHttps ? https : http
|
||||
const method = (opts.method || 'GET').toUpperCase()
|
||||
const body = opts.body == null ? null : Buffer.isBuffer(opts.body) ? opts.body : Buffer.from(String(opts.body))
|
||||
const headers = { ...(opts.headers || {}) }
|
||||
if (body && headers['Content-Length'] == null && headers['content-length'] == null) {
|
||||
headers['Content-Length'] = body.length
|
||||
}
|
||||
const timeoutMs = opts.timeoutMs ?? 8000
|
||||
|
||||
// Fresh Agent — never inherit globalAgent (often patched by proxy bootstraps).
|
||||
const agent = new lib.Agent({ keepAlive: false })
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = lib.request({
|
||||
protocol: target.protocol,
|
||||
hostname: target.hostname,
|
||||
port: target.port || (isHttps ? 443 : 80),
|
||||
path: target.pathname + target.search,
|
||||
method,
|
||||
headers,
|
||||
agent,
|
||||
timeout: timeoutMs,
|
||||
}, (res) => {
|
||||
const chunks = []
|
||||
res.on('data', (c) => chunks.push(c))
|
||||
res.on('end', () => {
|
||||
const text = Buffer.concat(chunks).toString('utf8')
|
||||
let json = null
|
||||
try { json = JSON.parse(text) } catch { json = null }
|
||||
resolve({
|
||||
statusCode: res.statusCode || 0,
|
||||
headers: res.headers,
|
||||
body: text,
|
||||
json,
|
||||
})
|
||||
})
|
||||
})
|
||||
req.on('error', reject)
|
||||
req.on('timeout', () => {
|
||||
req.destroy()
|
||||
reject(new Error(`directRequest timeout after ${timeoutMs}ms: ${target.host}`))
|
||||
})
|
||||
if (body) req.write(body)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST userSearchUrl with X-Emp-No + X-Auth-Value (intranet, no proxy).
|
||||
*/
|
||||
export async function searchUserByEmpNoToken({
|
||||
userSearchUrl,
|
||||
empNo,
|
||||
token,
|
||||
empNoHeader = 'X-Emp-No',
|
||||
authValueHeader = 'X-Auth-Value',
|
||||
origin,
|
||||
timeoutMs = 8000,
|
||||
}) {
|
||||
if (!userSearchUrl || !empNo || !token) {
|
||||
return { ok: false, reason: 'missing_args' }
|
||||
}
|
||||
const body = JSON.stringify({
|
||||
employeeShortId: empNo,
|
||||
enableLabel: true,
|
||||
keyword: empNo,
|
||||
})
|
||||
const headers = {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
[empNoHeader]: empNo,
|
||||
[authValueHeader]: token,
|
||||
}
|
||||
if (origin) {
|
||||
headers.Origin = origin
|
||||
headers.Referer = String(origin).replace(/\/?$/, '/')
|
||||
}
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await directRequest(userSearchUrl, { method: 'POST', headers, body, timeoutMs })
|
||||
} catch (err) {
|
||||
return { ok: false, reason: 'network', error: err.message }
|
||||
}
|
||||
|
||||
if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'http_auth',
|
||||
statusCode: res.statusCode,
|
||||
msg: res.json?.code?.msg || res.json?.msg || res.body.slice(0, 200),
|
||||
hint: 'userSearchUrl must be reachable on intranet WITHOUT HTTP(S)_PROXY',
|
||||
}
|
||||
}
|
||||
|
||||
const result = res.json
|
||||
if (!result) {
|
||||
return { ok: false, reason: 'bad_json', statusCode: res.statusCode, raw: res.body.slice(0, 300) }
|
||||
}
|
||||
|
||||
const code = result?.code?.code ?? result?.code
|
||||
if (code !== '0000' && code !== 0 && code !== '0') {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'biz_code',
|
||||
statusCode: res.statusCode,
|
||||
code,
|
||||
msg: result?.code?.msg || result?.msg,
|
||||
}
|
||||
}
|
||||
|
||||
const list = Array.isArray(result?.bo) ? result.bo
|
||||
: Array.isArray(result?.bo?.rows) ? result.bo.rows
|
||||
: Array.isArray(result?.bo?.list) ? result.bo.list
|
||||
: []
|
||||
if (!list.length) {
|
||||
return { ok: false, reason: 'empty', statusCode: res.statusCode, result }
|
||||
}
|
||||
|
||||
const emp = list[0]
|
||||
const resolvedEmpNo = String(
|
||||
emp.employeeShortId || emp.employeeNO || emp.empUIID || emp.empNo || empNo,
|
||||
).trim()
|
||||
if (resolvedEmpNo && resolvedEmpNo !== String(empNo).trim()
|
||||
&& !String(empNo).includes(resolvedEmpNo)
|
||||
&& !resolvedEmpNo.includes(String(empNo).trim())) {
|
||||
const a = String(empNo).replace(/\D/g, '')
|
||||
const b = resolvedEmpNo.replace(/\D/g, '')
|
||||
if (!a || !b || !(a.includes(b) || b.includes(a))) {
|
||||
return { ok: false, reason: 'emp_mismatch', resolvedEmpNo }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
profile: {
|
||||
empNo: String(empNo).trim(),
|
||||
username: emp.name || emp.empName || emp.userName || empNo,
|
||||
department: emp.deptFullName || emp.deptName || emp.deptShortName || emp.orgNamePath || emp.orgName || emp.department || '',
|
||||
organization: emp.orgNamePath || emp.orgName || '',
|
||||
email: emp.email || emp.mail || '',
|
||||
phone: emp.mobile || emp.phone || '',
|
||||
raw: emp,
|
||||
token,
|
||||
},
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ export class UdsValidator {
|
|||
this.empNoHeader = (config.empNoHeader || 'X-Emp-No').toLowerCase()
|
||||
this.authValueHeader = (config.authValueHeader || 'X-Auth-Value').toLowerCase()
|
||||
this.langIdHeader = (config.langIdHeader || 'X-Lang-Id').toLowerCase()
|
||||
this.authMode = config.authMode || 'trust'
|
||||
this.authMode = config.authMode || 'token+profile'
|
||||
this.hrApiUrl = config.hrApiUrl || 'https://icosg.dt.zte.com.cn/ihol/usercenter/pginfo/usercenter/plain/queryPersonGeneralInfo'
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +149,12 @@ export class UdsValidator {
|
|||
} else {
|
||||
userData = {
|
||||
userId: credentialsOrResponse.empNo,
|
||||
username: credentialsOrResponse.empNo,
|
||||
username: credentialsOrResponse.username || credentialsOrResponse.empNo,
|
||||
displayName: credentialsOrResponse.displayName || credentialsOrResponse.username || credentialsOrResponse.empNo,
|
||||
department: credentialsOrResponse.department || '',
|
||||
organization: credentialsOrResponse.organization || '',
|
||||
email: credentialsOrResponse.email || '',
|
||||
phone: credentialsOrResponse.phone || '',
|
||||
empNo: credentialsOrResponse.empNo,
|
||||
token: credentialsOrResponse.token,
|
||||
lang: credentialsOrResponse.lang,
|
||||
|
|
@ -173,7 +178,7 @@ export class UdsValidator {
|
|||
lastActiveAt: now,
|
||||
sessionCreatedAt: now,
|
||||
isAuthenticated: true,
|
||||
authMode: this.authMode,
|
||||
authMode: this.authMode || 'token+profile',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue