// // Copyright © 2025 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. import { AccountRole, type PersonUuid, SocialIdType, type WorkspaceDataId, type WorkspaceUuid, type SocialKey, type AccountUuid } from '@hcengineering/core' import { type AccountDB, createAccount } from '@hcengineering/account' import { getMongoAccountDB } from './utils' import { type Account as OldAccount, type Workspace as OldWorkspace } from './types' import { type MongoAccountDB } from './collections/mongo' async function shouldMigrate ( oldAccountDb: MongoAccountDB, migrationKey: string ): Promise<{ completed: boolean, exists: boolean }> { while (true) { const migration = await oldAccountDb.migration.findOne({ key: migrationKey }) if (migration?.completed === true) { return { completed: true, exists: true } } if (migration == null) { return { completed: false, exists: false } } if (migration.lastProcessedTime === undefined || Date.now() - migration.lastProcessedTime > 1000 * 15) { return { completed: false, exists: true } } console.log('Migration of accounts database from old accounts is still in progress, waiting...') await new Promise((resolve) => setTimeout(resolve, 5000)) } } export async function migrateFromOldAccounts ( oldAccsUrl: string, accountDB: AccountDB, oldAccsNs?: string ): Promise { const migrationKey = 'migrate-from-old-accounts' // Check if old accounts exist const [oldAccountDb, closeOldDb] = await getMongoAccountDB(oldAccsUrl, oldAccsNs) let processingHandle try { const { completed, exists } = await shouldMigrate(oldAccountDb, migrationKey) if (completed) { return } if (!exists) { await oldAccountDb.migration.insertOne({ key: migrationKey, completed: false, lastProcessedTime: Date.now() }) } else { await oldAccountDb.migration.updateOne({ key: migrationKey }, { completed: false, lastProcessedTime: Date.now() }) } processingHandle = setInterval(() => { void oldAccountDb.migration.updateOne({ key: migrationKey }, { lastProcessedTime: Date.now() }) }, 1000 * 5) // Mapping between const accountsIdToUuid: Record = {} // Mapping between const accountsEmailToUuid: Record = {} // Mapping between const workspacesIdToUuid: Record = {} console.log('Migrating accounts database from old accounts') let accountsProcessed = 0 const accountsCursor = oldAccountDb.account.findCursor({}) try { while (await accountsCursor.hasNext()) { const account = await accountsCursor.next() if (account == null) { break } const accountUuid = await migrateAccount(account, accountDB) if (accountUuid == null) { console.log('Account not migrated', account) continue } accountsIdToUuid[account._id.toString()] = accountUuid accountsEmailToUuid[account.email] = accountUuid accountsProcessed++ if (accountsProcessed % 100 === 0) { console.log('Processed accounts:', accountsProcessed) } } } finally { await accountsCursor.close() } console.log('Total accounts processed:', accountsProcessed) let processedWorkspaces = 0 const workspacesCursor = oldAccountDb.workspace.findCursor({}) try { while (await workspacesCursor.hasNext()) { const workspace = await workspacesCursor.next() if (workspace == null) { break } const workspaceUuid = await migrateWorkspace(workspace, accountDB, accountsIdToUuid, accountsEmailToUuid) if (workspaceUuid !== undefined) { workspacesIdToUuid[workspace.workspace] = workspaceUuid } processedWorkspaces++ if (processedWorkspaces % 100 === 0) { console.log('Processed workspaces:', processedWorkspaces) } } } finally { await workspacesCursor.close() } console.log('Total workspaces processed:', processedWorkspaces) console.log('Total workspaces created/ensured:', Object.values(workspacesIdToUuid).length) let invitesProcessed = 0 const invitesCursor = oldAccountDb.invite.findCursor({}) try { while (await invitesCursor.hasNext()) { const invite = await invitesCursor.next() if (invite == null) { break } const workspaceUuid = workspacesIdToUuid[invite.workspace.name] if (workspaceUuid === undefined) { console.log('No workspace with id', invite.workspace.name, 'found for invite', invite._id) continue } await accountDB.invite.insertOne({ migratedFrom: invite._id.toString(), workspaceUuid, expiresOn: invite.exp, emailPattern: invite.emailMask, remainingUses: invite.limit, role: invite.role ?? AccountRole.User }) invitesProcessed++ if (invitesProcessed % 100 === 0) { console.log('Processed invites:', invitesProcessed) } } } finally { await invitesCursor.close() } console.log('Total invites processed:', invitesProcessed) await oldAccountDb.migration.updateOne({ key: migrationKey }, { completed: true }) console.log('Migration of accounts database from old accounts COMPLETED') } finally { if (processingHandle !== undefined) { clearTimeout(processingHandle) } closeOldDb() } } async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promise { let primaryKey: SocialKey let secondaryKey: SocialKey | undefined if (account.githubId != null) { if (account.githubUser == null) { console.log('No github user found for github id', account.githubId) return } primaryKey = { type: SocialIdType.GITHUB, value: account.githubUser } secondaryKey = !account.email.startsWith('github:') ? { type: SocialIdType.EMAIL, value: account.email } : undefined } else if (account.openId != null) { primaryKey = { type: SocialIdType.OIDC, value: account.openId } secondaryKey = !account.email.startsWith('openid:') ? { type: SocialIdType.EMAIL, value: account.email } : undefined } else { primaryKey = { type: SocialIdType.EMAIL, value: account.email } } let personUuid: PersonUuid const verified = account.confirmed === true ? { verifiedOn: Date.now() } : {} const existing = await accountDB.socialId.findOne(primaryKey) if (existing == null) { // Create new global person personUuid = await accountDB.person.insertOne({ firstName: account.first, lastName: account.last }) await accountDB.socialId.insertOne({ ...primaryKey, personUuid, ...verified }) await createAccount(accountDB, personUuid, account.confirmed, false, account.createdOn) if (account.hash != null && account.salt != null) { // NOTE: for Mongo->CR migration use db method to update password instead await accountDB.account.updateOne({ uuid: personUuid as AccountUuid }, { hash: account.hash, salt: account.salt }) } } else { personUuid = existing.personUuid } if (secondaryKey != null) { const existingSecondary = await accountDB.socialId.findOne(secondaryKey) if (existingSecondary == null) { await accountDB.socialId.insertOne({ ...secondaryKey, personUuid, ...verified }) } } return personUuid as AccountUuid } async function migrateWorkspace ( workspace: OldWorkspace, accountDB: AccountDB, accountsIdToUuid: Record, accountsEmailToUuid: Record ): Promise { if (workspace.workspaceUrl == null) { console.log('No workspace url, skipping', workspace.workspace) return } const createdBy = workspace.createdBy !== undefined ? accountsEmailToUuid[workspace.createdBy] : ('N/A' as AccountUuid) if (createdBy === undefined) { console.log('No account found for workspace', workspace.workspace, 'created by', workspace.createdBy) return } const existingWorkspace = await accountDB.workspace.findOne({ url: workspace.workspaceUrl }) let workspaceUuid: WorkspaceUuid if (existingWorkspace == null) { workspaceUuid = await accountDB.workspace.insertOne({ uuid: workspace.uuid, name: workspace.workspaceName, url: workspace.workspaceUrl, dataId: workspace.workspace, branding: workspace.branding, region: workspace.region, createdBy, billingAccount: createdBy, createdOn: workspace.createdOn }) } else { workspaceUuid = existingWorkspace.uuid } const existingStatus = await accountDB.workspaceStatus.findOne({ workspaceUuid }) if (existingStatus == null) { await accountDB.workspaceStatus.insertOne({ workspaceUuid, mode: workspace.mode, processingProgress: workspace.progress, versionMajor: workspace.version?.major, versionMinor: workspace.version?.minor, versionPatch: workspace.version?.patch, lastProcessingTime: workspace.lastProcessingTime, lastVisit: workspace.lastVisit, isDisabled: workspace.disabled, processingAttempts: workspace.attempts, processingMessage: workspace.message, backupInfo: workspace.backupInfo }) } const existingMembers = new Set((await accountDB.getWorkspaceMembers(workspaceUuid)).map((mi) => mi.person)) for (const member of (workspace.accounts ?? []).map((it) => it.toString())) { const accountUuid = accountsIdToUuid[member] if (accountUuid === undefined) { console.log('No account found for workspace', workspace.workspace, 'member', member) continue } if (existingMembers.has(accountUuid)) { continue } // Actual roles are being set in workspace migration await accountDB.assignWorkspace(accountUuid, workspaceUuid, AccountRole.Guest) } return workspaceUuid }