import { createHmac } from 'crypto'; import { Prisma } from '@prisma/client'; import { prisma } from '../../config/database'; import { env } from '../../config/env'; import { logger } from '../../utils/logger'; import { rocketchatClient } from '../rocketchat.client'; import type { ServiceProvisioner, ProvisionerConfig, ProvisionResult, CMUser } from './provisioner.interface'; const ROLE_MAP: Record = { SUPER_ADMIN: ['admin'], INFLUENCE_ADMIN: ['moderator'], MAP_ADMIN: ['moderator'], USER: ['user'], TEMP: ['user'], }; /** Deterministic password — never exposed to users, only used for RC internal auth */ function generateRCPassword(userId: string): string { return createHmac('sha256', env.JWT_ACCESS_SECRET) .update(`rc:${userId}`) .digest('hex'); } /** Safe username from email with collision avoidance suffix */ function generateUsername(email: string, suffix = 0): string { const base = email.split('@')[0].toLowerCase().replace(/[^a-z0-9._-]/g, ''); return suffix > 0 ? `${base}${suffix}` : base; } class RocketChatProvisioner implements ServiceProvisioner { readonly config: ProvisionerConfig = { serviceKey: 'rocketchat', displayName: 'Rocket.Chat', featureFlag: 'enableChat', permissionsKey: '_rcUserId', roleMap: ROLE_MAP, excludeRoles: [], // TEMP users get 'user' role, still provisioned for chat }; async isAvailable(): Promise { return rocketchatClient.healthCheck(); } async provision(user: CMUser): Promise { try { // Check if already provisioned let rcUser = await rocketchatClient.findUserByEmail(user.email); if (!rcUser) { // Create with collision-safe username let username = generateUsername(user.email); let suffix = 0; const maxAttempts = 5; while (suffix < maxAttempts) { try { rcUser = await rocketchatClient.createUser({ email: user.email, name: user.name || user.email.split('@')[0], username, password: generateRCPassword(user.id), roles: ROLE_MAP[user.role] || ['user'], }); break; } catch (err) { if (err instanceof Error && err.message.includes('already in use')) { suffix++; username = generateUsername(user.email, suffix); } else { throw err; } } } if (!rcUser) { return { success: false, error: 'Failed to create RC user after retries' }; } logger.info(`RC provisioner: created user ${rcUser._id} for ${user.email}`); } // Persist RC user ID in permissions const permissions = (user.permissions as Record) || {}; await prisma.user.update({ where: { id: user.id }, data: { permissions: { ...permissions, [this.config.permissionsKey]: rcUser._id, } as unknown as Prisma.InputJsonValue, }, }); return { success: true, serviceUserId: rcUser._id }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger.error(`RC provisioner: provision failed for ${user.email}: ${msg}`); return { success: false, error: msg }; } } async syncUser(user: CMUser, serviceUserId: string): Promise { const rcRoles = ROLE_MAP[user.role] || ['user']; await rocketchatClient.updateUser(serviceUserId, { name: user.name || user.email.split('@')[0], roles: rcRoles, }); } async deactivate(serviceUserId: string): Promise { await rocketchatClient.setUserActive(serviceUserId, false); } async getAuthToken(user: CMUser, serviceUserId: string): Promise { try { // Sync roles on every auth token request await this.syncUser(user, serviceUserId).catch(err => { logger.warn('RC role sync failed during auth, continuing:', err); }); const tokenData = await rocketchatClient.createUserToken(serviceUserId); return tokenData.authToken; } catch (err) { logger.error('RC provisioner: getAuthToken failed:', err); return null; } } } export const rocketchatProvisioner = new RocketChatProvisioner();