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 } async function getWorkforceCreds() { if (_credsCache && Date.now() < _credsCache.expires_at) return _credsCache.creds const res = await fetch(`${SETTINGS_URL}/settings/api/internal/integration/workforce`, { headers: { Authorization: `Bearer ${SETTINGS_SECRET}` }, signal: AbortSignal.timeout(5000), }) if (!res.ok) throw new Error(`Failed to fetch Workforce credentials from settings: ${res.status}`) const creds = await res.json() if (!creds.email || !creds.password) throw new Error('Workforce credentials not configured in settings') _credsCache = { creds, expires_at: Date.now() + 5 * 60_000 } return creds } async function getWorkforceToken() { const creds = await getWorkforceCreds() const baseUrl = creds.base_url || 'https://my.workforce.com' 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: 'me department staff', }).toString(), signal: AbortSignal.timeout(10000), }) if (!res.ok) throw new Error(`Workforce OAuth failed: ${res.status}`) 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}` }, signal: AbortSignal.timeout(10000), }) if (!res.ok) { const body = await res.text().catch(() => '') throw new Error(`Workforce API error ${res.status} at ${path}${body ? ': ' + body.slice(0, 200) : ''}`) } return res.json() } async function wfFetchPaged(path) { const results = [] let page = 1 while (true) { const sep = path.includes('?') ? '&' : '?' const data = await wfFetch(`${path}${sep}page=${page}&per_page=100`) const items = Array.isArray(data) ? data : (data.users ?? data.departments ?? []) results.push(...items) if (items.length < 100) break page++ } return results } export async function findEmployeeByEmail(email) { const data = await wfFetch(`/api/v2/users?email=${encodeURIComponent(email)}`) const users = Array.isArray(data) ? data : (data.users ?? []) 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 } } export async function getAllDepartments() { return wfFetchPaged('/api/v2/departments') } export async function getEmployeeDepartments(wfUserId) { const depts = await getAllDepartments() const id = String(wfUserId) return depts.filter(d => { const staff = (d.staff ?? []).map(String) const managers = (d.managers ?? []).map(String) return staff.includes(id) || managers.includes(id) }) } export async function getSyncConfig() { const creds = await getWorkforceCreds() return { syncHours: parseFloat(creds.sync_hours ?? '6') } } // Invalidate caches (e.g. after credentials updated in settings) export function invalidateCache() { _credsCache = null _tokenCache = null }