diff --git a/server/dev-server.mjs b/server/dev-server.mjs index 3f60004..340f3dd 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -535,6 +535,44 @@ app.post("/api/internal/tasker/invite-requests/cancel", asyncRoute(async (req, r res.json({ ok: true, taskerInviteRequest: result.taskerInviteRequest }); })); +app.post("/api/internal/tasker/profile-sync", asyncRoute(async (req, res) => { + if (!isInternalRequestAuthorized(req)) { + res.status(config.internalAccessToken ? 401 : 503).json({ + ok: false, + error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured", + }); + return; + } + + const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC tasker profile sync" }); + const user = findInternalAccessUser(snapshot.data, req.body); + + if (!user) { + res.status(404).json({ ok: false, error: "user_not_found" }); + return; + } + + const patch = sanitizeTaskerProfilePatch(req.body); + + if (Object.keys(patch).length === 0) { + res.json({ ok: true, user, data: snapshot.data, skipped: true }); + return; + } + + const actor = { + sub: "tasker-profile-sync", + name: req.body?.source === "tasker" ? "Operational Core" : "NODE.DC profile sync", + email: typeof req.body?.email === "string" ? req.body.email : user.email, + source: "tasker", + }; + const result = await controlPlaneStore.updateUserProfile(user.id, patch, actor); + const syncResult = await syncUsersToAuthentik(result.data, [user.id], actor); + const updatedUser = syncResult.data.users.find((candidate) => candidate.id === user.id) ?? result.user; + + publishControlPlaneEvent("tasker.profile.updated", [user.id]); + res.json({ ok: true, user: updatedUser, data: syncResult.data }); +})); + app.patch("/api/profile", requireSession, asyncRoute(async (req, res) => { const { actor } = getLauncherProfileContext(req.nodedcSession); const result = await controlPlaneStore.updateUserProfile(actor.id, sanitizeSelfProfilePatch(req.body), req.nodedcSession.user); @@ -1735,6 +1773,37 @@ function sanitizeSelfProfilePatch(payload) { }; } +function sanitizeTaskerProfilePatch(payload) { + const patch = {}; + const name = firstNonEmptyString(payload?.displayName, payload?.display_name, payload?.name); + const hasAvatar = + Object.hasOwn(payload ?? {}, "avatarUrl") || + Object.hasOwn(payload ?? {}, "avatar_url") || + Object.hasOwn(payload ?? {}, "avatar"); + + if (name) { + patch.name = name; + } + + if (hasAvatar) { + patch.avatarUrl = nullableProfileUrl(payload?.avatarUrl ?? payload?.avatar_url ?? payload?.avatar); + } + + return patch; +} + +function firstNonEmptyString(...values) { + for (const value of values) { + if (typeof value === "string" && value.trim()) return value.trim(); + } + + return null; +} + +function nullableProfileUrl(value) { + return typeof value === "string" && value.trim() ? value.trim() : null; +} + function toProvisioningResponse(provisionedUser) { return { authentikUserId: provisionedUser.authentikUserId,