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
103
src/sync.js
Normal file
103
src/sync.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { pool } from './db.js'
|
||||
import { findEmployeeByEmail, getEmployeeDepartments, getSyncConfig } from './workforce.js'
|
||||
|
||||
export async function syncAllWorkforceUsers() {
|
||||
const { rows: users } = await pool.query(
|
||||
`SELECT id, email, name, workforce_user_id FROM users WHERE workforce_user_id IS NOT NULL AND active = true`
|
||||
)
|
||||
|
||||
let deactivated = 0, emailUpdated = 0, rolesUpdated = 0
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
// Look up by email (Workforce doesn't reliably expose GET /users/:id)
|
||||
// Use stored email first; if not found try to detect via workforce_user_id match in dept lists
|
||||
const wfUser = await findEmployeeByEmail(user.email)
|
||||
|
||||
if (!wfUser || String(wfUser.id) !== user.workforce_user_id) {
|
||||
// Not found or ID mismatch — deactivate
|
||||
await pool.query('UPDATE users SET active = false WHERE id = $1', [user.id])
|
||||
deactivated++
|
||||
continue
|
||||
}
|
||||
|
||||
// Sync email if changed
|
||||
const wfEmail = wfUser.email?.toLowerCase().trim()
|
||||
if (wfEmail && wfEmail !== user.email) {
|
||||
await pool.query('UPDATE users SET email = $1 WHERE id = $2', [wfEmail, user.id])
|
||||
emailUpdated++
|
||||
}
|
||||
|
||||
// Sync department-mapped roles
|
||||
const depts = await getEmployeeDepartments(wfUser.id)
|
||||
const deptIds = depts.map(d => String(d.id))
|
||||
|
||||
// Get current dept-mapped role_ids for this user
|
||||
const { rows: currentRoles } = await pool.query(
|
||||
`SELECT ur.role_id, wdr.department_id
|
||||
FROM user_roles ur
|
||||
JOIN workforce_department_roles wdr ON wdr.role_id = ur.role_id
|
||||
WHERE ur.user_id = $1 AND ur.source = 'workforce_department'`,
|
||||
[user.id]
|
||||
)
|
||||
const currentDeptIds = currentRoles.map(r => r.department_id)
|
||||
|
||||
const added = deptIds.filter(id => !currentDeptIds.includes(id))
|
||||
const removed = currentDeptIds.filter(id => !deptIds.includes(id))
|
||||
|
||||
if (added.length || removed.length) {
|
||||
// Remove roles for departments the user is no longer in
|
||||
if (removed.length) {
|
||||
await pool.query(
|
||||
`DELETE FROM user_roles WHERE user_id = $1 AND source = 'workforce_department'
|
||||
AND role_id IN (
|
||||
SELECT role_id FROM workforce_department_roles WHERE department_id = ANY($2)
|
||||
)`,
|
||||
[user.id, removed]
|
||||
)
|
||||
}
|
||||
// Add roles for new departments
|
||||
for (const deptId of added) {
|
||||
await pool.query(
|
||||
`INSERT INTO user_roles (user_id, role_id, source)
|
||||
SELECT $1, role_id, 'workforce_department'
|
||||
FROM workforce_department_roles WHERE department_id = $2
|
||||
ON CONFLICT DO NOTHING`,
|
||||
[user.id, deptId]
|
||||
)
|
||||
}
|
||||
rolesUpdated++
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Workforce sync error for user ${user.id} (${user.email}):`, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired pending registrations older than 24h
|
||||
await pool.query(`DELETE FROM pending_registrations WHERE expires_at < NOW() - INTERVAL '24 hours'`)
|
||||
|
||||
const summary = { checked: users.length, deactivated, emailUpdated, rolesUpdated }
|
||||
console.log('Workforce sync complete:', summary)
|
||||
return summary
|
||||
}
|
||||
|
||||
export function startSyncJob() {
|
||||
getSyncConfig()
|
||||
.then(({ syncHours }) => {
|
||||
if (!syncHours || syncHours <= 0) {
|
||||
console.log('Workforce sync disabled (sync_hours = 0)')
|
||||
return
|
||||
}
|
||||
const intervalMs = syncHours * 60 * 60_000
|
||||
// First run 60s after startup to let DB settle
|
||||
setTimeout(() => {
|
||||
syncAllWorkforceUsers().catch(err => console.error('Workforce sync failed:', err.message))
|
||||
setInterval(
|
||||
() => syncAllWorkforceUsers().catch(err => console.error('Workforce sync failed:', err.message)),
|
||||
intervalMs
|
||||
)
|
||||
}, 60_000)
|
||||
console.log(`Workforce sync scheduled every ${syncHours}h`)
|
||||
})
|
||||
.catch(err => console.warn('Could not read Workforce sync config (settings not configured?):', err.message))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue