From 4cb1f3e411dd4e505daceea14fb54dd0878bbd16 Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Wed, 9 Apr 2025 06:52:06 +0400 Subject: [PATCH] UBERF-9732: Use huly id as primary social id (#8499) * uberf-9732: use huly id as primary social id Signed-off-by: Alexey Zinoviev * uberf-9732: fix person id filter Signed-off-by: Alexey Zinoviev --- .vscode/launch.json | 5 +- models/view/src/index.ts | 12 +- packages/account-client/src/client.ts | 11 +- packages/core/src/classes.ts | 4 +- packages/core/src/utils.ts | 3 +- .../src/components/PersonIdFilter.svelte | 202 ++++++++++++++++++ plugins/contact-resources/src/index.ts | 10 +- plugins/contact/src/index.ts | 3 +- plugins/contact/src/utils.ts | 20 +- plugins/login-resources/src/utils.ts | 3 +- .../components/filter/PersonIdFilter.svelte | 26 +++ .../PersonIdFilterValuePresenter.svelte | 13 +- plugins/view-resources/src/index.ts | 3 + plugins/view/src/index.ts | 1 + .../calendar-resources/src/index.ts | 26 ++- server-plugins/contact/src/utils.ts | 34 +-- .../src/index.ts | 18 +- .../src/migration/migration.ts | 20 +- server/account/src/__tests__/mongo.test.ts | 27 +-- server/account/src/__tests__/postgres.test.ts | 24 +-- server/account/src/__tests__/utils.test.ts | 11 +- server/account/src/collections/mongo.ts | 20 +- server/account/src/collections/postgres.ts | 18 +- server/account/src/operations.ts | 78 +++---- server/account/src/serviceOperations.ts | 44 +++- server/account/src/types.ts | 23 +- server/account/src/utils.ts | 46 ++-- server/tool/src/initializer.ts | 8 +- .../ai-bot/pod-ai-bot/src/workspace/love.ts | 3 +- 29 files changed, 478 insertions(+), 238 deletions(-) create mode 100644 plugins/contact-resources/src/components/PersonIdFilter.svelte create mode 100644 plugins/view-resources/src/components/filter/PersonIdFilter.svelte diff --git a/.vscode/launch.json b/.vscode/launch.json index 1e18068d3b..bbec12c7f5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -53,13 +53,14 @@ "MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json", // "SERVER_PROVIDER":"uweb" "SERVER_PROVIDER": "ws", - "MODEL_VERSION": "0.7.1", + "MODEL_VERSION": "0.7.48", // "VERSION": "0.6.289", "ELASTIC_INDEX_NAME": "local_storage_index", "UPLOAD_URL": "/files", "AI_BOT_URL": "http://localhost:4010", "STATS_URL": "http://huly.local:4900", - "STREAM_URL": "http://huly.local:1080/recording" + "STREAM_URL": "http://huly.local:1080/recording", + "QUEUE_CONFIG": "localhost:19092" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "runtimeVersion": "20", diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 70f1e37ace..76e1958507 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -998,10 +998,6 @@ export function createModel (builder: Builder): void { group: 'bottom' }) - builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeFilter, { - component: view.component.ValueFilter - }) - builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.AttributeFilter, { component: view.component.ValueFilter }) @@ -1242,10 +1238,6 @@ export function createModel (builder: Builder): void { presenter: view.component.StringFilterPresenter }) - builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeFilterPresenter, { - presenter: view.component.StringFilterPresenter - }) - builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.AttributeFilterPresenter, { presenter: view.component.StringFilterPresenter }) @@ -1304,6 +1296,10 @@ export function createModel (builder: Builder): void { presenter: view.component.PersonIdFilterValuePresenter }) + builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.PersonIdFilter + }) + builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.AttributePresenter, { presenter: view.component.PersonIdPresenter, arrayPresenter: view.component.PersonArrayEditor diff --git a/packages/account-client/src/client.ts b/packages/account-client/src/client.ts index 6cbaf21e55..8a62cde5ed 100644 --- a/packages/account-client/src/client.ts +++ b/packages/account-client/src/client.ts @@ -29,7 +29,8 @@ import { type WorkspaceUserOperation, type WorkspaceUuid, type PersonId, - type SocialIdType + type SocialIdType, + type AccountUuid } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' import type { @@ -76,7 +77,7 @@ export interface AccountClient { navigateUrl?: string, expHours?: number ) => Promise - leaveWorkspace: (account: string) => Promise + leaveWorkspace: (account: AccountUuid) => Promise changeUsername: (first: string, last: string) => Promise changePassword: (oldPassword: string, newPassword: string) => Promise signUpJoin: ( @@ -152,7 +153,7 @@ export interface AccountClient { deleteIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise getIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise listIntegrationsSecrets: (filter: Partial) => Promise - getAccountInfo: (uuid: PersonUuid) => Promise + getAccountInfo: (uuid: AccountUuid) => Promise setCookie: () => Promise deleteCookie: () => Promise @@ -355,7 +356,7 @@ class AccountClientImpl implements AccountClient { return await this.rpc(request) } - async leaveWorkspace (account: string): Promise { + async leaveWorkspace (account: AccountUuid): Promise { const request = { method: 'leaveWorkspace' as const, params: { account } @@ -831,7 +832,7 @@ class AccountClientImpl implements AccountClient { return await this.rpc(request) } - async getAccountInfo (uuid: PersonUuid): Promise { + async getAccountInfo (uuid: AccountUuid): Promise { const request = { method: 'getAccountInfo' as const, params: { accountId: uuid } diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 34c3eb38f3..f365b45c6c 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -544,7 +544,7 @@ export interface Person { } export interface PersonInfo extends BasePerson { - socialIds: PersonId[] + socialIds: SocialId[] } /** @@ -830,7 +830,7 @@ export interface WorkspaceInfoWithStatus extends WorkspaceInfo { } export interface WorkspaceMemberInfo { - person: PersonUuid + person: AccountUuid role: AccountRole } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index f9cbf42e99..2c20921077 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -930,8 +930,9 @@ export function pickPrimarySocialId (socialIds: SocialId[]): SocialId { if (socialIds.length === 0) { throw new Error('No social ids provided') } + const hulySocialIds = socialIds.filter((si) => si.type === SocialIdType.HULY) - return socialIds[0] + return hulySocialIds[0] ?? socialIds[0] } export function notEmpty (id: T | undefined | null): id is T { diff --git a/plugins/contact-resources/src/components/PersonIdFilter.svelte b/plugins/contact-resources/src/components/PersonIdFilter.svelte new file mode 100644 index 0000000000..8b1fcb27f0 --- /dev/null +++ b/plugins/contact-resources/src/components/PersonIdFilter.svelte @@ -0,0 +1,202 @@ + + + +
dispatch('changeContent')}> +
+ +
+
+
+ {#if socialIdentitiesPromise} + + {:else} + {#each persons as person} + + {/each} + {/if} +
+
+ diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index 6bae3c536e..8e01a679fd 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -25,6 +25,7 @@ import { import { AccountRole, SocialIdType, + type AccountUuid, type Class, type Client, type Data, @@ -102,6 +103,7 @@ import PersonFilterValuePresenter from './components/PersonFilterValuePresenter. import PersonEditor from './components/PersonEditor.svelte' import PersonIcon from './components/PersonIcon.svelte' import PersonPresenter from './components/PersonPresenter.svelte' +import PersonIdFilter from './components/PersonIdFilter.svelte' import PersonRefPresenter from './components/PersonRefPresenter.svelte' import SelectAvatars from './components/SelectAvatars.svelte' import SelectUsersPopup from './components/SelectUsersPopup.svelte' @@ -199,7 +201,8 @@ export { UserDetails, UserInfo, UsersList, - UsersPopup + UsersPopup, + PersonIdFilter } const toObjectSearchResult = (e: WithLookup): ObjectSearchResult => ({ @@ -299,7 +302,7 @@ async function kickEmployee (doc: Person): Promise { if (doc.personUuid != null) { const leaveWorkspace = await getResource(login.function.LeaveWorkspace) - await leaveWorkspace(doc.personUuid) + await leaveWorkspace(doc.personUuid as AccountUuid) } } }) @@ -390,7 +393,8 @@ export default async (): Promise => ({ EditOrganizationPanel, ChannelIcon, SpaceMembersEditor, - ContactNamePresenter + ContactNamePresenter, + PersonIdFilter }, completion: { EmployeeQuery: async ( diff --git a/plugins/contact/src/index.ts b/plugins/contact/src/index.ts index b03fc03c60..3b0916f908 100644 --- a/plugins/contact/src/index.ts +++ b/plugins/contact/src/index.ts @@ -238,7 +238,8 @@ export const contactPlugin = plugin(contactId, { CreateGuest: '' as AnyComponent, SpaceMembersEditor: '' as AnyComponent, ContactNamePresenter: '' as AnyComponent, - PersonFilterValuePresenter: '' as AnyComponent + PersonFilterValuePresenter: '' as AnyComponent, + PersonIdFilter: '' as AnyComponent }, channelProvider: { Email: '' as Ref, diff --git a/plugins/contact/src/utils.ts b/plugins/contact/src/utils.ts index 3ab66b19b4..cf6f5c71a8 100644 --- a/plugins/contact/src/utils.ts +++ b/plugins/contact/src/utils.ts @@ -29,6 +29,7 @@ import { MeasureContext, notEmpty, PersonId, + pickPrimarySocialId, Ref, SocialId, toIdMap, @@ -283,15 +284,6 @@ export function formatContactName ( return name } -// TODO: remove me in favor of the same util in core package -export function pickPrimarySocialId (ids: PersonId[]): PersonId { - if (ids.length === 0) { - throw new Error('No social ids provided') - } - - return ids[0] -} - export function includesAny (members: PersonId[], ids: PersonId[]): boolean { return members.some((m) => ids.includes(m)) } @@ -337,13 +329,13 @@ export async function getPersonRefsBySocialIds ( } export async function getPrimarySocialId (client: Client, person: Ref): Promise { - const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: person }) + const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: person, verifiedOn: { $gt: 0 } }) if (socialIds.length === 0) { return } - return pickPrimarySocialId(socialIds.map((it) => it._id)) + return pickPrimarySocialId(socialIds)._id } export async function getAllSocialStringsByPersonId (client: Client, personId: PersonId): Promise { @@ -393,12 +385,6 @@ export async function getAllAccounts (client: Client): Promise { return employees.map((it) => it.personUuid).filter(notEmpty) } -export async function getAllEmployeesPrimarySocialStrings (client: Client): Promise { - const socialStringsByPerson = getSocialStringsByEmployee(client) - - return Object.values(socialStringsByPerson).map((it) => pickPrimarySocialId(it)) -} - export async function getAllUserAccounts (client: Client): Promise { const employees = await client.findAll(contact.mixin.Employee, { active: true }) diff --git a/plugins/login-resources/src/utils.ts b/plugins/login-resources/src/utils.ts index 1375ba179c..831dcb57e7 100644 --- a/plugins/login-resources/src/utils.ts +++ b/plugins/login-resources/src/utils.ts @@ -27,6 +27,7 @@ import { AccountRole, concatLink, parseSocialIdString, + type AccountUuid, type Person, type WorkspaceInfoWithStatus, type WorkspaceUserOperation @@ -661,7 +662,7 @@ export async function changeUsername (first: string, last: string): Promise { +export async function leaveWorkspace (account: AccountUuid): Promise { return await getAccountClient().leaveWorkspace(account) } diff --git a/plugins/view-resources/src/components/filter/PersonIdFilter.svelte b/plugins/view-resources/src/components/filter/PersonIdFilter.svelte new file mode 100644 index 0000000000..51b79a318d --- /dev/null +++ b/plugins/view-resources/src/components/filter/PersonIdFilter.svelte @@ -0,0 +1,26 @@ + + + + diff --git a/plugins/view-resources/src/components/filter/PersonIdFilterValuePresenter.svelte b/plugins/view-resources/src/components/filter/PersonIdFilterValuePresenter.svelte index d8a68fddbb..26aa48c393 100644 --- a/plugins/view-resources/src/components/filter/PersonIdFilterValuePresenter.svelte +++ b/plugins/view-resources/src/components/filter/PersonIdFilterValuePresenter.svelte @@ -18,17 +18,20 @@ import { getClient } from '@hcengineering/presentation' import contact, { getPersonRefsBySocialIds, Person } from '@hcengineering/contact' - export let value: [any, PersonId][] + export let value: PersonId[] const client = getClient() let persons: Ref[] = [] - $: void getPersons(value.map((p) => p[1]).flat()) + $: void getPersons(value) async function getPersons (ids: PersonId[]): Promise { if (ids !== undefined) { - const personsMap = await getPersonRefsBySocialIds(client, ids) - persons = Object.values(personsMap) - console.log(persons) + if (ids.length === 0) { + persons = [] + } else { + const personsMap = await getPersonRefsBySocialIds(client, ids) + persons = Object.values(personsMap) + } } } diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index d1e1f64dc3..871501b50a 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -98,6 +98,7 @@ import StatusPresenter from './components/status/StatusPresenter.svelte' import StatusRefPresenter from './components/status/StatusRefPresenter.svelte' import PersonArrayEditor from './components/PersonArrayEditor.svelte' import PersonIdPresenter from './components/PersonIdPresenter.svelte' +import PersonIdFilter from './components/filter/PersonIdFilter.svelte' import PersonIdFilterValuePresenter from './components/filter/PersonIdFilterValuePresenter.svelte' import AudioViewer from './components/viewer/AudioViewer.svelte' import ImageViewer from './components/viewer/ImageViewer.svelte' @@ -176,6 +177,7 @@ export { default as NavLink } from './components/navigator/NavLink.svelte' export { default as StatusPresenter } from './components/status/StatusPresenter.svelte' export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte' export { default as PersonIdPresenter } from './components/PersonIdPresenter.svelte' +export { default as PersonIdFilter } from './components/filter/PersonIdFilter.svelte' export { default as PersonIdFilterValuePresenter } from './components/filter/PersonIdFilterValuePresenter.svelte' export { default as FoldersBrowser } from './components/folders/FoldersBrowser.svelte' export { default as RelationsEditor } from './components/RelationsEditor.svelte' @@ -309,6 +311,7 @@ export default async (): Promise => ({ StatusRefPresenter, PersonArrayEditor, PersonIdPresenter, + PersonIdFilter, PersonIdFilterValuePresenter, DateFilterPresenter, StringFilterPresenter, diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 571f7be5ca..b992df7b26 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -175,6 +175,7 @@ const view = plugin(viewId, { SearchSelector: '' as AnyComponent, FoldersBrowser: '' as AnyComponent, PersonIdPresenter: '' as AnyComponent, + PersonIdFilter: '' as AnyComponent, RolePresenter: '' as AnyComponent }, ids: { diff --git a/server-plugins/calendar-resources/src/index.ts b/server-plugins/calendar-resources/src/index.ts index 1a121051cb..6629b2f96e 100644 --- a/server-plugins/calendar-resources/src/index.ts +++ b/server-plugins/calendar-resources/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // import calendar, { Calendar, Event, ExternalCalendar } from '@hcengineering/calendar' -import contactPlugin, { Employee, Person, SocialIdentity, pickPrimarySocialId } from '@hcengineering/contact' +import contactPlugin, { Employee, Person, SocialIdentity } from '@hcengineering/contact' import core, { Class, concatLink, @@ -33,12 +33,13 @@ import core, { TxProcessor, TxRemoveDoc, TxUpdateDoc, - AccountUuid + AccountUuid, + pickPrimarySocialId } from '@hcengineering/core' import serverCalendar from '@hcengineering/server-calendar' import { getMetadata, getResource } from '@hcengineering/platform' import { TriggerControl } from '@hcengineering/server-core' -import { getPerson, getSocialStrings } from '@hcengineering/server-contact' +import { getPerson, getSocialStrings, getSocialIds } from '@hcengineering/server-contact' import { getHTMLPresenter, getTextPresenter } from '@hcengineering/server-notification-resources' import { generateToken } from '@hcengineering/server-token' @@ -95,10 +96,10 @@ export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise< if (ctx.attributes?.active !== true) continue if (await checkCalendarsExist(control, ctx.objectId)) continue - const socialIds = await getSocialStrings(control, ctx.objectId) + const socialIds = await getSocialIds(control, ctx.objectId) if (socialIds.length === 0) continue - const socialId = pickPrimarySocialId(socialIds) + const socialId = pickPrimarySocialId(socialIds)._id const employee = ( await control.findAll( @@ -283,10 +284,12 @@ async function eventForNewParticipants ( const { _class, space, attachedTo, attachedToClass, collection, ...attr } = event const data = attr as any as Data for (const part of newParticipants) { - const socialStrings = await getSocialStrings(control, part) - if (socialStrings.length === 0) continue + const socialIds = await getSocialIds(control, part) + if (socialIds.length === 0) continue + const socialStrings = socialIds.map((si) => si._id) if (socialStrings.includes(event.createdBy ?? event.modifiedBy)) continue - const primarySocialString = pickPrimarySocialId(socialStrings) + + const primarySocialString = pickPrimarySocialId(socialIds)._id const calendar = getCalendar(calendars, socialStrings) if (calendar === undefined) continue const innerTx = control.txFactory.createTxCreateDoc( @@ -355,10 +358,11 @@ async function onEventCreate (ctx: TxCreateDoc, control: TriggerControl): const calendars = await control.findAll(control.ctx, calendar.class.Calendar, { hidden: false }) const access = 'reader' for (const part of event.participants) { - const socialStrings = await getSocialStrings(control, part as Ref) - if (socialStrings.length === 0) continue + const socialIds = await getSocialIds(control, part as Ref) + if (socialIds.length === 0) continue + const socialStrings = socialIds.map((si) => si._id) if (socialStrings.includes(event.createdBy ?? event.modifiedBy)) continue - const primarySocialString = pickPrimarySocialId(socialStrings) + const primarySocialString = pickPrimarySocialId(socialIds)._id const calendar = getCalendar(calendars, socialStrings) if (calendar === undefined) continue const innerTx = control.txFactory.createTxCreateDoc( diff --git a/server-plugins/contact/src/utils.ts b/server-plugins/contact/src/utils.ts index 872c9a54da..c209d39eae 100644 --- a/server-plugins/contact/src/utils.ts +++ b/server-plugins/contact/src/utils.ts @@ -14,14 +14,8 @@ // import { TriggerControl } from '@hcengineering/server-core' -import contact, { - Employee, - type Person, - PersonSpace, - pickPrimarySocialId, - SocialIdentityRef -} from '@hcengineering/contact' -import { AccountUuid, parseSocialIdString, PersonId, type Ref, toIdMap } from '@hcengineering/core' +import contact, { Employee, type Person, PersonSpace, SocialIdentityRef } from '@hcengineering/contact' +import { AccountUuid, parseSocialIdString, PersonId, type Ref, SocialId, toIdMap } from '@hcengineering/core' export async function getCurrentPerson (control: TriggerControl): Promise { const { type, value } = parseSocialIdString(control.txFactory.account) @@ -39,13 +33,16 @@ export async function getCurrentPerson (control: TriggerControl): Promise): Promise { - const socialIdentities = await control.findAll(control.ctx, contact.class.SocialIdentity, { +export async function getSocialIds (control: TriggerControl, person: Ref): Promise { + return await control.findAll(control.ctx, contact.class.SocialIdentity, { attachedTo: person, - attachedToClass: contact.class.Person + attachedToClass: contact.class.Person, + verifiedOn: { $gt: 0 } }) +} - return socialIdentities.map((s) => s._id) +export async function getSocialStrings (control: TriggerControl, person: Ref): Promise { + return (await getSocialIds(control, person)).map((s) => s._id) } export async function getSocialStringsByPersons ( @@ -194,19 +191,6 @@ export async function getSocialIdsByAccounts ( }, {}) } -export async function getPrimarySocialIdsByAccounts ( - control: TriggerControl, - accounts: AccountUuid[] -): Promise> { - return Object.entries(await getSocialIdsByAccounts(control, accounts)).reduce>( - (acc, [account, sids]) => { - acc[account as AccountUuid] = pickPrimarySocialId(sids) - return acc - }, - {} - ) -} - export async function getAccountBySocialId (control: TriggerControl, socialId: PersonId): Promise { const contextAccount = control.ctx.contextData.socialStringsToUsers.get(socialId) if (contextAccount != null) { diff --git a/server-plugins/controlled-documents-resources/src/index.ts b/server-plugins/controlled-documents-resources/src/index.ts index 8f48d284be..ec13df252e 100644 --- a/server-plugins/controlled-documents-resources/src/index.ts +++ b/server-plugins/controlled-documents-resources/src/index.ts @@ -1,7 +1,7 @@ // // Copyright © 2023-2024 Hardcore Engineering Inc. // -import { Person, pickPrimarySocialId, type Employee } from '@hcengineering/contact' +import { Person, type Employee } from '@hcengineering/contact' import core, { AccountRole, combineAttributes, @@ -13,14 +13,15 @@ import core, { TxCreateDoc, TxFactory, TxUpdateDoc, + systemAccountUuid, + pickPrimarySocialId, type Doc, type RolesAssignment, type Timestamp, - type TxCUD, - systemAccountUuid + type TxCUD } from '@hcengineering/core' import { NotificationType } from '@hcengineering/notification' -import { getEmployees, getSocialStrings } from '@hcengineering/server-contact' +import { getEmployees, getSocialIds } from '@hcengineering/server-contact' import { TriggerControl } from '@hcengineering/server-core' import documents, { @@ -130,9 +131,14 @@ async function createDocumentTrainingRequest (doc: ControlledDocument, control: const dueDate: Timestamp | null = documentTraining.dueDays === null ? null : doc.effectiveDate + documentTraining.dueDays * 24 * 60 * 60 * 1000 - const ownerSocialStrings = await getSocialStrings(control, doc.owner) + const ownerSocialIds = await getSocialIds(control, doc.owner) + if (ownerSocialIds.length === 0) { + console.error(`Owner ${doc.owner} has no social ids`) + return [] + } + // TODO: Encapsulate training request creation logic in training plugin? - const modifiedBy = pickPrimarySocialId(ownerSocialStrings) + const modifiedBy = pickPrimarySocialId(ownerSocialIds)._id let trainees: Array> = documentTraining.trainees const roles = documentTraining.roles diff --git a/server/account-service/src/migration/migration.ts b/server/account-service/src/migration/migration.ts index 79390ca6d3..e3a07416d2 100644 --- a/server/account-service/src/migration/migration.ts +++ b/server/account-service/src/migration/migration.ts @@ -18,7 +18,8 @@ import { SocialIdType, type WorkspaceDataId, type WorkspaceUuid, - type SocialKey + type SocialKey, + type AccountUuid } from '@hcengineering/core' import { type AccountDB, createAccount } from '@hcengineering/account' import { getMongoAccountDB } from './utils' @@ -71,9 +72,9 @@ export async function migrateFromOldAccounts (oldAccsUrl: string, accountDB: Acc }, 1000 * 5) // Mapping between - const accountsIdToUuid: Record = {} + const accountsIdToUuid: Record = {} // Mapping between - const accountsEmailToUuid: Record = {} + const accountsEmailToUuid: Record = {} // Mapping between const workspacesIdToUuid: Record = {} @@ -176,7 +177,7 @@ export async function migrateFromOldAccounts (oldAccsUrl: string, accountDB: Acc } } -async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promise { +async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promise { let primaryKey: SocialKey let secondaryKey: SocialKey | undefined @@ -233,7 +234,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) { - await accountDB.account.updateOne({ uuid: personUuid }, { hash: account.hash, salt: account.salt }) + await accountDB.account.updateOne({ uuid: personUuid as AccountUuid }, { hash: account.hash, salt: account.salt }) } } else { personUuid = existing.personUuid @@ -250,21 +251,22 @@ async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promi } } - return personUuid + return personUuid as AccountUuid } async function migrateWorkspace ( workspace: OldWorkspace, accountDB: AccountDB, - accountsIdToUuid: Record, - accountsEmailToUuid: Record + 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 PersonUuid) + 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 diff --git a/server/account/src/__tests__/mongo.test.ts b/server/account/src/__tests__/mongo.test.ts index a481fdc535..d1b999ef33 100644 --- a/server/account/src/__tests__/mongo.test.ts +++ b/server/account/src/__tests__/mongo.test.ts @@ -21,7 +21,8 @@ import { SocialIdType, AccountRole, type Version, - type Data + type Data, + type AccountUuid } from '@hcengineering/core' import { MongoDbCollection, @@ -299,7 +300,7 @@ describe('AccountMongoDbCollection', () => { const mockDocs = [ { _id: 'id1', - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, hash: { buffer: new Uint8Array([1, 2, 3]) }, salt: { buffer: new Uint8Array([4, 5, 6]) } } @@ -319,7 +320,7 @@ describe('AccountMongoDbCollection', () => { mockCollection.find.mockReturnValue(mockCursor) - const results = await collection.find({ uuid: 'acc1' as PersonUuid }) + const results = await collection.find({ uuid: 'acc1' as AccountUuid }) expect(results[0].hash).toBeInstanceOf(Buffer) expect(results[0].salt).toBeInstanceOf(Buffer) @@ -331,7 +332,7 @@ describe('AccountMongoDbCollection', () => { const mockDocs = [ { _id: 'id1', - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, hash: null, salt: null } @@ -351,7 +352,7 @@ describe('AccountMongoDbCollection', () => { mockCollection.find.mockReturnValue(mockCursor) - const results = await collection.find({ uuid: 'acc1' as PersonUuid }) + const results = await collection.find({ uuid: 'acc1' as AccountUuid }) expect(results[0].hash).toBeNull() expect(results[0].salt).toBeNull() @@ -362,14 +363,14 @@ describe('AccountMongoDbCollection', () => { it('should convert Buffer fields in found document', async () => { const mockDoc = { _id: 'id1', - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, hash: { buffer: new Uint8Array([1, 2, 3]) }, salt: { buffer: new Uint8Array([4, 5, 6]) } } mockCollection.findOne.mockResolvedValue(mockDoc) - const result = await collection.findOne({ uuid: 'acc1' as PersonUuid }) + const result = await collection.findOne({ uuid: 'acc1' as AccountUuid }) expect(result?.hash).toBeInstanceOf(Buffer) expect(result?.salt).toBeInstanceOf(Buffer) @@ -380,14 +381,14 @@ describe('AccountMongoDbCollection', () => { it('should handle null hash and salt in found document', async () => { const mockDoc = { _id: 'id1', - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, hash: null, salt: null } mockCollection.findOne.mockResolvedValue(mockDoc) - const result = await collection.findOne({ uuid: 'acc1' as PersonUuid }) + const result = await collection.findOne({ uuid: 'acc1' as AccountUuid }) expect(result?.hash).toBeNull() expect(result?.salt).toBeNull() @@ -396,7 +397,7 @@ describe('AccountMongoDbCollection', () => { it('should handle null result', async () => { mockCollection.findOne.mockResolvedValue(null) - const result = await collection.findOne({ uuid: 'non-existent' as PersonUuid }) + const result = await collection.findOne({ uuid: 'non-existent' as AccountUuid }) expect(result).toBeNull() }) @@ -789,7 +790,7 @@ describe('MongoAccountDB', () => { }) describe('workspace operations', () => { - const accountId = 'acc1' as PersonUuid + const accountId = 'acc1' as AccountUuid const workspaceId = 'ws1' as WorkspaceUuid const role = AccountRole.Owner @@ -851,7 +852,7 @@ describe('MongoAccountDB', () => { describe('getWorkspaceMembers', () => { it('should return mapped member info', async () => { const members = [ - { accountUuid: 'acc1' as PersonUuid, role: AccountRole.Owner }, + { accountUuid: 'acc1' as AccountUuid, role: AccountRole.Owner }, { accountUuid: 'acc2' as PersonUuid, role: AccountRole.Maintainer } ] @@ -1169,7 +1170,7 @@ describe('MongoAccountDB', () => { }) describe('password operations', () => { - const accountId = 'acc1' as PersonUuid + const accountId = 'acc1' as AccountUuid const passwordHash = Buffer.from('hash') const salt = Buffer.from('salt') diff --git a/server/account/src/__tests__/postgres.test.ts b/server/account/src/__tests__/postgres.test.ts index 4b847d95c2..0a81955c37 100644 --- a/server/account/src/__tests__/postgres.test.ts +++ b/server/account/src/__tests__/postgres.test.ts @@ -16,7 +16,7 @@ import { AccountRole, Data, Version, - type PersonUuid, + type AccountUuid, type WorkspaceMode, type WorkspaceUuid } from '@hcengineering/core' @@ -229,7 +229,7 @@ describe('AccountPostgresDbCollection', () => { it('should join with passwords table', async () => { const mockResult = [ { - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, timezone: 'UTC', locale: 'en', hash: null, @@ -238,7 +238,7 @@ describe('AccountPostgresDbCollection', () => { ] mockClient.unsafe.mockResolvedValue(mockResult) - const result = await collection.find({ uuid: 'acc1' as PersonUuid }) + const result = await collection.find({ uuid: 'acc1' as AccountUuid }) expect(mockClient.unsafe).toHaveBeenCalledWith( `SELECT * FROM ( @@ -260,7 +260,7 @@ describe('AccountPostgresDbCollection', () => { it('should convert buffer fields from database', async () => { const mockResult = [ { - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, timezone: 'UTC', locale: 'en', hash: { 0: 1, 1: 2, 3: 3 }, // Simulating buffer data from DB @@ -269,7 +269,7 @@ describe('AccountPostgresDbCollection', () => { ] mockClient.unsafe.mockResolvedValue(mockResult) - const result = await collection.find({ uuid: 'acc1' as PersonUuid }) + const result = await collection.find({ uuid: 'acc1' as AccountUuid }) expect(result[0].hash).toBeInstanceOf(Buffer) expect(result[0].salt).toBeInstanceOf(Buffer) @@ -290,7 +290,7 @@ describe('AccountPostgresDbCollection', () => { describe('insertOne', () => { it('should prevent inserting password fields', async () => { const doc = { - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, hash: Buffer.from([]), salt: Buffer.from([]) } @@ -300,7 +300,7 @@ describe('AccountPostgresDbCollection', () => { it('should allow inserting non-password fields', async () => { const doc = { - uuid: 'acc1' as PersonUuid, + uuid: 'acc1' as AccountUuid, timezone: 'UTC', locale: 'en' } @@ -324,12 +324,12 @@ describe('AccountPostgresDbCollection', () => { it('should prevent updating password fields', async () => { await expect( - collection.updateOne({ uuid: 'acc1' as PersonUuid }, { hash: Buffer.from([]), salt: Buffer.from([]) }) + collection.updateOne({ uuid: 'acc1' as AccountUuid }, { hash: Buffer.from([]), salt: Buffer.from([]) }) ).rejects.toThrow('Passwords are not allowed in update query') }) it('should allow updating non-password fields', async () => { - await collection.updateOne({ uuid: 'acc1' as PersonUuid }, { timezone: 'UTC', locale: 'en' }) + await collection.updateOne({ uuid: 'acc1' as AccountUuid }, { timezone: 'UTC', locale: 'en' }) expect(mockClient.unsafe).toHaveBeenCalledWith( 'UPDATE global_account.account SET "timezone" = $1, "locale" = $2 WHERE "uuid" = $3', @@ -346,7 +346,7 @@ describe('AccountPostgresDbCollection', () => { }) it('should allow deleting by non-password fields', async () => { - await collection.deleteMany({ uuid: 'acc1' as PersonUuid }) + await collection.deleteMany({ uuid: 'acc1' as AccountUuid }) expect(mockClient.unsafe).toHaveBeenCalledWith('DELETE FROM global_account.account WHERE "uuid" = $1', ['acc1']) }) @@ -399,7 +399,7 @@ describe('PostgresAccountDB', () => { }) describe('workspace operations', () => { - const accountId = 'acc1' as PersonUuid + const accountId = 'acc1' as AccountUuid const workspaceId = 'ws1' as WorkspaceUuid const role = AccountRole.Owner @@ -877,7 +877,7 @@ describe('PostgresAccountDB', () => { }) describe('password operations', () => { - const accountId = 'acc1' as PersonUuid + const accountId = 'acc1' as AccountUuid const hash: any = { buffer: Buffer.from('hash') } diff --git a/server/account/src/__tests__/utils.test.ts b/server/account/src/__tests__/utils.test.ts index a5f46ef81b..327f315b99 100644 --- a/server/account/src/__tests__/utils.test.ts +++ b/server/account/src/__tests__/utils.test.ts @@ -15,6 +15,7 @@ import { AccountRole, + type AccountUuid, Branding, MeasureContext, Person, @@ -938,6 +939,7 @@ describe('account utils', () => { test('should confirm unverified email', async () => { const mockSocialId = { + _id: '1000000001' as PersonId, key: 'email:test@example.com', type: SocialIdType.EMAIL, value: email, @@ -948,7 +950,7 @@ describe('account utils', () => { await confirmEmail(mockCtx, mockDb, account, email) expect(mockDb.socialId.updateOne).toHaveBeenCalledWith( - { key: mockSocialId.key }, + { _id: mockSocialId._id }, { verifiedOn: expect.any(Number) } ) }) @@ -1478,10 +1480,10 @@ describe('account utils', () => { }) test('should return account when found', async () => { - const mockAccount = { uuid: 'test-uuid' as PersonUuid } + const mockAccount = { uuid: 'test-uuid' as AccountUuid } ;(mockDb.account.findOne as jest.Mock).mockResolvedValue(mockAccount) - const result = await getAccount(mockDb, 'test-uuid' as PersonUuid) + const result = await getAccount(mockDb, 'test-uuid' as AccountUuid) expect(result).toEqual(mockAccount) expect(mockDb.account.findOne).toHaveBeenCalledWith({ uuid: 'test-uuid' }) }) @@ -1489,7 +1491,7 @@ describe('account utils', () => { test('should return null when account not found', async () => { ;(mockDb.account.findOne as jest.Mock).mockResolvedValue(null) - const result = await getAccount(mockDb, 'nonexistent-uuid' as PersonUuid) + const result = await getAccount(mockDb, 'nonexistent-uuid' as AccountUuid) expect(result).toBeNull() }) }) @@ -1726,6 +1728,7 @@ describe('account utils', () => { const mockDb = { socialId: { + find: jest.fn(() => []), findOne: jest.fn(), insertOne: jest.fn(), updateOne: jest.fn() diff --git a/server/account/src/collections/mongo.ts b/server/account/src/collections/mongo.ts index 40d77fda17..99efaa14bc 100644 --- a/server/account/src/collections/mongo.ts +++ b/server/account/src/collections/mongo.ts @@ -30,8 +30,8 @@ import { type AccountRole, type Data, type Version, - type PersonUuid, - type WorkspaceUuid + type WorkspaceUuid, + AccountUuid } from '@hcengineering/core' import type { @@ -357,7 +357,7 @@ export class WorkspaceStatusMongoDbCollection implements DbCollection { + async assignWorkspace (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole): Promise { await this.workspaceMembers.insertOne({ workspaceUuid: workspaceId, accountUuid: accountId, @@ -538,7 +538,7 @@ export class MongoAccountDB implements AccountDB { }) } - async unassignWorkspace (accountId: PersonUuid, workspaceId: WorkspaceUuid): Promise { + async unassignWorkspace (accountId: AccountUuid, workspaceId: WorkspaceUuid): Promise { await this.workspaceMembers.deleteMany({ workspaceUuid: workspaceId, accountUuid: accountId @@ -697,7 +697,7 @@ export class MongoAccountDB implements AccountDB { ) } - async updateWorkspaceRole (accountId: PersonUuid, workspaceId: WorkspaceUuid, role: AccountRole): Promise { + async updateWorkspaceRole (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole): Promise { await this.workspaceMembers.updateOne( { workspaceUuid: workspaceId, @@ -707,7 +707,7 @@ export class MongoAccountDB implements AccountDB { ) } - async getWorkspaceRole (accountId: PersonUuid, workspaceId: WorkspaceUuid): Promise { + async getWorkspaceRole (accountId: AccountUuid, workspaceId: WorkspaceUuid): Promise { const assignment = await this.workspaceMembers.findOne({ workspaceUuid: workspaceId, accountUuid: accountId @@ -723,18 +723,18 @@ export class MongoAccountDB implements AccountDB { })) } - async getAccountWorkspaces (accountId: PersonUuid): Promise { + async getAccountWorkspaces (accountId: AccountUuid): Promise { const members = await this.workspaceMembers.find({ accountUuid: accountId }) const wsIds = members.map((m) => m.workspaceUuid) return await this.workspace.find({ uuid: { $in: wsIds } }) } - async setPassword (accountId: PersonUuid, passwordHash: Buffer, salt: Buffer): Promise { + async setPassword (accountId: AccountUuid, passwordHash: Buffer, salt: Buffer): Promise { await this.account.updateOne({ uuid: accountId }, { hash: passwordHash, salt }) } - async resetPassword (accountId: PersonUuid): Promise { + async resetPassword (accountId: AccountUuid): Promise { await this.account.updateOne({ uuid: accountId }, { hash: null, salt: null }) } } diff --git a/server/account/src/collections/postgres.ts b/server/account/src/collections/postgres.ts index a61fca8862..a91c6f1e35 100644 --- a/server/account/src/collections/postgres.ts +++ b/server/account/src/collections/postgres.ts @@ -19,8 +19,8 @@ import { type Person, type WorkspaceMemberInfo, AccountRole, - type PersonUuid, - type WorkspaceUuid + type WorkspaceUuid, + type AccountUuid } from '@hcengineering/core' import type { @@ -477,22 +477,22 @@ export class PostgresAccountDB implements AccountDB { }) } - async assignWorkspace (accountUuid: PersonUuid, workspaceUuid: WorkspaceUuid, role: AccountRole): Promise { + async assignWorkspace (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid, role: AccountRole): Promise { await this .client`INSERT INTO ${this.client(this.getWsMembersTableName())} (workspace_uuid, account_uuid, role) VALUES (${workspaceUuid}, ${accountUuid}, ${role})` } - async unassignWorkspace (accountUuid: PersonUuid, workspaceUuid: WorkspaceUuid): Promise { + async unassignWorkspace (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid): Promise { await this .client`DELETE FROM ${this.client(this.getWsMembersTableName())} WHERE workspace_uuid = ${workspaceUuid} AND account_uuid = ${accountUuid}` } - async updateWorkspaceRole (accountUuid: PersonUuid, workspaceUuid: WorkspaceUuid, role: AccountRole): Promise { + async updateWorkspaceRole (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid, role: AccountRole): Promise { await this .client`UPDATE ${this.client(this.getWsMembersTableName())} SET role = ${role} WHERE workspace_uuid = ${workspaceUuid} AND account_uuid = ${accountUuid}` } - async getWorkspaceRole (accountUuid: PersonUuid, workspaceUuid: WorkspaceUuid): Promise { + async getWorkspaceRole (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid): Promise { const res: any = await this .client`SELECT role FROM ${this.client(this.getWsMembersTableName())} WHERE workspace_uuid = ${workspaceUuid} AND account_uuid = ${accountUuid}` @@ -509,7 +509,7 @@ export class PostgresAccountDB implements AccountDB { })) } - async getAccountWorkspaces (accountUuid: PersonUuid): Promise { + async getAccountWorkspaces (accountUuid: AccountUuid): Promise { const sql = `SELECT w.uuid, w.name, @@ -656,12 +656,12 @@ export class PostgresAccountDB implements AccountDB { return convertKeysToCamelCase(res[0]) as WorkspaceInfoWithStatus } - async setPassword (accountUuid: PersonUuid, hash: Buffer, salt: Buffer): Promise { + async setPassword (accountUuid: AccountUuid, hash: Buffer, salt: Buffer): Promise { await this .client`UPSERT INTO ${this.client(this.account.getPasswordsTableName())} (account_uuid, hash, salt) VALUES (${accountUuid}, ${hash.buffer as any}::bytea, ${salt.buffer as any}::bytea)` } - async resetPassword (accountUuid: PersonUuid): Promise { + async resetPassword (accountUuid: AccountUuid): Promise { await this .client`DELETE FROM ${this.client(this.account.getPasswordsTableName())} WHERE account_uuid = ${accountUuid}` } diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index c90b6d3aac..fae0fabb04 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -26,10 +26,10 @@ import { type Branding, type Person, type PersonId, - type PersonInfo, type PersonUuid, type WorkspaceMemberInfo, - type WorkspaceUuid + type WorkspaceUuid, + type AccountUuid } from '@hcengineering/core' import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform' import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token' @@ -89,7 +89,8 @@ import { addSocialId, releaseSocialId, updateWorkspaceRole, - setTimezoneIfNotDefined + setTimezoneIfNotDefined, + confirmHulyIds } from './utils' import { type AccountServiceMethods, getServiceMethods } from './serviceOperations' @@ -128,7 +129,7 @@ export async function login ( throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) } - const existingAccount = await db.account.findOne({ uuid: emailSocialId.personUuid }) + const existingAccount = await db.account.findOne({ uuid: emailSocialId.personUuid as AccountUuid }) if (existingAccount == null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) @@ -147,7 +148,7 @@ export async function login ( const extraToken: Record = isAdminEmail(email) ? { admin: 'true' } : {} ctx.info('Login succeeded', { email, normalizedEmail, isConfirmed, emailSocialId, ...extraToken }) - void setTimezoneIfNotDefined(ctx, db, emailSocialId.personUuid, existingAccount, meta) + void setTimezoneIfNotDefined(ctx, db, existingAccount.uuid, existingAccount, meta) return { account: existingAccount.uuid, @@ -183,13 +184,13 @@ export async function loginOtp ( throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) } - const account = await getAccount(db, emailSocialId.personUuid) + const account = await getAccount(db, emailSocialId.personUuid as AccountUuid) if (account == null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) } - void setTimezoneIfNotDefined(ctx, db, emailSocialId.personUuid, account, meta) + void setTimezoneIfNotDefined(ctx, db, account.uuid, account, meta) return await sendOtp(ctx, db, branding, emailSocialId) } @@ -247,8 +248,7 @@ export async function signUpOtp ( email: string firstName: string lastName: string - }, - meta?: Meta + } ): Promise { const { email, firstName, lastName } = params // Note: can support OTP based on any other social logins later @@ -257,7 +257,7 @@ export async function signUpOtp ( let personUuid: PersonUuid if (emailSocialId !== null) { - const existingAccount = await db.account.findOne({ uuid: emailSocialId.personUuid }) + const existingAccount = await db.account.findOne({ uuid: emailSocialId.personUuid as AccountUuid }) if (existingAccount !== null) { ctx.error('An account with the provided email already exists', { email }) @@ -274,7 +274,6 @@ export async function signUpOtp ( const emailSocialIdId = await db.socialId.insertOne(newSocialId) emailSocialId = { ...newSocialId, _id: emailSocialIdId, key: buildSocialIdString(newSocialId) } } - void setTimezoneIfNotDefined(ctx, db, personUuid, null, meta) return await sendOtp(ctx, db, branding, emailSocialId) } @@ -287,7 +286,8 @@ export async function validateOtp ( params: { email: string code: string - } + }, + meta?: Meta ): Promise { const { email, code } = params @@ -312,7 +312,7 @@ export async function validateOtp ( } // This method handles both login and signup - const account = await db.account.findOne({ uuid: emailSocialId.personUuid }) + const account = await db.account.findOne({ uuid: emailSocialId.personUuid as AccountUuid }) if (account == null) { // This is a signup @@ -320,18 +320,20 @@ export async function validateOtp ( ctx.info('OTP signup success', emailSocialId) } else { - // Confirm huly social id if hasn't been confirmed yet + await confirmHulyIds(ctx, db, account.uuid) ctx.info('OTP login success', emailSocialId) } + void setTimezoneIfNotDefined(ctx, db, emailSocialId.personUuid as AccountUuid, account, meta) + const person = await db.person.findOne({ uuid: emailSocialId.personUuid }) if (person == null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {})) } return { - account: emailSocialId.personUuid, + account: emailSocialId.personUuid as AccountUuid, name: getPersonName(person), socialId: emailSocialId._id, token: generateToken(emailSocialId.personUuid) @@ -764,7 +766,7 @@ export async function checkAutoJoin ( // If it's an existing account we should check for saved token or ask for login to prevent accidental access through shared link if (emailSocialId != null) { - const targetAccount = await getAccount(db, emailSocialId.personUuid) + const targetAccount = await getAccount(db, emailSocialId.personUuid as AccountUuid) if (targetAccount != null) { if (targetAccount.automatic == null || !targetAccount.automatic) { if (token == null) { @@ -883,6 +885,8 @@ export async function confirm ( const socialId = await confirmEmail(ctx, db, account, email) + await confirmHulyIds(ctx, db, account) + const person = await db.person.findOne({ uuid: account }) if (person == null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {})) @@ -951,7 +955,7 @@ export async function requestPasswordReset ( ) } - const account = await getAccount(db, emailSocialId.personUuid) + const account = await getAccount(db, emailSocialId.personUuid as AccountUuid) if (account == null) { ctx.info('Account not found', { email, normalizedEmail }) @@ -1038,7 +1042,7 @@ export async function leaveWorkspace ( db: AccountDB, branding: Branding | null, token: string, - params: { account: PersonUuid } + params: { account: AccountUuid } ): Promise { const { account: targetAccount } = params const { account, workspace } = decodeTokenVerbose(ctx, token) @@ -1254,7 +1258,7 @@ export async function getLoginInfoByToken ( branding: Branding | null, token: string ): Promise { - let accountUuid: PersonUuid + let accountUuid: AccountUuid let workspaceUuid: WorkspaceUuid let extra: any try { @@ -1382,32 +1386,6 @@ export async function getPerson ( return person } -export async function getPersonInfo ( - ctx: MeasureContext, - db: AccountDB, - branding: Branding | null, - token: string, - params: { account: PersonUuid } -): Promise { - const { account } = params - const { extra } = decodeTokenVerbose(ctx, token) - verifyAllowedServices(['workspace', 'tool'], extra) - - const person = await db.person.findOne({ uuid: account }) - - if (person == null) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.PersonNotFound, { person: account })) - } - - const verifiedSocialIds = await db.socialId.find({ personUuid: account, verifiedOn: { $gt: 0 } }) - - return { - personUuid: account, - name: getPersonName(person), - socialIds: verifiedSocialIds.map((it) => it._id) - } -} - export async function findPersonBySocialKey ( ctx: MeasureContext, db: AccountDB, @@ -1425,7 +1403,7 @@ export async function findPersonBySocialKey ( } if (params.requireAccount === true) { - const account = await db.account.findOne({ uuid: socialId.personUuid }) + const account = await db.account.findOne({ uuid: socialId.personUuid as AccountUuid }) return account?.uuid } @@ -1451,7 +1429,7 @@ export async function findPersonBySocialId ( // TODO: combine into one request with join if (requireAccount === true) { - const account = await db.account.findOne({ uuid: socialIdObj.personUuid }) + const account = await db.account.findOne({ uuid: socialIdObj.personUuid as AccountUuid }) if (account == null) { return } @@ -1478,7 +1456,7 @@ export async function findSocialIdBySocialKey ( // TODO: combine into one request with join if (requireAccount === true) { - const account = await db.account.findOne({ uuid: socialIdObj.personUuid }) + const account = await db.account.findOne({ uuid: socialIdObj.personUuid as AccountUuid }) if (account == null) { return } @@ -1513,7 +1491,7 @@ export async function getAccountInfo ( db: AccountDB, branding: Branding | null, token: string, - params: { accountId: PersonUuid } + params: { accountId: AccountUuid } ): Promise { decodeTokenVerbose(ctx, token) const { accountId } = params @@ -1689,7 +1667,6 @@ export type AccountMethods = | 'getLoginInfoByToken' | 'getSocialIds' | 'getPerson' - | 'getPersonInfo' | 'getWorkspaceMembers' | 'updateWorkspaceRole' | 'findPersonBySocialKey' @@ -1746,7 +1723,6 @@ export function getMethods (hasSignUp: boolean = true): Partial { const { socialKey, targetRole } = params const { extra } = decodeTokenVerbose(ctx, token) - - if (!['workspace', 'tool'].includes(extra?.service)) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } + verifyAllowedServices(['workspace', 'tool'], extra) const socialId = await getSocialIdByKey(db, socialKey.toLowerCase() as PersonId) if (socialId == null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) } - await updateWorkspaceRole(ctx, db, branding, token, { targetAccount: socialId.personUuid, targetRole }) + await updateWorkspaceRole(ctx, db, branding, token, { targetAccount: socialId.personUuid as AccountUuid, targetRole }) } /** @@ -468,7 +468,7 @@ export async function assignWorkspace ( throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) } - const account = await getAccount(db, emailSocialId.personUuid) + const account = await getAccount(db, emailSocialId.personUuid as AccountUuid) if (account == null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) @@ -489,6 +489,32 @@ export async function assignWorkspace ( } } +export async function getPersonInfo ( + ctx: MeasureContext, + db: AccountDB, + branding: Branding | null, + token: string, + params: { account: PersonUuid } +): Promise { + const { account } = params + const { extra } = decodeTokenVerbose(ctx, token) + verifyAllowedServices(['workspace', 'tool'], extra) + + const person = await db.person.findOne({ uuid: account }) + + if (person == null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.PersonNotFound, { person: account })) + } + + const verifiedSocialIds = await db.socialId.find({ personUuid: account, verifiedOn: { $gt: 0 } }) + + return { + personUuid: account, + name: getPersonName(person), + socialIds: verifiedSocialIds + } +} + export async function addSocialIdToPerson ( ctx: MeasureContext, db: AccountDB, @@ -779,6 +805,7 @@ export type AccountServiceMethods = | 'performWorkspaceOperation' | 'updateWorkspaceRoleBySocialKey' | 'addSocialIdToPerson' + | 'getPersonInfo' | 'createIntegration' | 'updateIntegration' | 'deleteIntegration' @@ -804,6 +831,7 @@ export function getServiceMethods (): Partial time: Timestamp @@ -190,12 +191,12 @@ export interface AccountDB { init: () => Promise createWorkspace: (data: WorkspaceData, status: WorkspaceStatusData) => Promise - assignWorkspace: (accountId: PersonUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise - updateWorkspaceRole: (accountId: PersonUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise - unassignWorkspace: (accountId: PersonUuid, workspaceId: WorkspaceUuid) => Promise - getWorkspaceRole: (accountId: PersonUuid, workspaceId: WorkspaceUuid) => Promise + assignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise + updateWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise + unassignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise + getWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise getWorkspaceMembers: (workspaceId: WorkspaceUuid) => Promise - getAccountWorkspaces: (accountId: PersonUuid) => Promise + getAccountWorkspaces: (accountId: AccountUuid) => Promise getPendingWorkspace: ( region: string, version: Data, @@ -203,8 +204,8 @@ export interface AccountDB { processingTimeoutMs: number, wsLivenessMs?: number ) => Promise - setPassword: (accountId: PersonUuid, passwordHash: Buffer, salt: Buffer) => Promise - resetPassword: (accountId: PersonUuid) => Promise + setPassword: (accountId: AccountUuid, passwordHash: Buffer, salt: Buffer) => Promise + resetPassword: (accountId: AccountUuid) => Promise } export interface DbCollection { @@ -268,7 +269,7 @@ export type WorkspaceEvent = | 'archiving-done' export type WorkspaceOperation = 'create' | 'upgrade' | 'all' | 'all+backup' export interface LoginInfo { - account: PersonUuid + account: AccountUuid name?: string socialId?: PersonId token?: string diff --git a/server/account/src/utils.ts b/server/account/src/utils.ts index 8d299437c6..9931acb6fa 100644 --- a/server/account/src/utils.ts +++ b/server/account/src/utils.ts @@ -29,7 +29,8 @@ import { isActiveMode, type PersonUuid, type PersonId, - type Person + type Person, + AccountUuid } from '@hcengineering/core' import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform' @@ -328,7 +329,7 @@ export async function setPassword ( ctx: MeasureContext, db: AccountDB, branding: Branding | null, - personUuid: PersonUuid, + personUuid: AccountUuid, password: string ): Promise { if (password == null || password === '') { @@ -444,17 +445,15 @@ export async function createAccount ( createdOn = Date.now() ): Promise { // Create Huly social id and account - // Currently, it's always created along with the account but never confirmed. - // What's the actual use case for it? await db.socialId.insertOne({ type: SocialIdType.HULY, value: personUuid, personUuid, ...(confirmed ? { verifiedOn: Date.now() } : {}) }) - await db.account.insertOne({ uuid: personUuid, automatic }) + await db.account.insertOne({ uuid: personUuid as AccountUuid, automatic }) await db.accountEvent.insertOne({ - accountUuid: personUuid, + accountUuid: personUuid as AccountUuid, eventType: AccountEventType.ACCOUNT_CREATED, time: createdOn }) @@ -470,22 +469,22 @@ export async function signUpByEmail ( lastName: string, confirmed = false, automatic = false -): Promise<{ account: PersonUuid, socialId: PersonId }> { +): Promise<{ account: AccountUuid, socialId: PersonId }> { const normalizedEmail = cleanEmail(email) const emailSocialId = await getEmailSocialId(db, normalizedEmail) - let account: PersonUuid + let account: AccountUuid let socialId: PersonId if (emailSocialId !== null) { - const existingAccount = await db.account.findOne({ uuid: emailSocialId.personUuid }) + const existingAccount = await db.account.findOne({ uuid: emailSocialId.personUuid as AccountUuid }) if (existingAccount !== null) { ctx.error('An account with the provided email already exists', { email }) throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountAlreadyExists, {})) } - account = emailSocialId.personUuid + account = emailSocialId.personUuid as AccountUuid socialId = emailSocialId._id // Person exists, but may have different name, need to update with what's been provided await db.person.updateOne({ uuid: account }, { firstName, lastName }) @@ -621,7 +620,7 @@ export async function updateWorkspaceRole ( branding: Branding | null, token: string, params: { - targetAccount: PersonUuid + targetAccount: AccountUuid targetRole: AccountRole } ): Promise { @@ -899,15 +898,22 @@ export async function confirmEmail ( ) } - await db.socialId.updateOne({ key: emailSocialId.key }, { verifiedOn: Date.now() }) + await db.socialId.updateOne({ _id: emailSocialId._id }, { verifiedOn: Date.now() }) return emailSocialId._id } +export async function confirmHulyIds (ctx: MeasureContext, db: AccountDB, account: AccountUuid): Promise { + const hulySocialIds = await db.socialId.find({ personUuid: account, type: SocialIdType.HULY, verifiedOn: null }) + for (const hulySocialId of hulySocialIds) { + await db.socialId.updateOne({ _id: hulySocialId._id }, { verifiedOn: Date.now() }) + } +} + export async function useInvite (db: AccountDB, inviteId: string): Promise { await db.invite.updateOne({ id: inviteId }, { $inc: { remainingUses: -1 } }) } -export async function getAccount (db: AccountDB, uuid: PersonUuid): Promise { +export async function getAccount (db: AccountDB, uuid: AccountUuid): Promise { return await db.account.findOne({ uuid }) } @@ -1016,7 +1022,7 @@ export async function doJoinByInvite ( db: AccountDB, branding: Branding | null, token: string, - account: PersonUuid, + account: AccountUuid, workspace: Workspace, invite: WorkspaceInvite ): Promise { @@ -1079,7 +1085,7 @@ export async function loginOrSignUpWithProvider ( throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {})) } - const account = await db.account.findOne({ uuid: personUuid }) + const account = await db.account.findOne({ uuid: personUuid as AccountUuid }) if (account == null) { if (signUpDisabled) { @@ -1095,7 +1101,7 @@ export async function loginOrSignUpWithProvider ( const confirmedSocialId = await db.socialId.findOne({ personUuid, verifiedOn: { $gt: 0 } }) if (confirmedSocialId == null) { - await db.resetPassword(personUuid) + await db.resetPassword(personUuid as AccountUuid) } let socialIdId: PersonId | undefined @@ -1120,8 +1126,10 @@ export async function loginOrSignUpWithProvider ( await db.socialId.updateOne({ key: emailSocialId.key }, { verifiedOn: Date.now() }) } + await confirmHulyIds(ctx, db, personUuid as AccountUuid) + return { - account: personUuid, + account: personUuid as AccountUuid, socialId: socialIdId, name: getPersonName(person), token: generateToken(personUuid) @@ -1376,7 +1384,7 @@ export async function releaseSocialId ( export async function getWorkspaceRole ( db: AccountDB, - account: PersonUuid, + account: AccountUuid, workspace: WorkspaceUuid ): Promise { if (account === systemAccountUuid) { @@ -1393,7 +1401,7 @@ export function generatePassword (len: number = 24): string { export async function setTimezoneIfNotDefined ( ctx: MeasureContext, db: AccountDB, - accountId: PersonUuid, + accountId: AccountUuid, account: Account | null | undefined, meta?: Meta ): Promise { diff --git a/server/tool/src/initializer.ts b/server/tool/src/initializer.ts index c8bca32750..d566c44ea9 100644 --- a/server/tool/src/initializer.ts +++ b/server/tool/src/initializer.ts @@ -9,12 +9,13 @@ import core, { MeasureContext, Mixin, parseSocialIdString, - type PersonId, - type PersonInfo, Ref, SocialIdType, Space, TxOperations, + pickPrimarySocialId, + type PersonId, + type PersonInfo, type WorkspaceIds } from '@hcengineering/core' import { ModelLogger } from '@hcengineering/model' @@ -23,7 +24,6 @@ import { HulyFormatImporter, StorageFileUploader } from '@hcengineering/importer import type { StorageAdapter } from '@hcengineering/server-core' import { jsonToMarkup } from '@hcengineering/text' import { markdownToMarkup } from '@hcengineering/text-markdown' -import { pickPrimarySocialId } from '@hcengineering/contact' import { v4 as uuid } from 'uuid' import path from 'path' @@ -114,7 +114,7 @@ export class WorkspaceInitializer { private readonly initRepoDir: string, private readonly creator: PersonInfo ) { - this.socialKey = pickPrimarySocialId(creator.socialIds) + this.socialKey = pickPrimarySocialId(creator.socialIds)._id const socialKeyObj = parseSocialIdString(this.socialKey) this.socialType = socialKeyObj.type this.socialValue = socialKeyObj.value diff --git a/services/ai-bot/pod-ai-bot/src/workspace/love.ts b/services/ai-bot/pod-ai-bot/src/workspace/love.ts index 765bf0e3c3..aaf7071c3d 100644 --- a/services/ai-bot/pod-ai-bot/src/workspace/love.ts +++ b/services/ai-bot/pod-ai-bot/src/workspace/love.ts @@ -178,7 +178,8 @@ export class LoveController { if (!this.socialIdByPerson.has(person)) { const identities = await this.client.findAll(contact.class.SocialIdentity, { attachedTo: person, - attachedToClass: contact.class.Person + attachedToClass: contact.class.Person, + verifiedOn: { $gt: 0 } }) if (identities.length > 0) { const id = pickPrimarySocialId(identities)._id