import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IImportService } from '@rocket.chat/core-services'; import type { IImportUser, IImport, ImportStatus } from '@rocket.chat/core-typings'; import { Imports, ImportData } from '@rocket.chat/models'; import { ObjectId } from 'mongodb'; import { Importers } from '../../lib/import'; import { validateRoleList } from '../../lib/roles/validateRoleList'; import { settings } from '../../settings'; import { getNewUserRoles } from '../user/lib/getNewUserRoles'; export class ImportService extends ServiceClassInternal implements IImportService { protected name = 'import'; public async clear(): Promise { await Imports.invalidateAllOperations(); await ImportData.col.deleteMany({}); } public async newOperation(userId: string, name: string, key: string): Promise { // Make sure there's no other operation running await this.clear(); const importId = ( await Imports.insertOne({ type: name, importerKey: key, ts: new Date(), status: 'importer_new', valid: true, user: userId, }) ).insertedId; const operation = await Imports.findOneById(importId); if (!operation) { throw new Error('failed to create import operation'); } return operation; } private getStateOfOperation(operation: IImport): 'none' | 'new' | 'loading' | 'ready' | 'importing' | 'done' | 'error' | 'canceled' { if (!operation.valid && operation.status !== 'importer_done') { return 'error'; } switch (operation.status) { case 'importer_new': return 'new'; case 'importer_uploading': case 'importer_downloading_file': case 'importer_file_loaded': case 'importer_preparing_started': case 'importer_preparing_users': case 'importer_preparing_channels': case 'importer_preparing_messages': case 'importer_preparing_contacts': return 'loading'; case 'importer_user_selection': return 'ready'; case 'importer_importing_started': case 'importer_importing_users': case 'importer_importing_channels': case 'importer_importing_messages': case 'importer_importing_contacts': case 'importer_importing_files': case 'importer_finishing': return 'importing'; case 'importer_done': return 'done'; case 'importer_import_failed': return 'error'; case 'importer_import_cancelled': return 'canceled'; } } public async status(): Promise { const operation = await Imports.findLastImport(); if (!operation) { return { state: 'none', }; } const state = this.getStateOfOperation(operation); return { state, operation, }; } private assertsValidStateForNewData(operation: IImport | undefined): asserts operation is IImport { if (!operation?.valid) { throw new Error('Import operation not initialized.'); } const state = this.getStateOfOperation(operation); switch (state) { case 'loading': case 'importing': throw new Error('The current import operation can not receive new data.'); case 'done': case 'error': case 'canceled': throw new Error('The current import operation is already finished.'); } } public async addUsers(users: Omit[]): Promise { if (!users.length) { return; } const operation = await Imports.findLastImport(); this.assertsValidStateForNewData(operation); const defaultRoles = getNewUserRoles(); const userRoles = new Set(defaultRoles); for await (const user of users) { if (!user.emails?.some((value) => value) || !user.importIds?.some((value) => value)) { throw new Error('Users are missing required data.'); } if (user.roles?.length) { for (const roleId of user.roles) { userRoles.add(roleId); } } } if (userRoles.size > 0 && !(await validateRoleList([...userRoles]))) { throw new Error('One or more of the users have been assigned invalid roles.'); } await ImportData.col.insertMany( users.map((data) => ({ _id: new ObjectId().toHexString(), data: { ...data, roles: data.roles ? [...new Set([...data.roles, ...defaultRoles])] : defaultRoles, }, dataType: 'user', _updatedAt: new Date(), })), ); await Imports.increaseTotalCount(operation._id, 'users', users.length); await Imports.setOperationStatus(operation._id, 'importer_user_selection'); } public async run(userId: string): Promise { const operation = await Imports.findLastImport(); if (!operation?.valid) { throw new Error('error-operation-not-found'); } if (operation.status !== 'importer_user_selection') { throw new Error('error-invalid-operation-status'); } const { importerKey } = operation; const importer = Importers.get(importerKey); if (!importer) { throw new Error('error-importer-not-defined'); } // eslint-disable-next-line new-cap const instance = new importer.importer(importer, operation, { skipUserCallbacks: true, skipDefaultChannels: true, enableEmail2fa: settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In'), quickUserInsertion: true, skipExistingUsers: true, }); await instance.startImport({ users: { all: true } }, userId); } }