feat(jsn-bridge): auto-invite JSN supporters to #supporters channel
After provisioning a Rocket.Chat account for a JSN-bridged user, explicitly invite them to the configured supporters channel via channels.invite. This is belt-and-braces alongside the RC default-channel flag — the default flag only fires at user creation time, so flipping a channel to default later doesn't catch existing users. The explicit invite path handles that case and also gives us a stable hook for migration scripts. Three changes: - New env: RC_SUPPORTERS_CHANNEL (default 'supporters'). z.string() in env.ts. Set in cmlite .env if you want to route to a different channel. - New rocketchatClient method: inviteUserToChannel(userId, channelName). Idempotent — RC's channels.invite no-ops if the user is already a member. - identityBridgeService.provisionRocketChatAccount now calls rocketchatClient.inviteUserToChannel after createUser + setting rocketChatUserId. Wrapped in its own try/catch so an invite failure logs warn but doesn't roll back the RC account creation. Tested end-to-end: JSN magic-link verify → cmlite User → RC account → auto-invited to #supporters (membership goes from 4 → 5 on the test signup). Three existing stragglers backfilled via direct API calls (idempotent). Bunker Admin
This commit is contained in:
parent
5a2c54dabf
commit
11f23c0072
@ -281,6 +281,12 @@ const envSchema = z.object({
|
||||
// Required for /api/auth/bridge/users and other /api/*/bridge/* endpoints to
|
||||
// accept calls. Generate with: openssl rand -hex 32
|
||||
JSN_BRIDGE_SECRET: z.string().min(32).optional(),
|
||||
|
||||
// RC channel that every JSN-bridged supporter is invited into by the
|
||||
// identity bridge after RC user provisioning. Belt-and-braces alongside RC's
|
||||
// default-channel flag (which only fires at user creation time, leaving
|
||||
// existing-user backfill as a separate concern). Default 'supporters'.
|
||||
RC_SUPPORTERS_CHANNEL: z.string().default('supporters'),
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
|
||||
@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto';
|
||||
import { UserCreatedVia, UserStatus } from '@prisma/client';
|
||||
import { prisma } from '../../config/database';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { env } from '../../config/env';
|
||||
import { siteSettingsService } from '../settings/settings.service';
|
||||
import { rocketchatClient } from '../../services/rocketchat.client';
|
||||
import type { BridgeProvisionUserInput } from './identity-bridge.schemas';
|
||||
@ -48,9 +49,24 @@ async function provisionRocketChatAccount(
|
||||
where: { id: userId },
|
||||
data: { rocketChatUserId: rcUser._id },
|
||||
});
|
||||
// Belt-and-braces: explicitly invite to the supporters channel. RC's
|
||||
// default-channel flag also catches new users on createUser, but this
|
||||
// guarantees membership even if the default-channel set changes later or
|
||||
// if the user was created before the flag was flipped (e.g. backfill).
|
||||
try {
|
||||
await rocketchatClient.inviteUserToChannel(rcUser._id, env.RC_SUPPORTERS_CHANNEL);
|
||||
} catch (inviteErr) {
|
||||
logger.warn('RC supporters-channel invite failed (non-blocking)', {
|
||||
userId,
|
||||
rcUserId: rcUser._id,
|
||||
channel: env.RC_SUPPORTERS_CHANNEL,
|
||||
err: inviteErr instanceof Error ? inviteErr.message : String(inviteErr),
|
||||
});
|
||||
}
|
||||
logger.info('RC account provisioned for JSN-bridged user', {
|
||||
userId,
|
||||
rcUserId: rcUser._id,
|
||||
invitedToChannel: env.RC_SUPPORTERS_CHANNEL,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('RC provisioning failed (non-blocking)', {
|
||||
|
||||
@ -284,6 +284,17 @@ class RocketChatClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to a public channel by channel name. Idempotent — returns the
|
||||
* channel info regardless of whether the user was already a member. Used by
|
||||
* the JSN identity bridge to make sure every JSN supporter lands in a
|
||||
* specific channel (#supporters by convention), independent of RC's
|
||||
* default-channel flag (which only fires at user creation time).
|
||||
*/
|
||||
async inviteUserToChannel(userId: string, channelName: string): Promise<void> {
|
||||
await this.request('POST', '/channels.invite', { roomName: channelName, userId });
|
||||
}
|
||||
|
||||
// --- Direct Messages ---
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user