From 5d6a9ea3cda9486c66b73c81a456fbe19b50ffcc Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 7 Jul 2026 12:42:47 +0000 Subject: [PATCH] Workforce: bearer token only, strip OAuth, pass location fields from departments Co-Authored-By: Claude Sonnet 4.6 --- src/routes/admin.js | 19 ++++++++++++------ src/workforce.js | 49 +++++---------------------------------------- 2 files changed, 18 insertions(+), 50 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 1cb0697..a0e5bcd 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -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 mappingMap = Object.fromEntries(mappings.map(m => [m.department_id, m.role_id])) - return depts.map(d => ({ - id: String(d.id), - name: d.name, - staff_count: (d.staff ?? []).length, - mapped_role_id: mappingMap[String(d.id)] ?? null, - })) + return depts + .map(d => ({ + id: String(d.id), + name: d.name, + 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) => { diff --git a/src/workforce.js b/src/workforce.js index 19c1644..3442019 100644 --- a/src/workforce.js +++ b/src/workforce.js @@ -1,9 +1,7 @@ const SETTINGS_URL = process.env.SETTINGS_URL || 'http://10.10.10.106:3080' const SETTINGS_SECRET = process.env.SETTINGS_SECRET || '' -// Credential + token caches -let _credsCache = null // { creds, expires_at } -let _tokenCache = null // { access_token, base_url, expires_at } +let _credsCache = null // { creds, expires_at } async function getWorkforceCreds() { 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}`) const creds = await res.json() - if (!creds.bearer_token && (!creds.email || !creds.password)) { - throw new Error('Workforce credentials not configured in settings — set a bearer token or email/password') - } + if (!creds.bearer_token) throw new Error('Workforce bearer token not configured in settings') _credsCache = { creds, expires_at: Date.now() + 5 * 60_000 } return creds } -async function getWorkforceToken() { +async function wfFetch(path) { const creds = await getWorkforceCreds() 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}`, { - headers: { Authorization: `Bearer ${token}` }, + headers: { Authorization: `Bearer ${creds.bearer_token}` }, signal: AbortSignal.timeout(10000), }) if (!res.ok) { @@ -71,7 +36,7 @@ async function wfFetchPaged(path) { while (true) { const sep = path.includes('?') ? '&' : '?' 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) if (items.length < 100) break page++ @@ -85,11 +50,9 @@ export async function findEmployeeByEmail(email) { 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) { try { 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) } catch { return null @@ -115,8 +78,6 @@ export async function getSyncConfig() { return { syncHours: parseFloat(creds.sync_hours ?? '6') } } -// Invalidate caches (e.g. after credentials updated in settings) export function invalidateCache() { _credsCache = null - _tokenCache = null }