131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
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<string, string[]> = {
|
|
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<boolean> {
|
|
return rocketchatClient.healthCheck();
|
|
}
|
|
|
|
async provision(user: CMUser): Promise<ProvisionResult> {
|
|
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<string, unknown>) || {};
|
|
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<void> {
|
|
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<void> {
|
|
await rocketchatClient.setUserActive(serviceUserId, false);
|
|
}
|
|
|
|
async getAuthToken(user: CMUser, serviceUserId: string): Promise<string | null> {
|
|
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();
|