diff --git a/dev/tool/src/calendar.ts b/dev/tool/src/calendar.ts index 7cc2659c7e..775c433e0e 100644 --- a/dev/tool/src/calendar.ts +++ b/dev/tool/src/calendar.ts @@ -87,17 +87,16 @@ export async function performCalendarAccountMigrations (db: Db, region: string | const workspacePersonsMap = new Map>() -async function getPersonIdByEmail ( - workspace: WorkspaceUuid, - email: string, - oldId: string -): Promise { +async function getPersonIdByEmail (workspace: WorkspaceUuid, email: string): Promise { const map = workspacePersonsMap.get(workspace) if (map != null) { return map[email] } else { const transactorUrl = await getWorkspaceTransactorEndpoint(workspace) - const token = generateToken(systemAccountUuid, workspace) + const token = generateToken(systemAccountUuid, workspace, { + model: 'upgrade', + mode: 'backup' + }) const client = await createClient(transactorUrl, token) try { const res: Record = {} @@ -138,7 +137,7 @@ async function migrateCalendarIntegrations ( if (!isActiveMode(ws.mode)) continue token.workspace = ws.uuid - const personId = await getPersonIdByEmail(ws.uuid, token.email, token.userId) + const personId = await getPersonIdByEmail(ws.uuid, token.email) if (personId == null) { console.error('No socialId found for token', token) continue @@ -224,7 +223,7 @@ async function migrateCalendarHistory ( } if (!isActiveMode(ws.mode)) continue - const personId = await getPersonIdByEmail(ws.uuid, history.email, history.userId) + const personId = await getPersonIdByEmail(ws.uuid, history.email) if (personId == null) { console.error('No socialId found for token', token) continue diff --git a/dev/tool/src/db.ts b/dev/tool/src/db.ts index ba63870073..49ed8b2312 100644 --- a/dev/tool/src/db.ts +++ b/dev/tool/src/db.ts @@ -22,7 +22,8 @@ import { type SocialKey, type AccountUuid, parseSocialIdString, - DOMAIN_SPACE + DOMAIN_SPACE, + AccountRole } from '@hcengineering/core' import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo' import { @@ -192,11 +193,15 @@ export async function moveAccountDbFromMongoToPG ( pgDb: AccountDB ): Promise { const mdb = mongoDb as MongoAccountDB + const BATCH_SIZE = 5000 + const WS_BATCH_SIZE = 2000 ctx.info('Starting migration of persons...') const personsCursor = mdb.person.findCursor({}) try { let personsCount = 0 + let personsBatch: any[] = [] + while (await personsCursor.hasNext()) { const person = await personsCursor.next() if (person == null) break @@ -211,13 +216,20 @@ export async function moveAccountDbFromMongoToPG ( person.lastName = '' } - await pgDb.person.insertOne(person) - personsCount++ - if (personsCount % 100 === 0) { + personsBatch.push(person) + if (personsBatch.length >= BATCH_SIZE) { + await pgDb.person.insertMany(personsBatch) + personsCount += personsBatch.length ctx.info(`Migrated ${personsCount} persons...`) + personsBatch = [] } } } + // Insert remaining batch + if (personsBatch.length > 0) { + await pgDb.person.insertMany(personsBatch) + personsCount += personsBatch.length + } ctx.info(`Migrated ${personsCount} persons`) } finally { await personsCursor.close() @@ -227,6 +239,9 @@ export async function moveAccountDbFromMongoToPG ( const accountsCursor = mdb.account.findCursor({}) try { let accountsCount = 0 + let accountsBatch: any[] = [] + let passwordsBatch: any[] = [] + while (await accountsCursor.hasNext()) { const account = await accountsCursor.next() if (account == null) break @@ -238,16 +253,34 @@ export async function moveAccountDbFromMongoToPG ( delete account.hash delete account.salt - await pgDb.account.insertOne(account) + accountsBatch.push(account) if (hash != null && salt != null) { - await pgDb.setPassword(account.uuid, hash, salt) + passwordsBatch.push([account.uuid, hash, salt]) } - accountsCount++ - if (accountsCount % 100 === 0) { + + if (accountsBatch.length >= BATCH_SIZE) { + await pgDb.account.insertMany(accountsBatch) + for (const [accountUuid, hash, salt] of passwordsBatch) { + await pgDb.setPassword(accountUuid, hash, salt) + } + + accountsCount += accountsBatch.length ctx.info(`Migrated ${accountsCount} accounts...`) + accountsBatch = [] + passwordsBatch = [] } } } + // Insert remaining batch + if (accountsBatch.length > 0) { + await pgDb.account.insertMany(accountsBatch) + accountsCount += accountsBatch.length + } + if (passwordsBatch.length > 0) { + for (const [accountUuid, hash, salt] of passwordsBatch) { + await pgDb.setPassword(accountUuid, hash, salt) + } + } ctx.info(`Migrated ${accountsCount} accounts`) } finally { await accountsCursor.close() @@ -257,6 +290,7 @@ export async function moveAccountDbFromMongoToPG ( const socialIdsCursor = mdb.socialId.findCursor({}) try { let socialIdsCount = 0 + let socialIdsBatch: any[] = [] while (await socialIdsCursor.hasNext()) { const socialId = await socialIdsCursor.next() if (socialId == null) break @@ -267,13 +301,22 @@ export async function moveAccountDbFromMongoToPG ( delete (socialId as any).id delete (socialId as any)._id // Types of _id are incompatible - await pgDb.socialId.insertOne(socialId) - socialIdsCount++ - if (socialIdsCount % 100 === 0) { - ctx.info(`Migrated ${socialIdsCount} social IDs...`) + socialIdsBatch.push(socialId) + + if (socialIdsBatch.length >= BATCH_SIZE) { + await pgDb.socialId.insertMany(socialIdsBatch) + + socialIdsCount += socialIdsBatch.length + ctx.info(`Migrated ${socialIdsCount} social ids...`) + socialIdsBatch = [] } } } + // Insert remaining batch + if (socialIdsBatch.length > 0) { + await pgDb.socialId.insertMany(socialIdsBatch) + socialIdsCount += socialIdsBatch.length + } ctx.info(`Migrated ${socialIdsCount} social IDs`) } finally { await socialIdsCursor.close() @@ -283,6 +326,7 @@ export async function moveAccountDbFromMongoToPG ( const accountEventsCursor = mdb.accountEvent.findCursor({}) try { let eventsCount = 0 + let eventsBatch: any[] = [] while (await accountEventsCursor.hasNext()) { const accountEvent = await accountEventsCursor.next() if (accountEvent == null) break @@ -296,13 +340,21 @@ export async function moveAccountDbFromMongoToPG ( const account = await pgDb.account.findOne({ uuid: accountEvent.accountUuid }) if (account == null) continue // Not a big deal if we don't move the event for non-existing account - await pgDb.accountEvent.insertOne(accountEvent) - eventsCount++ - if (eventsCount % 100 === 0) { + eventsBatch.push(accountEvent) + + if (eventsBatch.length >= BATCH_SIZE) { + await pgDb.accountEvent.insertMany(eventsBatch) + eventsCount += eventsBatch.length ctx.info(`Migrated ${eventsCount} account events...`) + eventsBatch = [] } } } + // Insert remaining batch + if (eventsBatch.length > 0) { + await pgDb.accountEvent.insertMany(eventsBatch) + eventsCount += eventsBatch.length + } ctx.info(`Migrated ${eventsCount} account events`) } finally { await accountEventsCursor.close() @@ -312,6 +364,9 @@ export async function moveAccountDbFromMongoToPG ( const workspacesCursor = mdb.workspace.findCursor({}) try { let workspacesCount = 0 + let workspacesBatch: any[] = [] + let workspacesStatusesBatch: any[] = [] + let workspacesMembersBatch: any[] = [] let membersCount = 0 while (await workspacesCursor.hasNext()) { const workspace = await workspacesCursor.next() @@ -333,25 +388,49 @@ export async function moveAccountDbFromMongoToPG ( } if (workspace.createdOn == null) { - delete workspace.createdOn + workspace.createdOn = Date.now() } - await pgDb.createWorkspace(workspace, status) - workspacesCount++ + workspacesBatch.push(workspace) + workspacesStatusesBatch.push(status) const members = await mdb.getWorkspaceMembers(workspace.uuid) for (const member of members) { const alreadyAssigned = await pgDb.getWorkspaceRole(member.person, workspace.uuid) if (alreadyAssigned != null) continue - await pgDb.assignWorkspace(member.person, workspace.uuid, member.role) - membersCount++ + workspacesMembersBatch.push([member.person, workspace.uuid, member.role]) } - if (workspacesCount % 100 === 0) { + if (workspacesBatch.length >= WS_BATCH_SIZE) { + const workspaceUuids = await pgDb.workspace.insertMany(workspacesBatch) + workspacesCount += workspacesBatch.length + workspacesBatch = [] + + await pgDb.workspaceStatus.insertMany( + workspacesStatusesBatch.map((s, i) => ({ ...s, workspaceUuid: workspaceUuids[i] })) + ) + workspacesStatusesBatch = [] + + await pgDb.batchAssignWorkspace(workspacesMembersBatch) + membersCount += workspacesMembersBatch.length + workspacesMembersBatch = [] + ctx.info(`Migrated ${workspacesCount} workspaces...`) } } + + // Insert remaining batch + if (workspacesBatch.length > 0) { + const workspaceUuids = await pgDb.workspace.insertMany(workspacesBatch) + workspacesCount += workspacesBatch.length + await pgDb.workspaceStatus.insertMany( + workspacesStatusesBatch.map((s, i) => ({ ...s, workspaceUuid: workspaceUuids[i] })) + ) + await pgDb.batchAssignWorkspace(workspacesMembersBatch) + membersCount += workspacesMembersBatch.length + } + ctx.info(`Migrated ${workspacesCount} workspaces with ${membersCount} member assignments`) } finally { await workspacesCursor.close() @@ -360,7 +439,10 @@ export async function moveAccountDbFromMongoToPG ( ctx.info('Starting migration of invites...') const invitesCursor = mdb.invite.findCursor({}) try { + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + const MAX_INT_8 = 9223372036854775807 let invitesCount = 0 + let invitesBatch: any[] = [] while (await invitesCursor.hasNext()) { const invite = await invitesCursor.next() if (invite == null) break @@ -370,15 +452,30 @@ export async function moveAccountDbFromMongoToPG ( delete (invite as any).id + if (invite.expiresOn > MAX_INT_8 || typeof invite.expiresOn !== 'number') { + invite.expiresOn = -1 + } + + if (["USER'", 'ADMIN'].includes(invite.role as any)) { + invite.role = AccountRole.User + } + const exists = await pgDb.invite.findOne({ migratedFrom: invite.migratedFrom }) if (exists == null) { - await pgDb.invite.insertOne(invite) - invitesCount++ - if (invitesCount % 100 === 0) { + invitesBatch.push(invite) + + if (invitesBatch.length >= BATCH_SIZE) { + await pgDb.invite.insertMany(invitesBatch) + invitesCount += invitesBatch.length ctx.info(`Migrated ${invitesCount} invites...`) + invitesBatch = [] } } } + if (invitesBatch.length > 0) { + await pgDb.invite.insertMany(invitesBatch) + invitesCount += invitesBatch.length + } ctx.info(`Migrated ${invitesCount} invites`) } finally { await invitesCursor.close() diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 11ead587bd..889d4cb5b5 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -174,11 +174,15 @@ export function devTool ( setMetadata(serverClientPlugin.metadata.Endpoint, accountsUrl) setMetadata(serverToken.metadata.Secret, serverSecret) - async function withAccountDatabase (f: (db: AccountDB) => Promise, dbOverride?: string): Promise { + async function withAccountDatabase ( + f: (db: AccountDB) => Promise, + dbOverride?: string, + nsOverride?: string + ): Promise { const uri = dbOverride ?? getAccountDBUrl() - console.log(`connecting to database '${uri}'...`) + const ns = nsOverride ?? process.env.ACCOUNT_DB_NS - const [accountDb, closeAccountsDb] = await getAccountDB(uri) + const [accountDb, closeAccountsDb] = await getAccountDB(uri, ns) try { await f(accountDb) } catch (err: any) { @@ -2299,10 +2303,16 @@ export function devTool ( throw new Error('MONGO_URL and DB_URL are the same') } + const mongoNs = process.env.OLD_ACCOUNTS_NS + await withAccountDatabase(async (pgDb) => { - await withAccountDatabase(async (mongoDb) => { - await moveAccountDbFromMongoToPG(toolCtx, mongoDb, pgDb) - }, mongodbUri) + await withAccountDatabase( + async (mongoDb) => { + await moveAccountDbFromMongoToPG(toolCtx, mongoDb, pgDb) + }, + mongodbUri, + mongoNs + ) }, dbUrl) }) diff --git a/plugins/contact-assets/assets/profile-background-light.png b/plugins/contact-assets/assets/profile-background-light.png index df0827bcc0..33cd8ff406 100644 Binary files a/plugins/contact-assets/assets/profile-background-light.png and b/plugins/contact-assets/assets/profile-background-light.png differ diff --git a/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte b/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte index 8cb211731e..4aed359842 100644 --- a/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte +++ b/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> @@ -80,7 +84,7 @@ person={employee} name={employee?.name} {disabled} - showStatus + showStatus={isEmployee} statusSize="medium" style="modern" clickable @@ -94,14 +98,16 @@ -
- -
+ {#if isEmployee} +
+ +
+ {/if} {:else}
diff --git a/plugins/contact-resources/src/components/person/ModernProfilePopup.svelte b/plugins/contact-resources/src/components/person/ModernProfilePopup.svelte index 8e67dd0d72..c8d2168a57 100644 --- a/plugins/contact-resources/src/components/person/ModernProfilePopup.svelte +++ b/plugins/contact-resources/src/components/person/ModernProfilePopup.svelte @@ -80,7 +80,7 @@ .image-container { /* image-container */ width: 100%; - min-height: 8rem; + min-height: 6.5rem; display: flex; /* Inside auto layout */ @@ -110,7 +110,7 @@ right: 0; top: 0; bottom: 0; - height: 7.25rem; + height: 5.5rem; border-radius: 0; background-size: contain; diff --git a/plugins/setting-assets/lang/cs.json b/plugins/setting-assets/lang/cs.json index d03ecb4846..e30cd0fc3f 100644 --- a/plugins/setting-assets/lang/cs.json +++ b/plugins/setting-assets/lang/cs.json @@ -153,6 +153,9 @@ "EnablePermissions": "Povolit řízení přístupu na základě rolí", "DisablePermissionsConfirmation": "Opravdu chcete zakázat řízení přístupu na základě rolí? Všechny role a oprávnění budou deaktivovány.", "EnablePermissionsConfirmation": "Opravdu chcete povolit řízení přístupu na základě rolí? Všechny role a oprávnění budou aktivovány.", - "BetaWarning": "Moduly označené jako beta jsou k dispozici pro experimentální účely a nemusí být plně funkční. V tuto chvíli nedoporučujeme spoléhat se na funkce beta pro kritickou práci." + "BetaWarning": "Moduly označené jako beta jsou k dispozici pro experimentální účely a nemusí být plně funkční. V tuto chvíli nedoporučujeme spoléhat se na funkce beta pro kritickou práci.", + "IntegrationFailed": "Nepodařilo se vytvořit integraci", + "IntegrationError": "Zkuste to prosím znovu nebo kontaktujte podporu, pokud problém přetrvává", + "EmailIsUsed": "E-mailová adresa je již použita jiným účtem" } } diff --git a/plugins/setting-assets/lang/de.json b/plugins/setting-assets/lang/de.json index 278678cfee..743fbcbb28 100644 --- a/plugins/setting-assets/lang/de.json +++ b/plugins/setting-assets/lang/de.json @@ -153,6 +153,9 @@ "EnablePermissions": "Rollenbasierte Zugriffskontrolle aktivieren", "DisablePermissionsConfirmation": "Sind Sie sicher, dass Sie die rollenbasierte Zugriffskontrolle deaktivieren möchten? Alle Rollen und Berechtigungen werden deaktiviert.", "EnablePermissionsConfirmation": "Sind Sie sicher, dass Sie die rollenbasierte Zugriffskontrolle aktivieren möchten? Alle Rollen und Berechtigungen werden aktiviert.", - "BetaWarningDe": "Als Beta gekennzeichnete Module sind zu experimentellen Zwecken verfügbar und funktionieren möglicherweise nicht vollständig. Wir empfehlen derzeit nicht, sich auf Beta-Funktionen für kritische Arbeiten zu verlassen." + "BetaWarningDe": "Als Beta gekennzeichnete Module sind zu experimentellen Zwecken verfügbar und funktionieren möglicherweise nicht vollständig. Wir empfehlen derzeit nicht, sich auf Beta-Funktionen für kritische Arbeiten zu verlassen.", + "IntegrationFailed": "Integration konnte nicht erstellt werden", + "IntegrationError": "Bitte versuchen Sie es erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht", + "EmailIsUsed": "E-Mail-Adresse wird in einem anderen Konto verwendet" } } diff --git a/plugins/setting-assets/lang/en.json b/plugins/setting-assets/lang/en.json index 9068059283..ed0031d642 100644 --- a/plugins/setting-assets/lang/en.json +++ b/plugins/setting-assets/lang/en.json @@ -153,5 +153,9 @@ "EnablePermissions": "Enable role-based access control", "DisablePermissionsConfirmation": "Are you sure you want to disable role-based access control? All roles and permissions will be disabled.", "EnablePermissionsConfirmation": "Are you sure you want to enable role-based access control? All roles and permissions will be enabled.", - "BetaWarning": "Modules labeled as beta are available for experimental purposes and may not be fully functional. We do not recommend relying on beta features for critical work at this time." } -} \ No newline at end of file + "BetaWarning": "Modules labeled as beta are available for experimental purposes and may not be fully functional. We do not recommend relying on beta features for critical work at this time.", + "IntegrationFailed": "Failed to create integration", + "IntegrationError": "Please try again or contact support if the problem persists", + "EmailIsUsed": "Email address is already used by another account" + } +} diff --git a/plugins/setting-assets/lang/es.json b/plugins/setting-assets/lang/es.json index f5604dbd6f..9b1eb3bafc 100644 --- a/plugins/setting-assets/lang/es.json +++ b/plugins/setting-assets/lang/es.json @@ -144,6 +144,9 @@ "EnablePermissions": "Activar el control de acceso basado en roles", "DisablePermissionsConfirmation": "¿Está seguro de que desea desactivar el control de acceso basado en roles? Todos los roles y permisos serán desactivados.", "EnablePermissionsConfirmation": "¿Está seguro de que desea activar el control de acceso basado en roles? Todos los roles y permisos serán activados.", - "BetaWarning": "Los módulos etiquetados como beta están disponibles con fines experimentales y pueden no ser completamente funcionales. No recomendamos confiar en las funciones beta para trabajos críticos en este momento." + "BetaWarning": "Los módulos etiquetados como beta están disponibles con fines experimentales y pueden no ser completamente funcionales. No recomendamos confiar en las funciones beta para trabajos críticos en este momento.", + "IntegrationFailed": "Error al crear la integración", + "IntegrationError": "Por favor, inténtelo de nuevo o contacte con soporte si el problema persiste", + "EmailIsUsed": "El correo electrónico ya está en uso" } } diff --git a/plugins/setting-assets/lang/fr.json b/plugins/setting-assets/lang/fr.json index 4b47013c01..1683c4d95d 100644 --- a/plugins/setting-assets/lang/fr.json +++ b/plugins/setting-assets/lang/fr.json @@ -153,6 +153,9 @@ "EnablePermissions": "Activer le contrôle d'accès basé sur les rôles", "DisablePermissionsConfirmation": "Êtes-vous sûr de vouloir désactiver le contrôle d'accès basé sur les rôles ? Tous les rôles et permissions seront désactivés.", "EnablePermissionsConfirmation": "Êtes-vous sûr de vouloir activer le contrôle d'accès basé sur les rôles ? Tous les rôles et permissions seront activés.", - "BetaWarning": "Les modules étiquetés comme bêta sont disponibles à des fins expérimentales et peuvent ne pas être entièrement fonctionnels. Nous ne recommandons pas de compter sur les fonctionnalités bêta pour un travail critique pour le moment." + "BetaWarning": "Les modules étiquetés comme bêta sont disponibles à des fins expérimentales et peuvent ne pas être entièrement fonctionnels. Nous ne recommandons pas de compter sur les fonctionnalités bêta pour un travail critique pour le moment.", + "IntegrationFailed": "Échec de la création de l'intégration", + "IntegrationError": "Veuillez réessayer ou contacter le support si le problème persiste", + "EmailIsUsed": "L'adresse e-mail est déjà utilisée par un autre compte" } } diff --git a/plugins/setting-assets/lang/it.json b/plugins/setting-assets/lang/it.json index 27d12c13db..d9b8584739 100644 --- a/plugins/setting-assets/lang/it.json +++ b/plugins/setting-assets/lang/it.json @@ -153,6 +153,9 @@ "EnablePermissions": "Abilita il controllo degli accessi basato sui ruoli", "DisablePermissionsConfirmation": "Sei sicuro di voler disabilitare il controllo degli accessi basato sui ruoli? Tutti i ruoli e le autorizzazioni verranno disabilitati.", "EnablePermissionsConfirmation": "Sei sicuro di voler abilitare il controllo degli accessi basato sui ruoli? Tutti i ruoli e le autorizzazioni verranno abilitati.", - "BetaWarning": "I moduli contrassegnati come beta sono disponibili per scopi sperimentali e potrebbero non funzionare completamente. Non ti consigliamo di fare affidamento sulle funzionalità beta per il lavoro critico in questo momento." + "BetaWarning": "I moduli contrassegnati come beta sono disponibili per scopi sperimentali e potrebbero non funzionare completamente. Non ti consigliamo di fare affidamento sulle funzionalità beta per il lavoro critico in questo momento.", + "IntegrationFailed": "Impossibile creare l'integrazione", + "IntegrationError": "Si prega di riprovare o contattare il supporto se il problema persiste", + "EmailIsUsed": "L'email è già in uso" } } diff --git a/plugins/setting-assets/lang/ja.json b/plugins/setting-assets/lang/ja.json index b829a38280..d5bc663aa9 100644 --- a/plugins/setting-assets/lang/ja.json +++ b/plugins/setting-assets/lang/ja.json @@ -153,6 +153,9 @@ "EnablePermissions": "ロールベースのアクセス制御を有効にする", "DisablePermissionsConfirmation": "本当にロールベースのアクセス制御を無効にしますか?すべての役割と権限が無効になります。", "EnablePermissionsConfirmation": "本当にロールベースのアクセス制御を有効にしますか?すべての役割と権限が有効になります。", - "BetaWarning": "ベータ版としてラベル付けされたモジュールは、実験的な目的で利用可能であり、完全に機能しない場合があります。現時点では、重要な作業にベータ版機能を依存することはお勧めしません。" + "BetaWarning": "ベータ版としてラベル付けされたモジュールは、実験的な目的で利用可能であり、完全に機能しない場合があります。現時点では、重要な作業にベータ版機能を依存することはお勧めしません。", + "IntegrationFailed": "統合に失敗しました", + "IntegrationError": "問題が解決しない場合は、もう一度お試しいただくか、サポートにお問い合わせください", + "EmailIsUsed": "メールアドレスは他のアカウントで使用されています" } } diff --git a/plugins/setting-assets/lang/pt.json b/plugins/setting-assets/lang/pt.json index aa110649e2..2e7cae902f 100644 --- a/plugins/setting-assets/lang/pt.json +++ b/plugins/setting-assets/lang/pt.json @@ -144,6 +144,9 @@ "EnablePermissions": "Ativar controle de acesso baseado em funções", "DisablePermissionsConfirmation": "Tem certeza de que deseja desativar o controle de acesso baseado em funções? Todos os papéis e permissões serão desativados.", "EnablePermissionsConfirmation": "Tem certeza de que deseja ativar o controle de acesso baseado em funções? Todos os papéis e permissões serão ativados.", - "BetaWarning": "Modules labeled as beta are available for experimental purposes and may not be fully functional. We do not recommend relying on beta features for critical work at this time." + "BetaWarning": "Modules labeled as beta are available for experimental purposes and may not be fully functional. We do not recommend relying on beta features for critical work at this time.", + "IntegrationFailed": "Falha na integração", + "IntegrationError": "Tente novamente ou entre em contato com o suporte se o problema persistir", + "EmailIsUsed": "Este e-mail já está em uso" } } diff --git a/plugins/setting-assets/lang/ru.json b/plugins/setting-assets/lang/ru.json index 462bb0adc1..88e4f96739 100644 --- a/plugins/setting-assets/lang/ru.json +++ b/plugins/setting-assets/lang/ru.json @@ -153,6 +153,9 @@ "EnablePermissions": "Включить ролевое управление доступом", "DisablePermissionsConfirmation": "Вы уверены, что хотите отключить ролевое управление доступом? Все роли и разрешения будут отключены.", "EnablePermissionsConfirmation": "Вы уверены, что хотите включить ролевое управление доступом? Все роли и разрешения будут включены.", - "BetaWarning": "Модули, помеченные как бета-версии, доступны для экспериментальных целей и могут быть не полностью функциональными. Мы не рекомендуем полагаться на функции бета-версии для критической работы в настоящее время." + "BetaWarning": "Модули, помеченные как бета-версии, доступны для экспериментальных целей и могут быть не полностью функциональными. Мы не рекомендуем полагаться на функции бета-версии для критической работы в настоящее время.", + "IntegrationFailed": "Не удалось создать интеграцию", + "IntegrationError": "Пожалуйста, попробуйте снова или свяжитесь с поддержкой, если проблема не исчезнет", + "EmailIsUsed": "Адрес электронной почты уже используется другим аккаунтом" } } diff --git a/plugins/setting-assets/lang/zh.json b/plugins/setting-assets/lang/zh.json index 80214e830c..3dca20e6e3 100644 --- a/plugins/setting-assets/lang/zh.json +++ b/plugins/setting-assets/lang/zh.json @@ -153,6 +153,9 @@ "EnablePermissions": "启用基于角色的访问控制", "DisablePermissionsConfirmation": "您确定要禁用基于角色的访问控制吗?所有角色和权限都将被禁用。", "EnablePermissionsConfirmation": "您确定要启用基于角色的访问控制吗?所有角色和权限都将被启用。", - "BetaWarning": "标记为测试版的模块可用于实验目的,可能无法完全正常工作。我们不建议在此时依赖测试版功能进行关键工作。" + "BetaWarning": "标记为测试版的模块可用于实验目的,可能无法完全正常工作。我们不建议在此时依赖测试版功能进行关键工作。", + "IntegrationFailed": "创建集成失败", + "IntegrationError": "请重试,如果问题仍然存在,请联系客服支持", + "EmailIsUsed": "该电子邮件地址已被其他账户使用" } } diff --git a/plugins/setting-resources/src/components/IntegrationErrorNotification.svelte b/plugins/setting-resources/src/components/IntegrationErrorNotification.svelte new file mode 100644 index 0000000000..e99e0de367 --- /dev/null +++ b/plugins/setting-resources/src/components/IntegrationErrorNotification.svelte @@ -0,0 +1,29 @@ + + + + + + + {notification.subTitle} + + diff --git a/plugins/setting-resources/src/components/Integrations.svelte b/plugins/setting-resources/src/components/Integrations.svelte index 2500bab21e..9afd052375 100644 --- a/plugins/setting-resources/src/components/Integrations.svelte +++ b/plugins/setting-resources/src/components/Integrations.svelte @@ -13,12 +13,16 @@ // limitations under the License. -->
diff --git a/plugins/setting/src/index.ts b/plugins/setting/src/index.ts index 057815fcf6..3048e8eb49 100644 --- a/plugins/setting/src/index.ts +++ b/plugins/setting/src/index.ts @@ -111,6 +111,10 @@ export interface WorkspaceSetting extends Doc { icon?: Ref | null } +export enum IntegrationError { + EMAIL_IS_ALREADY_USED = 'EMAIL_IS_ALREADY_USED' +} + /** * @public */ @@ -239,7 +243,10 @@ export default plugin(settingId, { MailboxErrorMailboxExists: '' as IntlString, MailboxErrorMailboxCountLimit: '' as IntlString, DeleteMailbox: '' as IntlString, - MailboxDeleteConfirmation: '' as IntlString + MailboxDeleteConfirmation: '' as IntlString, + IntegrationFailed: '' as IntlString, + IntegrationError: '' as IntlString, + EmailIsUsed: '' as IntlString }, icon: { AccountSettings: '' as Asset, diff --git a/pods/external/services.d/hulygun.service b/pods/external/services.d/hulygun.service index 1cc6db6241..971962a7d6 100644 --- a/pods/external/services.d/hulygun.service +++ b/pods/external/services.d/hulygun.service @@ -1 +1 @@ -hulygun hardcoreeng/service_hulygun:0.1.1 \ No newline at end of file +hulygun hardcoreeng/service_hulygun:0.1.3 \ No newline at end of file diff --git a/server/account-service/src/index.ts b/server/account-service/src/index.ts index 83cb85da26..cb6d01c1c6 100644 --- a/server/account-service/src/index.ts +++ b/server/account-service/src/index.ts @@ -50,6 +50,7 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap } const oldAccsUrl = process.env.OLD_ACCOUNTS_URL ?? (dbUrl.startsWith('mongodb://') ? dbUrl : undefined) + const oldAccsNs = process.env.OLD_ACCOUNTS_NS const transactorUri = process.env.TRANSACTOR_URL if (transactorUri === undefined) { @@ -112,7 +113,7 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap const accountsDb = getAccountDB(dbUrl, dbNs) const migrations = accountsDb.then(async ([db]) => { if (oldAccsUrl !== undefined) { - await migrateFromOldAccounts(oldAccsUrl, db) + await migrateFromOldAccounts(oldAccsUrl, db, oldAccsNs) console.log('Migrations verified/done') } }) diff --git a/server/account-service/src/migration/migration.ts b/server/account-service/src/migration/migration.ts index e3a07416d2..ebc95d94eb 100644 --- a/server/account-service/src/migration/migration.ts +++ b/server/account-service/src/migration/migration.ts @@ -49,10 +49,14 @@ async function shouldMigrate ( } } -export async function migrateFromOldAccounts (oldAccsUrl: string, accountDB: AccountDB): Promise { +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) + const [oldAccountDb, closeOldDb] = await getMongoAccountDB(oldAccsUrl, oldAccsNs) let processingHandle try { @@ -234,6 +238,7 @@ async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promi 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 { diff --git a/server/account/src/__tests__/mongo.test.ts b/server/account/src/__tests__/mongo.test.ts index d1b999ef33..118d35ce77 100644 --- a/server/account/src/__tests__/mongo.test.ts +++ b/server/account/src/__tests__/mongo.test.ts @@ -696,7 +696,8 @@ describe('MongoAccountDB', () => { hasNext: jest.fn().mockReturnValue(false), close: jest.fn() })), - updateOne: jest.fn() + updateOne: jest.fn(), + ensureIndices: jest.fn() } mockWorkspace = { diff --git a/server/account/src/collections/mongo.ts b/server/account/src/collections/mongo.ts index 82e3a0526d..b8c24a31f5 100644 --- a/server/account/src/collections/mongo.ts +++ b/server/account/src/collections/mongo.ts @@ -184,6 +184,10 @@ implements DbCollection { return (idKey !== undefined ? toInsert[idKey] : undefined) as K extends keyof T ? T[K] : undefined } + async insertMany (data: Array>): Promise : undefined> { + throw new Error('Not implemented') + } + async updateOne (query: Query, ops: Operations): Promise { const resOps: any = { $set: {} } @@ -346,6 +350,10 @@ export class WorkspaceStatusMongoDbCollection implements DbCollection[]): Promise { + throw new Error('Not implemented') + } + async updateOne (query: Query, ops: Operations): Promise { await this.wsCollection.updateOne(this.toWsQuery(query), this.toWsOperations(ops)) } @@ -420,6 +428,13 @@ export class MongoAccountDB implements AccountDB { } ]) + await this.socialId.ensureIndices([ + { + key: { type: 1, value: 1 }, + options: { unique: true, name: 'hc_account_social_id_type_value_1' } + } + ]) + await this.workspace.ensureIndices([ { key: { uuid: 1 }, @@ -538,6 +553,16 @@ export class MongoAccountDB implements AccountDB { }) } + async batchAssignWorkspace (data: [AccountUuid, WorkspaceUuid, AccountRole][]): Promise { + await this.workspaceMembers.insertMany( + data.map(([accountId, workspaceId, role]) => ({ + workspaceUuid: workspaceId, + accountUuid: accountId, + role + })) + ) + } + async unassignWorkspace (accountId: AccountUuid, workspaceId: WorkspaceUuid): Promise { await this.workspaceMembers.deleteMany({ workspaceUuid: workspaceId, diff --git a/server/account/src/collections/postgres/postgres.ts b/server/account/src/collections/postgres/postgres.ts index db0444607f..dd3d9365f6 100644 --- a/server/account/src/collections/postgres/postgres.ts +++ b/server/account/src/collections/postgres/postgres.ts @@ -292,6 +292,42 @@ implements DbCollection { return res[0][idKey] } + async insertMany (data: Array>, client?: Sql): Promise : undefined> { + const snakeData = convertKeysToSnakeCase(data) + const columns = new Set() + for (const record of snakeData) { + Object.keys(record).forEach((k) => columns.add(k)) + } + const columnsList = Array.from(columns).sort() + + const values: any[] = [] + for (const record of snakeData) { + const recordValues = columnsList.map((col) => record[col] ?? null) + values.push(...recordValues) + } + + const placeholders = snakeData + .map((_: any, i: number) => `(${columnsList.map((_, j) => `$${i * columnsList.length + j + 1}`).join(', ')})`) + .join(', ') + + const sql = ` + INSERT INTO ${this.getTableName()} + (${columnsList.map((k) => `"${k}"`).join(', ')}) + VALUES ${placeholders} + RETURNING * + ` + + const _client = client ?? this.client + const res: any = await _client.unsafe(sql, values) + const idKey = this.idKey + + if (idKey === undefined) { + return undefined as any + } + + return res.map((r: any) => r[idKey]) + } + protected buildUpdateClause (ops: Operations, lastRefIdx: number = 0): [string, any[]] { const updateChunks: string[] = [] const values: any[] = [] @@ -650,6 +686,19 @@ export class PostgresAccountDB implements AccountDB { .client`INSERT INTO ${this.client(this.getWsMembersTableName())} (workspace_uuid, account_uuid, role) VALUES (${workspaceUuid}, ${accountUuid}, ${role})` } + async batchAssignWorkspace (data: [AccountUuid, WorkspaceUuid, AccountRole][]): Promise { + const placeholders = data.map((_: any, i: number) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`).join(', ') + const values = data.flat() + + const sql = ` + INSERT INTO ${this.getWsMembersTableName()} + (account_uuid, workspace_uuid, role) + VALUES ${placeholders} + ` + + await this.client.unsafe(sql, values) + } + async unassignWorkspace (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid): Promise { await this .client`DELETE FROM ${this.client(this.getWsMembersTableName())} WHERE workspace_uuid = ${workspaceUuid} AND account_uuid = ${accountUuid}` diff --git a/server/account/src/types.ts b/server/account/src/types.ts index f2b5bc0ab9..b680ece60c 100644 --- a/server/account/src/types.ts +++ b/server/account/src/types.ts @@ -201,6 +201,7 @@ export interface AccountDB { init: () => Promise createWorkspace: (data: WorkspaceData, status: WorkspaceStatusData) => Promise assignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise + batchAssignWorkspace: (data: [AccountUuid, WorkspaceUuid, AccountRole][]) => Promise updateWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise unassignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise getWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise @@ -222,6 +223,7 @@ export interface DbCollection { find: (query: Query, sort?: Sort, limit?: number) => Promise findOne: (query: Query) => Promise insertOne: (data: Partial) => Promise + insertMany: (data: Partial[]) => Promise updateOne: (query: Query, ops: Operations) => Promise deleteMany: (query: Query) => Promise } diff --git a/services/calendar/pod-calendar/src/auth.ts b/services/calendar/pod-calendar/src/auth.ts index d99b6bd6df..7157cb54c0 100644 --- a/services/calendar/pod-calendar/src/auth.ts +++ b/services/calendar/pod-calendar/src/auth.ts @@ -243,7 +243,7 @@ export class AuthController { secret: JSON.stringify(_token) } try { - const currentIntegration = this.accountClient.getIntegrationSecret({ + const currentIntegration = await this.accountClient.getIntegrationSecret({ socialId: this.user.userId, kind: CALENDAR_INTEGRATION, workspaceUuid: this.user.workspace, diff --git a/services/calendar/pod-calendar/src/watch.ts b/services/calendar/pod-calendar/src/watch.ts index 8afe977e15..d92cd794ab 100644 --- a/services/calendar/pod-calendar/src/watch.ts +++ b/services/calendar/pod-calendar/src/watch.ts @@ -273,10 +273,14 @@ export class WatchController { return } } - if (calendarId != null) { - await watchCalendar(user, email, calendarId, googleClient) - } else { - await watchCalendars(user, email, googleClient) + try { + if (calendarId != null) { + await watchCalendar(user, email, calendarId, googleClient) + } else { + await watchCalendars(user, email, googleClient) + } + } catch (err: any) { + console.error('Watch add error', user.workspace, user.userId, calendarId, err) } } } diff --git a/services/gmail/pod-gmail/src/accounts.ts b/services/gmail/pod-gmail/src/accounts.ts index 93a8f99ada..9f1c5f9650 100644 --- a/services/gmail/pod-gmail/src/accounts.ts +++ b/services/gmail/pod-gmail/src/accounts.ts @@ -15,6 +15,7 @@ import { AccountUuid, Person, PersonId, SocialId, SocialIdType, buildSocialIdString } from '@hcengineering/core' import { getAccountClient } from '@hcengineering/server-client' import { generateToken } from '@hcengineering/server-token' +import { IntegrationError } from '@hcengineering/setting' import { serviceToken } from './utils' @@ -67,7 +68,7 @@ export async function getOrCreateSocialId (account: AccountUuid, email: string): } if (socialId.personUuid !== account) { - throw new Error('Social id connected to another account') + throw new Error(IntegrationError.EMAIL_IS_ALREADY_USED) } return socialId diff --git a/services/gmail/pod-gmail/src/main.ts b/services/gmail/pod-gmail/src/main.ts index 7da8a39086..86f83c3658 100644 --- a/services/gmail/pod-gmail/src/main.ts +++ b/services/gmail/pod-gmail/src/main.ts @@ -101,15 +101,23 @@ export const main = async (): Promise => { endpoint: '/signin/code', type: 'get', handler: async (req, res) => { + let state: State | undefined try { ctx.info('Signin code request received') const code = req.query.code as string - const state = JSON.parse(decode64(req.query.state as string)) as unknown as State + state = JSON.parse(decode64(req.query.state as string)) as unknown as State await gmailController.createClient(state, code) res.redirect(state.redirectURL) - } catch (err) { - ctx.error('Failed to process signin code', { message: (err as any).message }) - res.status(500).send() + } catch (err: any) { + ctx.error('Failed to process signin code', { message: err.message }) + if (state !== undefined) { + const errorMessage = encodeURIComponent(err.message) + const url = new URL(state.redirectURL) + url.searchParams.append('integrationError', errorMessage) + res.redirect(url.toString()) + } else { + res.status(500).send() + } } } },