From 38d4a1e3672c462ea7d9abb768039ab684e69c03 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Fri, 2 Feb 2024 18:36:52 +0700 Subject: [PATCH] UBERF-5265: Fix workspace creation (#4499) Signed-off-by: Andrey Sobolev --- dev/tool/src/index.ts | 12 ++++- pods/account/src/index.ts | 6 +-- server/account/src/index.ts | 95 ++++++++++++++++++++++--------------- server/tool/src/index.ts | 25 +++++----- 4 files changed, 83 insertions(+), 55 deletions(-) diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 9f15b4aa47..5f7c20905b 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -211,7 +211,17 @@ export function devTool ( .action(async (workspace, cmd) => { const { mongodbUri, txes, version, migrateOperations } = prepareTools() await withDatabase(mongodbUri, async (db) => { - await createWorkspace(version, txes, migrateOperations, db, productId, cmd.email, cmd.workspaceName, workspace) + const { client } = await createWorkspace( + version, + txes, + migrateOperations, + db, + productId, + cmd.email, + cmd.workspaceName, + workspace + ) + await client?.close() }) }) diff --git a/pods/account/src/index.ts b/pods/account/src/index.ts index f09d26ae00..aab9eb9464 100644 --- a/pods/account/src/index.ts +++ b/pods/account/src/index.ts @@ -15,6 +15,8 @@ // import account, { ACCOUNT_DB, AccountMethod, accountId } from '@hcengineering/account' +import accountEn from '@hcengineering/account/lang/en.json' +import accountRu from '@hcengineering/account/lang/ru.json' import platform, { Severity, Status, addStringsLoader, setMetadata } from '@hcengineering/platform' import serverToken from '@hcengineering/server-token' import toolPlugin from '@hcengineering/server-tool' @@ -24,8 +26,6 @@ import Koa from 'koa' import bodyParser from 'koa-bodyparser' import Router from 'koa-router' import { MongoClient } from 'mongodb' -import accountEn from '@hcengineering/account/lang/en.json' -import accountRu from '@hcengineering/account/lang/ru.json' /** * @public @@ -127,8 +127,8 @@ export function serveAccount (methods: Record, productId }) const close = (): void => { + void client.close() server.close() - process.exit(0) } process.on('uncaughtException', (e) => { diff --git a/server/account/src/index.ts b/server/account/src/index.ts index b9525af530..5162b8e48d 100644 --- a/server/account/src/index.ts +++ b/server/account/src/index.ts @@ -25,6 +25,8 @@ import contact, { } from '@hcengineering/contact' import core, { AccountRole, + BackupClient, + Client, concatLink, Data, generateId, @@ -45,7 +47,7 @@ import toolPlugin, { connect, initModel, upgradeModel } from '@hcengineering/ser import { pbkdf2Sync, randomBytes } from 'crypto' import { Binary, Db, Filter, ObjectId } from 'mongodb' import fetch from 'node-fetch' -import accountPlugin, { accountId } from './plugin' +import accountPlugin from './plugin' const WORKSPACE_COLLECTION = 'workspace' const ACCOUNT_COLLECTION = 'account' @@ -389,10 +391,10 @@ export async function confirmEmail (db: Db, _email: string): Promise { console.log(`confirm email:${email}`) if (account === null) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountId })) + throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: _email })) } if (account.confirmed === true) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountAlreadyConfirmed, { account: accountId })) + throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountAlreadyConfirmed, { account: _email })) } await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $set: { confirmed: true } }) @@ -407,7 +409,7 @@ export async function confirm (db: Db, productId: string, token: string): Promis const decode = decodeToken(token) const _email = decode.extra?.confirm if (_email === undefined) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountId })) + throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: _email })) } const email = cleanEmail(_email) const account = await confirmEmail(db, email) @@ -692,7 +694,7 @@ export async function createWorkspace ( email: string, workspaceName: string, workspace?: string -): Promise<{ workspaceInfo: Workspace, err?: any }> { +): Promise<{ workspaceInfo: Workspace, err?: any, client?: Client & BackupClient }> { // We need to search for duplicate workspaceUrl await searchPromise @@ -700,12 +702,14 @@ export async function createWorkspace ( searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace) const workspaceInfo = await searchPromise + let client: Client & BackupClient try { const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) const wsId = getWorkspaceId(workspaceInfo.workspace, productId) if (initWS !== undefined) { if ((await getWorkspaceById(db, productId, initWS)) !== null) { - await initModel(getTransactor(), wsId, txes, []) + client = await initModel(getTransactor(), wsId, txes, []) + await client.close() await cloneWorkspace( getTransactor(), getWorkspaceId(initWS, productId), @@ -714,11 +718,11 @@ export async function createWorkspace ( await upgradeModel(getTransactor(), wsId, txes, migrationOperation) } } - await initModel(getTransactor(), wsId, txes, migrationOperation) + client = await initModel(getTransactor(), wsId, txes, migrationOperation) } catch (err: any) { - return { workspaceInfo, err } + return { workspaceInfo, err, client: {} as any } } - return { workspaceInfo } + return { workspaceInfo, client } } /** @@ -793,7 +797,7 @@ export const createUserWorkspace = } } - const { workspaceInfo, err } = await createWorkspace( + const { workspaceInfo, err, client } = await createWorkspace( version, txes, migrationOperation, @@ -815,15 +819,19 @@ export const createUserWorkspace = ) throw err } - info.lastWorkspace = Date.now() + try { + info.lastWorkspace = Date.now() - // Update last workspace time. - await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } }) + // Update last workspace time. + await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } }) - const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) - const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null - await assignWorkspace(db, productId, email, workspaceInfo.workspace, shouldUpdateAccount) - await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner) + const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) + const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null + await assignWorkspace(db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client) + await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner, client) + } finally { + await client?.close() + } const result = { endpoint: getEndpoint(), email, @@ -924,27 +932,31 @@ async function getWorkspaceAndAccount ( productId: string, _email: string, workspaceUrl: string -): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> { +): Promise<{ account: Account, workspace: Workspace }> { const email = cleanEmail(_email) const wsPromise = await getWorkspaceById(db, productId, workspaceUrl) if (wsPromise === null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl })) } - const workspaceId = wsPromise._id const account = await getAccount(db, email) if (account === null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email })) } - const accountId = account._id - return { accountId, workspaceId } + return { account, workspace: wsPromise } } /** * @public */ -export async function setRole (_email: string, workspace: string, productId: string, role: AccountRole): Promise { +export async function setRole ( + _email: string, + workspace: string, + productId: string, + role: AccountRole, + client?: Client & BackupClient +): Promise { const email = cleanEmail(_email) - const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId)) + const connection = client ?? (await connect(getTransactor(), getWorkspaceId(workspace, productId))) try { const ops = new TxOperations(connection, core.account.System) @@ -957,7 +969,9 @@ export async function setRole (_email: string, workspace: string, productId: str }) } } finally { - await connection.close() + if (client === undefined) { + await connection.close() + } } } @@ -969,7 +983,8 @@ export async function assignWorkspace ( productId: string, _email: string, workspaceId: string, - shouldReplaceAccount: boolean = false + shouldReplaceAccount: boolean = false, + client?: Client & BackupClient ): Promise { const email = cleanEmail(_email) const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) @@ -977,21 +992,20 @@ export async function assignWorkspace ( throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) } const workspaceInfo = await getWorkspaceAndAccount(db, productId, email, workspaceId) - const account = await db.collection(ACCOUNT_COLLECTION).findOne({ _id: accountId }) - if (account !== null) { - await createPersonAccount(account, productId, workspaceId, shouldReplaceAccount) + if (workspaceInfo.account !== null) { + await createPersonAccount(workspaceInfo.account, productId, workspaceId, shouldReplaceAccount, client) } // Add account into workspace. await db .collection(WORKSPACE_COLLECTION) - .updateOne({ _id: workspaceInfo.workspaceId }, { $addToSet: { accounts: workspaceInfo.accountId } }) + .updateOne({ _id: workspaceInfo.workspace._id }, { $addToSet: { accounts: workspaceInfo.account._id } }) // Add workspace to account await db .collection(ACCOUNT_COLLECTION) - .updateOne({ _id: workspaceInfo.accountId }, { $addToSet: { workspaces: workspaceInfo.workspaceId } }) + .updateOne({ _id: workspaceInfo.account._id }, { $addToSet: { workspaces: workspaceInfo.workspace._id } }) } async function createEmployee (ops: TxOperations, name: string, _email: string): Promise> { @@ -1073,9 +1087,10 @@ async function createPersonAccount ( account: Account, productId: string, workspace: string, - shouldReplaceCurrent: boolean = false + shouldReplaceCurrent: boolean = false, + client?: Client & BackupClient ): Promise { - const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId)) + const connection = client ?? (await connect(getTransactor(), getWorkspaceId(workspace, productId))) try { const ops = new TxOperations(connection, core.account.System) @@ -1113,7 +1128,9 @@ async function createPersonAccount ( } } } finally { - await connection.close() + if (client === undefined) { + await connection.close() + } } } @@ -1207,12 +1224,12 @@ export async function restorePassword (db: Db, productId: string, token: string, const decode = decodeToken(token) const email = decode.extra?.restore if (email === undefined) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountId })) + throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email })) } const account = await getAccount(db, email) if (account === null) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountId })) + throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email })) } const salt = randomBytes(32) const hash = hashWithSalt(password, salt) @@ -1225,14 +1242,14 @@ export async function restorePassword (db: Db, productId: string, token: string, /** * @public */ -export async function removeWorkspace (db: Db, productId: string, email: string, workspace: string): Promise { - const { workspaceId, accountId } = await getWorkspaceAndAccount(db, productId, email, workspace) +export async function removeWorkspace (db: Db, productId: string, email: string, workspaceId: string): Promise { + const { workspace, account } = await getWorkspaceAndAccount(db, productId, email, workspaceId) // Add account into workspace. - await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $pull: { accounts: accountId } }) + await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspace._id }, { $pull: { accounts: account._id } }) // Add account a workspace - await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: accountId }, { $pull: { workspaces: workspaceId } }) + await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $pull: { workspaces: workspace._id } }) } /** diff --git a/server/tool/src/index.ts b/server/tool/src/index.ts index 105b203da9..15d089f8d1 100644 --- a/server/tool/src/index.ts +++ b/server/tool/src/index.ts @@ -116,13 +116,14 @@ export async function initModel ( rawTxes: Tx[], migrateOperations: [string, MigrateOperation][], logger: ModelLogger = consoleModelLogger -): Promise { +): Promise { const { mongodbUri, minio, txes } = prepareTools(rawTxes) if (txes.some((tx) => tx.objectSpace !== core.space.Model)) { throw Error('Model txes must target only core.space.Model') } const client = new MongoClient(mongodbUri) + let connection: CoreClient & BackupClient try { await client.connect() const db = getWorkspaceDB(client, workspaceId) @@ -136,30 +137,30 @@ export async function initModel ( logger.log(`${result.insertedCount} model transactions inserted.`) logger.log('creating data...', transactorUrl) - const connection = (await connect(transactorUrl, workspaceId, undefined, { + connection = (await connect(transactorUrl, workspaceId, undefined, { model: 'upgrade' })) as unknown as CoreClient & BackupClient + try { for (const op of migrateOperations) { logger.log('Migrage', op[0]) await op[1].upgrade(connection, logger) } + + // Create update indexes + await createUpdateIndexes(connection, db, logger) + + logger.log('create minio bucket') + if (!(await minio.exists(workspaceId))) { + await minio.make(workspaceId) + } } catch (e) { logger.log(e) - } finally { - await connection.close() - } - - // Create update indexes - await createUpdateIndexes(connection, db, logger) - - logger.log('create minio bucket') - if (!(await minio.exists(workspaceId))) { - await minio.make(workspaceId) } } finally { await client.close() } + return connection } /**