Add internal registry endpoint, self-registration, and workforce sync
- Register internalRoutes at /api/auth/internal (used by management) - Add registerRoutes for staff self-registration flow - Add workforce sync job (runs on schedule, fails gracefully if unconfigured) - Add email.js for registration/invite emails Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2d89fd9227
commit
f08d53d702
10 changed files with 704 additions and 18 deletions
97
src/workforce.js
Normal file
97
src/workforce.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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/json' },
|
||||
body: JSON.stringify({
|
||||
grant_type: 'password',
|
||||
username: creds.email,
|
||||
password: creds.password,
|
||||
scope: 'default',
|
||||
}),
|
||||
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) throw new Error(`Workforce API error ${res.status} at ${path}`)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue