Workforce: bearer token only, strip OAuth, pass location fields from departments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
75d373c2c5
commit
5d6a9ea3cd
2 changed files with 18 additions and 50 deletions
|
|
@ -310,12 +310,19 @@ export async function adminRoutes(app) {
|
||||||
const { rows: mappings } = await pool.query('SELECT department_id, role_id FROM workforce_department_roles')
|
const { rows: mappings } = await pool.query('SELECT department_id, role_id FROM workforce_department_roles')
|
||||||
const mappingMap = Object.fromEntries(mappings.map(m => [m.department_id, m.role_id]))
|
const mappingMap = Object.fromEntries(mappings.map(m => [m.department_id, m.role_id]))
|
||||||
|
|
||||||
return depts.map(d => ({
|
return depts
|
||||||
id: String(d.id),
|
.map(d => ({
|
||||||
name: d.name,
|
id: String(d.id),
|
||||||
staff_count: (d.staff ?? []).length,
|
name: d.name,
|
||||||
mapped_role_id: mappingMap[String(d.id)] ?? null,
|
staff_count: (d.staff ?? []).length,
|
||||||
}))
|
location_id: d.location_id ? String(d.location_id) : null,
|
||||||
|
location_name: d.location ?? d.location_name ?? null,
|
||||||
|
mapped_role_id: mappingMap[String(d.id)] ?? null,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const locCmp = (a.location_name ?? '').localeCompare(b.location_name ?? '')
|
||||||
|
return locCmp !== 0 ? locCmp : a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put('/workforce/department-mappings', async (request, reply) => {
|
app.put('/workforce/department-mappings', async (request, reply) => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
const SETTINGS_URL = process.env.SETTINGS_URL || 'http://10.10.10.106:3080'
|
const SETTINGS_URL = process.env.SETTINGS_URL || 'http://10.10.10.106:3080'
|
||||||
const SETTINGS_SECRET = process.env.SETTINGS_SECRET || ''
|
const SETTINGS_SECRET = process.env.SETTINGS_SECRET || ''
|
||||||
|
|
||||||
// Credential + token caches
|
let _credsCache = null // { creds, expires_at }
|
||||||
let _credsCache = null // { creds, expires_at }
|
|
||||||
let _tokenCache = null // { access_token, base_url, expires_at }
|
|
||||||
|
|
||||||
async function getWorkforceCreds() {
|
async function getWorkforceCreds() {
|
||||||
if (_credsCache && Date.now() < _credsCache.expires_at) return _credsCache.creds
|
if (_credsCache && Date.now() < _credsCache.expires_at) return _credsCache.creds
|
||||||
|
|
@ -13,49 +11,16 @@ async function getWorkforceCreds() {
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error(`Failed to fetch Workforce credentials from settings: ${res.status}`)
|
if (!res.ok) throw new Error(`Failed to fetch Workforce credentials from settings: ${res.status}`)
|
||||||
const creds = await res.json()
|
const creds = await res.json()
|
||||||
if (!creds.bearer_token && (!creds.email || !creds.password)) {
|
if (!creds.bearer_token) throw new Error('Workforce bearer token not configured in settings')
|
||||||
throw new Error('Workforce credentials not configured in settings — set a bearer token or email/password')
|
|
||||||
}
|
|
||||||
_credsCache = { creds, expires_at: Date.now() + 5 * 60_000 }
|
_credsCache = { creds, expires_at: Date.now() + 5 * 60_000 }
|
||||||
return creds
|
return creds
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWorkforceToken() {
|
async function wfFetch(path) {
|
||||||
const creds = await getWorkforceCreds()
|
const creds = await getWorkforceCreds()
|
||||||
const baseUrl = creds.base_url || 'https://my.workforce.com'
|
const baseUrl = creds.base_url || 'https://my.workforce.com'
|
||||||
|
|
||||||
// Use bearer token directly if configured — no OAuth exchange needed
|
|
||||||
if (creds.bearer_token) {
|
|
||||||
return { token: creds.bearer_token, baseUrl }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_tokenCache && _tokenCache.base_url === baseUrl && Date.now() < _tokenCache.expires_at) {
|
|
||||||
return { token: _tokenCache.access_token, baseUrl }
|
|
||||||
}
|
|
||||||
const res = await fetch(`${baseUrl}/api/oauth/token`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
body: new URLSearchParams({
|
|
||||||
grant_type: 'password',
|
|
||||||
username: creds.email,
|
|
||||||
password: creds.password,
|
|
||||||
scope: 'platform',
|
|
||||||
}).toString(),
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.text().catch(() => '')
|
|
||||||
throw new Error(`Workforce OAuth failed: ${res.status}${body ? ': ' + body.slice(0, 300) : ''}`)
|
|
||||||
}
|
|
||||||
const data = await res.json()
|
|
||||||
_tokenCache = { access_token: data.access_token, base_url: baseUrl, expires_at: Date.now() + 50 * 60_000 }
|
|
||||||
return { token: data.access_token, baseUrl }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function wfFetch(path) {
|
|
||||||
const { token, baseUrl } = await getWorkforceToken()
|
|
||||||
const res = await fetch(`${baseUrl}${path}`, {
|
const res = await fetch(`${baseUrl}${path}`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${creds.bearer_token}` },
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -71,7 +36,7 @@ async function wfFetchPaged(path) {
|
||||||
while (true) {
|
while (true) {
|
||||||
const sep = path.includes('?') ? '&' : '?'
|
const sep = path.includes('?') ? '&' : '?'
|
||||||
const data = await wfFetch(`${path}${sep}page=${page}&page_size=100`)
|
const data = await wfFetch(`${path}${sep}page=${page}&page_size=100`)
|
||||||
const items = Array.isArray(data) ? data : (data.users ?? data.departments ?? [])
|
const items = Array.isArray(data) ? data : (data.users ?? data.departments ?? data.teams ?? [])
|
||||||
results.push(...items)
|
results.push(...items)
|
||||||
if (items.length < 100) break
|
if (items.length < 100) break
|
||||||
page++
|
page++
|
||||||
|
|
@ -85,11 +50,9 @@ export async function findEmployeeByEmail(email) {
|
||||||
return users.length > 0 ? users[0] : null
|
return users.length > 0 ? users[0] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try direct ID lookup first (standard REST); returns null if endpoint not supported
|
|
||||||
export async function findEmployeeById(wfUserId) {
|
export async function findEmployeeById(wfUserId) {
|
||||||
try {
|
try {
|
||||||
const data = await wfFetch(`/api/v2/users/${wfUserId}`)
|
const data = await wfFetch(`/api/v2/users/${wfUserId}`)
|
||||||
// Some APIs wrap in { user: ... }, others return the object directly
|
|
||||||
return data?.id ? data : (data?.user ?? null)
|
return data?.id ? data : (data?.user ?? null)
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
|
|
@ -115,8 +78,6 @@ export async function getSyncConfig() {
|
||||||
return { syncHours: parseFloat(creds.sync_hours ?? '6') }
|
return { syncHours: parseFloat(creds.sync_hours ?? '6') }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate caches (e.g. after credentials updated in settings)
|
|
||||||
export function invalidateCache() {
|
export function invalidateCache() {
|
||||||
_credsCache = null
|
_credsCache = null
|
||||||
_tokenCache = null
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue