feat(slack): autoFlagPreExistingSlackAccess helper
Best-effort lookup of an activating member's email in the Slack workspace. On a hit, flips slackInvited:true and stamps slackInvitedAt without sending a fresh invite. Races against a 3s timeout and swallows all errors so activation never blocks on Slack. - Promotes SlackService.findUserByEmail from private to public so the helper can call it without a wrapper. - New activity-log action: slack_access_auto_detected (actor = subject). - Idempotent: short-circuits when slackInvited is already true. Callers wired in next commit.
This commit is contained in:
parent
2f6a92ac61
commit
b1d8cb1966
4 changed files with 263 additions and 1 deletions
61
server/utils/slackAccess.js
Normal file
61
server/utils/slackAccess.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Spec: docs/specs/wave-based-slack-onboarding.md
|
||||
//
|
||||
// Auto-detect existing Slack workspace membership for a Ghost Guild member
|
||||
// and flag them as `slackInvited` without sending a duplicate invite.
|
||||
//
|
||||
// Contract:
|
||||
// - Caller awaits this helper, but it never throws — all errors swallowed.
|
||||
// - Internal lookup races against a 3s timeout (resolves null on hang).
|
||||
// - No-op if Slack service isn't configured, lookup misses, or member is
|
||||
// already flagged.
|
||||
|
||||
import Member from '../models/member.js'
|
||||
import { getSlackService } from './slack.ts'
|
||||
|
||||
const LOOKUP_TIMEOUT_MS = 3000
|
||||
|
||||
export async function autoFlagPreExistingSlackAccess(member) {
|
||||
try {
|
||||
if (!member || !member._id || !member.email) return
|
||||
if (member.slackInvited === true) return
|
||||
|
||||
const slackService = getSlackService()
|
||||
if (!slackService) return
|
||||
|
||||
const timeoutPromise = new Promise((resolve) => {
|
||||
setTimeout(() => resolve(null), LOOKUP_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
let userId = null
|
||||
try {
|
||||
userId = await Promise.race([
|
||||
slackService.findUserByEmail(member.email),
|
||||
timeoutPromise
|
||||
])
|
||||
} catch (err) {
|
||||
console.error('[slackAccess] findUserByEmail failed:', err)
|
||||
return
|
||||
}
|
||||
|
||||
if (!userId) return
|
||||
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{
|
||||
slackInvited: true,
|
||||
slackInvitedAt: new Date(),
|
||||
slackUserId: userId
|
||||
},
|
||||
{ runValidators: false }
|
||||
)
|
||||
|
||||
await logActivity(
|
||||
member._id,
|
||||
'slack_access_auto_detected',
|
||||
{ slackUserId: userId },
|
||||
{ visibility: 'member' }
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('[slackAccess] autoFlagPreExistingSlackAccess error:', err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue